Initial commit
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
const _ = require('underscore');
|
||||
|
||||
module.exports = {
|
||||
sortSchema: function (schema) {
|
||||
var priorities = {
|
||||
'cartodb_id': 1,
|
||||
'the_geom': 2,
|
||||
'__default__': 3,
|
||||
'created_at': 4,
|
||||
'updated_at': 5
|
||||
};
|
||||
|
||||
function priority (v) {
|
||||
return priorities[v] || priorities['__default__'];
|
||||
}
|
||||
|
||||
return _.chain(schema)
|
||||
.clone()
|
||||
.sort(function (a, b) { // ..and then re-sort by priorities defined above
|
||||
var prioA = priority(a[0]);
|
||||
var prioB = priority(b[0]);
|
||||
if (prioA < prioB) {
|
||||
return -1;
|
||||
} else if (prioA > prioB) {
|
||||
return 1;
|
||||
} else { // priority is the same (i.e. __default__), so compare alphabetically
|
||||
return a[0] < b[0] ? -1 : 1;
|
||||
}
|
||||
})
|
||||
.value();
|
||||
},
|
||||
|
||||
/**
|
||||
* return true if the sql query alters table schema in some way
|
||||
*/
|
||||
alterTable: function (sql) {
|
||||
sql = sql.trim();
|
||||
return sql.search(/alter\s+[\w\."]+\s+/i) !== -1 ||
|
||||
sql.search(/drop\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^vacuum\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^create\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^reindex\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^grant\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^revoke\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^cluster\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^comment\s+on\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^explain\s+[\w\.\"]+/i) !== -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* return true if the sql query alters table data
|
||||
*/
|
||||
alterTableData: function (sql) {
|
||||
return this.alterTable(sql) ||
|
||||
sql.search(/^refresh\s+materialized\s+view\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/^truncate\s+[\w\.\"]+/i) !== -1 ||
|
||||
sql.search(/insert\s+into/i) !== -1 ||
|
||||
sql.search(/update\s+[\w\.\-"]+\s+.*set/i) !== -1 ||
|
||||
sql.search(/delete\s+from/i) !== -1;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,835 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const Backbone = require('backbone');
|
||||
const SQL = require('internal-carto.js').SQL;
|
||||
const safeTableNameQuoting = require('dashboard/helpers/safe-table-name-quoting');
|
||||
const cartoMetadataStatic = require('./carto-table-metadata-static');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const CartoTableData = require('dashboard/data/table/carto-table-data');
|
||||
const ColumnModel = require('dashboard/data/table/column-model');
|
||||
const RowModel = require('dashboard/data/table/row-model');
|
||||
const PermissionModel = require('dashboard/data/permission-model');
|
||||
const ImportModel = require('dashboard/data/import-model');
|
||||
const TableSynchronizationModel = require('dashboard/data/table-synchronization-model');
|
||||
const getSimpleGeometryType = require('builder/data/get-simple-geometry-type');
|
||||
const retrigger = require('dashboard/helpers/retrigger');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
const Base = Backbone.Model.extend({
|
||||
|
||||
columnNames: function () {
|
||||
return _.map(this.get('schema'), function (c) {
|
||||
return c[0];
|
||||
});
|
||||
},
|
||||
|
||||
columnName: function (idx) {
|
||||
return this.columnNames()[idx];
|
||||
}
|
||||
});
|
||||
|
||||
const CartoTableMetadata = Base.extend({
|
||||
currentLoading: 0, // class variable (shared). I'm still not sure if this is messy as hell or powerfull as a transformer
|
||||
|
||||
_TEXTS: {
|
||||
columnDeleted: 'Your column has been deleted',
|
||||
columnDeleting: 'Deleting your column',
|
||||
columnAdded: 'Your column has been added',
|
||||
columnAdding: 'Adding new column'
|
||||
},
|
||||
|
||||
hiddenColumns: [
|
||||
'the_geom',
|
||||
'the_geom_webmercator',
|
||||
'cartodb_georef_status',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'cartodb_id'
|
||||
],
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
_.bindAll(this, 'notice');
|
||||
this.readOnly = false;
|
||||
this.bind('change:schema', this._prepareSchema, this);
|
||||
this._prepareSchema();
|
||||
this.sqlView = null;
|
||||
this.synchronization = new TableSynchronizationModel({ configModel: opts.configModel });
|
||||
this.synchronization.linkToTable(this);
|
||||
this.synchronization.bind('change:id', function isSyncChanged () {
|
||||
this.trigger('change:isSync', this, this.synchronization.isSync());
|
||||
}, this);
|
||||
if (this.get('no_data_fetch')) {
|
||||
this.no_data_fetch = true;
|
||||
delete this.attributes.no_data_fetch;
|
||||
}
|
||||
this.data();
|
||||
this.bind('error', function (e, resp) {
|
||||
this.error('', resp);
|
||||
}, this);
|
||||
this._data.bind('error', function (e, resp) {
|
||||
this.notice('error loading rows, check your SQL query', 'error', 5000);
|
||||
}, this);
|
||||
|
||||
this._data.bind('reset', function () {
|
||||
var view = this._data;
|
||||
this.set({
|
||||
schema: view.schemaFromData(this.get('schema')),
|
||||
geometry_types: view.getGeometryTypes()
|
||||
});
|
||||
}, this);
|
||||
|
||||
retrigger.call(this, 'change', this._data, 'data:changed');
|
||||
retrigger.call(this, 'saved', this._data, 'data:saved');
|
||||
|
||||
this.bind('change:table_visualization', function () {
|
||||
this.permission = new PermissionModel(this.get('table_visualization').permission, { configModel: this._configModel });
|
||||
this.trigger('change:permission', this, this.permission);
|
||||
}, this);
|
||||
|
||||
// create permission if permission is set
|
||||
this.permission = new PermissionModel(this.get('permission'), { configModel: this._configModel });
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
var version = this._configModel.urlVersion('table', method);
|
||||
var base = '/api/' + version + '/tables';
|
||||
if (this.isNew()) {
|
||||
return base;
|
||||
}
|
||||
return base + '/' + this.id;
|
||||
},
|
||||
|
||||
// use the name as the id since the api works
|
||||
// in the same way to table name and id
|
||||
parse: function (resp, xhr) {
|
||||
if (resp.name) {
|
||||
resp.id = resp.name;
|
||||
}
|
||||
// move geometry_types to stats one
|
||||
// geometry_types from backend are not reliable anymore and it can only be used
|
||||
// for non editing stuff (showing icons, general checks on table list)
|
||||
resp.stats_geometry_types = resp.geometry_types;
|
||||
delete resp.geometry_types;
|
||||
delete resp.schema;
|
||||
return resp;
|
||||
},
|
||||
|
||||
notice: function (msg, type, timeout) {
|
||||
this.trigger('notice', msg, type, timeout);
|
||||
},
|
||||
|
||||
setReadOnly: function (_) {
|
||||
var trigger = false;
|
||||
if (this.readOnly !== _) {
|
||||
trigger = true;
|
||||
}
|
||||
this.readOnly = _;
|
||||
if (trigger) {
|
||||
this.trigger('change:readOnly', this, _);
|
||||
}
|
||||
},
|
||||
|
||||
isReadOnly: function () {
|
||||
return this.readOnly || this.data().isReadOnly() || this.synchronization.isSync();
|
||||
},
|
||||
|
||||
isSync: function () {
|
||||
return this.synchronization.isSync();
|
||||
},
|
||||
|
||||
getUnqualifiedName: function () {
|
||||
var name = this.get('name');
|
||||
if (!name) return null;
|
||||
var tk = name.split('.');
|
||||
if (tk.length == 2) { // eslint-disable-line eqeqeq
|
||||
return tk[1];
|
||||
}
|
||||
return name;
|
||||
},
|
||||
|
||||
// "user".table -> user.table
|
||||
getUnquotedName: function () {
|
||||
var name = this.get('name');
|
||||
return name && name.replace(/"/g, '');
|
||||
},
|
||||
|
||||
sortSchema: function () {
|
||||
this.set('schema', CartoTableMetadata.sortSchema(this.get('schema')));
|
||||
},
|
||||
|
||||
error: function (msg, resp) {
|
||||
let err = '';
|
||||
try {
|
||||
err = resp && resp.responseText && JSON.parse(resp.responseText).errors[0];
|
||||
} catch (e) {
|
||||
} finally {
|
||||
this.trigger('notice', msg + ': ' + err, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
_prepareSchema: function () {
|
||||
this._columnType = {};
|
||||
|
||||
_(this.get('schema')).each((schema) => {
|
||||
this._columnType[schema[0]] = schema[1];
|
||||
});
|
||||
|
||||
if (!this.isInSQLView()) {
|
||||
this.set('original_schema', this.get('schema'));
|
||||
}
|
||||
},
|
||||
|
||||
columnNames: function (sc) {
|
||||
sc = sc || 'schema';
|
||||
return _(this.get(sc)).pluck(0);
|
||||
},
|
||||
|
||||
containsColumn: function (name) {
|
||||
return _.contains(this.columnNames(), name);
|
||||
},
|
||||
|
||||
columnNamesByType: function (type, sc) {
|
||||
sc = sc || 'schema';
|
||||
var t = _(this.get(sc)).filter(function (c) {
|
||||
return c[1] == type; // eslint-disable-line eqeqeq
|
||||
});
|
||||
return _(t).pluck(0);
|
||||
},
|
||||
|
||||
// return geometry columns calculated backend stats
|
||||
// use geomColumnTypes if you need something reliable (but slower and async)
|
||||
statsGeomColumnTypes: function (geometryTypes) {
|
||||
return this.geomColumnTypes(this.get('stats_geometry_types'));
|
||||
},
|
||||
|
||||
// return the current column types in an array
|
||||
// the values inside the array can be:
|
||||
// 'point', 'line', 'polygon'
|
||||
geomColumnTypes: function (geometryTypes) {
|
||||
var types = geometryTypes || this.get('geometry_types');
|
||||
var geomTypes = [];
|
||||
if (!_.isArray(types)) {
|
||||
return [];
|
||||
}
|
||||
var _map = {
|
||||
'st_multipolygon': 'polygon',
|
||||
'st_polygon': 'polygon',
|
||||
'st_multilinestring': 'line',
|
||||
'st_linestring': 'line',
|
||||
'st_multipoint': 'point',
|
||||
'st_point': 'point'
|
||||
};
|
||||
for (var t in types) {
|
||||
var type = types[t];
|
||||
// when there are rows with no geo type null is returned as geotype
|
||||
if (type) {
|
||||
var a = _map[type.toLowerCase()];
|
||||
if (a) {
|
||||
geomTypes.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _.uniq(geomTypes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adding a new geometry type to the table
|
||||
* @param geom type {st_polygon, st_point,...}
|
||||
* @param set options
|
||||
*/
|
||||
addGeomColumnType: function (t, opts) {
|
||||
if (!t) return;
|
||||
var types = _.clone(this.get('geometry_types')) || [];
|
||||
if (!_.contains(types, t)) {
|
||||
types.push(t);
|
||||
|
||||
this.set({
|
||||
'geometry_types': types
|
||||
}, opts);
|
||||
}
|
||||
},
|
||||
|
||||
nonReservedColumnNames: function () {
|
||||
return _.filter(this.columnNames(), (columnName) => {
|
||||
return !this.isReservedColumn(columnName);
|
||||
});
|
||||
},
|
||||
|
||||
columnTypes: function () {
|
||||
return _.clone(this._columnType);
|
||||
},
|
||||
|
||||
_getColumn: function (columnName) {
|
||||
if (this._columnType[columnName] === undefined) {
|
||||
return;
|
||||
// throw "the column does not exists";
|
||||
}
|
||||
var c = new ColumnModel({
|
||||
table: this,
|
||||
name: columnName,
|
||||
type: this._columnType[columnName],
|
||||
configModel: this._configModel
|
||||
}, { configModel: this._configModel });
|
||||
return c;
|
||||
},
|
||||
|
||||
getColumnType: function (columnName, sc) {
|
||||
sc = sc || 'schema';
|
||||
var t = _(this.get(sc)).filter(function (c) {
|
||||
return c[0] == columnName;// eslint-disable-line eqeqeq
|
||||
});
|
||||
if (t.length > 0) {
|
||||
return t[0][1];
|
||||
}
|
||||
},
|
||||
|
||||
addColumn: function (columnName, columnType, opts) {
|
||||
var c = new ColumnModel({
|
||||
table: this,
|
||||
_name: columnName,
|
||||
type: columnType || 'string',
|
||||
configModel: this._configModel
|
||||
}, { configModel: this._configModel });
|
||||
this.notice(this._TEXTS.columnAdding, 'load', 0);
|
||||
c.save(null, {
|
||||
success: (model, obj) => {
|
||||
this.notice(this._TEXTS.columnAdded, 'info');
|
||||
this.trigger('columnAdd', columnName);
|
||||
this.data().fetch();
|
||||
opts && opts.success && opts.success(model, obj);
|
||||
},
|
||||
error: (error, resp) => {
|
||||
this.error('error adding column', resp);
|
||||
opts && opts.error && opts.error(error);
|
||||
},
|
||||
wait: true
|
||||
});
|
||||
},
|
||||
|
||||
deleteColumn: function (columnName, opts) {
|
||||
var c = this._getColumn(columnName);
|
||||
if (c !== undefined) {
|
||||
this.notice(this._TEXTS.columnDeleting, 'load', 0);
|
||||
c.destroy({
|
||||
success: () => {
|
||||
this.trigger('columnDelete', columnName);
|
||||
this.notice(this._TEXTS.columnDeleted, 'info');
|
||||
this.data().fetch();
|
||||
opts && opts.success && opts.success();
|
||||
},
|
||||
error: (e, resp) => {
|
||||
this.error('error deleting column', resp);
|
||||
opts && opts.error && opts.error();
|
||||
},
|
||||
wait: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renameColumn: function (columnName, newName, opts) {
|
||||
if (columnName == newName) return; // eslint-disable-line eqeqeq
|
||||
var c = this._getColumn(columnName);
|
||||
var oldName = c.get('name');
|
||||
c.set({
|
||||
new_name: newName,
|
||||
old_name: c.get('name')
|
||||
});
|
||||
this.notice('renaming column', 'load', 0);
|
||||
c.save(null, {
|
||||
success: (mdl, data) => {
|
||||
this.notice('Column has been renamed', 'info');
|
||||
this.trigger('columnRename', newName, oldName);
|
||||
this.data().fetch();
|
||||
opts && opts.success && opts.success(mdl, data);
|
||||
},
|
||||
error: (e, resp) => {
|
||||
console.error("can't rename column");
|
||||
this.error('error renaming column', resp);
|
||||
opts && opts.error && opts.error(e, resp);
|
||||
},
|
||||
wait: true
|
||||
});
|
||||
},
|
||||
|
||||
isTypeChangeAllowed: function (columnName, newType) {
|
||||
var deactivateMatrix = {
|
||||
'number': ['date'],
|
||||
'boolean': ['date'],
|
||||
'date': ['boolean']
|
||||
};
|
||||
var c = this._getColumn(columnName);
|
||||
if (!c) {
|
||||
return true;
|
||||
}
|
||||
var type = c.get('type');
|
||||
var deactivated = deactivateMatrix[type] || [];
|
||||
deactivated = deactivated.concat([type]);
|
||||
return !_.contains(deactivated, newType);
|
||||
},
|
||||
|
||||
isTypeChangeDestructive: function (columnName, newType) {
|
||||
var columnType = this.getColumnType(columnName);
|
||||
|
||||
var destructiveMatrix = {
|
||||
'string': {
|
||||
'string': false,
|
||||
'number': true,
|
||||
'date': true,
|
||||
'boolean': true
|
||||
},
|
||||
'number': {
|
||||
'string': false,
|
||||
'number': false,
|
||||
'date': true,
|
||||
'boolean': true
|
||||
},
|
||||
'date': {
|
||||
'string': false,
|
||||
'number': true,
|
||||
'date': false,
|
||||
'boolean': true
|
||||
},
|
||||
'boolean': {
|
||||
'string': false,
|
||||
'number': false,
|
||||
'date': true,
|
||||
'boolean': false
|
||||
}
|
||||
};
|
||||
return destructiveMatrix[columnType][newType];
|
||||
},
|
||||
|
||||
changeColumnType: function (columnName, newType, opts) {
|
||||
var c = this._getColumn(columnName);
|
||||
|
||||
if (this.getColumnType(columnName) == newType) { // eslint-disable-line eqeqeq
|
||||
opts && opts.success && opts.success();
|
||||
return;
|
||||
}
|
||||
this.saveNewColumnType(c, newType, opts);
|
||||
},
|
||||
|
||||
saveNewColumnType: function (column, newType, opts) {
|
||||
column.set({ type: newType });
|
||||
this.notice('Changing column type', 'load', 0);
|
||||
column.save(null, {
|
||||
success: () => {
|
||||
this.notice('Column type has been changed', 'info');
|
||||
this.trigger('typeChanged', newType); // to make it testable
|
||||
this.data().fetch();
|
||||
opts && opts.success && opts.success();
|
||||
},
|
||||
error: (e, resp) => {
|
||||
this.trigger('typeChangeFailed', newType, e); // to make it testable
|
||||
this.error('error changing column type', resp);
|
||||
opts && opts.error && opts.error(e, resp);
|
||||
},
|
||||
wait: true
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* returns the original data for the table not the current applied view
|
||||
*/
|
||||
originalData: function () {
|
||||
return this._data;
|
||||
},
|
||||
|
||||
data: function () {
|
||||
if (this._data === undefined) {
|
||||
this._data = new CartoTableData(null, {
|
||||
table: this,
|
||||
configModel: this._configModel
|
||||
});
|
||||
this.bindData();
|
||||
}
|
||||
if (this.sqlView) {
|
||||
return this.sqlView;
|
||||
}
|
||||
return this._data;
|
||||
},
|
||||
|
||||
bindData: function (data) {
|
||||
if (this._data && !this._data.bindedReset) {
|
||||
retrigger.call(this, 'sync', this._data, 'dataLoaded');
|
||||
retrigger.call(this, 'add', this._data, 'dataAdded');
|
||||
this._data.bindedReset = true;
|
||||
}
|
||||
if (this.sqlView && !this.sqlView.bindedReset) {
|
||||
retrigger.call(this, 'sync', this.sqlView, 'dataLoaded');
|
||||
retrigger.call(this, 'add', this.sqlView, 'dataAdded');
|
||||
this.sqlView.bindedReset = true;
|
||||
}
|
||||
},
|
||||
|
||||
useSQLView: function (view, options) {
|
||||
if (!view && !this.sqlView) return;
|
||||
options = options || {};
|
||||
|
||||
if (this.sqlView) {
|
||||
this.sqlView.unbind(null, null, this);
|
||||
this.sqlView.unbind(null, null, this._data);
|
||||
}
|
||||
|
||||
// reset previous
|
||||
if (!view && this.sqlView) {
|
||||
this.sqlView.table = null;
|
||||
}
|
||||
|
||||
this.sqlView = view;
|
||||
this.bindData();
|
||||
|
||||
if (view) {
|
||||
view.bind('sync reset', function () {
|
||||
if (!view.modify_rows) {
|
||||
this.set({
|
||||
schema: view.schemaFromData(this.get('schema')),
|
||||
geometry_types: view.getGeometryTypes()
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
// listen for errors
|
||||
view.bind('error', function (e, resp) {
|
||||
this.notice('error loading rows, check your SQL query', 'error', 5000);
|
||||
}, this);
|
||||
|
||||
view.bind('loading', function () {
|
||||
// this.notice(_t('loading query'), 'load', 0);
|
||||
}, this);
|
||||
|
||||
view.bind('reset loaded', function () {
|
||||
if (view.modify_rows) {
|
||||
this.notice(view.affected_rows + ' rows affected');
|
||||
this.useSQLView(null);
|
||||
} else {
|
||||
this.notice(_t('loaded'));
|
||||
}
|
||||
}, this);
|
||||
|
||||
// swicth source data
|
||||
this.dataModel = this.sqlView;
|
||||
view.table = this;
|
||||
} else {
|
||||
this.dataModel = this._data;
|
||||
// get the original schema
|
||||
this.set({
|
||||
'schema': this.get('original_schema')
|
||||
});/// *, { silent: true });
|
||||
this.data().fetch();
|
||||
}
|
||||
this.trigger('change:dataSource', this.dataModel, this);
|
||||
},
|
||||
|
||||
isInSQLView: function () {
|
||||
return !!this.sqlView;
|
||||
},
|
||||
|
||||
/**
|
||||
* replace fetch functionally to add some extra call for logging
|
||||
* it can be used in the same way fetch is
|
||||
*/
|
||||
fetch: function (opts) {
|
||||
const silent = opts ? opts.silent : false;
|
||||
if (!silent) {
|
||||
this.notice('loading table', 'load', this, 0, 0);
|
||||
}
|
||||
var xhr = Base.prototype.fetch.call(this, opts);
|
||||
$.when(xhr).done(() => {
|
||||
opts && opts.success && opts.success.old_success && opts.success.old_success();
|
||||
if (!silent) {
|
||||
this.notice('loaded');
|
||||
}
|
||||
}).fail(() => {
|
||||
if (!silent) {
|
||||
this.notice('error loading the table');
|
||||
}
|
||||
});
|
||||
return xhr;
|
||||
},
|
||||
|
||||
isReservedColumn: function (c) {
|
||||
return RowModel.isReservedColumn(c);
|
||||
},
|
||||
|
||||
/**
|
||||
* when a table is linked to a infowindow each time a column
|
||||
* is renamed or removed the table pings to infowindow to remove
|
||||
* or rename the fields
|
||||
*/
|
||||
linkToInfowindow: function (infowindow) {
|
||||
this.bind('columnRename', function (newName, oldName) {
|
||||
if (infowindow.containsField(oldName)) {
|
||||
infowindow.removeField(oldName);
|
||||
infowindow.addField(newName);
|
||||
}
|
||||
}, infowindow);
|
||||
this.bind('columnDelete', function (oldName, newName) {
|
||||
infowindow.removeField(oldName);
|
||||
}, infowindow);
|
||||
|
||||
this.bind('change:schema', function () {
|
||||
var columns = _(this.columnNames()).filter(function (c) {
|
||||
return !_.contains(infowindow.SYSTEM_COLUMNS, c);
|
||||
});
|
||||
|
||||
function _hash (str) {
|
||||
var hash = 0;
|
||||
var c;
|
||||
var i;
|
||||
for (i = 0; i < str.length; i++) {
|
||||
c = str.charCodeAt(i);
|
||||
hash = c + (hash << 6) + (hash << 16) - hash;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
if (this.isInSQLView()) {
|
||||
if (!infowindow.has('defaul_schema_fields')) {
|
||||
infowindow.saveFields('defaul_schema_fields');
|
||||
}
|
||||
var current_schema_key = 'schema_' + _hash(this.columnNames().sort().join(''));
|
||||
var previous_schema_key = 'schema_' + _hash(
|
||||
_(this.previous('schema')).pluck(0).sort().join('')
|
||||
);
|
||||
|
||||
if (!infowindow.has(previous_schema_key)) {
|
||||
infowindow.saveFields(previous_schema_key);
|
||||
}
|
||||
if (infowindow.has(current_schema_key)) {
|
||||
infowindow.restoreFields(null, current_schema_key);
|
||||
}
|
||||
} else {
|
||||
infowindow.restoreFields(null, 'defaul_schema_fields');
|
||||
}
|
||||
|
||||
if (infowindow.get('template')) {
|
||||
// merge fields checking actual schema
|
||||
infowindow.mergeFields(columns);
|
||||
} else {
|
||||
// remove fields that no longer exist
|
||||
infowindow.removeMissingFields(columns);
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
embedURL: function () {
|
||||
return '/tables/' + this.get('name') + '/embed_map';
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated use vis.viewUrl() or vis.viewUrl(currentUser) instead.
|
||||
*/
|
||||
viewUrl: function () {
|
||||
return this._configModel.prefixUrl() + '/tables/' + this.getUnqualifiedName();
|
||||
},
|
||||
|
||||
hasTheGeom: function () {
|
||||
var currentSchema = this.get('schema');
|
||||
// if we have "the_geom" in our current schema, returnstrue
|
||||
for (var n in currentSchema) {
|
||||
if (currentSchema[n][0] === 'the_geom') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks the server to see if the table has any georeferenced row, independently of the applyed query
|
||||
* @return {promise}
|
||||
*/
|
||||
fetchGeoreferenceStatus: function () {
|
||||
var dfd = $.Deferred();
|
||||
var username = (this.options && this.options.user_data) ? this.options.user_data.username
|
||||
: (window.user_data ? window.user_data.username : window.user_name);
|
||||
var api_key = (this.options && this.options.user_data) ? this.options.user_data.api_key
|
||||
: (window.user_data ? window.user_data.api_key : window.api_key);
|
||||
|
||||
this.sqlApi = new SQL({
|
||||
user: username,
|
||||
version: 'v1',
|
||||
api_key: api_key,
|
||||
sql_api_template: this._configModel.getSqlApiBaseUrl()
|
||||
});
|
||||
|
||||
var sql = 'SELECT the_geom FROM ' + this.get('name') + ' WHERE the_geom is not null';
|
||||
this.sqlApi.execute(sql).done(function (data) {
|
||||
if (data.rows.length > 0) {
|
||||
dfd.resolve(true);
|
||||
} else {
|
||||
dfd.resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks the current loaded records to see if they are georeferenced
|
||||
* @return {boolean}
|
||||
*/
|
||||
isGeoreferenced: function () {
|
||||
var geoColumns = this.geomColumnTypes();
|
||||
if (geoColumns && geoColumns.length > 0) {
|
||||
return true;
|
||||
} else {
|
||||
if (!this.isInSQLView()) {
|
||||
// sometimes the columns are changed in the frontend site
|
||||
// and the geocolumns are not updated.
|
||||
// check the columns in local
|
||||
return this._data.any(function (row) {
|
||||
return row.hasGeometry();
|
||||
});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* this function can only be called during change event
|
||||
* returns true if the geometry type has changed
|
||||
* for this method multipolygon and polygon are the same geometry type
|
||||
*/
|
||||
geometryTypeChanged: function () {
|
||||
if (!('geometry_types' in this.changed)) return false;
|
||||
var geoTypes = this.get('geometry_types');
|
||||
var prevGeoTypes = this.previousAttributes().geometry_types;
|
||||
function normalize (e) {
|
||||
e = e.toLowerCase();
|
||||
if (e === 'st_multipolygon') {
|
||||
return 'st_polygon';
|
||||
}
|
||||
if (e === 'st_multilinestring') {
|
||||
return 'st_linestring';
|
||||
}
|
||||
if (e === 'st_multipoint') {
|
||||
return 'st_point';
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
if (!geoTypes ||
|
||||
geoTypes.length === 0 ||
|
||||
!prevGeoTypes ||
|
||||
prevGeoTypes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var n = normalize(geoTypes[0]);
|
||||
var o = normalize(prevGeoTypes[0]);
|
||||
return n !== o;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get necessary data create a duplicated dataset from this table.
|
||||
*
|
||||
* @param {Object} newName name of new dataset.
|
||||
* @param {Object} callbacks
|
||||
* @returns {Object}
|
||||
*/
|
||||
duplicate: function (newName, callbacks) {
|
||||
callbacks = callbacks || {};
|
||||
|
||||
// Extracted from duplicate_table_dialog
|
||||
var data = {
|
||||
table_name: newName
|
||||
};
|
||||
|
||||
// Set correct data object, depending on if the app has a query applied or not
|
||||
if (this.isInSQLView()) {
|
||||
var query = this.data().getSQL();
|
||||
data.sql = (!query || query == '') ? 'SELECT * FROM ' + safeTableNameQuoting(this.get('name')) : query; // eslint-disable-line eqeqeq
|
||||
} else {
|
||||
data.table_copy = this.get('name');
|
||||
}
|
||||
|
||||
var importModel = new ImportModel();
|
||||
importModel.save(data, {
|
||||
error: callbacks.error,
|
||||
success: (model, changes) => {
|
||||
var checkImportModel = new ImportModel({
|
||||
item_queue_id: changes.item_queue_id
|
||||
});
|
||||
|
||||
checkImportModel.bind('importComplete', () => {
|
||||
checkImportModel.unbind();
|
||||
|
||||
// So import is done, create new table object from the new table and fetch, callback once finished.
|
||||
var newTable = new CartoTableMetadata({
|
||||
id: checkImportModel.get('table_id')
|
||||
}, { configModel: this._configModel });
|
||||
|
||||
newTable.fetch({
|
||||
success: function () {
|
||||
callbacks.success(newTable);
|
||||
},
|
||||
error: callbacks.error
|
||||
});
|
||||
});
|
||||
|
||||
checkImportModel.bind('importError', function () {
|
||||
checkImportModel.unbind();
|
||||
callbacks.error.apply(this, arguments);
|
||||
});
|
||||
|
||||
checkImportModel.pollCheck();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the visualizations that are using this table dataset.
|
||||
* Note! a .fetch() is required to be sure the data to be available.
|
||||
* @return {Array}
|
||||
*/
|
||||
dependentVisualizations: function () {
|
||||
// dependent = visualizations with a single layer
|
||||
// non-dependant = have more than this dataset as a layer
|
||||
return _.chain(this.get('dependent_visualizations'))
|
||||
.union(this.get('non_dependent_visualizations'))
|
||||
.compact()
|
||||
.value() || [];
|
||||
},
|
||||
|
||||
getGeometryType: function () {
|
||||
var types = this.get('geometry_types');
|
||||
var geomTypes = [];
|
||||
if (!_.isArray(types)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (var t in types) {
|
||||
var type = types[t];
|
||||
// when there are rows with no geo type null is returned as geotype
|
||||
if (type) {
|
||||
var a = getSimpleGeometryType(type.toLowerCase());
|
||||
if (a) {
|
||||
geomTypes.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _.uniq(geomTypes);
|
||||
}
|
||||
|
||||
}, {
|
||||
/**
|
||||
* creates a new table from query
|
||||
* the called is responsable of calling save to create
|
||||
* the table in the server
|
||||
*/
|
||||
createFromQuery: function (name, query, configModel) {
|
||||
return new CartoTableMetadata({
|
||||
sql: query,
|
||||
name: name
|
||||
}, { configModel });
|
||||
},
|
||||
|
||||
...cartoMetadataStatic
|
||||
});
|
||||
|
||||
module.exports = CartoTableMetadata;
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="CDB-Text Dialog-header u-inner">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--neutral Dialog-headerIcon--small">
|
||||
<i class="CDB-IconFont CDB-IconFont-markup"></i>
|
||||
</div>
|
||||
<p class="Dialog-headerTitle">Query this table</p>
|
||||
<p class="Dialog-headerText">Using the <a href="https://docs.carto.com/cartodb-platform/sql-api.html" target="_blank">CARTO SQL API</a> you can perform any SQL query to either yours or other users tables.</p>
|
||||
</div>
|
||||
|
||||
<div class="Dialog-body Dialog-body--tall u-inner OptionCards">
|
||||
<code class="OptionCard OptionCard--static OptionCard--code OptionCard--codeRequest">
|
||||
<pre><%- url %>?q=<%- sql %></pre>
|
||||
</code>
|
||||
<code class="OptionCard OptionCard--static OptionCard--code OptionCard--codeResult">
|
||||
<pre>{
|
||||
rows: [<% _.each(rows, function(row, i) { %>
|
||||
{<% var j = 0 %><% _.each(row.attributes, function(val, key) { %>
|
||||
"<%- key %>": "<%- val %>"<% if (j !== _.size(row.attributes)-1) { %>,<% } %><% j++ %><% }) %>
|
||||
}<% if (i !== rows.length-1) { %>,<% } %><% }) %>
|
||||
}],
|
||||
time: 0.013,
|
||||
fields: {<% _.each(schema, function(field, i) { %>
|
||||
"<%- field[0] %>": { "type": "<%- field[1] %>" }<% if (i !== schema.length-1) { %>,<% } %><% }) %>
|
||||
},
|
||||
total_rows: 20
|
||||
}</pre>
|
||||
<div class="OptionCard-shadow"></div>
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div class="Dialog-footer Dialog-footer--simple u-inner">
|
||||
<button class="CDB-Text CDB-Button CDB-Button--secondary u-upperCase CDB-Size-medium cancel js-close">
|
||||
<span>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,272 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const randomQuote = require('builder/components/loading/random-quote');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const loadingTemplate = require('builder/components/loading/loading.tpl');
|
||||
const failTemplate = require('dashboard/components/fail.tpl');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'modalModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* shows a dialog to get the table exported
|
||||
* new ExportDialog({
|
||||
* table: table_model
|
||||
* })
|
||||
*
|
||||
* (Migrated almost-as-is from old editor to not break functionality)
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
_CSV_FILTER: 'SELECT * FROM (%%sql%%) as subq ',
|
||||
_MAX_SQL_GET_LENGTH: 1000,
|
||||
events: {
|
||||
'click .js-option:not(.is-disabled)': '_export',
|
||||
'click .js-cancel': '_closeDialog'
|
||||
},
|
||||
|
||||
/**
|
||||
* Allowed formats on the exporter
|
||||
* @type {Array}
|
||||
*/
|
||||
formats: [
|
||||
{ format: 'csv', fetcher: 'fetchCSV', geomRequired: false, illustrationIconModifier: 'IllustrationIcon--neutral' },
|
||||
{ format: 'shp', fetcher: 'fetch', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--magenta' },
|
||||
{ format: 'kml', fetcher: 'fetch', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--sunrise' },
|
||||
{ format: 'geojson', label: 'geo json', fetcher: 'fetch', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--cyan' },
|
||||
{ format: 'svg', fetcher: 'fetchSVG', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--royalDark' },
|
||||
{ format: 'gpkg', fetcher: 'fetch', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--smaltBlue' }
|
||||
],
|
||||
|
||||
initialize: function () {
|
||||
checkAndBuildOpts(this.options, REQUIRED_OPTS, this);
|
||||
_.extend(this.options, {
|
||||
clean_on_hide: true,
|
||||
table_id: this.model.id
|
||||
});
|
||||
_.bindAll(this, '_export');
|
||||
this.baseUrl = this._configModel.getSqlApiUrl();
|
||||
this.model.bind('change:geometry_types', this.refresh, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* search a format based on its name in the format array
|
||||
* @param {string} format Format name
|
||||
* @return {Object}
|
||||
*/
|
||||
getFormat: function (format) {
|
||||
for (var n in this.formats) {
|
||||
if (this.formats[n].format === format) {
|
||||
return this.formats[n];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Answer to button event and lauchn the export method associated to that format
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_export: function (ev) {
|
||||
this.killEvent(ev);
|
||||
var $button = $(ev.currentTarget);
|
||||
var formatName = $button.data('format');
|
||||
var format = this.getFormat(formatName);
|
||||
this[format.fetcher](formatName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a dictionary with the options shared between all the methods
|
||||
* @return {Object}
|
||||
*/
|
||||
getBaseOptions: function () {
|
||||
var options = {};
|
||||
options.filename = this.model.get('name');
|
||||
|
||||
if (this.options.user_data) {
|
||||
options.api_key = this.options.user_data.api_key;
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the base sql to retrieve the data
|
||||
* @return {string}
|
||||
*/
|
||||
getPlainSql: function () {
|
||||
throw new Error('Method not migrated, only used in subclass');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a specific sql filtered by the_geom, used on CSV exports
|
||||
* @return {string}
|
||||
*/
|
||||
getGeomFilteredSql: function () {
|
||||
var sql = this.getPlainSql();
|
||||
// if we have "the_geom" in our current schema, we apply a custom sql
|
||||
if (this.model.isGeoreferenced()) {
|
||||
return this._CSV_FILTER.replace(/%%sql%%/g, sql);
|
||||
}
|
||||
// Otherwise, we apply regular sql
|
||||
return sql;
|
||||
},
|
||||
|
||||
/**
|
||||
* Populates the hidden form with the format related values and submits them to get the file
|
||||
* @param {Object} options Base options
|
||||
* @param {String} sql Sql of the document to be retrieved
|
||||
*/
|
||||
_fetch: function (options, sql) {
|
||||
this._showElAndHideRest('.js-preparing-download');
|
||||
this.$('.format').val(options.format);
|
||||
this.$('.q').val(sql);
|
||||
this.$('.filename').val(options.filename);
|
||||
this.$('.api_key').val(options.api_key);
|
||||
|
||||
if (options.format === 'csv') {
|
||||
this.$('.skipfields').val('the_geom_webmercator');
|
||||
} else {
|
||||
this.$('.skipfields').val('the_geom,the_geom_webmercator');
|
||||
}
|
||||
|
||||
// check if the sql is big or not, and send the request as a verb or other. This is a HACK.
|
||||
if (sql.length < this._MAX_SQL_GET_LENGTH) {
|
||||
var location = this.$('form').attr('action') + '?' + this.$('form').serialize();
|
||||
this._fetchGET(location);
|
||||
} else {
|
||||
// I can't find a way of making the iframe trigger load event when its get a form posted,
|
||||
// so we need to leave like it was until
|
||||
this.submit();
|
||||
}
|
||||
|
||||
this.$('.db').attr('disabled', 'disabled');
|
||||
this.$('.skipfields').attr('disabled', 'disabled');
|
||||
|
||||
if (this.options.autoClose) {
|
||||
this.close();
|
||||
this.trigger('generating', this.$('.js-preparing-download').html());
|
||||
}
|
||||
},
|
||||
|
||||
showError: function (error) {
|
||||
this.$('.js-error').html(
|
||||
failTemplate({
|
||||
msg: error
|
||||
})
|
||||
);
|
||||
this._showElAndHideRest('.js-error');
|
||||
},
|
||||
|
||||
_fetchGET: function (url) {
|
||||
function getError (content) {
|
||||
// sql api returns a json when it fails
|
||||
// but if the browser is running some plugin that
|
||||
// formats it, the window content is the html
|
||||
// so search for the word "error"
|
||||
var error = null;
|
||||
try {
|
||||
var json = JSON.parse(content);
|
||||
error = json.error[0];
|
||||
} catch (e) {
|
||||
if (content && content.indexOf('error') !== -1) {
|
||||
error = 'an error occurred';
|
||||
}
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
var checkInterval;
|
||||
|
||||
var w = window.open(url);
|
||||
w.onload = () => {
|
||||
clearInterval(checkInterval);
|
||||
var error = getError(w.document.body.textContent);
|
||||
if (error) {
|
||||
this.showError(error);
|
||||
} else {
|
||||
this._modalModel.destroy();
|
||||
}
|
||||
w.close();
|
||||
};
|
||||
window.focus();
|
||||
checkInterval = setInterval(() => {
|
||||
// safari needs to check the body because it never
|
||||
// calls onload
|
||||
if (w.closed || (w.document && w.document.body.textContent.length === 0)) {
|
||||
this._modalModel.destroy();
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
|
||||
/**
|
||||
* Submits the form. This method is separated to ease the testing
|
||||
*/
|
||||
submit: function () {
|
||||
this.$('form').submit();
|
||||
},
|
||||
|
||||
/**
|
||||
* Base fetch, for the formats that don't require special threatment
|
||||
* @param {String} formatName
|
||||
*/
|
||||
fetch: function (formatName) {
|
||||
var options = this.getBaseOptions();
|
||||
options.format = formatName;
|
||||
var sql = this.getPlainSql();
|
||||
this._fetch(options, sql);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the options needed for csv format and fetch the document
|
||||
* @param {String} formatName
|
||||
*/
|
||||
fetchCSV: function () {
|
||||
var options = this.getBaseOptions();
|
||||
options.format = 'csv';
|
||||
var sql = this.getGeomFilteredSql();
|
||||
this.$('.skipfields').removeAttr('disabled');
|
||||
this._fetch(options, sql);
|
||||
},
|
||||
/**
|
||||
* Gets the options needed for svg format and fetch the document
|
||||
* @param {String} formatName
|
||||
*/
|
||||
fetchSVG: function () {
|
||||
this.$('.db').removeAttr('disabled');
|
||||
this.fetch('svg');
|
||||
},
|
||||
/**
|
||||
* Formerly render_content
|
||||
* Returns the html populated with current data
|
||||
* @return {String}
|
||||
*/
|
||||
render: function () {
|
||||
throw new Error('Method not migrated, only used in subclass');
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
this.$('.content').html(this.render_content());
|
||||
},
|
||||
|
||||
_renderLoadingContent: function (title) {
|
||||
return loadingTemplate({
|
||||
title: title,
|
||||
descHTML: randomQuote()
|
||||
});
|
||||
},
|
||||
|
||||
_showElAndHideRest: function (classNameToShow) {
|
||||
[
|
||||
'.js-start',
|
||||
'.js-preparing-download',
|
||||
'.js-error'
|
||||
].forEach(function (className) {
|
||||
this.$(className)[ className === classNameToShow ? 'show' : 'hide' ]();
|
||||
}, this);
|
||||
}
|
||||
|
||||
});
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<div class="js-start">
|
||||
<div class="CDB-Text Dialog-header u-inner">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
|
||||
<i class="CDB-IconFont CDB-IconFont-cloudDownArrow"></i>
|
||||
</div>
|
||||
<p class="Dialog-headerTitle">Export dataset</p>
|
||||
<p class="Dialog-headerText">Select the preferred file format.</p>
|
||||
<% if (!isGeoreferenced) { %>
|
||||
<p class="Dialog-headerText">To download any geospatial format like SHP, KML or GeoJSON don't forget to select the_geom on your query.</p>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="CDB-Text Dialog-body">
|
||||
<% if (hasBounds) { %>
|
||||
<div class="OptionCheck">
|
||||
<div class="Checkbox js-bounds">
|
||||
<button class="Checkbox-input is-checked"></button>
|
||||
<label class="Checkbox-label"><strong class="Checkbox-strong">Match rows with the map view.</strong> This option reduces file size.</label>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="OptionCards">
|
||||
<% _.each( formats, function( format ){ %>
|
||||
<div data-format="<%- format.format %>"
|
||||
class="js-option OptionCard OptionCard--onlyIcons <%
|
||||
if (isGeoreferenced === false && format.geomRequired === true) { %> is-disabled <% }
|
||||
%>">
|
||||
<div class="IllustrationIcon <%- format.illustrationIconModifier %>">
|
||||
<div class="IllustrationIcon-text"><%- format.label || format.format %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Dialog-footer Dialog-footer--simple u-inner">
|
||||
<button class="CDB-Text CDB-Button CDB-Button--secondary CDB-Size-medium u-upperCase cancel js-close">
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="CDB-Text js-preparing-download" style="display: none;">
|
||||
<%- preparingDownloadContent %>
|
||||
</div>
|
||||
|
||||
<div class="CDB-Text js-error" style="display: none;"></div>
|
||||
|
||||
<form class="hack" method="POST" action="<%- url %>">
|
||||
<input type="hidden" class="filename" name="filename" />
|
||||
<input type="hidden" class="q" name="q" />
|
||||
<input type="hidden" class="format" name="format" />
|
||||
<input type="hidden" class="bounds" name="bounds" />
|
||||
<input type="hidden" class="api api_key" name="api_key" />
|
||||
<input type="hidden" class="skipfields" name="skipfields" disabled="disabled" value="" />
|
||||
<input type="hidden" class="dp" name="dp" value="4" disabled="disabled" />
|
||||
</form>
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const ExportView = require('./export-view');
|
||||
const templatePublicExport = require('./public-export-template.tpl');
|
||||
|
||||
/**
|
||||
* Shows a dialog to get the public table exported
|
||||
*
|
||||
* new PublicExportView({
|
||||
* table: table_model
|
||||
* })
|
||||
*
|
||||
*/
|
||||
module.exports = ExportView.extend({
|
||||
|
||||
// Events have to be extended from export view parent
|
||||
events: {
|
||||
'click .js-option:not(.is-disabled)': '_export',
|
||||
'click .js-bounds': '_changeBounds',
|
||||
'click .cancel': '_cancel',
|
||||
'click .close': '_cancel'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
ExportView.prototype.initialize.call(this);
|
||||
this.model.set('bounds', this.options.bounds);
|
||||
this.model.bind('change:bounds', this._setBoundsCheckbox, this);
|
||||
},
|
||||
|
||||
_changeBounds: function () {
|
||||
this.model.set('bounds', !this.model.get('bounds'));
|
||||
},
|
||||
|
||||
_setBoundsCheckbox: function () {
|
||||
this.$('.js-bounds .Checkbox-input').toggleClass('is-checked', !!this.model.get('bounds'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the bounds option to download the intersect or all the geometries
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_toggleBounds: function (ev) {
|
||||
this.killEvent(ev);
|
||||
var $button = $(ev.currentTarget);
|
||||
var formatName = $button.data('format');
|
||||
var format = this.getFormat(formatName);
|
||||
this[format.fetcher](formatName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a dictionary with the options shared between all the methods
|
||||
* @return {Object}
|
||||
*/
|
||||
getBaseOptions: function () {
|
||||
var options = {};
|
||||
options.filename = this.model.get('name');
|
||||
|
||||
// Keep dataset part in user.dataset names
|
||||
if (options.filename.indexOf('.') !== -1) {
|
||||
options.filename = options.filename.split('.')[1];
|
||||
}
|
||||
|
||||
if (this.options.user_data) {
|
||||
options.api_key = this.options.user_data.api_key;
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the base sql to retrieve the data
|
||||
* @return {string}
|
||||
*/
|
||||
getPlainSql: function () {
|
||||
let sql;
|
||||
if (this.options.sql) {
|
||||
sql = this.options.sql;
|
||||
} else {
|
||||
if (this.model.sqlView && this.model.get('bounds')) {
|
||||
sql = this.model.sqlView.getSQL();
|
||||
} else {
|
||||
sql = 'select * from ' + this.model.get('name');
|
||||
}
|
||||
}
|
||||
return sql;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the html populated with current data
|
||||
* @return {String}
|
||||
*/
|
||||
render: function () {
|
||||
var isGeoreferenced = this.model.isGeoreferenced();
|
||||
var hasBounds = this.model.get('bounds');
|
||||
|
||||
if (_.isBoolean(isGeoreferenced)) {
|
||||
this.$el.html(templatePublicExport({
|
||||
preparingDownloadContent: this._renderLoadingContent('We are preparing your download. Depending on the size, it could take some time.'),
|
||||
formats: this.formats,
|
||||
url: this.baseUrl,
|
||||
isGeoreferenced: isGeoreferenced,
|
||||
hasBounds: hasBounds
|
||||
}));
|
||||
} else {
|
||||
this.$el.html(this._renderLoadingContent('Checking georeferences…'));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
const Backbone = require('backbone');
|
||||
const $ = require('jquery');
|
||||
const carto = require('@carto/carto.js');
|
||||
const L = require('leaflet');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const template = require('./map-view.tpl');
|
||||
const { simple: styles } = require('builder/data/default-cartography.json');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'username',
|
||||
'serverUrl',
|
||||
'sqlUrl',
|
||||
'vizjson',
|
||||
'dataset'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'click .js-bounds': '_changeBounds'
|
||||
},
|
||||
|
||||
className: 'map',
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
this.model = new Backbone.Model();
|
||||
|
||||
this.model.on('change:bounds', this._boundsClick, this);
|
||||
},
|
||||
|
||||
_changeBounds: function () {
|
||||
this.model.set('bounds', !this.model.get('bounds'));
|
||||
},
|
||||
|
||||
_boundsClick: function () {
|
||||
this.trigger('boundsChanged', { bounds: this.model.get('bounds') });
|
||||
this.$('.js-bounds .Checkbox-input').toggleClass('is-checked', !!this.model.get('bounds'));
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(template());
|
||||
const mapEl = this.$el.find('.carto-map')[0];
|
||||
this.map = L.map(mapEl).setView([0, 0], 0);
|
||||
|
||||
this.map.on('zoomend moveend', () => {
|
||||
this.trigger('mapBoundsChanged', {
|
||||
bounds: this.map.getBounds(),
|
||||
center: this.map.getCenter(),
|
||||
zoom: this.map.getZoom()
|
||||
});
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}.png', {
|
||||
maxZoom: 18
|
||||
}).addTo(this.map);
|
||||
|
||||
this.client = new carto.Client({
|
||||
apiKey: 'default_public',
|
||||
serverUrl: this._serverUrl,
|
||||
username: this._username
|
||||
});
|
||||
|
||||
this.dataset = new carto.source.Dataset(this._dataset);
|
||||
const cartocss = new carto.style.CartoCSS('#layer {}');
|
||||
|
||||
this.cartoLayer = new carto.layer.Layer(this.dataset, cartocss);
|
||||
|
||||
this.client.addLayer(this.cartoLayer);
|
||||
|
||||
this.client.getLeafletLayer().addTo(this.map);
|
||||
|
||||
this._fetchBounds();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
enableMap: function () {
|
||||
this.map.invalidateSize();
|
||||
},
|
||||
|
||||
setGeometry: function ([geom]) {
|
||||
let style;
|
||||
|
||||
switch (geom) {
|
||||
case 'ST_Linestring':
|
||||
style = `#layer {
|
||||
line-color: ${styles.line.stroke.color.fixed};
|
||||
line-width: ${styles.line.stroke.size.fixed};
|
||||
line-opacity: ${styles.line.stroke.color.opacity};
|
||||
}
|
||||
`;
|
||||
break;
|
||||
case 'ST_Point':
|
||||
style = `#layer {
|
||||
marker-width: ${styles.point.fill.size.fixed};
|
||||
marker-fill: ${styles.point.fill.color.fixed};
|
||||
marker-fill-opacity: ${styles.point.fill.color.opacity};
|
||||
marker-line-color: ${styles.point.stroke.color.fixed};
|
||||
marker-line-width: ${styles.point.stroke.size.fixed};
|
||||
marker-line-opacity: ${styles.point.stroke.color.opacity};
|
||||
marker-placement: point;
|
||||
marker-type: ellipse;
|
||||
marker-allow-overlap: true;
|
||||
}`;
|
||||
break;
|
||||
case 'ST_Polygon':
|
||||
style = `#layer {
|
||||
polygon-fill: ${styles.polygon.fill.color.fixed};
|
||||
polygon-opacity: ${styles.polygon.fill.color.opacity};
|
||||
::outline {
|
||||
line-color: ${styles.polygon.stroke.color.fixed};
|
||||
line-width: ${styles.polygon.stroke.size.fixed};
|
||||
line-opacity: ${styles.polygon.stroke.color.opacity};
|
||||
}
|
||||
}`;
|
||||
break;
|
||||
default:
|
||||
style = `#layer {}`;
|
||||
break;
|
||||
}
|
||||
|
||||
this.cartoLayer.setStyle(new carto.style.CartoCSS(style));
|
||||
},
|
||||
|
||||
_fetchBounds: function () {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
data: 'q=' + encodeURIComponent(`SELECT ST_Extent(the_geom) as extent FROM ${this._dataset}`) + '&api_key=default_public',
|
||||
url: this._sqlUrl,
|
||||
success: (data) => {
|
||||
const bounds = data.rows[0].extent;
|
||||
const parsedBounds = /BOX\((.+) (.+),(.+) (.+)\)/.exec(bounds)
|
||||
.splice(1, 4)
|
||||
.map(e => parseFloat(e));
|
||||
this.map.fitBounds([
|
||||
[parsedBounds[1], parsedBounds[0]], [parsedBounds[3], parsedBounds[2]]
|
||||
]);
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
},
|
||||
|
||||
invalidateMap: function () {
|
||||
this.map.invalidateSize();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="Checkbox js-bounds">
|
||||
<button class="Checkbox-input"></button>
|
||||
<label class="Checkbox-label">Match rows with the map view.</label>
|
||||
</div>
|
||||
<div class="cartodb-map public">
|
||||
<div class="carto-map" style="height: 100%"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
const CartoTableMetadata = require('dashboard/views/public-dataset/carto-table-metadata');
|
||||
const CartoTableData = require('dashboard/data/table/carto-table-data');
|
||||
|
||||
module.exports = CartoTableMetadata.extend({
|
||||
|
||||
fetch: function () {
|
||||
this.trigger('sync');
|
||||
// nothing to fetch here
|
||||
},
|
||||
|
||||
data: function () {
|
||||
if (this._data === undefined) {
|
||||
this._data = new CartoTableData(null, {
|
||||
table: this,
|
||||
configModel: this._configModel
|
||||
});
|
||||
this._data.fetch = function () { };
|
||||
}
|
||||
|
||||
if (this.sqlView) {
|
||||
return this.sqlView;
|
||||
}
|
||||
return this._data;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
const _ = require('underscore');
|
||||
const DropdownAdminView = require('dashboard/components/dropdown/dropdown-admin-view');
|
||||
|
||||
const PublicDropdown = DropdownAdminView.extend({
|
||||
_TEMPLATE: `
|
||||
<ul>
|
||||
<li><a class="small" href="<%- urls[0] %>">View your datasets</a></li>
|
||||
<li><a class="small" href="<%- urls[0] %>/visualizations">View your maps</a></li>
|
||||
<li><a class="small" href="<%- urls[0].replace("dashboard", "logout") %>">Close session</a></li>
|
||||
</ul>
|
||||
`,
|
||||
|
||||
render: function () {
|
||||
this.$el
|
||||
.html(_.template(this._TEMPLATE)(this.model.attributes))
|
||||
.css({
|
||||
width: this.options.width
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = PublicDropdown;
|
||||
@@ -0,0 +1,53 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const PublicDropdown = require('dashboard/views/public-dataset/public-dropdown');
|
||||
const template = require('./public-header.tpl');
|
||||
|
||||
/**
|
||||
* Public header, dance starts!
|
||||
*
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function () {
|
||||
this.vis = this.options.vis;
|
||||
this.template = template;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.html(
|
||||
this.template(
|
||||
_.defaults({
|
||||
vis_url: this.vis && this.vis.viewUrl(this.model) || '',
|
||||
isMobileDevice: this.options.isMobileDevice,
|
||||
owner_username: this.options.owner_username,
|
||||
current_view: this.options.current_view,
|
||||
isCartoDBHosted: this.options.isCartoDBHosted
|
||||
}, this.model.attributes)
|
||||
)
|
||||
);
|
||||
this._initViews();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change', this.render, this);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
if (this.$('.account').length > 0) {
|
||||
var dropdown = new PublicDropdown({
|
||||
target: this.$('a.account'),
|
||||
model: this.model,
|
||||
vertical_offset: 20,
|
||||
width: 166
|
||||
});
|
||||
|
||||
this.addView(dropdown);
|
||||
$('body').append(dropdown.render().el);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class='inner'>
|
||||
<h1><a href='https://carto.com' class='logo' id='the_logo'>CARTO</a></h1>
|
||||
<ul class='options'>
|
||||
<% if (!username && !isMobileDevice) { %>
|
||||
<li><a href='https://carto.com/learn/guides' class='guides'>Guides</a></li>
|
||||
<% } %>
|
||||
|
||||
<% if (!username) { %>
|
||||
<% if ( !isCartoDBHosted ) { %>
|
||||
<li><a class='signup' href='https://carto.com/signup'>Sign up</a><li>
|
||||
<% } %>
|
||||
<li><a href='https://carto.com/login' class='border login'>Login</a></li>
|
||||
<% } else { %>
|
||||
<li>
|
||||
<a class='editor dropdown account' href='<%- urls[0] %>'>
|
||||
<% if (avatar_url) { %><img src='<%- avatar_url %>' width='18' title='<%- username %>' alt='<%- username %>' /><% } %><%- username %><span class='separator'></span>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<div class="emptyTableContainer">
|
||||
<div class="emptyPublicTableInfo emptynessInfo">
|
||||
<h4>This dataset is empty</h4>
|
||||
<p>If you are the owner of this dataset remember to login and add your desired rows.</p>
|
||||
<p>If not, you can’t do anything interesting here.</p>
|
||||
</div>
|
||||
</div>
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./public-table-header-view.tpl');
|
||||
|
||||
// On the original code, this extended a more complex base class, but the public table needs no functionality
|
||||
const HeaderView = CoreView.extend({
|
||||
|
||||
events: {
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.column = this.options.column;
|
||||
this.table = this.options.table;
|
||||
this.editing_name = false;
|
||||
this.changing_type = false;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html('');
|
||||
this.$el.append(template({
|
||||
col_name: this.column[0],
|
||||
col_type: this.column[1],
|
||||
editing_name: this.editing_name
|
||||
}));
|
||||
|
||||
// Focus in the input if it is being edited
|
||||
if (this.editing_name) {
|
||||
this.$el.find('input').focus();
|
||||
}
|
||||
|
||||
this.delegateEvents();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = HeaderView;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
const RowView = require('dashboard/views/public-dataset/table-view/row-view');
|
||||
|
||||
/**
|
||||
* view used to render each row in public tables
|
||||
*/
|
||||
module.exports = RowView.extend({
|
||||
|
||||
classLabel: 'cdb.open.PublicRowView',
|
||||
|
||||
events: {},
|
||||
|
||||
initialize: function () {
|
||||
this.options.row_header = false;
|
||||
this.order = this.options.order;
|
||||
},
|
||||
|
||||
_renderGeometry: function (value) {
|
||||
return this._renderDefault('GeoJSON');
|
||||
},
|
||||
|
||||
_getRowOptions: function () {},
|
||||
|
||||
click_header: function (e) {}
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<div>
|
||||
<label class="strong small interactuable">
|
||||
<div class="coloptions" href="#<%- col_name %>">
|
||||
<%- col_name %>
|
||||
<% if (col_type == 'geometry') { %>
|
||||
<span class="tiny geo">GEO</span>
|
||||
<% } %>
|
||||
</div>
|
||||
</label>
|
||||
<p class="small">
|
||||
<a class="disabled"><%- col_type %></a>
|
||||
</p>
|
||||
</div>
|
||||
<p class="auto"><%- col_name %>_<% if (col_type == 'geometry') { %>GEO<% } %></p>
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const TableView = require('dashboard/views/public-dataset/table-view/table-view');
|
||||
const PublicRowView = require('dashboard/views/public-dataset/public-table-view/public-row-view');
|
||||
const PublicHeaderView = require('dashboard/views/public-dataset/public-table-view/public-header-view');
|
||||
const templateEmptyDataset = require('./empty-table-public.tpl');
|
||||
|
||||
module.exports = TableView.extend({
|
||||
|
||||
events: {},
|
||||
|
||||
rowView: PublicRowView,
|
||||
|
||||
initialize: function (opts) {
|
||||
TableView.prototype.initialize.call(this, opts);
|
||||
this.options.row_header = true;
|
||||
this._editorsOpened = null;
|
||||
|
||||
this.initializeBindings();
|
||||
this.initPaginationAndScroll();
|
||||
},
|
||||
|
||||
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.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);
|
||||
},
|
||||
|
||||
headerView: function (column) {
|
||||
if (column[1] !== 'header') {
|
||||
var v = new PublicHeaderView({
|
||||
column: column,
|
||||
table: this.model,
|
||||
sqlView: this.options.sqlView
|
||||
});
|
||||
|
||||
this.addView(v);
|
||||
return v.render().el;
|
||||
} else {
|
||||
return '<div><div></div></div>';
|
||||
}
|
||||
},
|
||||
|
||||
_onSQLView: function () {},
|
||||
|
||||
_checkEmptyTable: function () {
|
||||
if (this.isEmptyTable()) {
|
||||
this.addEmptyTableInfo();
|
||||
} else {
|
||||
this.cleanEmptyTableInfo();
|
||||
this.$('footer').remove();
|
||||
}
|
||||
},
|
||||
|
||||
_swicthEnabled: function () {
|
||||
// this check is not needed in public table
|
||||
},
|
||||
|
||||
initPaginationAndScroll: function () {
|
||||
// disable scroll pagination, it's firing a function each 300 ms and it's not required on public table
|
||||
},
|
||||
|
||||
addEmptyTableInfo: function () {
|
||||
var content = templateEmptyDataset(this.import_);
|
||||
|
||||
var $footer = $('<tfoot><tr><td colspan="100">' + content + '</td></tr></tfoot>');
|
||||
this.$('footer').remove();
|
||||
this.$el.append($footer);
|
||||
},
|
||||
|
||||
_scrollMagic: function () { }
|
||||
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
/* global DISQUS */
|
||||
const Backbone = require('backbone');
|
||||
const _ = require('underscore');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const $ = require('jquery');
|
||||
const TablePublicView = require('dashboard/views/public-dataset/table-public-view');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Public table window 'view'
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this.$body = $(this.el.document.body);
|
||||
this.$map = this.$body.find('#map');
|
||||
this._initBinds();
|
||||
this._initViews();
|
||||
setTimeout(this._onStart, 250);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
// Table view
|
||||
var table_options = _.defaults({ el: this.el.document.body, model: new Backbone.Model({ bounds: false, map: null }) }, this.options);
|
||||
this.table = new TablePublicView(table_options);
|
||||
|
||||
const activeTab = (window.location.hash || '').split('/')[1];
|
||||
this._setActiveTab(activeTab);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
_.bindAll(this, '_onWindowResize', '_onOrientationChange', '_onStart');
|
||||
|
||||
this.$el.on('resize', this._onWindowResize);
|
||||
|
||||
if (!this.el.addEventListener) {
|
||||
this.el.attachEvent('orientationchange', this._onOrientationChange, this);
|
||||
} else {
|
||||
this.el.addEventListener('orientationchange', this._onOrientationChange);
|
||||
}
|
||||
|
||||
this.$body.find('.navigation a[data-pane]').click(this._onNavigationClick.bind(this));
|
||||
},
|
||||
|
||||
// On start view!
|
||||
_onStart: function () {
|
||||
this._setupMapDimensions();
|
||||
},
|
||||
|
||||
_onWindowResize: function () {
|
||||
// Resize window
|
||||
this._setupMapDimensions();
|
||||
},
|
||||
|
||||
_onOrientationChange: function () {
|
||||
// Reset disqus
|
||||
DISQUS && DISQUS.reset({ reload: true });
|
||||
// Resize window orientation
|
||||
this._setupMapDimensions(true);
|
||||
},
|
||||
|
||||
_onNavigationClick: function (event) {
|
||||
const clickedElement = event.currentTarget;
|
||||
const paneToShow = clickedElement.getAttribute('data-pane');
|
||||
|
||||
this._setActiveTab(paneToShow);
|
||||
|
||||
if (this.table) this.table.invalidateMap();
|
||||
},
|
||||
|
||||
_setActiveTab: function (tab) {
|
||||
if (tab === 'table') {
|
||||
this.table.showTable();
|
||||
} else if (tab === 'map') {
|
||||
this.table.showMap();
|
||||
}
|
||||
},
|
||||
|
||||
// When window is resized, let's touch some things ;)
|
||||
_setupMapDimensions: function (animated) {
|
||||
var windowHeight = this.$el.height();
|
||||
var mainInfoHeight = this.$body.find('.js-Navmenu').height();
|
||||
var headerHeight = this.$body.find('.Header').height();
|
||||
var landscapeMode = this.el.matchMedia && this.el.matchMedia('(orientation: landscape)').matches;
|
||||
var h, height;
|
||||
|
||||
if (this.options.isMobileDevice) {
|
||||
if (landscapeMode) {
|
||||
h = headerHeight + 7;
|
||||
} else {
|
||||
if (windowHeight > 670) {
|
||||
h = 220;
|
||||
} else { // iPhone, etc.
|
||||
h = 138;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
h = 260;
|
||||
}
|
||||
|
||||
height = windowHeight - h;
|
||||
|
||||
if (animated) {
|
||||
this.$map.animate({ height: height }, { easing: 'easeInQuad', duration: 150 });
|
||||
} else {
|
||||
if (this.options.isMobileDevice) {
|
||||
this.$map.css({ height: height, opacity: 1 });
|
||||
} else {
|
||||
// On non mobile devices
|
||||
this.$map.css({ height: windowHeight - (mainInfoHeight + headerHeight), opacity: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
// If landscape, let's scroll to show the map, and
|
||||
// leave the header hidden
|
||||
if (this.options.isMobileDevice && landscapeMode && $(window).scrollTop() < headerHeight) {
|
||||
this.$body.animate({ scrollTop: headerHeight }, 600);
|
||||
}
|
||||
|
||||
if (this.table) this.table.invalidateMap();
|
||||
|
||||
this._showNavigationBar();
|
||||
},
|
||||
|
||||
// Show navigation (table or map view) block
|
||||
_showNavigationBar: function () {
|
||||
this.$body.find('.navigation')
|
||||
.animate({ opacity: 1 }, 250);
|
||||
|
||||
if (this.table) this.table.invalidateMap();
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
const $ = require('jquery');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const UserModel = require('dashboard/data/user-model');
|
||||
const AuthenticatedUserModel = require('dashboard/data/authenticated-user-model');
|
||||
const SQLViewData = require('dashboard/data/table/sqlviewdata-model');
|
||||
const PublicCartoTableMetadata = require('./public-carto-table-metadata');
|
||||
const PublicHeader = require('./public-header/public-header');
|
||||
const PublicTableTab = require('dashboard/views/public-dataset/table-tab/public-table-tab');
|
||||
const TabPane = require('dashboard/components/tabpane/tabpane');
|
||||
const MapView = require('dashboard/views/public-dataset/map-view/map-view');
|
||||
const Tabs = require('dashboard/components/tabs/tabs');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
|
||||
const ViewFactory = require('builder/components/view-factory');
|
||||
const templateApiDialog = require('dashboard/views/public-dataset/dialogs/api-call.tpl');
|
||||
const PublicExportView = require('dashboard/views/public-dataset/dialogs/export/public-export-view');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Table public view
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click .js-Navmenu-link--download': '_exportTable',
|
||||
'click .js-Navmenu-link--api': '_apiCallTable'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this._initModels();
|
||||
this._initViews();
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initModels: function () {
|
||||
this._modals = new ModalsServiceModel();
|
||||
// Table
|
||||
this.table = new PublicCartoTableMetadata({
|
||||
id: this.options.table_name,
|
||||
name: this.options.table_name,
|
||||
description: this.options.vizjson.description || ''
|
||||
}, { configModel: this._configModel });
|
||||
|
||||
this.table.set({
|
||||
user_name: this.options.user_name,
|
||||
vizjson: this.options.vizjson,
|
||||
schema: this.options.schema
|
||||
});
|
||||
|
||||
this.columns = this.table.data();
|
||||
this.sqlView = new SQLViewData(null, {
|
||||
configModel: this._configModel
|
||||
});
|
||||
this.sqlView.syncMethod = 'read';
|
||||
|
||||
var query = this.query = this.table.data().getSQL();
|
||||
this.table.useSQLView(this.sqlView);
|
||||
this.sqlView.options.set('rows_per_page', 20, { silent: true });
|
||||
this._fetchData(query);
|
||||
|
||||
// User
|
||||
this.user = new UserModel({ username: this.options.user_name });
|
||||
|
||||
// Authenticated user
|
||||
this.authenticated_user = new AuthenticatedUserModel();
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
// Public header
|
||||
if (this.$('.cartodb-public-header').length > 0) {
|
||||
var header = new PublicHeader({
|
||||
el: this.$('.cartodb-public-header'),
|
||||
model: this.authenticated_user,
|
||||
vis: this.table,
|
||||
current_view: this._getCurrentView(),
|
||||
owner_username: this.options.owner_username,
|
||||
isMobileDevice: this.options.isMobileDevice,
|
||||
isCartoDBHosted: this._configModel.get('cartodb_com_hosted')
|
||||
});
|
||||
this.addView(header);
|
||||
|
||||
// Fetch authenticated user model
|
||||
this.authenticated_user.fetch();
|
||||
}
|
||||
|
||||
// Navigation - This is what switches the view on mobile
|
||||
// TODO: this is insanely complex for just two buttons
|
||||
// this.header = new cdb.open.PublicHeader({
|
||||
// el: this.$('.navigation'),
|
||||
// model: this.table,
|
||||
// user: this.user,
|
||||
// belong_organization: belong_organization,
|
||||
// config: this.options.config
|
||||
// });
|
||||
// this.addView(this.header);
|
||||
|
||||
// Tabpanes
|
||||
this.workViewTable = new TabPane({
|
||||
el: this.$('.pane_table')
|
||||
});
|
||||
this.addView(this.workViewTable);
|
||||
|
||||
this.workViewMap = new TabPane({
|
||||
el: this.$('.pane_map')
|
||||
});
|
||||
this.addView(this.workViewMap);
|
||||
|
||||
// Public app tabs
|
||||
this.tabs = new Tabs({
|
||||
el: this.$('.navigation ul'),
|
||||
slash: true
|
||||
});
|
||||
|
||||
this.addView(this.tabs);
|
||||
|
||||
// Help tooltip - I can't find any span with help class, I think this is unnecessary
|
||||
// var tooltip = new cdb.common.TipsyTooltip({
|
||||
// el: this.$('span.help'),
|
||||
// gravity: $.fn.tipsy.autoBounds(250, 's')
|
||||
// });
|
||||
// this.addView(tooltip);
|
||||
|
||||
// Table tab
|
||||
this.tableTab = new PublicTableTab({
|
||||
configModel: this._configModel,
|
||||
model: this.table,
|
||||
vizjson: this.options.vizjson,
|
||||
user_name: this.options.user_name
|
||||
});
|
||||
|
||||
// Map tab
|
||||
this.mapTab = new MapView({
|
||||
username: this.options.user_name,
|
||||
serverUrl: this._configModel.get('maps_api_template').replace('{user}', this.options.user_name),
|
||||
sqlUrl: this._configModel.getSqlApiUrl(),
|
||||
dataset: this.table.get('name'),
|
||||
vizjson: this.table.get('vizjson'),
|
||||
geometry: this.table.get('geometry_types')
|
||||
});
|
||||
this.listenTo(this.mapTab, 'mapBoundsChanged', function (options) {
|
||||
const {
|
||||
bounds,
|
||||
center,
|
||||
zoom
|
||||
} = options;
|
||||
this.model.set('map', {
|
||||
bounds: [
|
||||
bounds.getNorthEast().lng,
|
||||
bounds.getNorthEast().lat,
|
||||
bounds.getSouthWest().lng,
|
||||
bounds.getSouthWest().lat
|
||||
],
|
||||
center,
|
||||
zoom
|
||||
});
|
||||
});
|
||||
this.listenTo(this.mapTab, 'boundsChanged', function (options) {
|
||||
this.model.set('bounds', options.bounds);
|
||||
});
|
||||
|
||||
this.workViewTable.addTab('table', this.tableTab.render());
|
||||
this.workViewMap.addTab('map', this.mapTab.render());
|
||||
|
||||
this.workViewTable.active('table');
|
||||
this.workViewMap.active('map');
|
||||
this.mapTab.enableMap();
|
||||
|
||||
$('.pane_table').addClass('is-active');
|
||||
},
|
||||
|
||||
_updateTable: function () {
|
||||
var sql = (this.model.get('bounds') && this.model.get('map')) ? (this.query + ' WHERE the_geom && ST_MakeEnvelope(' + this.model.get('map')['bounds'][0] + ', ' + this.model.get('map')['bounds'][1] + ', ' + this.model.get('map')['bounds'][2] + ', ' + this.model.get('map')['bounds'][3] + ', 4326)') : this.query;
|
||||
this._fetchData(sql);
|
||||
},
|
||||
|
||||
_fetchData: function (sql) {
|
||||
if (sql) {
|
||||
this.sqlView.setSQL(sql);
|
||||
}
|
||||
|
||||
this.sqlView.fetch({
|
||||
success: () => {
|
||||
this.$('.js-spinner').remove();
|
||||
this.tableTab.deactivated();
|
||||
this.tableTab.activated();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_exportTable: function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// If a sql is applied but it is not valid, don't let the user export it
|
||||
if (!this.sqlView.getSQL()) return false;
|
||||
|
||||
this._modals.create((modalModel) =>
|
||||
new PublicExportView({
|
||||
modalModel,
|
||||
configModel: this._configModel,
|
||||
model: this.table,
|
||||
user_data: this.user.toJSON(),
|
||||
bounds: this.sqlView.getSQL() !== this.query
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_apiCallTable: function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// If a sql is applied but it is not valid, don't show the dialog
|
||||
if (!this.sqlView.getSQL()) return false;
|
||||
|
||||
this._modals.create(() =>
|
||||
ViewFactory.createByTemplate(
|
||||
templateApiDialog,
|
||||
{
|
||||
url: this._configModel.getSqlApiUrl(),
|
||||
sql: this.sqlView.getSQL(),
|
||||
schema: this.table.attributes.original_schema.slice(0, 5),
|
||||
rows: this.table.dataModel.models
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change:bounds', function (model) {
|
||||
this._updateTable();
|
||||
});
|
||||
|
||||
this.listenTo(this.model, 'change:map', function (model) {
|
||||
if (model.get('bounds')) {
|
||||
this._updateTable();
|
||||
}
|
||||
});
|
||||
|
||||
this.authenticated_user.bind('change', this._onUserLogged, this);
|
||||
|
||||
this.add_related_model(this.authenticated_user);
|
||||
|
||||
this.listenToOnce(this.table, 'change:geometry_types', function (model) {
|
||||
this.mapTab.setGeometry(model.get('geometry_types'));
|
||||
});
|
||||
},
|
||||
|
||||
// Get type of current view
|
||||
// - It could be, dashboard, table or visualization
|
||||
_getCurrentView: function () {
|
||||
var pathname = location.pathname;
|
||||
|
||||
if (pathname.indexOf('/tables/') !== -1) {
|
||||
return 'table';
|
||||
}
|
||||
|
||||
if (pathname.indexOf('/viz/') !== -1) {
|
||||
return 'visualization';
|
||||
}
|
||||
|
||||
// Other case -> dashboard (datasets, visualizations,...)
|
||||
return 'dashboard';
|
||||
},
|
||||
|
||||
keyUp: function (e) {},
|
||||
|
||||
_onUserLogged: function () {
|
||||
// Check if edit button should be visible
|
||||
if (this.options.owner_username === this.authenticated_user.get('username')) {
|
||||
this.$('.extra_options .edit').css('display', 'inline-block');
|
||||
this.$('.extra_options .oneclick').css('display', 'none');
|
||||
}
|
||||
},
|
||||
|
||||
invalidateMap: function () {
|
||||
this.mapTab.invalidateMap();
|
||||
},
|
||||
|
||||
showTable: function () {
|
||||
$('.pane_table').addClass('is-active');
|
||||
$('.pane_map').removeClass('is-active');
|
||||
this.tabs.activate('table');
|
||||
},
|
||||
|
||||
showMap: function () {
|
||||
$('.pane_table').removeClass('is-active');
|
||||
$('.pane_map').addClass('is-active');
|
||||
this.tabs.activate('map');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
const TableTab = require('dashboard/views/public-dataset/table-tab/table-tab');
|
||||
const PublicTableView = require('dashboard/views/public-dataset/public-table-view/public-table-view');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
module.exports = TableTab.extend({
|
||||
|
||||
className: 'table public',
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
this.user = this.options.user;
|
||||
this.sqlView = this.options.sqlView;
|
||||
},
|
||||
|
||||
_createTable: function () {
|
||||
this.tableView = new PublicTableView({
|
||||
configModel: this._configModel,
|
||||
dataModel: this.model.data(),
|
||||
model: this.model,
|
||||
sqlView: this.sqlView
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'table',
|
||||
|
||||
initialize: function () {
|
||||
this.user = this.options.user;
|
||||
this.sqlView = this.options.sqlView;
|
||||
this.geocoder = this.options.geocoder;
|
||||
this.backgroundPollingModel = this.options.backgroundPollingModel;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
setActiveLayer: function (layerView) {
|
||||
var recreate = !!this.tableView;
|
||||
this.deactivated();
|
||||
this.model = layerView.table;
|
||||
this.layer = layerView.model;
|
||||
this.sqlView = layerView.sqlView;
|
||||
if (recreate) {
|
||||
this.activated();
|
||||
}
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
// Geocoder binding
|
||||
this.geocoder.bind('geocodingComplete geocodingError geocodingCanceled', function () {
|
||||
if (this.model.data) {
|
||||
this.model.data().refresh();
|
||||
}
|
||||
}, this);
|
||||
this.add_related_model(this.geocoder);
|
||||
},
|
||||
|
||||
_createTable: function () {
|
||||
throw new Error('Method not migrated, check original implementation');
|
||||
},
|
||||
|
||||
activated: function () {
|
||||
if (!this.tableView) {
|
||||
this._createTable();
|
||||
this.tableView.render();
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
deactivated: function () {
|
||||
if (this.tableView) {
|
||||
this.tableView.clean();
|
||||
this.tableView = null;
|
||||
this.hasRenderedTableView = false;
|
||||
}
|
||||
},
|
||||
|
||||
render: function () {
|
||||
// Since render should be idempotent (i.e. should not append the tableView twice when called multiple times)
|
||||
if (this.tableView && !this.hasRenderedTableView) {
|
||||
this.hasRenderedTableView = true;
|
||||
this.$el.append(this.tableView.el);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="emptyTableContainer">
|
||||
<div class="emptySQLInfo emptynessInfo">
|
||||
<h4 class="strong">No results on this query</h4>
|
||||
<p>Try with a different one or just <a href="#" class="clearview">clear this view</a></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,135 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const RowView = require('dashboard/components/table/row-view');
|
||||
const RowModel = require('dashboard/data/table/row-model');
|
||||
|
||||
/**
|
||||
* view used to render each row
|
||||
*/
|
||||
module.exports = RowView.extend({
|
||||
classLabel: 'cdb.admin.RowView',
|
||||
events: {
|
||||
'click .row_header': '_onOptionsClick',
|
||||
'mouseout .row_header': '_onOptionsOut'
|
||||
},
|
||||
|
||||
cellRenderers: {
|
||||
'default': '_renderDefault',
|
||||
'boolean': '_renderBoolean',
|
||||
'number': '_renderNumber',
|
||||
'geometry': '_renderGeometry'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
RowView.prototype.initialize.call(this, opts);
|
||||
_.bindAll(this, '_onOptionsClick', '_onOptionsOut');
|
||||
this.options.row_header = true;
|
||||
|
||||
this.retrigger('saving', this.model);
|
||||
this.retrigger('saved', this.model);
|
||||
this.retrigger('errorSaving', this.model);
|
||||
},
|
||||
|
||||
_getRowOptions: function () {
|
||||
throw new Error('Method not migrated, check original implementation');
|
||||
},
|
||||
|
||||
_onOptionsOut: function (e) {
|
||||
var $relatedTarget = $(e.relatedTarget);
|
||||
|
||||
if ($relatedTarget.closest('.dropdown').length == 0 && $relatedTarget.closest('.row_header').length == 0) { // eslint-disable-line eqeqeq
|
||||
if (this.rowOptions) this.rowOptions.hide();
|
||||
}
|
||||
},
|
||||
|
||||
_onOptionsClick: function (e) {
|
||||
var rowOptions;
|
||||
var left;
|
||||
var top;
|
||||
var $target = $(e.target);
|
||||
|
||||
if (this.table.isReadOnly()) {
|
||||
// if data is read only do not allow
|
||||
// to add or remove rows
|
||||
return;
|
||||
}
|
||||
rowOptions = this._getRowOptions();
|
||||
e.preventDefault();
|
||||
$target.append(rowOptions.el);
|
||||
|
||||
left = 30;
|
||||
top = 5;
|
||||
|
||||
// If it is the last td of the table, show the dropdown upwards
|
||||
if ($target.closest('tr').is(':last-child') && $target.closest('tr').index() > 1) {
|
||||
rowOptions.options.vertical_position = 'top';
|
||||
rowOptions.options.tick = 'bottom';
|
||||
} else {
|
||||
rowOptions.options.vertical_position = 'down';
|
||||
rowOptions.options.tick = 'top';
|
||||
}
|
||||
|
||||
rowOptions.setRow(this.model);
|
||||
rowOptions.openAt(left, top);
|
||||
this.rowOptions = rowOptions; // to make it testable;
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* return each cell view
|
||||
*/
|
||||
valueView: function (colName, value) {
|
||||
this.table = this.table || this.getTableView().model;
|
||||
var render = '_renderDefault';
|
||||
if (colName.length) {
|
||||
var colType = this.table.getColumnType(colName);
|
||||
render = this.cellRenderers[colType] || render;
|
||||
}
|
||||
|
||||
var obj = $(this[render](value));
|
||||
obj.addClass('cell');
|
||||
if (RowModel.isReservedColumn(colName)) {
|
||||
obj.addClass('disabled');
|
||||
}
|
||||
|
||||
// It is the first cell?
|
||||
if (colName === '' && value === '') {
|
||||
if (this.table.isReadOnly()) {
|
||||
// No row options button
|
||||
obj
|
||||
.addClass('disabled')
|
||||
.html('');
|
||||
} else {
|
||||
// Add row options button
|
||||
obj.addClass('row_header');
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
_renderDefault: function (value, additionalClasses) {
|
||||
additionalClasses = additionalClasses || '';
|
||||
|
||||
var cell;
|
||||
|
||||
if (value === null) {
|
||||
additionalClasses += ' isNull';
|
||||
cell = '<div class="' + additionalClasses + '">null</div>';
|
||||
} else cell = '<div class="' + additionalClasses + '">' + _.escape(value) + '</div>';
|
||||
|
||||
return cell;
|
||||
},
|
||||
|
||||
_renderBoolean: function (value) {
|
||||
return this._renderDefault(value, 'boolean');
|
||||
},
|
||||
|
||||
_renderNumber: function (value) {
|
||||
return this._renderDefault(value, 'number');
|
||||
},
|
||||
|
||||
_renderGeometry: function (value) {
|
||||
// Overriden in public-row-view
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="float_info loading <%- panel_opened ? 'displaced' : '' %>">
|
||||
<div class="CDB-LoaderIcon CDB-LoaderIcon--big is-blue">
|
||||
<svg class="CDB-LoaderIcon-spinner" viewbox="0 0 50 50">
|
||||
<circle class="CDB-LoaderIcon-path" cx="25" cy="25" r="20" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h5 class="strong">Loading results</h5>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="sqlview">
|
||||
<p class="displaced <% if(warnMsg) print("warn") %>">
|
||||
<% if (!empty) { %>
|
||||
<% if (warnMsg) { %>
|
||||
<%- warnMsg %> ·
|
||||
<% } %>
|
||||
<a href="#/export-query" class="export_query">create dataset from query</a> or <a href="#/clear-view" class="clearview">clear view</a>
|
||||
<% } else { %>
|
||||
No resulting rows for this query, <a href="#/clear-view" class="clearview">clear this sql view</a>
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<tfoot class='page_loader <%- direction %>'>
|
||||
<tr>
|
||||
<th colspan='1'>
|
||||
<div class='fake'></div>
|
||||
<div class='float_info'>
|
||||
<h5>Scroll <%- direction == "up" ? 'down' : 'up' %> and then <%- direction %></h5>
|
||||
<p>It will fire the pagination.</p>
|
||||
</div>
|
||||
<div class='float_action'>
|
||||
<h5>Loading more rows...</h5>
|
||||
<p>Just a few seconds.</p>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
@@ -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