Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
@@ -0,0 +1,251 @@
var Backbone = require('backbone');
var UploadModel = require('builder/data/upload-model');
var VisualizationFetchModel = require('builder/data/visualizations-fetch-model');
var TablesCollection = require('builder/data/visualizations-collection');
var TableModel = require('builder/data/table-model');
var MetricsTracker = require('builder/components/metrics/metrics-tracker');
var MetricsTypes = require('builder/components/metrics/metrics-types');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var IMPORT = 'import';
var DATASETS = 'datasets';
var SCRATCH = 'scratch';
var IMPORT_FILE = 'file';
var IMPORT_TWITTER = 'twitter';
var REQUIRED_OPTS = [
'userModel',
'userActions',
'configModel',
'pollingModel'
];
/**
* Add layer model
*
* "Implements" the CreateListingModel.
*/
module.exports = Backbone.Model.extend({
defaults: {
type: 'addLayer',
contentPane: 'listing', // [listing, loading]
listing: DATASETS, // [IMPORT, DATASETS, SCRATCH]
collectionFetched: false,
activeImportPane: IMPORT_FILE
},
initialize: function (attrs, opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initModels();
this._initBinds();
this._fetchCollection();
},
_initModels: function () {
this._uploadModel = new UploadModel({
create_vis: false
}, {
userModel: this._userModel,
configModel: this._configModel
});
this._selectedDatasetsCollection = new Backbone.Collection();
this._tablesCollection = new TablesCollection([], {
configModel: this._configModel
});
this._visualizationFetchModel = new VisualizationFetchModel({
content_type: DATASETS,
library: this.showLibrary()
});
},
getTablesCollection: function () {
return this._tablesCollection;
},
getSelectedDatasetsCollection: function () {
return this._selectedDatasetsCollection;
},
getVisualizationFetchModel: function () {
return this._visualizationFetchModel;
},
getUploadModel: function () {
return this._uploadModel;
},
canSelect: function (dataset) {
return dataset.get('selected') || this._selectedDatasetsCollection.length < 1; // for now only allow 1 item
},
showLibrary: function () {
return false;
},
showDatasets: function () {
return true;
},
setActiveImportPane: function (name) {
this.set('activeImportPane', name);
},
canFinish: function () {
if (this._atImportPane()) {
return this._uploadModel.isValidToUpload();
} else if (this._atDatasetsPane()) {
return this._selectedDatasetsCollection.length > 0;
}
},
finish: function () {
if (this._atImportPane()) {
this._pollingModel.trigger('importByUploadData', this._uploadModel.toJSON(), this);
} else if (this._atDatasetsPane()) {
var mdl = this._selectedDatasetsCollection.at(0);
if (mdl.get('type') === 'remote') {
var d = {
create_vis: false,
type: 'remote',
value: mdl.get('name'),
remote_visualization_id: mdl.get('id'),
size: mdl.get('external_source') ? mdl.get('external_source').size : undefined
};
// See BackgroundImporter where the same event is bound to be handled..
this._pollingModel.trigger('importByUploadData', d, this);
} else {
this._addNewLayer(mdl.getTableModel());
}
}
},
getImportState: function () {
return this.get('activeImportPane');
},
showGuessingToggler: function () {
return this._atImportPane();
},
showPrivacyToggler: function () {
var hiddenDueToDeprecation = this._atTwitterImportPane() && !this._userModel.hasOwnTwitterCredentials();
var hasToBeShowed = this._atImportPane() && !hiddenDueToDeprecation;
return hasToBeShowed;
},
_atImportPane: function () {
return this.get('listing') === IMPORT;
},
_atDatasetsPane: function () {
return this.get('listing') === DATASETS;
},
_atScratchPane: function () {
return this.get('listing') === SCRATCH;
},
_atTwitterImportPane: function () {
return this.get('activeImportPane') === IMPORT_TWITTER;
},
createFromScratch: function () {
var self = this;
this.set('contentPane', 'creatingFromScratch');
var tableModel = new TableModel({}, {
configModel: this._configModel
});
tableModel.save({}, {
success: function () {
self._addNewLayer(tableModel, true);
},
error: function (req, resp) {
if (resp.responseText.indexOf('You have reached your table quota') !== -1) {
self.set('contentPane', 'datasetQuotaExceeded');
} else {
self.set('contentPane', 'addLayerFailed');
}
}
});
},
_initBinds: function () {
this._uploadModel.bind('change', function () {
this.trigger('change:upload', this);
}, this);
this._visualizationFetchModel.bind('change', this._fetchCollection, this);
this.bind('change:listing', this._fetchCollection, this);
this._tablesCollection.bind('change:selected', function (changedModel, wasSelected) {
this._selectedDatasetsCollection[wasSelected ? 'add' : 'remove'](changedModel);
}, this);
this._tablesCollection.bind('sync', function () {
this._selectedDatasetsCollection.each(function (model) {
var sameModel = this._tablesCollection.get(model.id);
if (sameModel) {
sameModel.set('selected', true);
}
}, this);
}, this);
},
_fetchCollection: function () {
var params = this._visualizationFetchModel.attributes;
var types;
if (this._visualizationFetchModel.isSearching()) {
// Supporting search in data library and user datasets at the same time
types = 'table,remote';
} else {
types = params.library ? 'remote' : 'table';
}
this._tablesCollection.fetch({
data: {
locked: '',
q: params.q,
page: params.page,
tags: params.tag,
shared: params.shared,
only_liked: params.liked,
type: '',
types: types
}
});
},
_onCollectionChange: function () {
this._selectedDatasetsCollection.reset(
this._tablesCollection.where({ selected: true })
);
},
_addNewLayer: function (tableModel, empty) {
this.set('contentPane', 'addingNewLayer');
this._userActions.createLayerFromTable(tableModel, {
success: function (model) {
this._userModel.updateTableCount();
this.trigger('addLayerDone');
MetricsTracker.track(MetricsTypes.CREATED_LAYER, {
empty: !!empty,
layer_id: model.get('id')
});
}.bind(this),
error: function (req, resp) {
if (resp.responseText.indexOf('You have reached your table quota') !== -1) {
this.set('contentPane', 'datasetQuotaExceeded');
} else {
this.set('contentPane', 'addLayerFailed');
}
}.bind(this)
});
}
});
@@ -0,0 +1,160 @@
var _ = require('underscore');
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var template = require('./add-layer.tpl');
var FooterView = require('./footer/footer-view');
var NavigationView = require('./content/navigation-view');
var ListingView = require('./content/listing-view');
var TabPaneView = require('builder/components/tab-pane/tab-pane-view');
var TabPaneCollection = require('builder/components/tab-pane/tab-pane-collection');
var ErrorDetailsView = require('builder/components/background-importer/error-details-view');
var ViewFactory = require('builder/components/view-factory');
var renderLoading = require('builder/components/loading/render-loading');
var ErrorView = require('builder/components/error/error-view');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'modalModel',
'createModel',
'configModel',
'userModel',
'pollingModel'
];
/**
* Add layer dialog, typically used from editor
*/
module.exports = CoreView.extend({
className: 'Dialog-content Dialog-content--expanded',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initModels();
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this._initViews();
},
_initModels: function () {
this._guessingModel = new Backbone.Model({
guessing: true
});
this._privacyModel = new Backbone.Model({
privacy: this._userModel.canCreatePrivateDatasets() ? 'PRIVATE' : 'PUBLIC'
});
},
_initBinds: function () {
this.listenTo(this._createModel, 'addLayerDone', this._modalModel.destroy.bind(this._modalModel));
this.listenTo(this._createModel, 'change:contentPane', this._onChangeContentView);
this.listenTo(this._pollingModel, 'importByUploadData', this._modalModel.destroy.bind(this._modalModel));
},
_initViews: function () {
var self = this;
this._navigationView = new NavigationView({
el: this.$('.js-navigation'),
userModel: this._userModel,
routerModel: this._createModel.getVisualizationFetchModel(),
createModel: this._createModel,
tablesCollection: this._createModel.getTablesCollection(),
configModel: this._configModel
});
this._navigationView.render();
this.addView(this._navigationView);
this._tabPaneCollection = new TabPaneCollection([
{
name: 'listing',
selected: this._createModel.get('contentPane') === 'listing',
createContentView: function () {
return new ListingView({
createModel: self._createModel,
configModel: self._configModel,
userModel: self._userModel,
privacyModel: self._privacyModel,
guessingModel: self._guessingModel
});
}
}, {
name: 'creatingFromScratch',
selected: this._createModel.get('contentPane') === 'creatingFromScratch',
createContentView: function () {
return ViewFactory.createByHTML(
renderLoading({
title: _t('components.modals.add-layer.create-loading-title')
})
);
}
}, {
name: 'addingNewLayer',
selected: this._createModel.get('contentPane') === 'addingNewLayer',
createContentView: function () {
return ViewFactory.createByHTML(
renderLoading({
title: _t('components.modals.add-layer.adding-new-layer')
})
);
}
}, {
name: 'addLayerFailed',
selected: this._createModel.get('contentPane') === 'addLayerFailed',
createContentView: function () {
return new ErrorView({
title: _t('components.modals.add-layer.add-layer-error')
});
}
}, {
name: 'datasetQuotaExceeded',
selected: this._createModel.get('contentPane') === 'datasetQuotaExceeded',
createContentView: function () {
return new ErrorDetailsView({
error: { errorCode: 8002 },
userModel: self._userModel,
configModel: self._configModel
});
}
}
]);
var tabPaneView = new TabPaneView({
collection: this._tabPaneCollection
});
this.addView(tabPaneView);
this.$('.js-content-container').append(tabPaneView.render().el);
this._footerView = new FooterView({
configModel: this._configModel,
createModel: this._createModel,
userModel: this._userModel,
privacyModel: self._privacyModel,
guessingModel: self._guessingModel
});
this.addView(this._footerView);
this.$('.js-footer').append(this._footerView.render().el);
},
_onChangeContentView: function () {
var context = this._createModel.get('contentPane');
var paneModel = _.first(this._tabPaneCollection.where({ name: context }));
var paneModelName = paneModel.get('name');
paneModel.set('selected', true);
if (paneModelName === 'loading') {
this._footerView.hide();
}
if (paneModelName !== 'listing') {
this._navigationView.hide();
}
}
});
@@ -0,0 +1,10 @@
<div class="Dialog-header Dialog-header--expanded CreateDialog-header with-separator">
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
<i class="CDB-IconFont CDB-IconFont-add"></i>
</div>
<h2 class="CDB-Text CDB-Size-large u-mainTextColor u-bSpace"><%- _t('components.modals.add-layer.modal-title') %></h2>
<h3 class="CDB-Text CDB-Size-medium u-altTextColor"><%- _t('components.modals.add-layer.modal-desc') %></h3>
</div>
<div class="Filters Filters--navListing Filters--static js-navigation"></div>
<div class="js-content-container Dialog-body Dialog-body--expanded Dialog-body--create Dialog-body--noPaddingTop Dialog-body--withoutBorder"></div>
<div class="Dialog-footer Dialog-footer--expanded CreateDialog-footer js-footer"></div>
@@ -0,0 +1,57 @@
var CoreView = require('backbone/core-view');
var randomQuote = require('builder/components/loading/random-quote');
/*
* Content result default view
*/
module.exports = CoreView.extend({
events: {
'click .js-connect': '_onConnectClick'
},
initialize: function (opts) {
if (!opts.routerModel) throw new Error('routerModel is required');
if (!opts.tablesCollection) throw new Error('tablesCollection is required');
if (!opts.userModel) throw new Error('userModel is required');
if (!opts.template) throw new Error('template is required');
this._userModel = opts.userModel;
this._routerModel = opts.routerModel;
this._tablesCollection = opts.tablesCollection;
this.template = opts.template;
this._initBinds();
},
render: function () {
var type = this._routerModel.get('content_type');
this.$el.html(this.template({
page: this._routerModel.get('page'),
tag: this._routerModel.get('tag'),
q: this._routerModel.get('q'),
shared: this._routerModel.get('shared'),
locked: this._routerModel.get('locked'),
library: this._routerModel.get('library'),
quote: randomQuote(),
type: type,
totalItems: this._tablesCollection.size(),
totalEntries: this._tablesCollection.getTotalStat('total_entries')
}));
return this;
},
_initBinds: function () {
this._routerModel.bind('change', this.render, this);
this._tablesCollection.bind('sync', this.render, this);
this.add_related_model(this._routerModel);
this.add_related_model(this._tablesCollection);
},
_onConnectClick: function () {
this.trigger('connectDataset', this);
}
});
@@ -0,0 +1,116 @@
var cdb = require('internal-carto.js');
var CoreView = require('backbone/core-view');
var $ = require('jquery');
var _ = require('underscore');
var moment = require('moment');
var Utils = require('builder/helpers/utils');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var template = require('./dataset-item.tpl');
/**
* View representing an item in the list under datasets route.
*/
module.exports = CoreView.extend({
tagName: 'li',
className: 'ModalBlockList-item ModalBlockList-item--full',
events: {
'click .js-tag-link': '_onTagClick',
'click': '_toggleSelected'
},
initialize: function (opts) {
if (!opts.createModel) throw new Error('createModel is required');
if (!opts.userModel) throw new Error('userModel is required');
this._createModel = opts.createModel;
this._userModel = opts.userModel;
this._routerModel = this._createModel.getVisualizationFetchModel();
this.model.on('change', this.render, this);
},
render: function () {
var tableModel = this.model.getTableModel();
var permissionModel = this.model.getPermissionModel();
var synchronizationModel = this.model.getSynchronizationModel();
var tags = this.model.get('tags') || [];
var description = cdb.core.sanitize.html(this.model.get('description'));
var tableGeomColumnTypes = (tableModel.getGeometryType ? tableModel.getGeometryType() : tableModel.geomColumnTypes()) || [];
var d = {
isRaster: this.model.isRaster(),
geometryType: tableGeomColumnTypes.length > 0 ? tableGeomColumnTypes[0] : '',
title: this.model.get('name'),
isOwner: permissionModel.isOwner(this._userModel),
owner: permissionModel.getOwner().renderData(this._userModel),
showPermissionIndicator: !permissionModel.hasWriteAccess(this._userModel),
description: description,
privacy: this.model.get('privacy').toLowerCase(),
timeDiff: moment(this.model.get('updated_at')).fromNow(),
tags: tags,
tagsCount: tags.length,
maxTagsToShow: 3,
rowCount: undefined,
datasetSize: undefined,
syncStatus: undefined,
syncRanAt: undefined
};
var rowCount = tableModel.get('row_count');
if (rowCount >= 0) {
d.rowCount = rowCount;
d.rowCountFormatted = (rowCount < 10000 ? Utils.formatNumber(rowCount) : Utils.readizableNumber(rowCount));
}
var datasetSize = tableModel.get('size');
if (datasetSize >= 0) {
d.datasetSize = Utils.readablizeBytes(datasetSize, true);
}
if (!_.isEmpty(synchronizationModel)) {
d.syncRanAt = moment(synchronizationModel.get('ran_at') || new Date()).fromNow();
d.syncStatus = synchronizationModel.get('state');
}
this.$el.html(template(d));
this._renderTooltips();
// Item selected?
this.$el.toggleClass('is-selected', !!this.model.get('selected'));
return this;
},
_renderTooltips: function () {
if (!_.isEmpty(this.model.get('synchronization'))) {
this.addView(
new TipsyTooltipView({
el: this.$('.DatasetsList-itemStatus'),
title: function (e) {
return $(this).attr('data-title');
}
})
);
}
},
_onTagClick: function (ev) {
var tag = $(ev.target).val();
if (tag) {
this._routerModel.set('tag', tag);
}
},
_toggleSelected: function (ev) {
// Let links use default behaviour
if (ev.target.tagName !== 'A') {
this.killEvent(ev);
if (this._createModel.canSelect(this.model)) {
this.model.set('selected', !this.model.get('selected'));
}
}
}
});
@@ -0,0 +1,73 @@
<div class="DatasetsList-itemCategory is--<%- isRaster ? 'raster' : geometryType %>Dataset">
<% if (syncStatus) { %>
<i
<% if (syncStatus === "failure") { %>
data-title="<%- _t('components.modals.add-layer.datasets.item.sync-failed') %> <%- syncRanAt %>"
<% } else if (syncStatus === "syncing") { %>
data-title="<%- _t('components.modals.add-layer.datasets.item.syncing') %>"
<% } else { %>
data-title="<%- _t('components.modals.add-layer.datasets.item.synced') %> <%- syncRanAt %>"
<% } %>
class="CDB-IconFont CDB-IconFont-wifi DatasetsList-itemStatus is-<%- syncStatus %>"></i>
<% } %>
</div>
<div class="ModalDataset-itemInfo">
<div class="ModalDataset-itemInfoTitle">
<h3 class="CDB-Text CDB-Size-large u-bSpace u-ellipsis">
<%- title %>
<% if (showPermissionIndicator) { %>
<span class="Tag Tag--outline Tag-outline--grey CDB-Text CDB-Size-small u-upperCase">
<%- _t('components.modals.add-layer.datasets.item.read') %>
</span>
<% } %>
</h3>
<% if (description && description.length > 0) { %>
<p class="u-ellipsis CDB-Text CDB-Size-medium u-altTextColor" title="<%- description %>"><%- description %></p>
<% } else { %>
<span class="NoResults CDB-Text CDB-Size-medium"><%- _t('components.modals.add-layer.datasets.item.no-description') %></span>
<% } %>
</div>
<div>
<div class="DatasetsList-itemMeta">
<span class="CDB-Tag is-<%- privacy %> CDB-Text is-semibold CDB-Size-small u-upperCase">
<%- privacy %>
</span>
<% if (rowCount) { %>
<span class="RowsIndicator">
<span class="CDB-Text CDB-Size-small u-altTextColor"><%- rowCountFormatted %> <%- _t('components.modals.add-layer.datasets.item.rows-pluralize', { smart_count: rowCount }) %></span>
</span>
<% } %>
<% if (datasetSize) { %>
<span class="SizeIndicator">
<span class="CDB-Text CDB-Size-small u-altTextColor"><%- datasetSize %></span>
</span>
<% } %>
<span class="DatasetsList-itemTimeDiff DefaultTimeDiff">
<span class="CDB-Text CDB-Size-small u-altTextColor"><%- timeDiff %></span>
<% if (!isOwner) { %>
<span class="CDB-Text CDB-Size-small u-altTextColor u-lSpace--xl u-rSpace">
<%- _t('components.modals.add-layer.datasets.item.by') %>
</span>
<span class="DatasetsList-avatar">
<img class="DatasetsList-avatarImg" src="<%- owner.avatar_url %>" alt="<%- owner.name || owner.username %>" title="<%- owner.name || owner.username %>" />
</span>
<% } %>
</span>
</div>
<div class="DatasetsList-itemMeta DatasetsList-itemTags">
<% if (tagsCount > 0) { %>
<div class="DefaultTags CDB-Text CDB-Size-small">
<% for (var i = 0, l = Math.min(maxTagsToShow, tags.length); i < l; ++i) { %>
<button class="CDB-Text CDB-Size-small u-upperCase DefaultTags-item js-tag-link u-actionTextColor" value="<%- tags[i] %>"><%- tags[i] %></button><% if (i !== (l-1)) { %><% } %>
<% } %>
<% if (tagsCount > maxTagsToShow) { %>
<%- _t('components.modals.add-layer.datasets.item.tags-more', { tagsCount: tagsCount - maxTagsToShow }) %>
<% } %>
</div>
<% } else { %>
<span class="NoResults CDB-Text CDB-Size-small u-altTextColor"><%- _t('components.modals.add-layer.datasets.item.no-tags') %></span>
<% } %>
</div>
</div>
</div>
@@ -0,0 +1,9 @@
<div class="IntermediateInfo">
<div class="LayoutIcon LayoutIcon--negative">
<i class="CDB-IconFont CDB-IconFont-cockroach"></i>
</div>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">
<%- _t('components.modals.add-layer.datasets.error.title') %>
</h4>
<p class="CDB-Text CDB-Size-medium u-altTextColor"><%- _t('components.modals.add-layer.datasets.error.desc') %> <a class="js-mail-link" href="mailto:support@carto.com">support@carto.com</a>.</p>
</div>
@@ -0,0 +1,58 @@
var CoreView = require('backbone/core-view');
var DATASETS_ITEMS = {
'datasets': require('./dataset-item-view'),
'remotes': require('./remote-dataset-item-view')
};
/**
* View representing the list of datasets
*/
module.exports = CoreView.extend({
tagName: 'ul',
className: 'DatasetsList fs-DatasetsList',
initialize: function (opts) {
if (!opts.createModel) throw new Error('createModel is required');
if (!opts.userModel) throw new Error('userModel is required');
this._createModel = opts.createModel;
this._userModel = opts.userModel;
this._tablesCollection = this._createModel.getTablesCollection();
this._initBinds();
},
render: function () {
this.clearSubViews();
this._tablesCollection.each(this._addItem, this);
return this;
},
_initBinds: function () {
this._tablesCollection.bind('sync', this.render, this);
this.add_related_model(this._tablesCollection);
},
_addItem: function (m, i) {
var type = m.get('type') === 'remote' ? 'remotes' : 'datasets';
var item = new DATASETS_ITEMS[type]({
model: m,
createModel: this._createModel,
userModel: this._userModel
});
this.addView(item);
this.$el.append(item.render().el);
},
show: function () {
this.$el.removeClass('is-hidden');
},
hide: function () {
this.$el.addClass('is-hidden');
}
});
@@ -0,0 +1,11 @@
<div class="IntermediateInfo">
<div class="CDB-LoaderIcon CDB-LoaderIcon--big is-dark js-loader">
<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>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-bSpace u-secondaryTextColor u-tSpace-xl">
<%- _t('components.modals.add-layer.datasets.' + (q || tag ? 'searching' : 'loading')) %>...
</h4>
<div class="CDB-Text CDB-Size-medium u-altTextColor"><%= quote %></div>
</div>
@@ -0,0 +1,27 @@
<div class="IntermediateInfo">
<div class="LayoutIcon <% (q || tag) ? 'LayoutIcon--negative' : '' %>">
<i class="CDB-IconFont
<% if (shared) { %> CDB-IconFont-defaultUser
<% } else if (locked) { %> CDB-IconFont-lock
<% } else if (library) { %> CDB-IconFont-dribbble
<% } else { %> CDB-IconFont-lens <% } %>" />
</div>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-bSpace u-secondaryTextColor u-tSpace-xl">
<% if (page > 1 || totalItems === 0 && totalEntries > 0) { %>
<%- _t('components.modals.add-layer.datasets.no-results.desc') %>
<% } %>
<% if (( tag || q ) && totalItems === 0 && totalEntries === 0) { %>
0 <%- tag || q %> <%- type %> <%- _t('components.modals.add-layer.datasets.no-results.found') %>
<% } %>
<% if (page === 1 && !tag && !q && totalItems === 0 && totalEntries === 0) { %>
<%- _t('components.modals.add-layer.datasets.no-results.there-are-no') %> <%- shared === "only" ? 'shared' : '' %> <%- locked ? 'locked' : '' %> <%- library ? 'library' : '' %> <%- type %>
<% } %>
</h4>
<p class="CDB-Text CDB-Size-medium u-altTextColor">
<% if (!tag && !q && totalItems === 0 && totalEntries === 0) { %>
<%- _t('components.modals.add-layer.datasets.no-results.no-fun', { type: type }) %>
<% } %>
</p>
</div>
@@ -0,0 +1,72 @@
var CoreView = require('backbone/core-view');
var PaginationModel = require('builder/components/pagination/pagination-model');
var PaginationView = require('builder/components/pagination/pagination-view');
/**
* Responsible for the datasets paginator
* ___________________________________________________________________________
* | |
* | Page 2 of 42 [1] 2 [3][4][5] |
* |___________________________________________________________________________|
*
*/
module.exports = CoreView.extend({
className: 'DatasetsPaginator',
initialize: function (opts) {
if (!opts.routerModel) throw new TypeError('routerModel is required');
if (!opts.tablesCollection) throw new TypeError('tablesCollection is required');
this._routerModel = opts.routerModel;
this._tablesCollection = opts.tablesCollection;
this.model = new PaginationModel({
current_page: this._routerModel.get('page')
});
this._initBinds();
this._initViews();
},
render: function () {
this.clearSubViews();
this.$el.html('');
if (this.model.shouldBeVisible()) {
this.$el.append(this.paginationView.render().el);
}
return this;
},
_initBinds: function () {
this.model.bind('change', this.render, this);
this.model.bind('change:current_page', function () {
this._routerModel.set('page', this.model.get('current_page'));
}, this);
this._tablesCollection.bind('sync', this._updatePaginationModelByCollection, this);
this._routerModel.bind('change:page', this._updatePaginationModelByRouterModel, this);
this.add_related_model(this._routerModel);
this.add_related_model(this._tablesCollection);
this.add_related_model(this.model);
},
_initViews: function () {
this.paginationView = new PaginationView({
model: this.model
});
this.addView(this.paginationView);
},
_updatePaginationModelByCollection: function () {
this.model.set({
per_page: this._tablesCollection.getDefaultParam('per_page'),
total_count: this._tablesCollection.getTotalStat('total_entries')
});
},
_updatePaginationModelByRouterModel: function () {
this.model.set('current_page', this._routerModel.get('page'));
}
});
@@ -0,0 +1,218 @@
var CoreView = require('backbone/core-view');
var _ = require('underscore');
var ContentResultView = require('./content-result-view');
var DatasetsListView = require('./datasets-list-view');
var DatasetsPaginationView = require('./datasets-pagination-view');
var noResultsTemplate = require('./no-datasets.tpl');
var datasetsNoResultTemplate = require('./datasets-no-result.tpl');
var datasetsErrorTemplate = require('./datasets-error.tpl');
var datasetsLoaderTemplate = require('./datasets-loader.tpl');
/**
* Datasets list view
*
* Show datasets view to select them for
* creating a map or importing a dataset
*
*/
module.exports = CoreView.extend({
initialize: function (opts) {
if (!opts.createModel) throw new Error('createModel is required');
if (!opts.userModel) throw new Error('userModel is required');
this._createModel = opts.createModel;
this._userModel = opts.userModel;
this._routerModel = this._createModel.getVisualizationFetchModel();
this._tablesCollection = this._createModel.getTablesCollection();
this._initViews();
this._initBinds();
this._onDataLoading();
},
_initBinds: function () {
this._routerModel.bind('change', this._onRouterChange, this);
this._tablesCollection.bind('loading', this._onDataLoading, this);
this._tablesCollection.bind('sync', this._onDataFetched, this);
this._tablesCollection.bind('error', function (e) {
// Old requests can be stopped, so aborted requests are not
// considered as an error
if (!e || (e && e.statusText !== 'abort')) {
this._onDataError();
}
}, this);
this.add_related_model(this._routerModel);
this.add_related_model(this._tablesCollection);
},
_initViews: function () {
this.controlledViews = {}; // All available views
this.enabledViews = []; // Visible views
var noDatasetsView = new ContentResultView({
className: 'ContentResult no-datasets',
userModel: this._userModel,
routerModel: this._routerModel,
tablesCollection: this._tablesCollection,
template: noResultsTemplate
});
noDatasetsView.bind('connectDataset', function () {
if (this._userModel.canCreateDatasets()) {
this._createModel.set('listing', 'import');
}
}, this);
noDatasetsView.render().hide();
this.controlledViews['no_datasets'] = noDatasetsView;
this.$el.append(noDatasetsView.el);
this.addView(noDatasetsView);
var listView = new DatasetsListView({
userModel: this._userModel,
createModel: this._createModel
});
this.controlledViews.list = listView;
this.$el.append(listView.render().el);
this.addView(listView);
var noResultsView = new ContentResultView({
userModel: this._userModel,
routerModel: this._routerModel,
tablesCollection: this._tablesCollection,
template: datasetsNoResultTemplate
});
noResultsView.render().hide();
this.controlledViews.no_results = noResultsView;
this.$el.append(noResultsView.el);
this.addView(noResultsView);
var errorView = new ContentResultView({
userModel: this._userModel,
routerModel: this._routerModel,
tablesCollection: this._tablesCollection,
template: datasetsErrorTemplate
});
errorView.render().hide();
this.controlledViews.error = errorView;
this.$el.append(errorView.el);
this.addView(errorView);
var mainLoaderView = new ContentResultView({
userModel: this._userModel,
routerModel: this._routerModel,
tablesCollection: this._tablesCollection,
template: datasetsLoaderTemplate
});
this.controlledViews.main_loader = mainLoaderView;
this.$el.append(mainLoaderView.render().el);
this.addView(mainLoaderView);
var datasetsPaginationView = new DatasetsPaginationView({
routerModel: this._routerModel,
tablesCollection: this._tablesCollection
});
this.controlledViews.content_footer = datasetsPaginationView;
this.$el.append(datasetsPaginationView.render().el);
this.addView(datasetsPaginationView);
},
_onRouterChange: function () {
this._hideBlocks();
this._showBlocks([ 'main_loader' ]);
},
/**
* Arguments may vary, depending on if it's the collection or a model that triggers the event callback.
* @private
*/
_onDataFetched: function () {
var activeViews = [ 'content_footer' ];
var tag = this._routerModel.get('tag');
var q = this._routerModel.get('q');
var shared = this._routerModel.get('shared');
var locked = this._routerModel.get('locked');
var library = this._routerModel.get('library');
if (library && this._tablesCollection.getTotalStat('total_user_entries') === 0) {
activeViews.push('no_datasets');
}
if (this._tablesCollection.size() === 0) {
if (!tag && !q && shared === 'no' && !locked) {
if (!library) {
this._goToLibrary();
return;
} else {
activeViews.push('no_results');
}
} else {
activeViews.push('no_results');
}
} else {
activeViews.push('list');
}
this._hideBlocks();
this._showBlocks(activeViews);
},
_onDataLoading: function () {
this._hideBlocks();
this._showBlocks([ 'main_loader' ]);
},
_onDataError: function (e) {
this._hideBlocks();
this._showBlocks([ 'error' ]);
},
_showBlocks: function (views) {
var self = this;
if (views) {
_.each(views, function (v) {
if (self.controlledViews[v]) {
self.controlledViews[v].show();
self.enabledViews.push(v);
}
});
} else {
self.enabledViews = [];
_.each(this.controlledViews, function (v) {
v.show();
self.enabledViews.push(v);
});
}
},
_goToLibrary: function () {
this._routerModel.set({
shared: 'no',
library: true,
page: 1
});
},
_hideBlocks: function (views) {
var self = this;
if (views) {
_.each(views, function (v) {
if (self.controlledViews[v]) {
self.controlledViews[v].hide();
self.enabledViews = _.without(self.enabledViews, v);
}
});
} else {
_.each(this.controlledViews, function (v) {
v.hide();
});
self.enabledViews = [];
}
},
_isBlockEnabled: function (name) {
if (name) {
return _.contains(this.enabledViews, name);
}
return false;
}
});
@@ -0,0 +1,12 @@
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m u-tSpace-xl">
<%- _t('components.modals.add-layer.datasets.no-datasets.title') %>
</h4>
<p class="CDB-Text CDB-Size-medium u-altTextColor">
<% var connectDatasetHTML = '<button class="Button--link js-connect">' + _t('components.modals.add-layer.datasets.no-datasets.connect-datasets') + '</button>'; %>
<% var searchHTML = '<strong>' + _t('components.modals.add-layer.datasets.no-datasets.search') + '</strong>'; %>
<%= _t('components.modals.add-layer.datasets.no-datasets.desc', {
connectDataset: connectDatasetHTML,
search: searchHTML
}) %>
</p>
<div class="NoDatasets-illustration"></div>
@@ -0,0 +1,123 @@
var cdb = require('internal-carto.js');
var $ = require('jquery');
var DatasetItem = require('./dataset-item-view');
var moment = require('moment');
var markdown = require('markdown');
var Utils = require('builder/helpers/utils');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var UploadConfig = require('builder/config/upload-config');
var template = require('./remote-dataset-item.tpl');
/**
* Remote dataset item view
*
*/
module.exports = DatasetItem.extend({
tagName: 'li',
className: 'ModalBlockList-item ModalBlockList-item--full',
events: {
'click .js-tag-link': '_onTagClick',
'click': '_toggleSelected'
},
render: function () {
var tableModel = this.model.getTableModel();
var tags = this.model.get('tags') || [];
var description = cdb.core.sanitize.html(this.model.get('description') || '');
var source = markdown.toHTML(this.model.get('source') || '');
var tableGeomColumnTypes = tableModel.getGeometryType() || [];
var d = {
isRaster: this.model.isRaster(),
geometryType: tableGeomColumnTypes.length > 0 ? tableGeomColumnTypes[0] : '',
title: this.model.get('display_name') || this.model.get('name'),
source: source,
description: description,
timeDiff: moment(this.model.get('updated_at')).fromNow(),
tags: tags,
tagsCount: tags.length,
routerModel: this._routerModel,
maxTagsToShow: 3,
canImportDataset: this._canImportDataset(),
rowCount: undefined,
datasetSize: undefined
};
var rowCount = tableModel.get('row_count');
if (rowCount >= 0) {
d.rowCount = rowCount;
d.rowCountFormatted = (rowCount < 10000 ? Utils.formatNumber(rowCount) : Utils.readizableNumber(rowCount));
}
var datasetSize = tableModel.get('size');
if (datasetSize >= 0) {
d.datasetSize = Utils.readablizeBytes(
datasetSize,
datasetSize.toString().length > 9
);
}
this.$el.html(template(d));
this._setItemClasses();
this._renderTooltips();
return this;
},
_setItemClasses: function () {
// Item selected?
this.$el.toggleClass('is-selected', !!this.model.get('selected'));
// Check if it is selectable
this.$el.toggleClass('is-selectable', !!this._canImportDataset());
// Check if it is importable
this.$el.toggleClass('is-banned', !this._canImportDataset());
},
_renderTooltips: function () {
this.addView(
new TipsyTooltipView({
el: this.$('.DatasetsList-itemStatus'),
title: function (e) {
return $(this).attr('data-title');
}
})
);
},
_onTagClick: function (ev) {
if (ev) {
this.killEvent(ev);
}
var tag = $(ev.target).val();
if (tag) {
this._routerModel.set({
tag: tag,
library: true
});
}
},
_canImportDataset: function () {
var tableModel = this.model.getTableModel();
var tableSize = tableModel.get('size') || 0;
return (
this._userModel.get('remaining_byte_quota') * UploadConfig.fileTimesBigger >= tableSize &&
this._userModel.get('limits')['import_file_size'] > tableSize
);
},
_toggleSelected: function (ev) {
// Let links use default behaviour
if (ev.target.tagName !== 'A') {
this.killEvent(ev);
if (this._canImportDataset() && this._createModel.canSelect(this.model)) {
this.model.set('selected', !this.model.get('selected'));
}
}
}
});
@@ -0,0 +1,44 @@
<div class="DatasetsList-itemCategory is--<%- isRaster ? 'raster' : geometryType %>Dataset">
<i data-title="Public dataset" class="CDB-IconFont CDB-IconFont-book DatasetsList-itemStatus <%- canImportDataset ? 'is-public' : 'is-banned' %>"></i>
</div>
<div class="ModalDataset-itemInfo">
<div class="ModalDataset-itemInfoTitle">
<h3 class="CDB-Text CDB-Size-large u-bSpace u-ellipsis"><%- title %></h3>
<% if (description && description.length > 0) { %>
<p class="u-ellipsis CDB-Text CDB-Size-medium u-altTextColor" title="<%- description %>"><%- description %></p>
<% } else { %>
<span class="NoResults CDB-Text CDB-Size-medium"><%- _t('components.modals.add-layer.datasets.item.no-description')%></span>
<% } %>
</div>
<div>
<ul class="DatasetsList-itemMeta CDB-Text CDB-Size-small u-altTextColor">
<% if (rowCount) { %>
<li class="RowsIndicator">
<%- rowCountFormatted %> <%- _t('components.modals.add-layer.datasets.item.rows-pluralize', { smart_count: rowCount }) %>
</li>
<% } %>
<% if (datasetSize) { %>
<li class="SizeIndicator">
<%- datasetSize %>
</li>
<% } %>
<li class="DatasetsList-itemTimeDiff DefaultTimeDiff CDB-Text CDB-Size-small u-altTextColor">
<%- timeDiff %> <span class="DatasetsList-itemSource js-source"><% if (source) { %><%- _t('components.modals.add-layer.datasets.item.from') %> <%= cdb.core.sanitize.html(source) %><% } %></span>
</li>
</ul>
<div class="DatasetsList-itemMeta DatasetsList-itemTags">
<% if (tagsCount > 0) { %>
<div class="DefaultTags CDB-Text CDB-Size-small">
<% for (var i = 0, l = Math.min(maxTagsToShow, tags.length); i < l; ++i) { %>
<button class="CDB-Text CDB-Size-small u-upperCase DefaultTags-item js-tag-link u-actionTextColor" value="<%- tags[i] %>"><%- tags[i] %></button><% if (i !== (l-1)) { %><% } %>
<% } %>
<% if (tagsCount > maxTagsToShow) { %>
<%- _t('components.modals.add-layer.datasets.item.tags-more', { tagsCount: tagsCount - maxTagsToShow }) %>
<% } %>
</div>
<% } else { %>
<span class="NoResults CDB-Text CDB-Size-small u-altTextColor"><%- _t('components.modals.add-layer.datasets.item.no-tags') %></span>
<% } %>
</div>
</div>
</div>
@@ -0,0 +1,14 @@
<div class="ImportPanel-header">
<div class="LayoutIcon u-bSpace--xl">
<i class="CDB-IconFont CDB-IconFont-gift"></i>
</div>
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">ArcGIS<sup>&trade;</sup> <%- _t('components.modals.add-layer.imports.connector') %></h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor u-bSpace--xl">
<%= _t('components.modals.add-layer.imports.arcgis.fallback-desc', {
brand: 'ArcGIS<sup>&trade;</sup>'
}) %>
</p>
<a href="mailto:sales@carto.com?subject=<%- _t('components.modals.add-layer.imports.demo-email-title', { name: 'ArcGIS' }) %>&body=<%- _t('components.modals.add-layer.imports.demo-email-desc', { name: 'ArcGIS' }) %>" class="CDB-Button CDB-Button--primary CDB-Button--medium">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase"><%- _t('components.modals.add-layer.imports.ask-for-demo') %></span>
</a>
</div>
@@ -0,0 +1,12 @@
<div class="ImportPanel-header">
<div class="LayoutIcon u-bSpace--xl">
<i class="CDB-IconFont CDB-IconFont-gift"></i>
</div>
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">Box <%- _t('components.modals.add-layer.imports.connector') %></h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor u-bSpace--xl">
<%- _t('components.modals.add-layer.imports.box.fallback-desc', { brand: 'Box' }) %>
</p>
<a href="mailto:sales@carto.com?subject=<%- _t('components.modals.add-layer.imports.demo-email-title', { name: 'Box' }) %>&body=<%- _t('components.modals.add-layer.imports.demo-email-desc', { name: 'Box' }) %>" class="CDB-Button CDB-Button--primary CDB-Button--medium">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase"><%- _t('components.modals.add-layer.imports.ask-for-demo') %></span>
</a>
</div>
@@ -0,0 +1,10 @@
<div class="ImportPanel-header">
<div class="LayoutIcon u-bSpace--xl">
<i class="CDB-IconFont CDB-IconFont-gift"></i>
</div>
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">Instagram <%- _t('components.modals.add-layer.imports.connector') %></h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor u-bSpace--xl"><%- _t('components.modals.add-layer.imports.instagram.fallback-desc', { brand: 'Instagram' }) %></p>
<a href="mailto:sales@carto.com?subject=<%- _t('components.modals.add-layer.imports.demo-email-title', { name: 'Instagram' }) %>&body=<%- _t('components.modals.add-layer.imports.demo-email-desc', { name: 'Instagram' }) %>" class="CDB-Button CDB-Button--primary CDB-Button--medium">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase"><%- _t('components.modals.add-layer.imports.ask-for-demo') %></span>
</a>
</div>
@@ -0,0 +1,10 @@
<div class="ImportPanel-header">
<div class="LayoutIcon u-bSpace--xl">
<i class="CDB-IconFont CDB-IconFont-gift"></i>
</div>
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">MailChimp <%- _t('components.modals.add-layer.imports.connector') %></h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor u-bSpace--xl"><%- _t('components.modals.add-layer.imports.mailchimp.fallback-desc', { brand: 'Mailchimp' }) %></p>
<a href="mailto:sales@carto.com?subject=<%- _t('components.modals.add-layer.imports.demo-email-title', { name: 'MailChimp' }) %>&body=<%- _t('components.modals.add-layer.imports.demo-email-desc', { name: 'MailChimp' }) %>" class="CDB-Button CDB-Button--primary CDB-Button--medium">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase"><%- _t('components.modals.add-layer.imports.ask-for-demo') %></span>
</a>
</div>
@@ -0,0 +1,12 @@
<div class="ImportPanel-header">
<div class="LayoutIcon u-bSpace--xl">
<i class="CDB-IconFont CDB-IconFont-gift"></i>
</div>
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">Salesforce <%- _t('components.modals.add-layer.imports.connector') %></h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor u-bSpace--xl">
<%- _t('components.modals.add-layer.imports.salesforce.fallback-desc', { brand: 'Salesforce' }) %>
</p>
<a href="mailto:sales@carto.com?subject=<%- _t('components.modals.add-layer.imports.demo-email-title', { name: 'Salesforce' }) %>&body=<%- _t('components.modals.add-layer.imports.demo-email-desc', { name: 'Salesforce' }) %>" class="CDB-Button CDB-Button--primary CDB-Button--medium">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase"><%- _t('components.modals.add-layer.imports.contact-us') %></span>
</a>
</div>
@@ -0,0 +1,12 @@
<div class="ImportPanel-header">
<div class="LayoutIcon u-bSpace--xl">
<i class="CDB-IconFont CDB-IconFont-gift"></i>
</div>
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m"><%- _t('components.modals.add-layer.imports.twitter.fallback-title', { brand: 'Twitter' }) %></h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor u-bSpace--xl">
<%- _t('components.modals.add-layer.imports.twitter.fallback-desc', { brand: 'Twitter' }) %>
</p>
<a href="mailto:sales@carto.com?subject=<%- _t('components.modals.add-layer.imports.demo-email-title', { name: 'Twitter' }) %>&body=<%- _t('components.modals.add-layer.imports.demo-email-desc', { name: 'Twitter' }) %>" class="CDB-Button CDB-Button--primary CDB-Button--medium">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">ask for a demo</span>
</a>
</div>
@@ -0,0 +1,23 @@
<form class="Form js-form">
<div class="Form-row">
<div class="Form-rowLabel">
<label class="CDB-Text CDB-Size-medium"><%- _t('components.modals.add-layer.imports.form-import.title') %></label>
</div>
<div class="Form-rowData Form-rowData--longer">
<input type="text" class="CDB-Text CDB-Size-medium Form-input Form-input--longer has-submit js-textInput" value="" placeholder="<%= _t('components.modals.add-layer.imports.arcgis.input-placeholder', { brand: 'ArcGIS Server&trade;' }) %>" />
<button type="submit" class="CDB-Text CDB-Size-small Form-inputSubmit u-upperCase u-actionTextColor Form-inputSubmit">
<span><%- _t('components.modals.add-layer.imports.form-import.submit') %></span>
</button>
<div class="Form-inputError CDB-Text"><%- _t('components.modals.add-layer.imports.form-import.error-desc') %></div>
</div>
</div>
<div class="Form-row">
<div class="Form-rowLabel"></div>
<div class="Form-rowData Form-rowData--longer">
<p class="CDB-Text CDB-Size-small Form-rowInfoText--centered Form-rowInfoText--block u-altTextColor">
<%- _t('components.modals.add-layer.imports.form-import.format') %>: http://&#60;host&#62;/arcgis/rest/services/&#60;folder&#62;/&#60;serviceName&#62;/&#60;serviceType&#62;<br/>
<%- _t('components.modals.add-layer.imports.arcgis.url-desc') %>
</p>
</div>
</div>
</form>
@@ -0,0 +1,19 @@
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">
<% if (state === 'selected') { %>
<%= _t('components.modals.add-layer.imports.header-import.type-selected', { brand: 'ArcGIS<sup>&trade;</sup>' }) %>
<% } else { %>
<%= _t('components.modals.add-layer.imports.header-import.type-import', { brand: 'ArcGIS<sup>&trade;</sup>' }) %>
<% } %>
</h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor">
<% if (state !== "selected") { %>
<%= _t('components.modals.add-layer.imports.arcgis.import-data', { brand: 'ArcGIS<sup>&trade;</sup>' }) %>
<% } else { %>
<%- _t('components.modals.add-layer.imports.arcgis.sync-options') %>
<% } %>
</p>
<% if (state === "selected") { %>
<button class="NavButton NavButton--back ImportPanel-headerButton js-back">
<i class="CDB-IconFont CDB-IconFont-arrowPrev"></i>
</button>
<% } %>
@@ -0,0 +1,48 @@
var Utils = require('builder/helpers/utils');
var SelectedDatasetView = require('builder/components/modals/add-layer/content/imports/import-selected-dataset-view');
var template = require('builder/components/modals/add-layer/content/imports/import-selected-dataset.tpl');
/**
* Selected ArcGIS dataset
*
* - Displays the result when an ArcGIS url/dataset is selected, no matter the type.
* - It will show available sync options if user can and the url is an ArcGIS layer.
* - Upgrade link for people who don't have sync permissions.
*
*/
module.exports = SelectedDatasetView.extend({
render: function () {
var title = this.options.fileAttrs.title && this.model.get('value')[this.options.fileAttrs.title] || this.model.get('value');
var description = this._genDescription();
var ext = this.options.fileAttrs.ext ? Utils.getFileExtension(title) : '';
if (this.options.fileAttrs.ext) {
title = title && title.replace('.' + ext, '');
}
var upgradeUrl = window.upgrade_url;
var userCanSync = this._userModel.isActionEnabled('sync_tables');
var customInstall = this._configModel.get('cartodb_com_hosted');
this.$el.html(
template({
title: title,
description: description,
ext: ext,
interval: this.model.get('interval'),
importCanSync: this.options.acceptSync && this._isArcGISLayer(title),
userCanSync: userCanSync,
showTrial: this._userModel.canStartTrial(),
showUpgrade: !userCanSync && !customInstall && upgradeUrl && !this._userModel.isInsideOrg(),
upgradeUrl: upgradeUrl
})
);
return this;
},
_isArcGISLayer: function (url) {
return url.search(/([0-9]+\/|[0-9]+)/) !== -1;
}
});
@@ -0,0 +1,64 @@
var FormView = require('builder/components/modals/add-layer/content/imports/import-data/import-data-form-view');
var HeaderView = require('builder/components/modals/add-layer/content/imports/import-data/import-data-header-view');
var SelectedDatasetView = require('./import-arcgis-selected-dataset-view');
var ImportDataView = require('builder/components/modals/add-layer/content/imports/import-data/import-data-view');
var headerTemplate = require('./import-arcgis-header.tpl');
var formTemplate = require('./import-arcgis-form.tpl');
/**
* Import ArcGIS panel
*
* - It only accepts an url, and it could be a map or a layer.
*
*/
module.exports = ImportDataView.extend({
options: {
fileExtensions: [],
type: 'service',
service: 'arcgis',
acceptSync: true,
fileEnabled: false,
fileAttrs: {
ext: false,
title: '',
description: ''
}
},
_initViews: function () {
var headerView = new HeaderView({
el: this.$('.ImportPanel-header'),
model: this.model,
userModel: this._userModel,
collection: this.collection,
fileEnabled: this.options.fileEnabled,
acceptSync: this.options.acceptSync,
template: headerTemplate
});
headerView.render();
this.addView(headerView);
var selected = new SelectedDatasetView({
el: this.$('.DatasetSelected'),
userModel: this._userModel,
model: this.model,
acceptSync: this.options.acceptSync,
fileAttrs: this.options.fileAttrs,
configModel: this._configModel
});
selected.render();
this.addView(selected);
var formView = new FormView({
el: this.$('.ImportPanel-form'),
userModel: this._userModel,
model: this.model,
template: formTemplate,
fileEnabled: this.options.fileEnabled
});
formView.render();
this.addView(formView);
}
});
@@ -0,0 +1,223 @@
require('dragster');
var Dropzone = require('dropzone');
var $ = require('jquery');
var CoreView = require('backbone/core-view');
var template = require('./import-data-form.tpl');
var checkAndBuildOpts = require('builder/helpers/required-opts');
/**
* Form view for url import for example
*
* - It accepts an url
* - It checks if it is valid
* - It could have a file option
*
*/
var REQUIRED_OPTS = [
'userModel'
];
module.exports = CoreView.extend({
options: {
template: '',
fileEnabled: false
},
events: {
'keyup .js-textInput': '_onTextChanged',
'submit .js-form': '_onSubmitForm'
},
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this.template = opts.template || template;
this._initBinds();
this._checkVisibility();
},
render: function () {
this.$el.html(
this.template(this.options)
);
this._initViews();
return this;
},
_initBinds: function () {
this.model.bind('change:state', this._checkVisibility, this);
},
_initViews: function () {
if (this.options.fileEnabled) {
var self = this;
this.$('.js-fileInput').bind('change', function (e) {
if (this.files && this.files.length > 0) {
self._onFileChanged(this.files);
}
this.value = '';
});
this._initDropzone();
}
},
_initDropzone: function () {
var el = $('html')[0]; // :(
var self = this;
this.dragster = new Dragster(el); // eslint-disable-line
$(el).bind('dragster:enter', function (e) {
self._showDropzone();
});
$(el).bind('dragster:leave', function (e) {
self._hideDropzone();
});
if (el.dropzone) { // avoid loading the dropzone twice
el.dropzone.destroy();
}
this.dropzone = new Dropzone(el, {
url: ':)',
autoProcessQueue: false,
previewsContainer: false
});
this.dropzone.on('dragover', function () {
self._showDropzone();
});
this.dropzone.on('drop', function (ev) {
var files = ev.dataTransfer.files;
self._onFileChanged(files);
self._hideDropzone();
});
},
_destroyDropzone: function () {
var el = $('html')[0]; // :(
if (this.dragster) {
this.dragster.removeListeners();
this.dragster.reset();
$(el).unbind('dragster:enter dragster:leave');
}
if (this.dropzone) {
this.dropzone.destroy();
}
},
_setValidFileExtensions: function (list) {
return RegExp('(\.|\/)(' + list.join('|') + ')$', 'i');
},
_onTextChanged: function () {
var value = this.$('.js-textInput').val();
if (!value) {
this._hideTextError();
}
},
_onFileChanged: function (files) {
this.trigger('fileSelected', this);
if (files && files.length === 1) {
files = files[0];
}
this.model.setUpload({
type: 'file',
value: files
});
if (this.model.get('state') !== 'error') {
this._hideFileError();
this.model.set('state', 'selected');
} else {
this._showFileError();
}
},
_showTextError: function () {
this.$('.Form-inputError').addClass('is-visible');
},
_hideTextError: function () {
this.$('.Form-inputError').removeClass('is-visible');
},
_showDropzone: function () {
this.$('.Form-upload').addClass('is-dropping');
this._hideFileError();
},
_hideDropzone: function () {
this.$('.Form-upload').removeClass('is-dropping');
},
_showFileError: function () {
if (this.model.get('state') === 'error') {
this.$('.js-fileError')
.text(this.model.get('get_error_text').what_about)
.show();
this.$('.js-fileLabel').hide();
this.$('.js-fileButton').addClass('Button--negative');
}
},
_hideFileError: function () {
this.$('.js-fileError').hide();
this.$('.js-fileLabel').show();
this.$('.js-fileButton').removeClass('Button--negative');
},
_onSubmitForm: function (e) {
if (e) this.killEvent(e);
var value = this.$('.js-textInput').val();
if (!value) {
this._hideTextError();
return;
}
// Change file attributes :S
this.trigger('urlSelected', this);
// Change model
var importType = this.model.get('service_name') ? 'service' : 'url';
this.model.setUpload({
type: importType,
value: value,
service_item_id: value,
state: 'idle'
});
if (this.model.get('state') !== 'error') {
this._hideFileError();
this._hideTextError();
this.model.set('state', 'selected');
this.trigger('urlSubmitted', this);
} else {
this._showTextError();
}
},
_checkVisibility: function () {
var state = this.model.get('state');
this[ state !== 'selected' ? 'show' : 'hide' ]();
},
clean: function () {
this._destroyDropzone();
this.$('.js-fileInput').unbind('change');
CoreView.prototype.clean.apply(this);
}
});
@@ -0,0 +1,26 @@
<form class="Form js-form">
<div class="Form-row Form-row--centered">
<% if (fileEnabled) { %>
<div class="Form-rowData Form-rowData--med Form-rowData--noMargin js-dropzone">
<div class="Form-upload">
<label class="Form-fileLabel js-fileLabel CDB-Text CDB-Size-medium"><%- _t('components.modals.add-layer.imports.form-import.drag-and-drop') %></label>
<label class="Form-fileLabel Form-fileLabel--error CDB-Text CDB-Size-small js-fileError"></label>
<div class="Form-file">
<input type="file" class="js-fileInput" />
<span class="CDB-Button CDB-Button--primary Form-fileButton CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase js-fileButton">
<%- _t('components.modals.add-layer.imports.form-import.browse') %>
</span>
</div>
</div>
</div>
<span class="u-lSpace--xl u-rSpace--xl u-flex u-alignCenter CDB-Text CDB-Size-medium u-altTextColor"><%- _t('components.modals.add-layer.imports.form-import.or') %></span>
<% } %>
<div class="Form-rowData Form-rowData--noMargin Form-rowData--med">
<input type="text" class="Form-input Form-input--med has-submit js-textInput CDB-Text CDB-Size-medium" value="" placeholder="https://carto.com/data-library" />
<button type="submit" class="CDB-Text CDB-Size-small Form-inputSubmit u-upperCase u-actionTextColor Form-inputSubmit">
<span><%- _t('components.modals.add-layer.imports.form-import.submit') %></span>
</button>
<div class="Form-inputError CDB-Text"><%- _t('components.modals.add-layer.imports.form-import.error-desc') %></div>
</div>
</div>
</form>
@@ -0,0 +1,58 @@
var CoreView = require('backbone/core-view');
var template = require('./import-data-header.tpl');
/**
* Data header view
*
* - It will change when upload state changes
* - Possibility to change state with a header button
*
*/
module.exports = CoreView.extend({
events: {
'click .js-back': '_goToStart'
},
options: {
fileEnabled: false,
acceptSync: false
},
initialize: function (opts) {
if (!opts.userModel) throw new Error('userModel is required');
this._userModel = opts.userModel;
this.template = opts.template || template;
this._initBinds();
this._checkVisibility();
},
render: function () {
var acceptSync = this.options.acceptSync && this._userModel.get('actions') && this._userModel.isActionEnabled('sync_tables') && this.model.get('type') !== 'file';
this.$el.html(
this.template({
type: this.model.get('type'),
fileEnabled: this.options.fileEnabled,
acceptSync: acceptSync,
state: this.model.get('state')
})
);
this._checkVisibility();
return this;
},
_initBinds: function () {
this.model.bind('change:state', this.render, this);
},
_checkVisibility: function () {
this.show();
},
_goToStart: function () {
this.model.set('state', 'idle');
}
});
@@ -0,0 +1,31 @@
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">
<% if (state === 'selected') { %>
<%- _t('components.modals.add-layer.imports.header-import.file-selected') %>
<% } else { %>
<%- _t('components.modals.add-layer.imports.header-import.upload-file-url', { smart_count: fileEnabled ? 1 : 0 }) %>
<% } %>
</h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor">
<% if (state !== "selected") { %>
<% fileEnabledText = _t('components.modals.add-layer.imports.header-import.select-a-file') +
' <a target="_blank" href="https://carto.com/developers/import-api/guides/importing-geospatial-data/#supported-geospatial-data-formats">' +
_t('components.modals.add-layer.imports.header-import.see-all-formats') +
'</a>'
%>
<%= _t('components.modals.add-layer.imports.header-import.paste-url', {
fileEnabled: fileEnabled ? fileEnabledText : ''
}) %>
<% } %>
<% if (state === "selected") { %>
<% if (acceptSync) { %>
<%- _t('components.modals.add-layer.imports.header-import.sync-enabled') %>
<% } else { %>
<%- _t('components.modals.add-layer.imports.header-import.sync-disabled') %>
<% } %>
<% } %>
</p>
<% if (state === "selected") { %>
<button class="NavButton NavButton--back ImportPanel-headerButton js-back">
<i class="CDB-IconFont CDB-IconFont-arrowPrev"></i>
</button>
<% } %>
@@ -0,0 +1,161 @@
var ImportView = require('builder/components/modals/add-layer/content/imports/import-view');
var UploadModel = require('builder/data/upload-model');
var FormView = require('./import-data-form-view');
var HeaderView = require('./import-data-header-view');
var SelectedDatasetView = require('builder/components/modals/add-layer/content/imports/import-selected-dataset-view');
var template = require('./import-data.tpl');
var checkAndBuildOpts = require('builder/helpers/required-opts');
/**
* Import data panel
*
* - It accepts an url
* - It checks if it is valid
*
*/
var REQUIRED_OPTS = [
'userModel',
'configModel',
'createModel',
'privacyModel',
'guessingModel'
];
module.exports = ImportView.extend({
options: {
fileExtensions: [],
type: 'url',
service: '',
acceptSync: false,
fileEnabled: false,
formTemplate: '',
headerTemplate: '',
fileAttrs: {}
},
className: 'ImportPanel ImportDataPanel',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initModels();
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this._initViews();
this._initBinds();
return this;
},
_initModels: function () {
this.model = new UploadModel({
type: this.options.type,
service_name: this.options.service
}, {
userModel: this._userModel,
configModel: this._configModel
});
},
_initViews: function () {
this.model.setFresh();
var headerView = new HeaderView({
el: this.$('.ImportPanel-header'),
model: this.model,
userModel: this._userModel,
fileEnabled: this.options.fileEnabled,
acceptSync: this.options.acceptSync,
template: this.options.headerTemplate
});
headerView.render();
this.addView(headerView);
var selected = new SelectedDatasetView({
el: this.$('.DatasetSelected'),
userModel: this._userModel,
model: this.model,
acceptSync: this.options.acceptSync,
fileAttrs: this.options.fileAttrs,
configModel: this._configModel
});
selected.render();
this.addView(selected);
var formView = new FormView({
el: this.$('.ImportPanel-form'),
userModel: this._userModel,
model: this.model,
template: this.options.formTemplate,
fileEnabled: this.options.fileEnabled
});
formView.bind('fileSelected', function () {
selected.setOptions({
acceptSync: false,
fileAttrs: {
ext: true,
title: 'name',
description: {
content: [{
name: 'size',
format: 'size'
}]
}
}
});
});
formView.bind('urlSelected', function () {
selected.setOptions({
acceptSync: true,
fileAttrs: {
ext: false,
title: '',
description: ''
}
});
});
formView.bind('urlSubmitted', function () {
this._finish();
}.bind(this));
formView.render();
this.addView(formView);
},
_finish: function () {
if (this._createModel.canFinish()) {
this.model.setPrivacy(this._privacyModel.get('privacy'));
this.model.setGuessing(this._guessingModel.get('guessing'));
this._createModel.finish();
}
},
_initBinds: function () {
this.model.unbind('change:state', this._checkState, this);
this.model.unbind('change', this._triggerChange, this);
this.model.bind('change:state', this._checkState, this);
this.model.bind('change', this._triggerChange, this);
},
_checkState: function () {
if (this.model.previous('state') === 'selected') {
this.model.set({
type: undefined,
value: '',
service_name: '',
service_item_id: '',
interval: 0
});
}
}
});
@@ -0,0 +1,10 @@
<div class="ImportPanel-header">
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m"><%- _t('components.modals.add-layer.imports.form-import.title') %></h3>
<p class="ImportPanel-headerDescription"><%- _t('components.modals.add-layer.imports.form-import.desc') %></p>
</div>
<div class="ImportPanel-body">
<div class="ImportPanel-bodyWrapper">
<div class="ImportPanel-state ImportPanel-form is-idle"></div>
<div class="ImportPanel-state is-selected DatasetSelected"></div>
</div>
</div>
@@ -0,0 +1,34 @@
<% if (state !== 'list' ) { %>
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">
<% if (state === 'selected') { %>
<%- _t('components.modals.add-layer.imports.mailchimp.campaign-selected', { brand: 'MailChimp' }) %>
<% } else { %>
<%- _t('components.modals.add-layer.imports.mailchimp.map-campaign', { brand: 'MailChimp' }) %>
<% } %>
</h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor <% if (state === "error") { %>ImportPanel-headerDescription--negative<% } %>">
<% if (state === "idle") { %>
<%- _t('components.modals.add-layer.imports.mailchimp.state-idle', { brand: 'MailChimp' }) %>
<% } %>
<% if (state === "error") { %>
<%- _t('components.modals.add-layer.imports.mailchimp.state-error', { brand: 'MailChimp' }) %>
<% } %>
<% if (state === "token") { %>
<%- _t('components.modals.add-layer.imports.mailchimp.state-token', { brand: 'MailChimp' }) %>
<% } %>
<% if (state === "oauth") { %>
<%- _t('components.modals.add-layer.imports.mailchimp.state-oauth', { brand: 'MailChimp' }) %>
<% } %>
<% if (state === "retrieving") { %>
<%- _t('components.modals.add-layer.imports.mailchimp.state-retrieving', { brand: 'MailChimp' }) %>
<% } %>
<% if (state === "selected") { %>
<%- _t('components.modals.add-layer.imports.mailchimp.state-selected', { brand: 'MailChimp' }) %>
<% } %>
</p>
<% if (state === "selected") { %>
<button class="NavButton NavButton--back ImportPanel-headerButton js-back">
<i class="CDB-IconFont CDB-IconFont-arrowPrev"></i>
</button>
<% } %>
<% } %>
@@ -0,0 +1,187 @@
var ImportDataView = require('./import-data/import-data-view');
var ImportServiceView = require('./import-service/import-service-view');
var ImportArcGISView = require('./import-arcgis/import-arcgis-view');
var ImportTwitterView = require('./import-twitter/import-twitter-view');
/**
* Attributes:
*
* view: import pane class view
* enabled: function that takes configModel and returns whether the service is enabled
* fallbackClassName: ...
* name: local name
* title: text for tab link
* options:
* - service:
* - fileExtensions:
* - showAvailableFormats:
* - acceptSync:
* - fileAttrs:
*
*/
module.exports = {
File: {
view: ImportDataView,
enabled: function (config, userModel) { return true; },
name: 'file',
title: 'Data file',
options: {
type: 'url',
fileEnabled: true,
acceptSync: true
}
},
GDrive: {
view: ImportServiceView,
enabled: function (config, userModel) { return !!config.get('oauth_gdrive'); },
name: 'gdrive',
title: 'Google Drive',
options: {
service: 'gdrive',
fileExtensions: ['Google SpreadSheet', 'CSV'],
showAvailableFormats: false,
acceptSync: true,
fileAttrs: {
ext: true,
title: 'filename',
description: {
content: [{
name: 'size',
format: 'size',
key: true
}]
}
}
}
},
Dropbox: {
view: ImportServiceView,
enabled: function (config, userModel) { return !!config.get('oauth_dropbox'); },
name: 'dropbox',
title: 'Dropbox',
options: {
service: 'dropbox',
fileExtensions: ['CSV', 'XLS'],
showAvailableFormats: false,
acceptSync: true,
fileAttrs: {
ext: true,
title: 'filename',
description: {
content: [
{
name: 'id',
format: ''
},
{
name: 'size',
format: 'size',
key: true
}
],
separator: '-'
}
}
}
},
Box: {
view: ImportServiceView,
enabled: function (config, userModel) { return !!config.get('oauth_box'); },
name: 'box',
title: 'Box',
fallback: require('./fallbacks/import-box-fallback.tpl'),
options: {
service: 'box',
fileExtensions: ['CSV', 'XLS'],
showAvailableFormats: false,
acceptSync: true,
fileAttrs: {
ext: true,
title: 'filename',
description: {
content: [
{
name: 'size',
format: 'size',
key: true
}
],
separator: '-'
}
}
}
},
Twitter: {
view: ImportTwitterView,
enabled: function (config, userModel) { return userModel.get('twitter').enabled && !!config.get('datasource_search_twitter'); },
fallback: require('./fallbacks/import-twitter-fallback.tpl'),
name: 'twitter',
title: 'Twitter'
},
Mailchimp: {
view: ImportServiceView,
enabled: function (config, userModel) { return userModel.get('mailchimp').enabled && !!config.get('oauth_mailchimp'); },
fallback: require('./fallbacks/import-mailchimp-fallback.tpl'),
name: 'mailchimp',
title: 'MailChimp',
options: {
service: 'mailchimp',
fileExtensions: [],
acceptSync: true,
showAvailableFormats: false,
headerTemplate: require('./import-mailchimp/import-data-header-mailchimp.tpl'),
fileAttrs: {
ext: true,
title: 'filename',
description: {
content: [{
name: 'member_count',
format: 'number',
key: true
}],
itemName: 'member',
separator: ''
}
}
}
},
// Instagram: {
// view: ImportServiceView,
// fallback: require('./fallbacks/import-instagram-fallback.tpl'),
// name: 'instagram',
// title: 'Instagram',
// options: {
// service: 'instagram',
// fileExtensions: [],
// acceptSync: false,
// showAvailableFormats: false,
// fileAttrs: {
// ext: false,
// title: 'title'
// }
// }
// },
Arcgis: {
view: ImportArcGISView,
enabled: function (config, userModel) { return config.get('arcgis_enabled'); },
fallback: require('./fallbacks/import-arcgis-fallback.tpl'),
name: 'arcgis',
title: 'ArcGIS Server&trade;'
},
Salesforce: {
view: ImportDataView,
enabled: function (config, userModel) { return config.get('salesforce_enabled'); },
fallback: require('./fallbacks/import-salesforce-fallback.tpl'),
name: 'salesforce',
title: 'Salesforce'
// options: {
// type: 'service',
// service_name: 'salesforce',
// acceptSync: true,
// formTemplate: require('./import-salesforce/import-data-form-salesforce.tpl'),
// headerTemplate: require('./import-salesforce/import-data-header-salesforce.tpl')
// }
}
};
@@ -0,0 +1,16 @@
<form class="Form js-form">
<div class="Form-row">
<div class="Form-rowLabel">
<label class="CDB-Text CDB-Size-medium"><span><%- _t('components.modals.add-layer.imports.form-import.title') %></span></label>
</div>
<div class="Form-rowData Form-rowData--longer">
<input type="text" class="CDB-Text CDB-Size-medium Form-input Form-input--longer has-submit js-textInput" value="" placeholder="<%- _t('components.modals.add-layer.imports.salesforce.salesforce.input-placeholder', { brand: 'Salesforce' }) %>" />
<button type="submit" class="CDB-Text CDB-Size-small Form-inputSubmit u-upperCase u-actionTextColor Form-inputSubmit">
<span><%- _t('components.modals.add-layer.imports.form-import.submit') %></span>
</button>
<div class="Form-inputError CDB-Text">
<span><%- _t('components.modals.add-layer.imports.form-import.error-desc') %></span>
</div>
</div>
</div>
</form>
@@ -0,0 +1,23 @@
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">
<% if (state === 'selected') { %>
<%- _t('components.modals.add-layer.imports.header-import.type-selected', { brand: 'Salesforce'}) %>
<% } else { %>
<%- _t('components.modals.add-layer.imports.header-import.type-import', { brand: 'Salesforce'}) %>
<% } %>
</h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor">
<% if (state !== "selected") { %>
<%- _t('components.modals.add-layer.imports.header-import.import-url', { brand: 'Salesforce'}) %>
<% } else { %>
<% if (acceptSync) { %>
<%- _t('components.modals.add-layer.imports.header-import.sync-enabled', { brand: 'Salesforce'}) %>
<% } else { %>
<%- _t('components.modals.add-layer.imports.header-import.sync-disabled', { brand: 'Salesforce'}) %>
<% } %>
<% } %>
</p>
<% if (state === "selected") { %>
<button class="NavButton NavButton--back ImportPanel-headerButton js-back">
<i class="CDB-IconFont CDB-IconFont-arrowPrev"></i>
</button>
<% } %>
@@ -0,0 +1,168 @@
var CoreView = require('backbone/core-view');
var _ = require('underscore');
var Utils = require('builder/helpers/utils');
var template = require('./import-selected-dataset.tpl');
/**
* Selected dataset
*
* - Displays the result when a dataset is selected, no matter the type.
* - It will show available sync options if that import lets it.
* - Upgrade link for people who don't have sync permissions.
*
*/
module.exports = CoreView.extend({
className: 'DatasetSelected',
_FORMATTERS: {
'size': Utils.readablizeBytes,
'number': Utils.formatNumber
},
options: {
acceptSync: false,
fileAttrs: {
ext: false,
title: '',
description: {
content: [{
name: 'id',
format: ''
}],
itemName: '',
separator: ''
}
}
},
events: {
'click .js-interval-0': '_onIntervalZero',
'click .js-interval-1': '_onIntervalHour',
'click .js-interval-2': '_onIntervalDay',
'click .js-interval-3': '_onIntervalWeek',
'click .js-interval-4': '_onIntervalMonth'
},
initialize: function (opts) {
if (!opts.userModel) throw new TypeError('userModel is required');
if (!opts.configModel) throw new TypeError('configModel is required');
this._configModel = opts.configModel;
this._userModel = opts.userModel;
this._initBinds();
this._checkVisibility();
},
render: function () {
var title = this.options.fileAttrs.title && this.model.get('value')[this.options.fileAttrs.title] || this.model.get('value');
var description = this._genDescription();
var ext = this.options.fileAttrs.ext ? Utils.getFileExtension(title) : '';
if (this.options.fileAttrs.ext) {
title = title && title.replace('.' + ext, '');
}
var upgradeUrl = window.upgrade_url;
var userCanSync = this._userModel.isActionEnabled('sync_tables');
var customInstall = this._configModel.get('cartodb_com_hosted');
this.$el.html(
template({
title: title,
description: description,
ext: ext,
interval: this.model.get('interval'),
importCanSync: this.options.acceptSync,
userCanSync: userCanSync,
showTrial: this._userModel.canStartTrial(),
showUpgrade: !userCanSync && !customInstall && upgradeUrl && !this._userModel.isInsideOrg(),
upgradeUrl: upgradeUrl
})
);
return this;
},
_initBinds: function () {
this.model.bind('change:value', this.render, this);
this.model.bind('change:interval', this.render, this);
this.model.bind('change:state', this._checkVisibility, this);
},
_genDescription: function () {
if (this.options.fileAttrs && this.options.fileAttrs.description) {
var descriptionOpts = this.options.fileAttrs.description;
var descriptionKeyValue = '';
var descriptionStr = '';
var self = this;
if (descriptionOpts.content && descriptionOpts.content.length > 0) {
_.each(descriptionOpts.content, function (item, i) {
if (i > 0 && descriptionOpts.separator) {
descriptionStr += ' ' + descriptionOpts.separator + ' ';
}
var value = self.model.get('value')[item.name];
var format = item.format && self._FORMATTERS[item.format];
descriptionStr += format && format(value) || value;
if (item.key) {
descriptionKeyValue = item.name;
}
});
}
if (descriptionOpts.itemName && descriptionKeyValue) {
descriptionStr += ' ' + (descriptionOpts.itemName && _t('components.modals.add-layer.imports.' + descriptionOpts.itemName + '-pluralize', { smart_count: descriptionKeyValue }) || '');
}
return descriptionStr;
}
return '';
},
_onIntervalZero: function () {
this.model.set('interval', 0);
},
_onIntervalHour: function () {
if (this.options.acceptSync && this._userModel.isActionEnabled('sync_tables')) {
this.model.set('interval', 3600);
}
},
_onIntervalDay: function () {
if (this.options.acceptSync && this._userModel.isActionEnabled('sync_tables')) {
this.model.set('interval', 86400);
}
},
_onIntervalWeek: function () {
if (this.options.acceptSync && this._userModel.isActionEnabled('sync_tables')) {
this.model.set('interval', 604800);
}
},
_onIntervalMonth: function () {
if (this.options.acceptSync && this._userModel.isActionEnabled('sync_tables')) {
this.model.set('interval', 2592000);
}
},
setOptions: function (d) {
if (d && !_.isEmpty(d)) {
_.extend(this.options, d);
}
},
_checkVisibility: function () {
var state = this.model.get('state');
if (state === 'selected') {
this.show();
} else {
this.hide();
}
}
});
@@ -0,0 +1,66 @@
<div class="DatasetSelected-item">
<div class="DatasetSelected-itemExt CDB-Text u-ellipsis">
<%- ext || '?' %>
</div>
<div class="DatasetSelected-itemInfo u-ellipsis">
<h6 class="CDB-Text CDB-Size-large u-ellipsis" title="<%- title %>"><%- title %></h6>
<p class="CDB-Text CDB-Size-small u-ellipsis u-altTextColor"><%- description %></p>
</div>
</div>
<% if (importCanSync) { %>
<div class="DatasetSelected-sync">
<div class="DatasetSelected-syncOptions">
<label class="DatasetSelected-syncLabel CDB-Text CDB-Size-medium u-mainTextColor">
<%- _t('components.modals.add-layer.imports.selected-state.sync-my-data') %>
</label>
<ul class="DatasetSelected-syncOptionsList">
<li class="DatasetSelected-syncOptionsItem">
<div class="RadioButton">
<button class="RadioButton-input js-interval-0 <%- interval === 0 ? 'is-checked' : '' %>"></button>
<label class="CDB-Text CDB-Size-medium u-altTextColor u-lSpace"><%- _t('components.modals.add-layer.imports.selected-state.never') %></label>
</div>
</li>
<li class="DatasetSelected-syncOptionsItem">
<div class="RadioButton <%- !userCanSync ? 'is-disabled' : '' %>">
<button class="RadioButton-input js-interval-1 <%- interval === 3600 ? 'is-checked' : '' %>"></button>
<label class="CDB-Text CDB-Size-medium u-altTextColor u-lSpace"><%- _t('components.modals.add-layer.imports.selected-state.every-hour') %></label>
</div>
</li>
<li class="DatasetSelected-syncOptionsItem">
<div class="RadioButton <%- !userCanSync ? 'is-disabled' : '' %>">
<button class="RadioButton-input js-interval-2 <%- interval === 86400 ? 'is-checked' : '' %>"></button>
<label class="CDB-Text CDB-Size-medium u-altTextColor u-lSpace"><%- _t('components.modals.add-layer.imports.selected-state.every-day') %></label>
</div>
</li>
<li class="DatasetSelected-syncOptionsItem">
<div class="RadioButton <%- !userCanSync ? 'is-disabled' : '' %>">
<button class="RadioButton-input js-interval-3 <%- interval === 604800 ? 'is-checked' : '' %>"></button>
<label class="CDB-Text CDB-Size-medium u-altTextColor u-lSpace"><%- _t('components.modals.add-layer.imports.selected-state.every-week') %></label>
</div>
</li>
<li class="DatasetSelected-syncOptionsItem">
<div class="RadioButton <%- !userCanSync ? 'is-disabled' : '' %>">
<button class="RadioButton-input js-interval-4 <%- interval === 2592000 ? 'is-checked' : '' %>"></button>
<label class="CDB-Text CDB-Size-medium u-altTextColor u-lSpace"><%- _t('components.modals.add-layer.imports.selected-state.every-month') %></label>
</div>
</li>
</ul>
</div>
<% if (showUpgrade) { %>
<div class="Upgrade-info">
<p class="CDB-Text CDB-Size-medium u-ellipsis u-secondaryTextColor">
<% featuresLink = '<a href="https://carto.com/pricing">' + _t('components.modals.add-layer.imports.selected-state.more-features') + '</a>' %>
<%- _t('components.modals.add-layer.imports.selected-state.upgrade-desc', { features: featuresLink }) %>
</p>
<div class="Upgrade-infoActions">
<% if (showTrial) { %>
<p class="CDB-Text CDB-Size-medium u-ellipsis is-semibold u-rSpace--xl"><%- _t('components.modals.add-layer.imports.selected-state.free-trial', { days: 14 }) %></p>
<% } %>
<a href="<%- upgradeUrl %>" class="CDB-Button CDB-Button--secondary">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase"><%- _t('components.modals.add-layer.imports.selected-state.upgrade') %></span>
</a>
</div>
</div>
<% } %>
</div>
<% } %>
@@ -0,0 +1,61 @@
var CoreView = require('backbone/core-view');
var template = require('./import-service-header.tpl');
/**
* Service header
*
* - It will change when upload state changes
* - Possibility to change state with a header button
*
*/
module.exports = CoreView.extend({
events: {
'click .js-back': '_goToList'
},
options: {
title: 'Service',
showAvailableFormats: false,
acceptSync: false,
fileExtensions: [],
template: ''
},
initialize: function (opts) {
if (!opts.userModel) throw new Error('userModel is required');
this._userModel = opts.userModel;
this.template = opts.template || template;
this._initBinds();
},
render: function () {
this.$el.html(
this.template({
items: this.collection.size(),
service_name: this.model.get('service_name'),
showAvailableFormats: this.options.showAvailableFormats,
fileExtensions: this.options.fileExtensions,
acceptSync: this.options.acceptSync && this._userModel.isActionEnabled('sync_tables'),
state: this.model.get('state'),
title: this.options.title
})
);
this._checkVisibility();
return this;
},
_initBinds: function () {
this.model.bind('change:state', this.render, this);
},
_checkVisibility: function () {
var state = this.model.get('state');
this[ state !== 'list' ? 'show' : 'hide' ]();
},
_goToList: function () {
this.model.set('state', 'list');
}
});
@@ -0,0 +1,51 @@
<% if (state !== 'list' ) { %>
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">
<% if (state === 'selected') { %>
<% if (service_name === 'instagram') { %>
<%- _t('components.modals.add-layer.imports.service-import.account-connected') %>
<% } else { %>
<%- _t('components.modals.add-layer.imports.service-import.item-selected') %>
<% } %>
<% } else { %>
<%- _t('components.modals.add-layer.imports.service-import.connect-with', { title: title }) %>
<% } %>
</h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor <% if (state === "error") { %>ImportPanel-headerDescription--negative<% } %>">
<% if (state === "idle") { %>
<% if (fileExtensions.length > 0) { %>
<% formatsLink = _t('components.modals.add-layer.imports.service-import.and') + ' <a target="_blank" href="https://docs.carto.com/cartodb-editor/datasets/#supported-file-formats" class="ImportPanel-headerDescriptionLink">' + _t('components.modals.add-layer.imports.service-import.many-more-formats') + '</a>' %>
<%- fileExtensions.join(', ') %><% if (showAvailableFormats) { %> <%- formatsLink %> <% } %> <%- _t('components.modals.add-layer.imports.service-import.supported') %>.
<% } else { %>
<%- _t('components.modals.add-layer.imports.service-import.state-idle-login') %>
<% } %>
<% } %>
<% if (state === "error") { %>
<%- _t('components.modals.add-layer.imports.service-import.state-error', { title: title }) %>
<% } %>
<% if (state === "token") { %>
<%- _t('components.modals.add-layer.imports.service-import.state-token') %>
<% } %>
<% if (state === "oauth") { %>
<%- _t('components.modals.add-layer.imports.service-import.state-oauth') %>
<% } %>
<% if (state === "retrieving") { %>
<%- _t('components.modals.add-layer.imports.service-import.state-retrieving', { title: title }) %>
<% } %>
<% if (state === "selected") { %>
<% if (acceptSync) { %>
<%- _t('components.modals.add-layer.imports.service-import.state-selected-sync') %>
<% } else { %>
<% if (service_name === 'instagram') { %>
<%- _t('components.modals.add-layer.imports.service-import.state-selected-instagram') %>
<% } else { %>
<%- _t('components.modals.add-layer.imports.service-import.state-selected-no-sync') %>
<% } %>
<% } %>
<% } %>
</p>
<% if (state === "selected" && items > 1) { %>
<button class="CDB-Size-large ImportPanel-headerButton js-back">
<i class="CDB-IconFont CDB-IconFont-arrowPrev u-actionTextColor"></i>
</button>
<% } %>
<% } %>
@@ -0,0 +1,20 @@
var Utils = require('builder/helpers/utils');
/**
* Service list item format utils
*
* - Create customized functions for service list items.
*
*/
module.exports = {
// Due to the fact that backend data source service
// returns 0 size when it doesn't know it
formatSize: function (s) {
if (s && s > 0) {
return Utils.readablizeBytes(s);
} else {
return 'Unknown';
}
}
};
@@ -0,0 +1,17 @@
var Backbone = require('backbone');
/**
* Service item model
*
*/
module.exports = Backbone.Model.extend({
defaults: {
id: '',
filename: '',
checksum: '',
service: '',
size: '',
title: ''
}
});
@@ -0,0 +1,39 @@
var ServiceItem = require('./import-service-item-model');
var Backbone = require('backbone');
/**
* Service item model + Service items collection
*
* - It needs a datasource name or it won't work.
*
*/
module.exports = Backbone.Collection.extend({
_DATASOURCE_NAME: 'dropbox',
model: ServiceItem,
url: function (method) {
var version = this._configModel.urlVersion('imports_service');
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/' + version + '/imports/service/' + this._DATASOURCE_NAME + '/list_files';
},
initialize: function (coll, opts) {
if (!opts.configModel) throw new Error('configModel is required');
if (!opts.datasourceName) throw new Error('datasourceName is required');
this._configModel = opts.configModel;
this._DATASOURCE_NAME = opts.datasourceName;
},
fetch: function () {
this.trigger('fetch', this);
return Backbone.Collection.prototype.fetch.apply(this, arguments);
},
parse: function (r) {
return r.files;
}
});
@@ -0,0 +1,96 @@
var CoreView = require('backbone/core-view');
var Utils = require('builder/helpers/utils');
var ServiceUtilsFormat = require('./import-service-item-description-format');
var template = require('./import-service-list-item.tpl');
var _ = require('underscore');
/**
* Service list item view
*
* - Displays the item info.
* - It lets user to select the item for a future import.
*
*/
module.exports = CoreView.extend({
options: {
title: '',
fileAttrs: {
ext: false,
title: 'filename',
description: 'size',
itemName: 'file',
formatDescription: ''
}
},
_FORMATTERS: {
'size': ServiceUtilsFormat.formatSize,
'number': Utils.formatNumber
},
className: 'ServiceList-item',
tagName: 'li',
events: {
'click .js-choose': '_onSelectItem'
},
render: function () {
var title = this.model.get(this.options.fileAttrs.title);
var description = this._genDescription();
var ext = this.options.fileAttrs.ext ? Utils.getFileExtension(title) : '';
if (this.options.fileAttrs.ext) {
title = title && title.replace('.' + ext, '');
}
this.$el.html(
template({
name: this.options.title,
ext: ext,
title: title,
description: description
})
);
return this;
},
_genDescription: function () {
if (this.options.fileAttrs && this.options.fileAttrs.description) {
var descriptionOpts = this.options.fileAttrs.description;
var descriptionKeyValue = '';
var descriptionStr = '';
var self = this;
if (descriptionOpts.content && descriptionOpts.content.length > 0) {
_.each(descriptionOpts.content, function (item, i) {
if (i > 0 && descriptionOpts.separator) {
descriptionStr += ' ' + descriptionOpts.separator + ' ';
}
var value = self.model.get(item.name);
var format = item.format && self._FORMATTERS[item.format];
descriptionStr += format && format(value) || value;
if (item.key) {
descriptionKeyValue = item.name;
}
});
}
if (descriptionOpts.itemName && descriptionKeyValue) {
descriptionStr += ' ' + (descriptionOpts.itemName && _t('components.modals.add-layer.imports.' + descriptionOpts.itemName + '-pluralize', { smart_count: descriptionKeyValue }) || '');
}
return descriptionStr;
}
return '';
},
_onSelectItem: function () {
this.trigger('selected', this.model, this);
}
});
@@ -0,0 +1,14 @@
<div class="ServiceList-itemExt u-ellipsLongText CDB-Text CDB-Size-medium u-rSpace--xl">
<%- ext || '?' %>
</div>
<div class="ServiceList-itemInfo u-flex u-alignCenter u-justifySpace">
<div class="ServiceList-itemInfoTitle">
<h6 class="CDB-Text CDB-Size-large u-bSpace u-ellipsis" ><%- title %></h6>
<p class="CDB-Text CDB-Size-medium u-altTextColor u-ellipsis"><%- description %></p>
</div>
<div class="ServiceList-itemActions">
<button class="CDB-Button CDB-Button--secondary js-choose">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase"><%- _t('components.modals.add-layer.imports.service-import.choose') %></span>
</button>
</div>
</div>
@@ -0,0 +1,75 @@
var CoreView = require('backbone/core-view');
var ServiceListItemView = require('./import-service-list-item-view');
var template = require('./import-service-list.tpl');
/**
* Service list view
*
* - It will display all the items available under
* the service and the possibility to chose one of
* them.
*
*/
module.exports = CoreView.extend({
options: {
title: 'service',
fileAttrs: {}
},
initialize: function () {
this._initBinds();
this._checkVisibility();
},
render: function () {
this.clearSubViews();
var size = this.collection.size();
this.$el.html(
template({
size: size,
title: this.options.title,
pluralize: _t('components.modals.add-layer.imports.item-pluralize', { smart_count: size })
})
);
if (this.collection.size() > 0) {
this.collection.each(this._addItem, this);
}
return this;
},
_initBinds: function () {
this.collection.bind('sync', this.render, this);
this.model.bind('change:state', this._checkVisibility, this);
this.add_related_model(this.collection);
},
_addItem: function (m) {
var item = new ServiceListItemView({
model: m,
title: this.options.title,
fileAttrs: this.options.fileAttrs
});
item.bind('selected', this._onSelectedItem, this);
this.$('.ServiceList-items').append(item.render().el);
this.addView(item);
},
_onSelectedItem: function (mdl) {
this.model.setUpload({
state: 'selected',
value: mdl.toJSON(),
service_item_id: mdl.get('id')
});
},
_checkVisibility: function () {
var state = this.model.get('state');
if (state === 'list') {
this.show();
} else {
this.hide();
}
}
});
@@ -0,0 +1,26 @@
<div class="ConnectDataset-header">
<h2 class="CDB-Text CDB-Size-medium u-altTextColor">
<%- _t('components.modals.add-layer.imports.service-import.found-in', {
size: size,
pluralize: pluralize,
title: title
}) %>
</h2>
</div>
<div class="ServiceList-body">
<% if (size > 0) { %>
<ul class="ServiceList-items"></ul>
<% } else { %>
<div class="IntermediateInfo ServiceList-empty">
<div class="LayoutIcon">
<i class="CDB-IconFont CDB-IconFont-lens"></i>
</div>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">
<%- _t('components.modals.add-layer.imports.service-import.no-results-title') %>
</h4>
<p class="CDB-Text CDB-Size-medium u-altTextColor">
<%- _t('components.modals.add-layer.imports.service-import.no-results-desc') %>
</p>
</div>
<% } %>
</div>
@@ -0,0 +1,77 @@
var CoreView = require('backbone/core-view');
var template = require('./import-service-loader.tpl');
/**
* Service loader view
*
* - It will be on charge to make token and oauth petitions
*
*/
module.exports = CoreView.extend({
events: {
'click .js-connect': '_checkToken'
},
initialize: function (opts) {
if (!opts.serviceTokenModel) throw new Error('serviceTokenModel provider is required');
if (!opts.serviceOauthModel) throw new Error('serviceOauthModel is required');
this._serviceTokenModel = opts.serviceTokenModel;
this._serviceOauthModel = opts.serviceOauthModel;
this._initBinds();
this._checkVisibility();
},
render: function () {
this.$el.html(
template({
state: this.model.get('state')
})
);
return this;
},
_initBinds: function () {
this.model.bind('change:state', function () {
this.render();
this._checkVisibility();
}, this);
},
_checkToken: function () {
var self = this;
this.model.set('state', 'token');
this._serviceTokenModel.fetch({
success: function (r) {
if (!r.get('oauth_valid')) {
self._getOauthURL();
}
},
error: function (e) {
self._getOauthURL();
}
});
},
_checkVisibility: function () {
var state = this.model.get('state');
if (state !== 'list' && state !== 'selected') {
this.show();
} else {
this.hide();
}
},
_getOauthURL: function () {
var self = this;
this.model.set('state', 'oauth');
this._serviceOauthModel.set({ url: '' }, { silent: true });
this._serviceOauthModel.fetch({
error: function () {
self.model.set('state', 'error');
}
});
}
});
@@ -0,0 +1,17 @@
<div class="ImportPanel-actions">
<% if (state === "idle") { %>
<button class="CDB-Button CDB-Button--primary js-connect">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">
<%- _t('components.modals.add-layer.imports.service-import.connect') %>
</span>
</button>
<% } else if (state === "error") { %>
<button class="CDB-Button CDB-Button--primary js-connect">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">
<%- _t('components.modals.add-layer.imports.service-import.try-again') %>
</span>
</button>
<% } else { %>
<div class="Spinner ImportPanel-actionsLoader"></div>
<% } %>
</div>
@@ -0,0 +1,28 @@
var Backbone = require('backbone');
/**
* Get oauth url from the service requested
*
* - It needs a datasource name or it won't work.
*
* new ServiceOauthModel({ datasourceName: 'dropbox', configModel: configModel })
*/
module.exports = Backbone.Model.extend({
_DATASOURCE_NAME: 'dropbox',
url: function (method) {
var version = this._configModel.urlVersion('imports_service');
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/' + version + '/imports/service/' + this._DATASOURCE_NAME + '/auth_url';
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
if (!opts.datasourceName) throw new Error('datasource_name is required');
this._configModel = opts.configModel;
this._DATASOURCE_NAME = opts.datasourceName;
}
});
@@ -0,0 +1,28 @@
var Backbone = require('backbone');
/**
* Model to check if oAuth token is valid or not
*
* - It needs a datasource name or it won't work.
*
* new ServiceTokenModel({ datasourceName: 'dropbox', configModel: configModel })
*/
module.exports = Backbone.Model.extend({
_DATASOURCE_NAME: 'dropbox',
url: function (method) {
var version = this._configModel.urlVersion('imports_service');
var baseUrl = this._configModel.get('base_url');
return baseUrl + '/api/' + version + '/imports/service/' + this._DATASOURCE_NAME + '/token_valid';
},
initialize: function (attrs, opts) {
if (!opts.configModel) throw new Error('configModel is required');
if (!opts.datasourceName) throw new Error('datasource_name is required');
this._configModel = opts.configModel;
this._DATASOURCE_NAME = opts.datasourceName;
}
});
@@ -0,0 +1,196 @@
var ImportView = require('builder/components/modals/add-layer/content/imports/import-view');
var UploadModel = require('builder/data/upload-model');
var ServiceHeaderView = require('./import-service-header-view');
var ServiceLoaderView = require('./import-service-loader-view');
var ServiceListView = require('./import-service-list-view');
var ServiceSelectedFileView = require('builder/components/modals/add-layer/content/imports/import-selected-dataset-view');
var ServiceTokenModel = require('./import-service-token-model');
var ServiceOauthModel = require('./import-service-oauth-model');
var ServiceCollection = require('./import-service-items-collection');
var template = require('./import-service.tpl');
var WINDOW_INTERVAL = 1000; // miliseconds
/**
* Import service view
*
* - Use a service import panel
* - It will request login to the service
* - If it works, a list of available files will appear.
* - Once a file is selected, sync options will appear.
*
*/
module.exports = ImportView.extend({
_DATASOURCE_NAME: 'dropbox',
className: 'ImportPanel ImportPanelService',
options: {
service: '', // Name of the service
showAvailableFormats: false, // If all available format link should appear or not
fileExtensions: [], // File extensions
acceptSync: false, // Accept sync this service?
fileAttrs: { // Attributes or changes for service list or selected file:
ext: false, // If files should show extension
title: 'filename', // Title attribute
description: '<%- size %>', // Description attribute
formatDescription: 'size', // If any format function should be applied over the description
headerTemplate: '' // Header template
}
},
initialize: function (opts) {
if (!opts.service) throw new Error('service prodiver is required');
if (!opts.userModel) throw new Error('userModel is required');
if (!opts.configModel) throw new Error('configModel is required');
this._userModel = opts.userModel;
this._configModel = opts.configModel;
this._DATASOURCE_NAME = this.options.service;
this.model = new UploadModel({
type: 'service',
service_name: this._DATASOURCE_NAME
}, {
userModel: this._userModel,
configModel: this._configModel
});
this._initModels();
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(template(this.options));
this._initViews();
return this;
},
_initModels: function () {
this._serviceTokenModel = new ServiceTokenModel(null, {
datasourceName: this._DATASOURCE_NAME,
configModel: this._configModel
});
this._serviceOauthModel = new ServiceOauthModel(null, {
datasourceName: this._DATASOURCE_NAME,
configModel: this._configModel
});
this.collection = new ServiceCollection(null, {
datasourceName: this._DATASOURCE_NAME,
configModel: this._configModel
});
},
_initBinds: function () {
this.model.bind('change', this._triggerChange, this);
this.model.bind('change:state', this._checkState, this);
this._serviceTokenModel.bind('change:oauth_valid', this._onOauthChange, this);
this._serviceOauthModel.bind('change:url', this._openWindow, this);
this.add_related_model(this._serviceOauthModel);
this.add_related_model(this._serviceTokenModel);
},
_initViews: function () {
var header = new ServiceHeaderView({
el: this.$('.ImportPanel-header'),
userModel: this._userModel,
model: this.model,
collection: this.collection,
title: this.options.title,
showAvailableFormats: this.options.showAvailableFormats,
fileExtensions: this.options.fileExtensions,
acceptSync: this.options.acceptSync,
template: this.options.headerTemplate
});
header.render();
this.addView(header);
var loader = new ServiceLoaderView({
el: this.$('.ServiceLoader'),
model: this.model,
serviceTokenModel: this._serviceTokenModel,
serviceOauthModel: this._serviceOauthModel
});
loader.render();
this.addView(loader);
var list = new ServiceListView({
el: this.$('.ServiceList'),
model: this.model,
collection: this.collection,
title: this.options.title,
fileAttrs: this.options.fileAttrs
});
list.render();
this.addView(list);
var selected = new ServiceSelectedFileView({
el: this.$('.ServiceSelected'),
userModel: this._userModel,
model: this.model,
acceptSync: this.options.acceptSync,
fileAttrs: this.options.fileAttrs,
configModel: this._configModel
});
selected.render();
this.addView(selected);
},
_onOauthChange: function () {
if (this._serviceTokenModel.get('oauth_valid')) {
this._getFiles();
}
},
_getFiles: function () {
var self = this;
this.model.set('state', 'retrieving');
this.collection.fetch({
error: function () {
self.model.set('state', 'error');
},
success: function () {
self.model.set('state', 'list');
}
});
},
_checkState: function () {
if (this.model.get('state') === 'list') {
if (this.collection.size() === 1) {
var item = this.collection.at(0);
this.model.setUpload({
state: 'selected',
value: item.toJSON(),
service_item_id: item.get('id')
});
}
}
if (this.model.get('state') !== 'selected') {
this.model.set({
value: '',
service_item_id: '',
interval: 0
});
}
},
_openWindow: function () {
var url = this._serviceOauthModel.get('url');
var self = this;
var i = window.open(url, null, 'menubar=no,toolbar=no,width=600,height=495');
var e = window.setInterval(function () {
if (i && i.closed) {
self._getFiles();
clearInterval(e);
} else if (!i) {
self.model.set('state', 'error');
clearInterval(e);
}
}, WINDOW_INTERVAL);
}
});
@@ -0,0 +1,8 @@
<div class="ImportPanel-header"></div>
<div class="ImportPanel-body">
<div class="ImportPanel-bodyWrapper">
<div class="ImportPanel-state ServiceLoader is-idle is-token is-oauth is-retrieving is-error"></div>
<div class="ImportPanel-state ServiceList is-list"></div>
<div class="ImportPanel-state ServiceSelected DatasetSelected is-selected"></div>
</div>
</div>
@@ -0,0 +1,2 @@
<i class='TabIcon is-<%- name %>'></i>
<span><%= cdb.core.sanitize.html(title || name) %></span>
@@ -0,0 +1,44 @@
var CoreView = require('backbone/core-view');
var Utils = require('builder/helpers/utils');
var template = require('./credits-info.tpl');
/**
* Credits info view
*
* - Percentage of use
* - Possible money spent
*
*/
module.exports = CoreView.extend({
initialize: function (opts) {
this._userModel = opts.userModel;
this._initBinds();
},
render: function () {
var twitterData = this._userModel.get('twitter');
var remaining = twitterData.quota - twitterData.monthly_use;
var per = Math.min(100, Math.ceil((this.model.get('value') * 100) / remaining));
this.$el.html(
template({
value: this.model.get('value'),
remaining: remaining,
per: per,
hardLimit: twitterData.hard_limit,
remainingFormatted: Utils.formatNumber(remaining),
quota: twitterData.quota,
block_price: twitterData.block_price,
block_size: Utils.readizableNumber(twitterData.block_size)
})
);
return this;
},
_initBinds: function () {
this.model.bind('change', this.render, this);
}
});
@@ -0,0 +1,15 @@
<% extraTweets = '<strong>$' + block_price/100 + '/' + block_size + ' ' + _t('components.modals.add-layer.imports.twitter.extra-tweets') + '</strong>' %>
<% if (value <= remaining) { %>
<%- _t('components.modals.add-layer.imports.twitter.credits-left', {
per: per,
remainingFormatted: remainingFormatted
}) %>
<% } else if (remaining <= 0 && !hardLimit) { %>
<%= _t('components.modals.add-layer.imports.twitter.credits-consumed', {
extraTweets: extraTweets
}) %>
<% } else { %>
<%= _t('components.modals.add-layer.imports.twitter.credits-no-limit', {
extraTweets: extraTweets
}) %>
<% } %>
@@ -0,0 +1,111 @@
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var _ = require('underscore');
require('jquery-ui');
var CreditsInfoView = require('./credits-info-view');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'userModel'
];
var DEFAULT_PER_VALUE = 80;
var MIN_PER_VALUE = 1;
/**
* Set max use of credits for Twitter
*
* - Slider range = 1000 credits
* - Last step should be infinite if user doesn't
* have "soft_limit".
*
*/
module.exports = CoreView.extend({
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._disabled = (opts && opts.disabled) ? opts.disabled : false;
this.model = new Backbone.Model();
this._initBinds();
this._setModel();
},
render: function () {
this.clearSubViews();
this._destroySlider();
this._initViews();
return this;
},
_initBinds: function () {
this.model.bind('change:value', this._onValueChange, this);
},
_setModel: function () {
var twitterData = this._userModel.get('twitter');
var max = twitterData.quota - twitterData.monthly_use;
var min = (MIN_PER_VALUE * max) / 100; // Just 1% of the quota
var defaultValue = (max * DEFAULT_PER_VALUE) / 100;
var value = max > 0 ? defaultValue : (max + 1);
var self = this;
this.model.set({
max: (twitterData.hard_limit ? max : max + 1),
min: min,
step: min,
value: max > 0 ? value : twitterData.quota,
disabled: self._disabled || max <= 0
});
},
_initViews: function () {
this._setModel();
this.$('.js-slider').slider(
_.extend({
range: 'min',
orientation: 'horizontal',
slide: this._onSlideChange.bind(this),
change: this._onSlideChange.bind(this)
},
this.model.attributes
)
);
var creditsInfo = new CreditsInfoView({
el: this.$('.js-info'),
userModel: this._userModel,
model: this.model
});
creditsInfo.render();
this.addView(creditsInfo);
},
_onSlideChange: function (ev, ui) {
this.model.set('value', ui.value);
},
_onValueChange: function () {
this.trigger('maxCreditsChange', this.getMaxCredits(), this);
},
getMaxCredits: function () {
var twitterData = this._userModel.get('twitter');
var max = twitterData.quota - twitterData.monthly_use;
var value = this.model.get('value');
return value > max ? 0 : value;
},
_destroySlider: function () {
if (this.$('.js-slider').data('ui-slider')) {
this.$('.js-slider').slider('destroy');
}
},
clean: function () {
this._destroySlider();
CoreView.prototype.clean.call(this);
}
});
@@ -0,0 +1,130 @@
var _ = require('underscore');
var moment = require('moment');
var ImportView = require('builder/components/modals/add-layer/content/imports/import-view');
var UploadModel = require('builder/data/upload-model');
var DatesRangePickerView = require('builder/components/date-picker-range/date-picker-range-view');
var TwitterCategoriesView = require('./twitter-categories/twitter-categories-view');
var CreditsUsageView = require('./credits-usage-view.js');
var template = require('./import-twitter.tpl');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'configModel',
'userModel'
];
/**
* Import twitter panel
*
* - It accepts up to 3 categories
* - Date range can't be longer than 30 days
*
*/
module.exports = ImportView.extend({
options: {
acceptSync: false,
type: 'service',
service: 'twitter_search'
},
className: 'ImportPanel ImportTwitterPanel',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this.model = new UploadModel({
type: this.options.type,
service_name: this.options.service
}, {
userModel: this._userModel,
configModel: this._configModel
});
this._initBinds();
},
_disabledDueLackOfCredentials: function () {
return !this._userModel.hasOwnTwitterCredentials();
},
render: function () {
this.clearSubViews();
this.$el.html(
template({
currentGMT: moment().format('Z')
})
);
this._initViews();
return this;
},
_initViews: function () {
// Categories
var categories = this.categories = new TwitterCategoriesView({
disabled: this._disabledDueLackOfCredentials()
});
categories.bind('changeCategory', this._setModel, this);
this.$('.ImportTwitterPanel-categories').append(categories.render().el);
this.addView(categories);
// Date picker
var datepicker = this.datepicker = new DatesRangePickerView({
className: 'DatePicker DatePicker--withBorder',
disabled: this._disabledDueLackOfCredentials()
});
datepicker.bind('changeDate', this._setModel, this);
this.$('.js-picker').append(datepicker.render().el);
this.addView(datepicker);
// Use slider
var creditsUsage = this.creditsUsage = new CreditsUsageView({
el: this.$('.CreditsUsage'),
userModel: this._userModel,
disabled: this._disabledDueLackOfCredentials()
});
creditsUsage.bind('maxCreditsChange', this._setModel, this);
creditsUsage.render();
this.addView(creditsUsage);
this._setModel();
},
_initBinds: function () {
this.model.bind('change', this._triggerChange, this);
},
_getCategories: function () {
var categories = this.categories.getCategories();
return _.filter(categories, function (c) {
return c.category && c.terms.length > 0;
});
},
_getDates: function () {
return this.datepicker.getDates();
},
_getMaxCredits: function () {
return this.creditsUsage.getMaxCredits();
},
_setModel: function () {
var categories = this._getCategories();
var dates = this._getDates();
var maxCredits = this._getMaxCredits();
var d = {
categories: categories,
dates: dates
};
this.model.setUpload({
value: d,
service_item_id: d,
user_defined_limits: {
twitter_credits_limit: maxCredits
}
});
}
});
@@ -0,0 +1,35 @@
<div class="ImportPanel-header">
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">
<%- _t('components.modals.add-layer.imports.twitter.title') %>
</h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor">
<%- _t('components.modals.add-layer.imports.twitter.terms-desc') %>
</p>
</div>
<div class="ImportPanel-body">
<form class="Form">
<div class="ImportTwitterPanel-categories"></div>
<div class="ImportTwitterPanel-datePicker">
<div class="Form-row">
<div class="Form-rowLabel">
<label class="CDB-Text CDB-Size-medium js-category"><%- _t('components.modals.add-layer.imports.twitter.from-to') %></label>
</div>
<div class="Form-rowData Form-rowData--longer js-picker CDB-Text CDB-Size-medium"></div>
<div class="Form-rowInfo DatePicker-info">
<p class="CDB-Text CDB-Size-small u-altTextColor"><%- _t('components.modals.add-layer.imports.twitter.twitter-gmt') %> <br/>(<%- _t('components.modals.add-layer.imports.twitter.your-gmt') %><%- currentGMT %>)</p>
</div>
</div>
</div>
<div class="ImportTwitterPanel-creditsUsage">
<div class="Form-row">
<div class="Form-rowLabel">
<label class="CDB-Text CDB-Size-medium js-category"><%- _t('components.modals.add-layer.imports.twitter.use') %></label>
</div>
<div class="Form-rowData Form-rowData--longer CreditsUsage">
<div class="UISlider CreditsUsage-slider js-slider"></div>
<div class="CreditsUsage-info CDB-Text CDB-Size-medium js-info"></div>
</div>
</div>
</div>
</form>
</div>
@@ -0,0 +1,8 @@
var Backbone = require('backbone');
var TwitterCategoryModel = require('./twitter-category-model');
// Twitter categories collection
module.exports = Backbone.Collection.extend({
model: TwitterCategoryModel
});
@@ -0,0 +1,151 @@
var CoreView = require('backbone/core-view');
var _ = require('underscore');
var $ = require('jquery');
var TwitterCategoriesCollection = require('./twitter-categories-collection');
var TwitterCategoryModel = require('./twitter-category-model');
var TwitterCategoryView = require('./twitter-category-view');
/**
* Twitter category list view
* - It will generate a collection to store all the
* terms added.
*/
module.exports = CoreView.extend({
_MAX_CATEGORIES: 4,
_MAX_TERMS: 29,
initialize: function (opts) {
this._disabled = (opts && opts.disabled) ? opts.disabled : false;
// Add a first empty model
var m = this._generateCategory();
this.collection = new TwitterCategoriesCollection([ m ]);
this._initBinds();
},
render: function () {
this.clearSubViews();
this.collection.each(this._addCategory, this);
return this;
},
_initBinds: function () {
this.collection.bind('change', this._manageCategories, this);
this.collection.bind('change', this._onCategoryChange, this);
this.add_related_model(this.collection);
},
_manageCategories: function () {
var collection_size = this.collection.size();
// Check if already created models are completed
var nonFilled = this.collection.filter(function (m) {
return m.get('terms').length === 0;
});
// if so, generate new one
if (nonFilled.length === 0 && collection_size < this._MAX_CATEGORIES) {
var categoryModel = this._generateCategory();
this.collection.add(categoryModel);
this._addCategory(categoryModel);
return false;
}
// else, let's check
if (nonFilled.length > 0) {
var mdl = _.first(nonFilled);
var view = _.find(this._subviews, function (view) {
return mdl.cid === view.model.cid;
});
var pos = view.$el.index();
// Only one item in the collection, do nothing
if (collection_size === 1) return false;
// If it is the last item but there is no more items, do nothing
if (pos === (collection_size - 1)) return false;
// If it is not the last item and there is another non-filled element
// let's remove that one.
if (pos !== (collection_size - 1) && nonFilled.length > 1) {
mdl = nonFilled[1];
view = _.find(this._subviews, function (view) {
return mdl.cid === view.model.cid;
});
this._removeCategory(view);
}
// Reorder category indexes :(
this._sortCategoryIndex();
}
},
// Set proper index after any category removed
_sortCategoryIndex: function () {
var self = this;
// Hack to set properly category numbers
this.$('.twitter-category').each(function (i, el) {
// Get category, removing Category word
var category = $(el).find('.js-category').text().replace(_t('components.modals.add-layer.imports.twitter.category') + ' ', '');
if (category !== (i + 1)) {
// Find model
var m = self.collection.find(function (m) { return m.get('category') === category; });
// Find view
m.set('category', (i + 1).toString());
}
});
},
_generateCategory: function () {
return new TwitterCategoryModel({
disabled: this._disabled,
terms: [],
category: (this.collection ? (this.collection.size() + 1) : 1).toString()
});
},
_addCategory: function (m) {
var category = new TwitterCategoryView({ model: m });
category.bind('submit', this._onCategorySubmit, this);
category.bind('limit', this._onCategoryLimit, this);
category.bind('nolimit', this._onCategoryNoLimit, this);
this.$el.append(category.render().el);
this.addView(category);
this.trigger('addCategory');
},
_removeCategory: function (v) {
v.hide();
v.clean();
v.model.destroy();
this.trigger('removeCategory');
},
_onCategorySubmit: function () {
this.trigger('submitCategory', this.collection.toJSON(), this);
},
_onCategoryLimit: function () {
this.trigger('limitCategory', this.collection.toJSON(), this);
},
_onCategoryNoLimit: function () {
this.trigger('noLimitCategory', this.collection.toJSON(), this);
},
_onCategoryChange: function () {
this.trigger('changeCategory', this.collection.toJSON(), this);
},
getCategories: function () {
return this.collection.toJSON();
}
});
@@ -0,0 +1,55 @@
var Backbone = require('backbone');
var _ = require('underscore');
var TERMS_CHARS = 4;
// Twitter category model
module.exports = Backbone.Model.extend({
_MAX_COUNTER: 1014,
_CHAR_MAP: {
' ': 2,
'-': 2,
'_': 2,
'.': 2
},
defaults: {
terms: [],
category: '',
counter: 1014
},
initialize: function () {
this._initBinds();
},
_initBinds: function () {
this.bind('change:terms', this._setCounter, this);
},
_setCounter: function () {
var count = this._MAX_COUNTER;
var self = this;
// Check terms number
if (this.get('terms').length > 1) {
count = count - ((this.get('terms').length - 1) * TERMS_CHARS);
}
// Count characters
_.each(this.get('terms'), function (term) {
_.each(term, function (c) {
if (self._CHAR_MAP[c] !== undefined) {
count = count - self._CHAR_MAP[c];
} else {
count--;
}
});
});
// Count never should be less than 0 please!
this.set('counter', Math.max(0, count));
}
});
@@ -0,0 +1,96 @@
var CoreView = require('backbone/core-view');
var _ = require('underscore');
var $ = require('jquery');
var template = require('./twitter-category.tpl');
/**
* Twitter category item view
* - It just needs a twitter category model
*/
module.exports = CoreView.extend({
className: 'TwitterCategory',
_MAX_CATEGORIES: 4,
_MAX_TERMS: 29,
events: {
'keydown .js-terms': '_onInputChange',
'keypress .js-terms': '_onInputChange',
'keyup .js-terms': '_onInputChange'
},
initialize: function () {
this._initBinds();
},
render: function () {
this.$el.empty();
this.$el.append(
template({
disabled: this.model.get('disabled'),
terms: this.model.get('terms'),
category: this.model.get('category'),
counter: this.model.get('counter')
})
);
this.show();
return this;
},
_initBinds: function () {
_.bindAll(this, '_onInputChange');
this.model.bind('change:category', this._onCategoryChange, this);
},
_onCategoryChange: function () {
this.$('.js-category').text(_t('components.modals.add-layer.imports.twitter.category') + ' ' + this.model.get('category'));
},
_onInputChange: function (e) {
var value = $(e.target).val();
var d = {};
if (e.keyCode === 13/* ENTER */) {
e.preventDefault();
this.trigger('submit', this.model, this);
return false;
}
this.$('.CDB-IconFont-twitter').toggleClass('is-highlighted', value.length > 0);
// Check if it is possible to add new characters
// if not, stop the action, unless user is deleting
// any previous character
if ((this.model.get('counter') === 0 || this.model.get('terms').length > this._MAX_TERMS) &&
e.keyCode !== 37 /* left */ && e.keyCode !== 39 /* right */ && e.keyCode !== 8 && value.length > 0) {
this.killEvent(e);
this.trigger('limit', this.model, this);
return false;
} else {
this.trigger('nolimit', this.model, this);
}
// Get valid terms array
if (!value) {
value = [];
} else {
value = value.split(',');
}
d['terms'] = value;
this.model.set(d);
},
show: function () {
this.$el.addClass('enabled');
},
hide: function () {
this.$el.removeClass('enabled');
}
});
@@ -0,0 +1,9 @@
<div class="Form-row">
<div class="Form-rowLabel">
<label class="CDB-Text CDB-Size-medium js-category"><%- _t('components.modals.add-layer.imports.twitter.category') %> <%- category %></label>
</div>
<div class="Form-rowData Form-rowData--longer">
<input type="text" <% if (disabled) { %>disabled="disabled"<% } %> class="CDB-Text CDB-Size-medium Form-input Form-input--longer has-icon js-terms <%- disabled ? 'is-disabled' : '' %>" value="<%- terms.join(",") %>" placeholder="<%- _t('components.modals.add-layer.imports.twitter.terms-placeholder') %>" />
<i class="CDB-IconFont CDB-IconFont-twitter Form-inputIcon"></i>
</div>
</div>
@@ -0,0 +1,42 @@
var CoreView = require('backbone/core-view');
var UploadModel = require('builder/data/upload-model');
/**
* Default view for an import item
*
* - It is based in an upload model.
* - Will trigger a change when model changes.
* - It returns their data if it is requested with a method.
*/
module.exports = CoreView.extend({
initialize: function (opts) {
if (!opts.userModel) throw new Error('userModel is required');
if (!opts.configModel) throw new Error('configModel is required');
this._configModel = opts.configModel;
this._userModel = opts.userModel;
this.model = new UploadModel(null, {
configModel: this._configModel,
userModel: this._userModel
});
this._initBinds();
},
_initBinds: function () {
this.model.bind('change', this._triggerChange, this);
},
_triggerChange: function () {
this.trigger('change', this.model.toJSON(), this);
},
getModelData: function () {
if (this.model) {
return this.model.toJSON();
}
return {};
}
});
@@ -0,0 +1,338 @@
var cdb = require('internal-carto.js');
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var ViewFactory = require('builder/components/view-factory');
var importViewTemplate = require('./imports.tpl');
var importTabViewTemplate = require('./import-tab.tpl');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var TabPaneView = require('builder/components/tab-pane/tab-pane-view');
var TabPaneCollection = require('builder/components/tab-pane/tab-pane-collection');
var _ = require('underscore');
var IMPORT_OPTIONS = require('./import-options');
var TABS_PER_ROW = 5;
var DEFAULT_IMPORT = 'file';
var ROW_WIDTH = 800;
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'createModel',
'userModel',
'configModel',
'privacyModel',
'guessingModel'
];
/**
* Imports view
*
* Displays all the import options available
* through new create dialog.
*
* IMPORTANT!!
*
* If you need to add a new import pane:
*
* - Create the proper class within imports folder and its tests.
* - Add necessary info in import_options file.
* - Create a check function here if needed, if not will appear
* always enabled (for everybody!).
*
*/
module.exports = CoreView.extend({
className: 'ImportOptions',
events: {
'click .js-goNext': '_moveToNextTabs',
'click .js-goPrev': '_moveToPrevTabs'
},
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initModels();
},
render: function () {
this._destroyBinds();
this.clearSubViews();
this.$el.empty();
this._generateContent();
this._generateNavigationTabs();
this._initBinds();
this._setOption();
return this;
},
_initModels: function () {
this.model = new Backbone.Model({
page: 1,
maxPages: 0
});
},
// //////////////////
// TABS && PANES! //
// //////////////////
_generateContent: function () {
var paneItems = [];
_.each(IMPORT_OPTIONS, function (importConfig, i) {
var obj = {};
if (!_.isEmpty(importConfig) && importConfig.enabled(this._configModel, this._userModel)) {
obj = {
name: importConfig.name,
selected: importConfig.name === 'file'
};
obj.createButtonView = function () {
return ViewFactory.createByTemplate(
importTabViewTemplate,
{
title: cdb.core.sanitize.html(importConfig.title || importConfig.name),
name: importConfig.name
},
{
tagName: 'button',
className: 'TabLink ' + 'js-' + importConfig.name + 'Tab'
}
);
};
var pane;
// Check if import option function exists
var fn = this['_check' + i + 'Import'];
var isEnabled;
if (fn) {
isEnabled = fn.bind(this)();
}
if ((isEnabled || isEnabled === undefined) && !_.isEmpty(importConfig)) {
var ImportView = importConfig.view;
pane = new ImportView(
_.extend(
importConfig.options || {},
{
userModel: this._userModel,
configModel: this._configModel,
title: importConfig.title,
createModel: this._createModel,
privacyModel: this._privacyModel,
guessingModel: this._guessingModel
}
)
);
} else if (importConfig.fallback) {
pane = ViewFactory.createByTemplate(importConfig.fallback);
}
if (pane) {
pane.render();
pane.bind('change', this._setUploadModel, this);
obj.createContentView = function () {
return pane;
};
}
paneItems.push(obj);
}
}, this);
this._tabPaneCollection = new TabPaneCollection(paneItems);
this._tabPaneView = new TabPaneView({
template: importViewTemplate,
collection: this._tabPaneCollection,
tabPaneItemOptions: {
tagName: 'li',
klassName: 'ImportOptions-tab'
}
});
this.$el.append(this._tabPaneView.render().el);
},
_generateNavigationTabs: function () {
var numTabs = this.$('.ImportOptions-tab').size();
if (numTabs <= 1) {
this.$('.AddLayer-tabs').hide();
}
this.model.set('maxPages', Math.ceil(numTabs / TABS_PER_ROW));
this._checkNavigationTabs();
if (this.model.get('maxPages') <= 1) {
this.$('.ImportOptions-navigation').hide();
}
},
_moveToNextTabs: function () {
var page = this.model.get('page');
var maxPages = this.model.get('maxPages');
if (page < maxPages) {
this.model.set('page', page + 1);
}
},
_moveToPrevTabs: function () {
var page = this.model.get('page');
if (page > 1) {
this.model.set('page', page - 1);
}
},
_moveNavigationTabs: function () {
var page = this.model.get('page');
var rowWidth = ROW_WIDTH;
this.$('.js-menu').css('margin-left', '-' + (rowWidth * (page - 1)) + 'px');
this._checkNavigationTabs();
},
_checkNavigationTabs: function () {
var page = this.model.get('page');
var maxPages = this.model.get('maxPages');
this.$('.js-goPrev').toggleClass('is-disabled', page < 2);
this.$('.js-goNext').toggleClass('is-disabled', page >= maxPages);
},
_checkGDriveImport: function () {
if (!this._configModel.get('oauth_gdrive')) {
this._setFailedTab('gdrive', 'key');
return false;
}
return true;
},
_checkDropboxImport: function () {
if (!this._configModel.get('oauth_dropbox')) {
this._setFailedTab('dropbox', 'key');
return false;
}
return true;
},
_checkBoxImport: function () {
if (!this._configModel.get('oauth_box')) {
this._setFailedTab('box', 'key');
return false;
}
return true;
},
_checkTwitterImport: function () {
if (!this._configModel.get('datasource_search_twitter')) {
this._setFailedTab('twitter', 'key');
return false;
}
if (!this._userModel.get('twitter').enabled) {
return false;
}
if (!this._userModel.canCreateTwitterDataset()) {
this._setFailedTab('twitter', 'credits');
return false;
}
return true;
},
_checkInstagramImport: function () {
if (!this._userModel.featureEnabled('instagram_import')) {
return false;
}
if (!this._configModel.get('oauth_instagram')) {
this._setFailedTab('instagram', 'key');
return false;
}
return true;
},
_checkSalesforceImport: function () {
// Connector is now disabled for everyone.
// You might be able to use the following check to determine
// if user/org has this connector enabled:
// this._userModel.get('salesforce').enabled
return false;
},
_checkMailchimpImport: function () {
if (!this._configModel.get('oauth_mailchimp')) {
this._setFailedTab('mailchimp', 'key');
return false;
}
if (!this._userModel.featureEnabled('mailchimp_import')) {
return false;
}
return true;
},
_setFailedTab: function (tabName, type) {
var $tab = this.$('.js-' + tabName + 'Tab');
$tab.addClass('disabled');
var tooltip = new TipsyTooltipView({
el: $tab,
title: function () {
return _t('components.modals.add-layer.imports.tab-options-error.' + type, { name: tabName });
}
});
this.addView(tooltip);
},
_setUploadModel: function (d) {
var uploadModel = this._createModel.getUploadModel();
uploadModel.setFresh(d);
},
_initBinds: function () {
this.model.bind('change:page', this._moveNavigationTabs, this);
if (this._tabPaneCollection) {
this._tabPaneCollection.bind('change:selected', this._onTabChange, this);
}
},
_destroyBinds: function () {
if (this._tabPaneCollection) {
this._tabPaneCollection.bind('change:selected', null, this);
}
},
_setOption: function () {
// First option > data file
var tabPaneModel = _.first(this._tabPaneCollection.where({ name: DEFAULT_IMPORT }));
if (tabPaneModel) {
tabPaneModel.set('selected', true);
this._updateImportOption();
}
},
_updateImportOption: function () {
this._createModel.setActiveImportPane(this._tabPaneView.getSelectedTabPaneName());
},
// //////////
// Events //
// //////////
_onTabChange: function (tabModel) {
// When the tab changes we lose all event listeners
this._tabPaneCollection.each(function (model) {
if (!model.get('selected')) return;
const pane = model.attributes.createContentView();
pane.unbind('change', this._setUploadModel, this);
pane.bind('change', this._setUploadModel, this);
}, this);
this._updateImportOption();
},
clean: function () {
this._destroyBinds();
CoreView.prototype.clean.call(this);
}
});
@@ -0,0 +1,21 @@
<div class="ImportOptions-navigation">
<button class="Carousel-Navigation CDB-Size-large ImportOptions-navigationButton ImportOptions-navigationButton--prev js-goPrev">
<i class="CDB-IconFont CDB-IconFont-arrowPrev u-actionTextColor"></i>
</button>
<button class="Carousel-Navigation CDB-Size-large ImportOptions-navigationButton ImportOptions-navigationButton--next js-goNext">
<i class="CDB-IconFont CDB-IconFont-arrowNext u-actionTextColor"></i>
</button>
</div>
<div class="AddLayer-tabs">
<ul class="ImportOptions-tabsList js-menu CDB-Text CDB-Size-medium u-altTextColor"></ul>
<!--
<p class="CDB-Text CDB-Size-medium u-altTextColor u-tSpace-xl">
<%- _t('components.modals.add-layer.imports.feedback.text') %>
<a href="<%- _t('components.modals.add-layer.imports.feedback.url') %>" target="_blank">
<%- _t('components.modals.add-layer.imports.feedback.button') %>
</a>
</p>
-->
</div>
<div class="ImportOptions-panes js-content"></div>
@@ -0,0 +1,93 @@
var CoreView = require('backbone/core-view');
var _ = require('underscore');
var DatasetsView = require('./datasets/datasets-view');
var ImportsView = require('./imports/imports-view');
var TabPaneView = require('builder/components/tab-pane/tab-pane-view');
var TabPaneCollection = require('builder/components/tab-pane/tab-pane-collection');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'createModel',
'userModel',
'configModel',
'privacyModel',
'guessingModel'
];
/**
* Create listing view
*
* It will display all the possibilities to select
* any of your current datasets or connect a new dataset.
*
*/
module.exports = CoreView.extend({
className: 'CreateDialog-listing CreateDialog-listing--noPaddingTop',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initBinds();
this._onChangeListing();
},
render: function () {
this.clearSubViews();
this._initViews();
return this;
},
_initBinds: function () {
// Bug with binding... do not work with the usual one for some reason :(
this.listenTo(this._createModel, 'change:listing', this._onChangeListing);
},
_initViews: function () {
var self = this;
var paneModels = [];
paneModels.push({
name: 'datasets',
selected: false,
createContentView: function () {
return new DatasetsView({
userModel: self._userModel,
createModel: self._createModel
});
}
});
if (this._userModel.canCreateDatasets()) {
paneModels.push({
name: 'import',
selected: false,
createContentView: function () {
return new ImportsView({
userModel: self._userModel,
configModel: self._configModel,
createModel: self._createModel,
privacyModel: self._privacyModel,
guessingModel: self._guessingModel
});
}
});
}
this._tabPaneCollection = new TabPaneCollection(paneModels);
var paneTabPaneView = new TabPaneView({
collection: this._tabPaneCollection
});
this.addView(paneTabPaneView);
this.$el.append(paneTabPaneView.render().el);
},
_onChangeListing: function () {
if (this._tabPaneCollection) {
var context = this._createModel.get('listing');
var paneModel = _.first(this._tabPaneCollection.where({ name: context }));
paneModel.set('selected', true);
}
}
});
@@ -0,0 +1,222 @@
var cdb = require('internal-carto.js');
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var $ = require('jquery');
var _ = require('underscore');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var template = require('./navigation.tpl');
var REQUIRED_OPTS = [
'configModel',
'createModel',
'userModel',
'routerModel',
'tablesCollection'
];
/**
* Listing datasets navigation.
*
* - 'Filter by' datasets.
* - 'Search' any pattern within dataset collection.
*
*/
module.exports = CoreView.extend({
events: {
'submit .js-search-form': '_submitSearch',
'keydown .js-search-form': '_onSearchKeyDown',
'click .js-search-form': 'killEvent',
'click .js-search-link': '_onSearchClick',
'click .js-clean-search': '_onCleanSearchClick',
'click .js-shared': '_onSharedClick',
'click .js-library': '_onLibraryClick',
'click .js-connect': '_onConnectClick',
'click .js-datasets': '_onDatasetsClick',
'click .js-create-empty': '_onCreateEmptyClick'
},
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this.model = new Backbone.Model();
this._preRender();
this._initBinds();
},
// It is necessary to add two static elements because
// they can't be removed/replaced using render method
// each time a change (in a model or a collection) happens.
// This is due to the behaviour of the CSS animations.
_preRender: function () {
var $uInner = $('<div>').addClass('u-inner');
var $filtersInner = $('<div>').addClass('Filters-inner');
this.$el.append($uInner.append($filtersInner));
},
render: function (m, c) {
this.clearSubViews();
this.$('.Filters-inner').html(
template(
_.extend(
{
showDatasets: this._createModel.showDatasets(),
createModelType: this._createModel.get('type'),
canCreateDataset: this._userModel.canCreateDatasets(),
listingType: this._createModel.get('listing'),
isInsideOrg: this._userModel.isInsideOrg(),
totalShared: this._tablesCollection.getTotalStat('total_shared'),
selectedItemsCount: this._selectedItems().length,
hasDataLibrary: this._configModel.dataLibraryEnabled()
},
this._routerModel.attributes
)
)
);
this._animate();
if (this._routerModel.isSearching()) {
this._focusSearchInput();
}
return this;
},
_initBinds: function () {
this.listenTo(this.model, 'change:isSearchEnabled', this._onChangeIsSearchEnabled, this);
this.listenTo(this._createModel, 'change:listing', this.render, this);
this.listenTo(this._routerModel, 'change', this.render, this);
this.listenTo(this._tablesCollection, 'sync', this.render, this);
},
_onChangeIsSearchEnabled: function (model, isSearchEnabled) {
this._enableSearchUI(isSearchEnabled);
if (this._routerModel.isSearching()) {
this._cleanSearch();
} else if (isSearchEnabled) {
this._$searchInput().val('');
this._focusSearchInput();
}
},
_$searchInput: function () {
return this.$('.js-search-input');
},
_focusSearchInput: function () {
this._$searchInput()
.select()
.focus();
},
_onSearchKeyDown: function (e) {
if (e.keyCode === 27) { // ESC
this.killEvent(e);
this._cleanSearch();
}
},
_selectedItems: function () {
return this._tablesCollection.where({ selected: true });
},
_animate: function () {
this._enableSearchUI(!!this._routerModel.isSearching());
// Check if user doesn't have any table and it is in library section
// to remove useless shadow
var inLibrarySection = this._routerModel.get('library');
var inDatasetsSection = this._createModel.get('listing') === 'datasets';
var hasDatasets = this._tablesCollection.getTotalStat('total_user_entries') > 0;
this.$el.toggleClass('no-shadow', inLibrarySection && !hasDatasets && inDatasetsSection);
},
_enableSearchUI: function (enable) {
this.$('.js-search-field').toggle(enable);
this.$('.js-links-list').toggleClass('is-hidden', enable);
this.$('.js-order-list').toggleClass('is-hidden', enable);
},
_onDatasetsClick: function () {
this._routerModel.set({
shared: 'no',
library: false,
page: 1
});
this._routerModel.trigger('change', this._routerModel);
this._createModel.set('listing', 'datasets');
},
_onSharedClick: function () {
this._routerModel.set({
shared: 'only',
library: false,
page: 1
});
this._routerModel.trigger('change', this._routerModel);
this._createModel.set('listing', 'datasets');
},
_onLibraryClick: function () {
this._routerModel.set({
shared: 'no',
library: true,
page: 1
});
this._routerModel.trigger('change', this._routerModel);
this._createModel.set('listing', 'datasets');
},
_onConnectClick: function () {
if (this._userModel.canCreateDatasets()) {
this._createModel.set('listing', 'import');
}
},
_onCreateEmptyClick: function () {
if (this._userModel.canCreateDatasets()) {
this._createModel.createFromScratch();
}
},
// Selection actions
_onSearchClick: function (e) {
this.killEvent(e);
this.model.set('isSearchEnabled', !this.model.get('isSearchEnabled'));
},
// Filter actions
_onCleanSearchClick: function (e) {
if (e) {
e.preventDefault();
}
this._cleanSearch();
},
_cleanSearch: function () {
this._routerModel.set({
q: '',
tag: '',
shared: 'no',
library: this._createModel.showLibrary()
});
this.model.set('isSearchEnabled', false);
},
_submitSearch: function (e) {
this.killEvent(e);
var val = cdb.core.sanitize.html(this.$('.js-search-input').val().trim());
var tag = val.search(':') === 0 ? val.replace(':', '') : '';
var q = val.search(':') !== 0 ? val : '';
this._routerModel.set({
page: 1,
tag: tag,
q: q,
shared: 'yes'
});
this._createModel.set('listing', 'datasets');
}
});
@@ -0,0 +1,60 @@
<span class="Filters-separator"></span>
<div class="Filters-row">
<div class="Filters-group">
<div class="Filters-typeItem Filters-typeItem--searchEnabler js-search-enabler">
<button class="Filters-searchLink CDB-Text is-semibold u-upperCase CDB-Size-medium js-search-link">
<i class="Filters-searchLinkIcon CDB-IconFont CDB-IconFont-lens"></i><%- _t('components.modals.add-layer.navigation.search') %>
</button>
</div>
<ul class="Filters-group js-links-list">
<li class="Filters-typeItem js-filter-type">
<button class="Filters-typeLink js-typeItem u-actionTextColor CDB-Text is-semibold u-upperCase CDB-Size-medium js-connect <%- listingType === 'import' ? 'is-selected' : '' %> <%- !canCreateDataset ? 'is-disabled' : '' %>">
<%- _t('components.modals.add-layer.navigation.connect-dataset') %>
</button>
</li>
<% if (showDatasets) { %>
<li class="Filters-typeItem js-filter-type">
<button class="Filters-typeLink js-typeItem u-actionTextColor CDB-Text is-semibold u-upperCase CDB-Size-medium js-datasets <%- listingType === 'datasets' && shared !== 'only' && !library ? 'is-selected' : '' %>">
<%- _t('components.modals.add-layer.navigation.your-datasets') %>
</button>
</li>
<% if (isInsideOrg) { %>
<li class="Filters-typeItem js-filter-type">
<button class="Filters-typeLink js-typeItem u-actionTextColor CDB-Text is-semibold u-upperCase CDB-Size-medium js-shared <%- listingType === 'datasets' && shared === "only" ? 'is-selected' : '' %>">
<% if (totalShared) { %>
<strong><%- totalShared %></strong>
<% } %>
<%- _t('components.modals.add-layer.navigation.shared-with-you') %>
</button>
</li>
<% } %>
<% } %>
<% if (hasDataLibrary) { %>
<li class="Filters-typeItem js-filter-type">
<button class="Filters-typeLink js-typeItem u-actionTextColor CDB-Text is-semibold u-upperCase CDB-Size-medium js-library <%- listingType === 'datasets' && library ? 'is-selected' : '' %>">
<%- _t('components.modals.add-layer.navigation.data-library') %>
</button>
</li>
<% } %>
</ul>
</div>
<div class="Filters-typeItem Filters-typeItem--searchField js-search-field">
<form class="Filters-searchForm js-search-form">
<input class="Filters-searchInput CDB-Text CDB-Size-medium js-search-input" type="text" value="<%- ( tag && (':' + tag) ) || q %>" placeholder="<%- _t('components.modals.add-layer.navigation.search-placeholder') %>" />
<% if (tag || q) { %>
<button type="button" class="Filters-cleanSearch js-clean-search u-actionTextColor">
<i class="CDB-IconFont CDB-IconFont-close"></i>
</button>
<% } %>
</form>
</div>
<div class="Filters-addLayer js-order-list">
<button class="Filters-typeLink js-typeItem u-actionTextColor CDB-Text is-semibold u-upperCase CDB-Size-medium js-create-empty <%- listingType === 'scratch' ? 'is-selected' : '' %> <%- !canCreateDataset ? 'is-disabled' : '' %>">
<%- _t('components.modals.add-layer.navigation.create-empty-' + createModelType) %>
</button>
</div>
</div>
@@ -0,0 +1,95 @@
var CoreView = require('backbone/core-view');
var GuessingTogglerView = require('./guessing-toggler-view');
var PrivacyTogglerView = require('./privacy-toggler-view');
var template = require('./footer.tpl');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'privacyModel',
'guessingModel',
'createModel',
'userModel',
'configModel'
];
/**
* Footer view for the add layer modal.
*/
module.exports = CoreView.extend({
events: {
'click .js-ok': '_finish'
},
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(
template({
canFinish: this._createModel.canFinish(),
listing: this._createModel.get('listing')
})
);
this._initViews();
return this;
},
_initViews: function () {
var guessingTogglerView = new GuessingTogglerView({
guessingModel: this._guessingModel,
userModel: this._userModel,
createModel: this._createModel,
configModel: this._configModel
});
this.$('.js-footer-info').append(guessingTogglerView.render().el);
this.addView(guessingTogglerView);
var privacyTogglerView = new PrivacyTogglerView({
privacyModel: this._privacyModel,
userModel: this._userModel,
createModel: this._createModel,
configModel: this._configModel
});
this.$('.js-footerActions').prepend(privacyTogglerView.render().el);
this.addView(privacyTogglerView);
},
_initBinds: function () {
this.listenTo(this._createModel.getUploadModel(), 'change', this.render);
this.listenTo(this._createModel.getSelectedDatasetsCollection(), 'all', this._update);
this.listenTo(this._createModel, 'change', this._update);
},
_update: function () {
var contentPane = this._createModel.get('contentPane');
var listing = this._createModel.get('listing');
if (contentPane === 'listing' && listing !== 'scratch') {
this.render().show();
} else {
this.hide();
}
},
_finish: function (e) {
this.killEvent(e);
if (this._createModel.canFinish()) {
// Set proper guessing values before starting the upload
// if dialog is in import section
if (this._createModel.get('listing') === 'import') {
var uploadModel = this._createModel.getUploadModel();
uploadModel.setPrivacy(this._privacyModel.get('privacy'));
uploadModel.setGuessing(this._guessingModel.get('guessing'));
}
this._userModel.updateTableCount();
this._createModel.finish();
}
}
});
@@ -0,0 +1,11 @@
<div class="CreateDialog-footerShadow"></div>
<div class="CreateDialog-footerLine"></div>
<div class="CreateDialog-footerInner u-flex u-justifySpace u-alignCenter">
<div class="js-footer-info CreateDialog-footerInfo u-secondaryTextColor">
</div>
<div class="CreateDialog-footerActions js-footerActions">
<button class="js-ok CDB-Button CDB-Button--primary <%- canFinish ? '' : 'is-disabled' %>">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase"><%- _t('components.modals.add-layer.footer.add-layer') %></span>
</button>
</div>
</div>
@@ -0,0 +1,55 @@
var CoreView = require('backbone/core-view');
var template = require('./guessing-toggler.tpl');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'createModel',
'guessingModel',
'configModel',
'userModel'
];
/**
* Manages if upcoming import should guess or not.
* Expected to be rendered in the footer of a create dialog.
*/
module.exports = CoreView.extend({
events: {
'click .js-toggle': '_toggle'
},
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initBinds();
},
render: function () {
var htmlStr = '';
if (this._createModel.showGuessingToggler()) {
var uploadModel = this._createModel.getUploadModel();
htmlStr = template({
isGuessingEnabled: this._guessingModel.get('guessing'),
importState: this._createModel.getImportState(),
isUploadValid: uploadModel.isValidToUpload(),
customHosted: this._configModel.get('cartodb_com_hosted'),
isTwitterDeprecatedForUser: !this._userModel.hasOwnTwitterCredentials()
});
}
this.$el.html(htmlStr);
return this;
},
_initBinds: function () {
this._createModel.bind('change', this.render, this);
this._guessingModel.bind('change', this.render, this);
this.add_related_model(this._createModel);
},
_toggle: function () {
var value = !this._guessingModel.get('guessing');
this._guessingModel.set('guessing', value);
}
});
@@ -0,0 +1,21 @@
<% if (importState !== "twitter") { %>
<div class="u-iBlock CDB-Text CDB-Size-medium u-rSpace--xl js-toggle">
<input class="CDB-Checkbox" type="checkbox" <%- isGuessingEnabled ? 'checked' : '' %>>
<span class="u-iBlock CDB-Checkbox-face"></span>
<label class="u-iBlock u-lSpace"><%- _t('components.modals.add-layer.footer.guessing-desc') %></label>
</div>
<% } else if (isTwitterDeprecatedForUser) { %>
<div class="CDB-Text CDB-Size-medium">
<span class="Dialog-footerWarning">
<%- _t('components.modals.add-layer.footer.deprecated-connector') %>
</span>
<%= _t('components.modals.add-layer.footer.twitter-contact-support') %>
</div>
<% } else if (!customHosted) { %>
<span class="CDB-Text CDB-Size-medium">
<%- _t('components.modals.add-layer.footer.twitter-how-to-historical') %>
<a href="mailto:support@carto.com">
<%- _t('components.modals.add-layer.footer.contact-team') %>
</a>
</span>
<% } %>
@@ -0,0 +1,89 @@
var CoreView = require('backbone/core-view');
var $ = require('jquery');
var template = require('./privacy-toggler.tpl');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var publicPrivacy = 'PUBLIC';
var privatePrivacy = 'PRIVATE';
var REQUIRED_OPTS = [
'createModel',
'privacyModel',
'configModel',
'userModel'
];
/**
* Change the privacy of the new dataset.
* - If the user can't change the privacy, it will refer to the upgrade page
* unless app is the "open source" version
*
*/
module.exports = CoreView.extend({
events: {
'click': '_onClick'
},
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.empty();
if (this._createModel.showPrivacyToggler()) {
var canChangePrivacy = this._userModel.canCreatePrivateDatasets();
var privacy = this._privacyModel.get('privacy');
var isPublic = privacy === publicPrivacy;
var nextPrivacy = isPublic ? privatePrivacy : publicPrivacy;
var icon = isPublic ? 'unlock' : 'lock';
var upgradeUrl = this._configModel.get('upgrade_url') || window.upgrade_url;
var canUpgrade = !this._configModel.get('cartodb_com_hosted') && !canChangePrivacy && upgradeUrl;
this.$el.html(
template({
privacy: privacy,
isDisabled: !canChangePrivacy,
canUpgrade: canUpgrade,
nextPrivacy: nextPrivacy,
upgradeUrl: upgradeUrl,
icon: icon
})
);
this._initViews();
}
return this;
},
_initBinds: function () {
this._privacyModel.bind('change:privacy', this.render, this);
},
_initViews: function () {
// Tooltip
this.addView(
new TipsyTooltipView({
el: this.$('.js-toggler'),
html: true,
title: function () {
return $(this).attr('data-title');
}
})
);
},
_onClick: function () {
if (this._userModel.canCreatePrivateDatasets()) {
var privacy = this._privacyModel.get('privacy');
this._privacyModel.set('privacy', privacy === publicPrivacy ? privatePrivacy : publicPrivacy);
}
}
});
@@ -0,0 +1,17 @@
<% if (canUpgrade) { %>
<a href="<%- upgradeUrl %>" class="PrivacyToggler js-toggler PrivacyToggler--<%- privacy %> is-disabled"
data-title="<%- _t('components.modals.add-layer.footer.privacy-change-not-allowed') %>">
<i class="CDB-IconFont CDB-IconFont-<%- icon %>"></i>
</a>
<% } else {%>
<button type="button" class="PrivacyToggler js-toggler PrivacyToggler--<%- privacy %> <%- isDisabled ? 'is-disabled' : '' %>"
data-title="
<% if (!isDisabled) { %>
<%- _t('components.modals.add-layer.footer.privacy-change', { privacy: privacy.toLowerCase() }) %>.<br /><%- _t('components.modals.add-layer.footer.privacy-click', { nextPrivacy: nextPrivacy.toLowerCase() }) %>.
<% } else { %>
<%- _t('components.modals.add-layer.footer.privacy-change-banned') %>
<% } %>
">
<i class="CDB-IconFont CDB-IconFont-<%- icon %>"></i>
</button>
<% } %>