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

View File

@@ -0,0 +1,19 @@
<div class="Modal">
<div class="Modal-header">
<div class="Modal-headerContainer">
<h2 class="CDB-Text CDB-Size-huge is-light u-bSpace"><%- _t('components.modals.add-analysis.modal-title') %></h2>
<h3 class="CDB-Text CDB-Size-medium u-secondaryTextColor"><%- _t('components.modals.add-analysis.modal-desc') %></h3>
</div>
</div>
<div class="Modal-container Modal-container--analysis">
<div class="Modal-inner js-body">
</div>
</div>
<div class="Modal-footer">
<div class="Modal-footerContainer u-flex u-justifyEnd">
<button class="CDB-Button CDB-Button--primary CDB-Button--big is-disabled js-add">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase"><%- _t('components.modals.add-analysis.add-btn') %></span>
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,106 @@
var _ = require('underscore');
var CoreView = require('backbone/core-view');
var AnalysisOptionsCollection = require('./analysis-options-collection');
var analysisOptions = require('./analysis-options');
var AnalysisViewPane = require('./analysis-view-pane');
var renderLoading = require('builder/components/loading/render-loading');
var DataServicesApiCheck = require('builder/editor/layers/layer-content-views/analyses/analyses-quota/analyses-quota-info');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'modalModel',
'configModel',
'userModel',
'layerDefinitionModel'
];
/**
* View to add a new analysis node.
* Expected to be rendered in a modal.
*/
module.exports = CoreView.extend({
className: 'Dialog-content Dialog-content--expanded',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._analysisDefinitionNodeModel = this._layerDefinitionModel.getAnalysisDefinitionNodeModel();
this._queryGeometryModel = this._analysisDefinitionNodeModel.queryGeometryModel;
this.listenTo(this._queryGeometryModel, 'change', this.render);
this._analysisOptions = analysisOptions({
userModel: this._userModel,
configModel: this._configModel,
queryGeometryModel: this._queryGeometryModel
});
this._analysisOptionsCollection = new AnalysisOptionsCollection();
this._dataservicesApiHealth = DataServicesApiCheck.get();
if (!this._isFetchingGeometry()) {
this._queryGeometryModel.fetch();
}
this._initOptions();
},
render: function () {
this.clearSubViews();
var render = this.render.bind(this);
var dsApiNeedsCheck = this._dataservicesApiHealth.needsCheck();
if (dsApiNeedsCheck) {
this._dataservicesApiHealth.fetch({
success: render
});
}
if (this._isFetchingGeometry() || dsApiNeedsCheck) {
this._renderLoadingView();
} else {
this._renderStackView();
}
return this;
},
_renderLoadingView: function () {
this.$el.html(
renderLoading({
title: _t('components.modals.add-widgets.loading-title')
})
);
},
_renderStackView: function () {
var view = new AnalysisViewPane({
modalModel: this._modalModel,
analysisOptions: this._analysisOptions,
analysisOptionsCollection: this._analysisOptionsCollection,
layerDefinitionModel: this._layerDefinitionModel,
queryGeometryModel: this._queryGeometryModel
});
this.$el.html(view.render().$el);
this.addView(view);
},
_isFetchingGeometry: function () {
return this._queryGeometryModel.get('status') === 'fetching';
},
_initOptions: function () {
// Flatten the options hierarchy to a flat array structure that's easier to handle programmatically.
var modelsAttrs = _.reduce(Object.keys(this._analysisOptions), function (memo, category) {
var categoryDef = this._analysisOptions[category];
categoryDef.analyses.forEach(function (d) {
memo.push(_.extend({}, d, { category: category, type: d.nodeAttrs.type }));
});
return memo;
}, [], this);
this._analysisOptionsCollection.reset(modelsAttrs);
}
});

View File

@@ -0,0 +1,51 @@
var _ = require('underscore');
var AnalysisCategoryView = require('./analysis-category-pane-view');
module.exports = function (analysisOptions) {
return [{
type: 'create_clean',
createTabPaneItem: function (optionsCollection, opts) {
return {
label: _t('analysis-category.create-clean'),
name: 'create_clean',
createContentView: function () {
return new AnalysisCategoryView(_.extend({
analysisOptions: analysisOptions,
analysisType: 'create_clean',
collection: optionsCollection
}, opts));
}
};
}
}, {
type: 'analyze_predict',
createTabPaneItem: function (optionsCollection, opts) {
return {
label: _t('analysis-category.analyze-predict'),
name: 'analyze_predict',
createContentView: function () {
return new AnalysisCategoryView(_.extend({
analysisOptions: analysisOptions,
analysisType: 'analyze_predict',
collection: optionsCollection
}, opts));
}
};
}
}, {
type: 'data_transformation',
createTabPaneItem: function (optionsCollection, opts) {
return {
label: _t('analysis-category.data-transformation'),
name: 'data_transformation',
createContentView: function () {
return new AnalysisCategoryView(_.extend({
analysisOptions: analysisOptions,
analysisType: 'data_transformation',
collection: optionsCollection
}, opts));
}
};
}
}];
};

View File

@@ -0,0 +1,46 @@
var CoreView = require('backbone/core-view');
var ScrollView = require('builder/components/scroll/scroll-view');
var AnalysisCategoryPane = require('./analysis-category-pane');
/**
* View to add a new analysis node.
* Expected to be rendered in a modal.
*/
module.exports = CoreView.extend({
className: 'Dialog-content Dialog-content--expanded',
initialize: function (opts) {
if (!opts.modalModel) throw new Error('modalModel is required');
if (!opts.analysisOptionsCollection) throw new Error('analysisOptionsCollection is required');
if (!opts.analysisType) throw new Error('analysisType is required');
if (!opts.analysisOptions) throw new Error('analysisOptions is required');
if (!opts.queryGeometryModel) throw new Error('queryGeometryModel is required');
this._modalModel = opts.modalModel;
this._queryGeometryModel = opts.queryGeometryModel;
this._analysisOptionsCollection = opts.analysisOptionsCollection;
this._analysisType = opts.analysisType;
this._analysisOptions = opts.analysisOptions;
},
render: function () {
this.clearSubViews();
var view = new ScrollView({
createContentView: function () {
return new AnalysisCategoryPane({
collection: this._analysisOptionsCollection,
title: this.options.categoryTitle || this.options.category,
category: this._analysisType === 'all' ? null : this._analysisType,
categoryTitle: this._analysisType === 'all' ? null : this._analysisOptions[this._analysisType].label,
simpleGeometryType: this._queryGeometryModel.get('simple_geom')
});
}.bind(this)
});
this.addView(view);
this.$el.append(view.render().el);
return this;
}
});

View File

@@ -0,0 +1,50 @@
var CoreView = require('backbone/core-view');
var AnalysisOptionView = require('./analysis-option-view');
var template = require('./analysis-category.tpl');
/**
* View to render an individual analysis category and its analysis options
*/
module.exports = CoreView.extend({
className: 'Modal-analysisContainer',
options: {
category: '',
categoryTitle: '',
simpleGeometryType: ''
},
render: function () {
this.clearSubViews();
this.$el.html(this._html());
this.collection.each(this._renderOption, this);
return this;
},
_html: function () {
return template({
title: this.options.title
});
},
_renderOption: function (analysisOptionModel) {
if (this.options.category && !analysisOptionModel.belongsTo(this.options.category)) {
return;
}
var view = new AnalysisOptionView({
model: analysisOptionModel,
simpleGeometryTypeInput: this.options.simpleGeometryType
});
this.addView(view);
this._$list().append(view.render().el);
},
_$list: function () {
return this.$('.js-analyses-list');
}
});

View File

@@ -0,0 +1 @@
<ul class="ModalBlockList js-analyses-list"></ul>

View File

@@ -0,0 +1,51 @@
var _ = require('underscore');
var Backbone = require('backbone');
var camshaftReference = require('builder/data/camshaft-reference');
var nodeIds = require('builder/value-objects/analysis-node-ids');
module.exports = Backbone.Model.extend({
defaults: {
title: '',
category: '',
selected: false,
type_group: '',
desc: '',
link: ''
},
initialize: function (attrs, opts) {
if (!opts.nodeAttrs) throw new Error('nodeAttrs is required');
this._nodeAttrs = opts.nodeAttrs;
},
belongsTo: function (category) {
return this.get('category') === category;
},
acceptsGeometryTypeAsInput: function (simpleGeometryType) {
if (this.get('dummy') === true) return true;
return camshaftReference.isValidInputGeometryForType(simpleGeometryType, this._nodeAttrs.type);
},
getValidInputGeometries: function () {
return camshaftReference.getValidInputGeometriesForType(this._nodeAttrs.type);
},
getFormAttrs: function (layerDefModel) {
var letter = layerDefModel.get('letter');
var sourceId = layerDefModel.get('source');
return _.extend(
{
source: sourceId,
id: letter === nodeIds.letter(sourceId)
? nodeIds.next(sourceId)
: letter + '1'
},
this._nodeAttrs
);
}
});

View File

@@ -0,0 +1,22 @@
var _ = require('underscore');
var AnalysisOptionModel = require('./analysis-option-model');
/**
* Custom model for deprecated SQL function type, to set correct primary source
*/
module.exports = AnalysisOptionModel.extend({
/**
* @override {AnalysisOptionModel.getNodeAttrs}
*/
getFormAttrs: function (layerDefModel) {
var attrs = AnalysisOptionModel.prototype.getFormAttrs.apply(this, arguments);
delete attrs.source;
_.extend(attrs, {
primary_source: layerDefModel.get('source')
});
return attrs;
}
});

View File

@@ -0,0 +1,35 @@
var _ = require('underscore');
var AnalysisOptionModel = require('./analysis-option-model');
var camshaftReference = require('builder/data/camshaft-reference');
/**
* Model to represent a generated analysis option.
*/
module.exports = AnalysisOptionModel.extend({
/**
* @override {AnalysisOptionModel.getNodeAttrs}
*/
getFormAttrs: function () {
var attrs = AnalysisOptionModel.prototype.getFormAttrs.apply(this, arguments);
this._removeSourceIfThereAreMultipleSources(attrs);
return attrs;
},
_removeSourceIfThereAreMultipleSources: function (attrs) {
var params = camshaftReference.paramsForType(attrs.type);
var sourceCount = _.reduce(Object.keys(params), function (memo, name) {
if (params[name].type === 'node') {
memo++;
}
return memo;
}, 0);
if (sourceCount > 1) {
delete attrs.source;
}
}
});

View File

@@ -0,0 +1,24 @@
var _ = require('underscore');
var AnalysisOptionModel = require('./analysis-option-model');
/**
* Custom model for merge type, to set correct primary source
*/
module.exports = AnalysisOptionModel.extend({
/**
* @override {AnalysisOptionModel.getNodeAttrs}
*/
getFormAttrs: function (layerDefModel) {
var attrs = AnalysisOptionModel.prototype.getFormAttrs.apply(this, arguments);
delete attrs.source;
_.extend(attrs, {
primary_source_name: 'left_source',
left_source: layerDefModel.get('source')
});
return attrs;
}
});

View File

@@ -0,0 +1,91 @@
var _ = require('underscore');
var $ = require('jquery');
var CoreView = require('backbone/core-view');
var template = require('./analysis-option.tpl');
var Analyses = require('builder/data/analyses');
/**
* View for an individual analysis option.
*/
module.exports = CoreView.extend({
tagName: 'li',
className: 'ModalBlockList-item',
events: {
'mouseenter': '_onMouseEnter',
'mouseleave': '_onMouseLeave',
'click': '_onClick'
},
initialize: function (opts) {
this._simpleGeometryTypeInput = opts.simpleGeometryTypeInput; // might be undefined/null, so don't check
this.listenTo(this.model, 'change:selected', this.render);
},
render: function () {
var props = this.model.pick('title', 'type_group', 'desc', 'selected', 'link');
props.enabled = this._acceptsInputGeometry();
if (!props.enabled) {
props.desc = this._disabledDesc();
}
var analysisType = this.model.get('type');
var analysisItem = Analyses.getAnalysisByType(analysisType);
var animationTemplate = analysisItem && analysisItem.animationTemplate;
var genericType = '';
if (animationTemplate) {
genericType = analysisItem.genericType || analysisType;
}
this.$el.html(template(_.extend(props, { type: genericType })));
if (animationTemplate) {
this.$('.js-animation').append(animationTemplate);
}
this.$el.toggleClass('is-selected', this.model.get('selected'));
this.$el.toggleClass('is-disabled', !props.enabled);
return this;
},
_onMouseEnter: function () {
if (this._acceptsInputGeometry()) {
this.$('.js-animation').addClass('has-autoplay');
}
},
_onMouseLeave: function () {
if (this._acceptsInputGeometry()) {
this.$('.js-animation').removeClass('has-autoplay');
}
},
_onClick: function (event) {
var linkClicked = event && event.target && $(event.target).hasClass('js-more');
if (!linkClicked && this._acceptsInputGeometry()) {
this.model.set('selected', true);
}
},
_disabledDesc: function (desc) {
return _t('components.modals.add-analysis.disabled-option-desc', {
simpleGeometryType: this._simpleGeometryTypeInput || _t('components.modals.add-analysis.unknown-geometry-type'),
requiredInputGeometries: this._getRequiredInputGeometries()
});
},
_getRequiredInputGeometries: function () {
return _t('components.modals.add-analysis.geometry-types.' + this.model.getValidInputGeometries());
},
_acceptsInputGeometry: function () {
return this.model.acceptsGeometryTypeAsInput(this._simpleGeometryTypeInput);
}
});

View File

@@ -0,0 +1,27 @@
<div class="Analysis-animation <% if (enabled) { %>is-enabled<% } %> <% if (type) { %>is-<%- type %><% } %> js-animation u-flex u-alignCenter u-justifyCenter"></div>
<div class="Analysis-info u-flex">
<div class="ModalBlockList-itemInput CDB-Size-large">
<input class="CDB-Radio" type="radio" value="true"
<% if (selected) { %>checked="checked"<% } %>
<% if (!enabled) { %>disabled="disabled"<% } %>
>
<span class="u-iBlock CDB-Radio-face"></span>
</div>
<div class="ModalBlockList-inner">
<div class="ModalBlockList-item-header u-bSpace">
<h2 class="ModalBlockList-item-headerTitle CDB-Text CDB-Size-large u-rSpace--m u-ellipsis" title="<%- title %>">
<%- title %>
</h2>
</div>
<div class="ModalBlockList-item-body">
<p class="CDB-Text CDB-Size-small u-secondaryTextColor">
<%- desc %>
</p>
<% if (link) { %>
<a class="Analysis-link CDB-Text CDB-Size-small u-tSpace-xl u-actionTextColor u-upperCase js-more track-<%- type %>-modal-learn" href="<%- link %>" target="_blank">
<%- _t('components.modals.add-analysis.more-info') %>
</a>
<% } %>
</div>
</div>
</div>

View File

@@ -0,0 +1,28 @@
var _ = require('underscore');
var Backbone = require('backbone');
var AnalysisOptionModel = require('./analysis-option-models/analysis-option-model');
module.exports = Backbone.Collection.extend({
model: function (d, opts) {
var Model = d.Model || AnalysisOptionModel;
var attrs = _.omit(d, 'Model', 'nodeAttrs');
return new Model(attrs, {nodeAttrs: d.nodeAttrs});
},
initialize: function (models, opts) {
this.on('change:selected', this._onSelectedChange, this);
},
_onSelectedChange: function (changedModel, isSelected) {
if (isSelected) {
this.each(function (m) {
if (m !== changedModel) {
m.set('selected', false);
}
});
}
}
});

View File

@@ -0,0 +1,64 @@
var _ = require('underscore');
var GeneratedAnalysisOptionModel = require('./analysis-option-models/generated-analysis-option-model');
var camshaftReference = require('camshaft-reference');
var latestCamshaftReference = camshaftReference.getVersion('latest');
var Analyses = require('builder/data/analyses');
/**
* Analysis definitions, organized in buckets per top-level category
*
* - {Obj} -> options (userModel, configModel, queryGeometryModel)
*/
module.exports = function (options) {
var categories = {
create_clean: {
title: _t('analysis-category.create-clean'),
analyses: Analyses.getAnalysesByModalCategory('create_clean', options)
},
data_transformation: {
title: _t('analysis-category.data-transformation'),
analyses: Analyses.getAnalysesByModalCategory('data_transformation', options)
},
analyze_predict: {
title: _t('analysis-category.analyze-predict'),
analyses: Analyses.getAnalysesByModalCategory('analyze_predict', options)
}
};
var alsoGenerateFromCamshaftReference = options.userModel.featureEnabled('generate_analysis_options');
if (alsoGenerateFromCamshaftReference) {
var implementedAnalyses = _.reduce(Object.keys(categories), function (memo, category) {
return memo.concat(
categories[category].analyses.map(function (analysis) {
return analysis.nodeAttrs.type;
})
);
}, []);
categories['generated'] = {
title: 'Generated analyses by the Camshaft reference v' + _.last(camshaftReference.versions),
analyses: _.compact(
Object
.keys(latestCamshaftReference.analyses)
.filter(function (type) {
return type !== 'source' && !_.contains(implementedAnalyses, type);
})
.map(function (type) {
if (!Analyses.isAnalysisValidByType(type, options)) {
return false;
}
return {
Model: GeneratedAnalysisOptionModel,
nodeAttrs: {type: type},
title: type,
desc: JSON.stringify(latestCamshaftReference.analyses[type])
};
})
)
};
}
return categories;
};

View File

@@ -0,0 +1,122 @@
var _ = require('underscore');
var CoreView = require('backbone/core-view');
var AnalysisCategoryView = require('./analysis-category-pane-view');
var createTemplateTabPane = require('builder/components/tab-pane/create-template-tab-pane');
var template = require('./add-analyses.tpl');
var tabPaneButtonTemplate = require('./tab-pane-button-template.tpl');
var tabPaneTemplate = require('./tab-pane-template.tpl');
var analysesTypes = require('./analyses-types');
var Router = require('builder/routes/router');
/**
* View to select the analysis to create.
*/
module.exports = CoreView.extend({
className: 'Dialog-content Dialog-content--expanded',
events: {
'click .js-add': '_onAddAnalysis'
},
initialize: function (opts) {
if (!opts.modalModel) throw new Error('modalModel is required');
if (!opts.analysisOptionsCollection) throw new Error('analysisOptionsCollection is required');
if (!opts.analysisOptions) throw new Error('analysisOptions is required');
if (!opts.layerDefinitionModel) throw new Error('layerDefinitionModel is required');
if (!opts.queryGeometryModel) throw new Error('queryGeometryModel is required');
this._modalModel = opts.modalModel;
this._analysisOptions = opts.analysisOptions;
this._analysisOptionsCollection = opts.analysisOptionsCollection;
this._layerDefinitionModel = opts.layerDefinitionModel;
this._queryGeometryModel = opts.queryGeometryModel;
this.listenTo(this._analysisOptionsCollection, 'change:selected', this._toggleAddButton);
this._generateTabPaneItems();
},
render: function () {
this.clearSubViews();
this.$el.html(template());
var options = {
tabPaneOptions: {
template: tabPaneTemplate,
tabPaneItemOptions: {
tagName: 'li',
klassName: 'CDB-NavMenu-item'
}
},
tabPaneTemplateOptions: {
tagName: 'button',
className: 'CDB-NavMenu-link u-upperCase',
template: tabPaneButtonTemplate
}
};
this._tabPane = createTemplateTabPane(this._tabPaneItems, options);
this.addView(this._tabPane);
this._$body().append(this._tabPane.render().el);
return this;
},
goToTabItem: function (tabItemName) {
var tabItem = _.first(this._tabPane.collection.where({ name: tabItemName }));
if (tabItem) {
tabItem.set('selected', true);
this._toggleAddButton(); // set the right state for the add button
}
},
_generateTabPaneItems: function () {
var availableTypes = _.unique(_.keys(this._analysisOptions));
this._tabPaneItems = _.map(analysesTypes(this._analysisOptions), function (d) {
if (_.contains(availableTypes, d.type)) {
return d.createTabPaneItem(this._analysisOptionsCollection, {
modalModel: this._modalModel,
analysisOptionsCollection: this._analysisOptionsCollection,
queryGeometryModel: this._queryGeometryModel
});
}
}.bind(this));
this._tabPaneItems.unshift({
label: _t('analysis-category.all'),
name: 'all',
createContentView: function () {
return new AnalysisCategoryView({
analysisType: 'all',
modalModel: this._modalModel,
analysisOptions: this._analysisOptions,
analysisOptionsCollection: this._analysisOptionsCollection,
queryGeometryModel: this._queryGeometryModel
});
}.bind(this)
});
},
_$body: function () {
return this.$('.js-body');
},
_onAddAnalysis: function () {
var selectedOptionModel = this._analysisOptionsCollection.find(this._isSelected);
var layerDefinitionModel = this._layerDefinitionModel;
if (selectedOptionModel) {
var analysisFormAttrs = selectedOptionModel.getFormAttrs(layerDefinitionModel);
this._modalModel.destroy(analysisFormAttrs);
Router.goToAnalysisNode(layerDefinitionModel.get('id'), analysisFormAttrs.id);
}
},
_toggleAddButton: function () {
this.$('.js-add').toggleClass('is-disabled', !this._analysisOptionsCollection.any(this._isSelected));
},
_isSelected: function (m) {
return !!m.get('selected');
}
});

View File

@@ -0,0 +1,16 @@
<svg width="308px" height="98px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g fill="#9DE0AD" fill-rule="evenodd" transform="translate(50, 8)">
<circle class="aoi-area area01" fill="#5A7E6D" cx="20" cy="36" r="20"></circle>
<circle class="aoi-point area-point01" cx="20" cy="36" r="4"></circle>
<circle class="aoi-area area02" fill="#5A7E6D" cx="56" cy="28" r="20"></circle>
<circle class="aoi-point area-point02" cx="56" cy="28" r="4"></circle>
<circle class="aoi-area area03" fill="#5A7E6D" cx="76" cy="44" r="20"></circle>
<circle class="aoi-point area-point03" cx="76" cy="44" r="4"></circle>
<circle class="aoi-area area04" fill="#5A7E6D" cx="138" cy="60" r="20"></circle>
<circle class="aoi-point area-point04" cx="138" cy="60" r="4"></circle>
<circle class="aoi-area area05" fill="#5A7E6D" cx="156" cy="20" r="20"></circle>
<circle class="aoi-point area-point05" cx="156" cy="20" r="4"></circle>
<circle class="aoi-area area06" fill="#5A7E6D" cx="188" cy="28" r="20"></circle>
<circle class="aoi-point area-point06" cx="188" cy="28" r="4"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,16 @@
<svg width="148px" height="141px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g fill="#636D72" fill-rule="evenodd">
<circle class="Centroid-pointsHighlight" fill="#9DE0AD" cx="74" cy="70" r="4"></circle>
<circle class="Centroid-line" fill="none" stroke="#9DE0AD" cx="74.5" cy="70" r="70"></circle>
<circle class="Centroid-points Centroid-points01" cx="4" cy="65" r="4"></circle>
<circle class="Centroid-points Centroid-points02" cx="34" cy="85" r="4"></circle>
<circle class="Centroid-points Centroid-points03" cx="103" cy="50" r="4"></circle>
<circle class="Centroid-points Centroid-points04" cx="83" cy="40" r="4"></circle>
<circle class="Centroid-points Centroid-points05" cx="63" cy="90" r="4"></circle>
<circle class="Centroid-points Centroid-points06" cx="144" cy="65" r="4"></circle>
<circle class="Centroid-points Centroid-points07" cx="93" cy="96" r="4"></circle>
<circle class="Centroid-points Centroid-points08" cx="44" cy="55" r="4"></circle>
<circle class="Centroid-points Centroid-points09" cx="112" cy="83" r="4"></circle>
<circle class="Centroid-points Centroid-points10" cx="24" cy="45" r="4"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,37 @@
<svg width="306" height="98" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g transform="translate(30, 16)" fill="#636D72">
<g>
<path class="Line Line-01" d="M109,32 L42,29"></path>
<path class="Line Line-02" d="M109,32 L72,49"></path>
<path class="Line Line-03" d="M109,32 L101,54"></path>
<path class="Line Line-04" d="M109,32 L131,60"></path>
<path class="Line Line-05" d="M109,32 L82,19"></path>
<path class="Line Line-06" d="M109,32 L150,47"></path>
<path class="Line Line-07" d="M109,32 L121,4"></path>
<path class="Line Line-08" d="M109,32 L141,14"></path>
<path class="Line Line-09" d="M109,32 L159,25"></path>
</g>
<circle class="Dot Dot--withColorChange Dot-01" cx="42" cy="29" r="4"></circle>
<circle class="Dot Dot--withColorChange Dot-02" cx="72" cy="49" r="4"></circle>
<circle class="Dot Dot--withColorChange Dot-03" cx="101" cy="54" r="4"></circle>
<circle class="Dot Dot--withColorChange Dot-04" cx="131" cy="60" r="4"></circle>
<circle class="Dot Dot--withColorChange Dot-05" cx="82" cy="19" r="4"></circle>
<circle class="Dot Dot--withColorChange Dot-06" cx="150" cy="47" r="4"></circle>
<circle class="Dot Dot--withColorChange Dot-07" cx="141" cy="14" r="4"></circle>
<circle class="Dot Dot--withColorChange Dot-08" cx="121" cy="4" r="4"></circle>
<circle class="Dot Dot--withColorChange Dot-09" cx="159" cy="25" r="4"></circle>
<circle class="Dot Dot--withFadeOut Dot-10" cx="52" cy="54" r="4"></circle>
<circle class="Dot Dot--withFadeOut Dot-11" cx="4" cy="54" r="4"></circle>
<circle class="Dot Dot--withFadeOut Dot-12" cx="28" cy="14" r="4"></circle>
<circle class="Dot Dot--withFadeOut Dot-13" cx="188" cy="6" r="4"></circle>
<circle class="Dot Dot--withFadeOut Dot-14" cx="196" cy="46" r="4"></circle>
<circle class="Dot Dot--withFadeOut Dot-15" cx="228" cy="26" r="4"></circle>
<circle class="Dot Dot--withFadeOut Dot-16" cx="182" cy="29" r="4"></circle>
<circle class="Dot Dot--withFadeOut Dot-17" cx="62" cy="9" r="4"></circle>
<circle class="Dot" fill="white" cx="109" cy="32" r="4"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,16 @@
<svg width="148px" height="64px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" fill="#636D72">
<polyline class="Line" stroke="#9DE0AD" points="4.08007812 29.7382812 34.2617188 49.4335938 24.2285156 9.83007812 43.7246094 18.7578125 62.9414062 54.7128906 73.6425781 30.9160156 92.3808594 59.359375 83.328125 3.75585938 103.042969 14.6777344 111.734375 47.1640625 144.171875 28.3574219"></polyline>
<circle class="Dot Dot-01" cx="4" cy="29" r="4"></circle>
<circle class="Dot Dot-02" cx="34" cy="49" r="4"></circle>
<circle class="Dot Dot-03" cx="24" cy="9" r="4"></circle>
<circle class="Dot Dot-04" cx="44" cy="19" r="4"></circle>
<circle class="Dot Dot-05" cx="63" cy="54" r="4"></circle>
<circle class="Dot Dot-06" cx="74" cy="32" r="4"></circle>
<circle class="Dot Dot-07" cx="93" cy="60" r="4"></circle>
<circle class="Dot Dot-08" cx="83" cy="4" r="4"></circle>
<circle class="Dot Dot-09" cx="103" cy="14" r="4"></circle>
<circle class="Dot Dot-10" cx="112" cy="47" r="4"></circle>
<circle class="Dot Dot-11" cx="144" cy="29" r="4"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,54 @@
<svg width="302px" height="98px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<rect id="path-1" x="0" y="0" width="304" height="96"></rect>
<rect id="path-3" x="48" y="0" width="48" height="64"></rect>
<mask id="mask-4" x="0" y="0" width="48" height="64" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<rect id="path-5" x="0" y="31" width="49" height="33"></rect>
<mask id="mask-6" x="0" y="0" width="49" height="33" fill="white">
<use xlink:href="#path-5"></use>
</mask>
<rect id="path-7" x="95" y="0" width="57" height="24"></rect>
<mask id="mask-8" x="0" y="0" width="57" height="24" fill="white">
<use xlink:href="#path-7"></use>
</mask>
<rect id="path-9" x="151" y="8" width="18" height="16"></rect>
<mask id="mask-10" x="0" y="0" width="18" height="16" fill="white">
<use xlink:href="#path-9"></use>
</mask>
<rect id="path-11" x="24" y="16" width="25" height="16"></rect>
<mask id="mask-12" x="0" y="0" width="25" height="16" fill="white">
<use xlink:href="#path-11"></use>
</mask>
<rect id="path-13" x="95" y="23" width="25" height="25"></rect>
<mask id="mask-14" x="0" y="0" width="25" height="25" fill="white">
<use xlink:href="#path-13"></use>
</mask>
<rect id="path-15" x="119" y="23" width="58" height="33"></rect>
<mask id="mask-16" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="58" height="33" fill="white">
<use xlink:href="#path-15"></use>
</mask>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g>
<g>
<g>
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Rectangle-10" xlink:href="#path-1"></use>
</g>
<g transform="translate(64, 16)" stroke="#282C2F" stroke-width="2" fill="#9DE0AD">
<use class="rect-01" mask="url(#mask-4)" fill-opacity="0.32" xlink:href="#path-3"></use>
<use class="rect-02" mask="url(#mask-6)" fill-opacity="0.8" xlink:href="#path-5"></use>
<use class="rect-03" mask="url(#mask-8)" fill-opacity="0.639999986" xlink:href="#path-7"></use>
<use class="rect-04" mask="url(#mask-10)" fill-opacity="0.160000011" xlink:href="#path-9"></use>
<use class="rect-05" mask="url(#mask-12)" fill-opacity="0.48" xlink:href="#path-11"></use>
<use class="rect-06" mask="url(#mask-14)" xlink:href="#path-13"></use>
<use class="rect-07" mask="url(#mask-16)" fill-opacity="0.800000012" xlink:href="#path-15"></use>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,24 @@
<svg width="306" height="98" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle class="Dot" cx="117" cy="33" r="4"></circle>
<circle class="Dot" cx="91" cy="57" r="4"></circle>
<circle class="Dot--withSplash Dot--withSplash--01" cx="91" cy="57" r="4"></circle>
<circle class="Dot" cx="73" cy="27" r="4"></circle>
<circle class="Dot" cx="223" cy="39" r="4"></circle>
<circle class="Dot--withSplash Dot--withSplash--02" cx="223" cy="39" r="4"></circle>
<circle class="Dot" cx="125" cy="69" r="4"></circle>
<circle class="Dot" cx="64" cy="65" r="4"></circle>
<circle class="Dot" cx="151" cy="71" r="4"></circle>
<circle class="Dot--withSplash Dot--withSplash--03" cx="151" cy="71" r="4"></circle>
<circle class="Dot" cx="205" cy="75" r="4"></circle>
<circle class="Dot" cx="163" cy="61" r="4"></circle>
<circle class="Dot" cx="133" cy="43" r="4"></circle>
<circle class="Dot" cx="189" cy="46" r="4"></circle>
<circle class="Dot--withSplash Dot--withSplash--04" cx="189" cy="46" r="4"></circle>
<circle class="Dot" cx="242" cy="45" r="4"></circle>
<circle class="Dot" cx="155" cy="39" r="4"></circle>
<circle class="Dot" cx="195" cy="23" r="4"></circle>
<circle class="Dot--green Dot-01" cx="91" cy="57" r="4"></circle>
<circle class="Dot--green Dot-02" cx="223" cy="39" r="4"></circle>
<circle class="Dot--green Dot-03" cx="151" cy="71" r="4"></circle>
<circle class="Dot--green Dot-04" cx="189" cy="46" r="4"></circle>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,38 @@
<svg width="304" height="96" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g fill="#636D72" stroke="none" stroke-width="1">
<g stroke="#FFFFFF" stroke-linecap="square">
<path class="Line" d="M174.5,58.5 L166.5,35.5" id="Line"></path>
<path class="Line" d="M174.5,58.5 L145.5,40.5" id="Line"></path>
<path class="Line" d="M128.5,29.5 L145.5,40.5" id="Line"></path>
<path class="Line" d="M128.5,29.5 L84.5,24.5" id="Line"></path>
<path class="Line--withHighlight" d="M174.5,58.5 L136.5,66.5" id="Line"></path>
<path class="Line--withHighlight" d="M103.5,53.5 L136.5,66.5" id="Line"></path>
<path class="Line--withHighlight" d="M76.5,61.5 L104.5,53.5" id="Line"></path>
<path class="Line" d="M174.5,58.5 L162.5,68.5" id="Line"></path>
<path class="Line" d="M174.5,58.5 L217.5,71.5" id="Line"></path>
<path class="Line" d="M235.5,35.5 L217.5,71.5" id="Line"></path>
<path class="Line" d="M235.5,35.5 L253.5,41.5" id="Line"></path>
<path class="Line" d="M202.5,42.5 L166.5,35.5" id="Line"></path>
<path class="Line" d="M202.5,42.5 L207.5,18.5" id="Line"></path>
<path class="Line--withFade" d="M174.5,58.5 L136.5,66.5" id="Line"></path>
<path class="Line--withFade" d="M103.5,53.5 L136.5,66.5" id="Line"></path>
<path class="Line--withFade" d="M76.5,61.5 L104.5,53.5" id="Line"></path>
</g>
<circle class="Dot Dot-01" cx="129" cy="30" r="4"></circle>
<circle class="Dot Dot-02" cx="85" cy="24" r="4"></circle>
<circle class="Dot Dot-03" cx="235" cy="36" r="4"></circle>
<circle class="Dot Dot-04" cx="163" cy="68" r="4"></circle>
<circle class="Dot Dot-05" cx="217" cy="72" r="4"></circle>
<circle class="Dot Dot-06" cx="145" cy="40" r="4"></circle>
<circle class="Dot Dot-07" cx="201" cy="43" r="4"></circle>
<circle class="Dot Dot-08" cx="254" cy="42" r="4"></circle>
<circle class="Dot Dot-09" cx="167" cy="36" r="4"></circle>
<circle class="Dot Dot-10" cx="207" cy="20" r="4"></circle>
<circle class="Dot-11 Dot--withHighlight" cx="103" cy="54" r="4"></circle>
<circle class="Dot-12 Dot--withHighlight" cx="137" cy="66" r="4"></circle>
<circle class="Dot-13 Dot--withHighlight" cx="76" cy="62" r="4"></circle>
<circle class="Dot-14 Dot--withHighlight" cx="175" cy="58" r="4"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,19 @@
<svg width="306px" height="98px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g fill="#636D72" fill-rule="evenodd" transform="translate(70, 15)">
<polygon fill="none" class="Filter-line" stroke="#9DE0AD" points="73.703125 13.6435547 97.5439453 17.5722656 106.554688 42.9824219 92.5517578 55.5849609 63.3261719 53.5224609 54.0126953 15.3515625 59.0839844 11.4414062"></polygon>
<circle class="Filter-dotHighlight Filter-dotHighlight--01" cx="95" cy="20" r="4"></circle>
<circle class="Filter-dotHighlight Filter-dotHighlight--06" cx="73" cy="24" r="4"></circle>
<circle class="Filter-dotHighlight Filter-dotHighlight--02" cx="103" cy="42" r="4"></circle>
<circle class="Filter-dotHighlight Filter-dotHighlight--03" cx="91" cy="52" r="4"></circle>
<circle class="Filter-dotHighlight Filter-dotHighlight--05" cx="57" cy="14" r="4"></circle>
<circle class="Filter-dotHighlight Filter-dotHighlight--04" cx="65" cy="50" r="4"></circle>
<circle class="Filter-dot circle08" cx="163" cy="20" r="4"></circle>
<circle class="Filter-dot circle07" cx="31" cy="38" r="4"></circle>
<circle class="Filter-dot circle06" cx="13" cy="8" r="4"></circle>
<circle class="Filter-dot circle05" cx="4" cy="46" r="4"></circle>
<circle class="Filter-dot circle04" cx="145" cy="56" r="4"></circle>
<circle class="Filter-dot circle03" cx="129" cy="27" r="4"></circle>
<circle class="Filter-dot circle02" cx="182" cy="26" r="4"></circle>
<circle class="Filter-dot circle01" cx="135" cy="4" r="4"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,16 @@
<svg width="302px" height="99px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g fill="#9DE0AD" fill-rule="evenodd">
<path d="M0.5,41.5 L305.5,24.5" class="Georeference-street00" stroke="#282C2F" stroke-width="8" stroke-linecap="square"></path>
<path d="M134,0 L141,91" class="Georeference-street01" stroke="#282C2F" stroke-width="8" stroke-linecap="square"></path>
<path d="M258,0 L265,91" class="Georeference-street02" stroke="#282C2F" stroke-width="8" stroke-linecap="square"></path>
<text class="Georeference-point02-text" font-family="Open Sans" font-size="10" font-weight="300" fill="#FFFFFF">
<tspan x="63" y="62">-63.578373</tspan>
<tspan x="63" y="76">44.642619</tspan>
</text>
<text class="Georeference-point01-text" font-family="Open Sans" font-size="10" font-weight="300" fill="#FFFFFF">
<tspan x="198" y="54">IP 214.80.33.47</tspan>
</text>
<circle class="Dot Dot-01" fill="#9DE0AD" cx="43" cy="40" r="8"></circle>
<circle class="Dot Dot-02" fill="#9DE0AD" cx="183" cy="32" r="8"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,20 @@
<svg width="176" height="64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g fill="#9DE0AD">
<polygon fill="none" class="Group-line" stroke="#9EE0AD" points="88.4493047 0.0859375 175.58896 0.0859375 175.58896 23.2574383 135.581141 32.7431641 135.581141 54.9504597 117.092373 54.9504597 101.982508 63.6816406 59.4503976 63.6816406 44.9366094 55.6914063 1 55.6914063 1 24.8914834 44.6201536 24.8914834 56.9346563 0"></polygon>
<circle class="Group-point Group-point01" cx="60" cy="60" r="4"></circle>
<circle class="Group-point Group-point02" cx="100" cy="4" r="4"></circle>
<circle class="Group-point Group-point03" cx="132" cy="52" r="4"></circle>
<circle class="Group-point Group-point04" cx="116" cy="52" r="4"></circle>
<circle class="Group-point Group-point05" cx="116" cy="36" r="4"></circle>
<circle class="Group-point Group-point06" cx="116" cy="20" r="4"></circle>
<circle class="Group-point Group-point07" cx="172" cy="20" r="4"></circle>
<circle class="Group-point Group-point08" cx="172" cy="4" r="4"></circle>
<circle class="Group-point Group-point09" cx="44" cy="28" r="4"></circle>
<circle class="Group-point Group-point10" cx="4" cy="28" r="4"></circle>
<circle class="Group-point Group-point11" cx="4" cy="52" r="4"></circle>
<circle class="Group-point Group-point12" cx="44" cy="52" r="4"></circle>
<circle class="Group-point Group-point13" cx="100" cy="60" r="4"></circle>
<circle class="Group-point Group-point14" cx="132" cy="36" r="4"></circle>
<circle class="Group-point Group-point15" cx="60" cy="4" r="4"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,19 @@
<svg width="308px" height="98px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g fill="#636D72" fill-rule="evenodd" transform="translate(55, 15)">
<circle class="intersect intersect-layer" fill="#5A7E6D" stroke="#9DE0AD" cx="101" cy="32" r="32"></circle>
<circle class="intersect intersect-circle1" cx="57" cy="14" r="4"></circle>
<circle class="intersect intersect-circle2" cx="31" cy="38" r="4"></circle>
<circle class="intersect intersect-circle3" cx="13" cy="8" r="4"></circle>
<circle class="intersect intersect-circle4" cx="163" cy="20" r="4"></circle>
<circle class="intersect intersect-circle5" cx="65" cy="50" r="4"></circle>
<circle class="intersect intersect-circle6" cx="4" cy="46" r="4"></circle>
<circle class="intersect intersect-circle1Highlight" cx="91" cy="52" r="4"></circle>
<circle class="intersect intersect-circle7" cx="145" cy="56" r="4"></circle>
<circle class="intersect intersect-circle2Highlight" cx="103" cy="42" r="4"></circle>
<circle class="intersect intersect-circle3Highlight" cx="73" cy="24" r="4"></circle>
<circle class="intersect intersect-circle4Highlight" cx="129" cy="27" r="4"></circle>
<circle class="intersect intersect-circle8" cx="182" cy="26" r="4"></circle>
<circle class="intersect intersect-circle5Highlight" cx="95" cy="20" r="4"></circle>
<circle class="intersect intersect-circle9" cx="135" cy="4" r="4"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,13 @@
<svg width="185px" height="64px" viewBox="512 388 185 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Group" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(513.000000, 389.000000)">
<polygon class="Path Path-01" stroke="#FFFFFF" points="7.92876627e-08 47 22.7004054 47 22.7004054 62.1132254 0 62.1132254"></polygon>
<polygon class="Path Path-03" stroke="#FFFFFF" points="160 24 182.700012 24 182.700012 38.9848139 160 38.9848139"></polygon>
<polygon class="Path Path-02" stroke="#FFFFFF" points="87.7216797 0.278320312 110.836973 0.278320312 110.836973 24.2279707 87.8211856 24.2279707"></polygon>
<polygon class="Path Path-03Green" stroke="#9DE0AD" points="128 24 182.862039 24 182.862039 55.6141883 128.202651 55.6141883"></polygon>
<polygon class="Path Path-01Green" stroke="#9DE0AD" points="0.0502868199 31 47.7467919 31 47.7467919 62.177811 0 62.177811"></polygon>
<polygon class="Path Path-02Green" stroke="#9DE0AD" points="63.9970703 0.033203125 110.710209 0.033203125 110.710209 62.0109574 63.8597689 62.0109574"></polygon>
<circle class="Dot Dot-01" fill="#FFFFFF" cx="7" cy="55" r="4"></circle>
<circle class="Dot Dot-02" fill="#FFFFFF" cx="99" cy="11" r="4"></circle>
<circle class="Dot Dot-03" fill="#FFFFFF" cx="171" cy="31" r="4"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<svg width="306" height="98" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g transform="translate(50, 20)">
<circle class="Dot Dot--01 Dot--gray" cx="4" cy="46" r="4"></circle>
<circle class="Dot Dot--02 Dot--gray" cx="31" cy="38" r="4"></circle>
<circle class="Dot Dot--03 Dot--gray" cx="13" cy="8" r="4"></circle>
<circle class="Dot Dot--04 Dot--green" cx="57" cy="14" r="4"></circle>
<circle class="Dot Dot--05 Dot--green" cx="65" cy="50" r="4"></circle>
<circle class="Dot Dot--06 Dot--green" cx="91" cy="52" r="4"></circle>
<circle class="Dot Dot--07 Dot--green" cx="103" cy="42" r="4"></circle>
<circle class="Dot Dot--08 Dot--green" cx="73" cy="24" r="4"></circle>
<circle class="Dot Dot--09 Dot--green" cx="95" cy="20" r="4"></circle>
<circle class="Dot Dot--10 Dot--darkGray" cx="145" cy="56" r="4"></circle>
<circle class="Dot Dot--11 Dot--darkGray" cx="129" cy="27" r="4"></circle>
<circle class="Dot Dot--12 Dot--darkGray" cx="135" cy="4" r="4"></circle>
<circle class="Dot Dot--13 Dot--white" cx="163" cy="20" r="4"></circle>
<circle class="Dot Dot--14 Dot--white" cx="182" cy="26" r="4"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,36 @@
<svg width="306px" height="98px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Mod-Analysis" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g fill="#9DE0AD">
<text x="8" y="19" class="Text Text-LH" font-family="OpenSans, Open Sans" font-size="10" font-weight="normal">
<%- _t('components.modals.add-analysis.option-types.moran-cluster.low-high') %>
</text>
<text x="227" y="19" class="Text Text-HH" font-family="OpenSans, Open Sans" font-size="10" font-weight="normal">
<%- _t('components.modals.add-analysis.option-types.moran-cluster.high-high') %>
</text>
<text x="233" y="85" class="Text Text-HL" font-family="OpenSans, Open Sans" font-size="10" font-weight="normal">
<%- _t('components.modals.add-analysis.option-types.moran-cluster.high-low') %>
</text>
<text x="10" y="85" class="Text Text-LL" font-family="OpenSans, Open Sans" font-size="10" font-weight="normal">
<%- _t('components.modals.add-analysis.option-types.moran-cluster.low-low') %>
</text>
</g>
<g fill="#FFF">
<circle class="Dot Dot-04" fill-opacity="0.4" cx="106" cy="57" r="4"></circle>
<circle class="Dot Dot-04" fill-opacity="0.4" cx="137" cy="66" r="4"></circle>
<circle class="Dot Dot-04" fill-opacity="0.4" cx="79" cy="62" r="4"></circle>
<circle class="Dot Dot-06" fill-opacity="0.6" cx="193" cy="70" r="4"></circle>
<circle class="Dot Dot-06" fill-opacity="0.6" cx="175" cy="58" r="4"></circle>
<circle class="Dot Dot-08" fill-opacity="0.8" cx="129" cy="30" r="4"></circle>
<circle class="Dot Dot-08" fill-opacity="0.8" cx="145" cy="40" r="4"></circle>
<circle class="Dot Dot-1" cx="231" cy="36" r="4"></circle>
<circle class="Dot Dot-1" cx="197" cy="40" r="4"></circle>
<circle class="Dot Dot-1" cx="250" cy="38" r="4"></circle>
<circle class="Dot Dot-1" cx="167" cy="36" r="4"></circle>
<circle class="Dot Dot-1" cx="203" cy="20" r="4"></circle>
</g>
<g stroke="#9DE0AD">
<path d="M0,49 L304,49" class="Line Line-horizontal" stroke-dasharray="2,3"></path>
<path d="M152,0 L152,98" class="Line Line-vertical" stroke-dasharray="2,3"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,37 @@
<svg width="306px" height="98px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g transform="translate(87, 22)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="square">
<path class="Arrow-04" d="M98.5,45.5 L97.5,40.5" stroke="#9CE0AC"></path>
<path class="Arrow-04" d="M98.5,45.5 L92.5,46.5" stroke="#9CE0AC"></path>
<path class="Line-04" d="M75.5,34.5 L98.5,45.5" stroke="#9DE0AD"></path>
<path class="Line-03" d="M0.5,44.5 L3.5,39.5" stroke="#9CE0AC"></path>
<path class="Line-03" d="M0.5,44.5 L5.5,47.5" stroke="#9CE0AC"></path>
<path class="Line-03" d="M0.5,44.5 L55.5,32.5" stroke="#9DE0AD"></path>
<g transform="translate(75.000000, 1.000000)">
<path class="Arrow-01" d="M31.5,8.5 L32.5,2.5" stroke="#95D6A6"></path>
<path class="Arrow-01" d="M26.5,0.5 L32.5,2.5" stroke="#95D6A6"></path>
<path class="Line-01" d="M0.5,18.5 L32.5,2.5" stroke="#9DE0AD"></path>
</g>
<g transform="translate(19.000000, 0.000000)" stroke="#979797">
<path class="Arrow-02" d="M36.5,19.5 L35.5,14.5"></path>
<path class="Arrow-02" d="M36.5,19.5 L31.5,21.5"></path>
<path class="Line-02" d="M0.5,0.5 L36.5,19.5"></path>
</g>
</g>
</g>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g stroke="#636D72">
<path d="M0,49 L304,49" class="Line Line-horizontal" stroke-dasharray="2,3"></path>
<path d="M152,0 L152,98" class="Line Line-vertical" stroke-dasharray="2,3"></path>
</g>
</g>
<g transform="translate(72, 11)">
<circle class="Dot Dot--center" cx="80" cy="38" r="4" stroke="#9DE0AD" fill="#2E3C43"></circle>
<circle class="Dot Dot-01" fill="#9DE0AD" cx="133" cy="10" r="4"></circle>
<circle class="Dot Dot-02" fill="#979EA1" cx="24" cy="4" r="4"></circle>
<circle class="Dot Dot-03" fill="#9DE0AD" cx="4" cy="57" r="4"></circle>
<circle class="Dot Dot-04" fill="#9DE0AD" cx="122" cy="61" r="4"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,22 @@
<svg width="306" height="98" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g transform="translate(30, 16)" fill="#9DE0AD">
<circle class="Dot" cx="42" cy="29" r="4"></circle>
<circle class="Dot--withFadeOut Dot-01" cx="72" cy="49" r="4"></circle>
<circle class="Dot" cx="52" cy="54" r="4"></circle>
<circle class="Dot--withFadeOut Dot-02" cx="4" cy="54" r="4"></circle>
<circle class="Dot--withFadeOut Dot-03" cx="28" cy="14" r="4"></circle>
<circle class="Dot" cx="188" cy="6" r="4"></circle>
<circle class="Dot" cx="196" cy="46" r="4"></circle>
<circle class="Dot--withFadeOut Dot-03" cx="159" cy="25" r="4"></circle>
<circle class="Dot" cx="228" cy="26" r="4"></circle>
<circle class="Dot" cx="141" cy="14" r="4"></circle>
<circle class="Dot--withFadeOut Dot-04" cx="121" cy="4" r="4"></circle>
<circle class="Dot" cx="101" cy="54" r="4"></circle>
<circle class="Dot" cx="182" cy="29" r="4"></circle>
<circle class="Dot--withFadeOut Dot-05" cx="131" cy="60" r="4"></circle>
<circle class="Dot" cx="82" cy="19" r="4"></circle>
<circle class="Dot" cx="150" cy="47" r="4"></circle>
<circle class="Dot--withFadeOut Dot-06" cx="62" cy="9" r="4"></circle>
<circle class="Dot" cx="109" cy="32" r="4"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,5 @@
<div class="Modal-navigation">
<ul class="Modal-navigationInner CDB-Text is-semibold CDB-Size-medium js-menu"></ul>
</div>
<div class="Modal-inner Modal-inner--with-navigation js-content">
</div>

View File

@@ -0,0 +1,136 @@
var Backbone = require('backbone');
var XYZModel = require('./xyz/xyz-model');
var mosaicThumbnail = require('builder/components/mosaic/mosaic-thumbnail.tpl');
var MapboxModel = require('./mapbox/mapbox-model');
var WMSModel = require('./wms/wms-model');
var TileJSONModel = require('./tilejson/tilejson-model');
var NASAModel = require('./nasa/nasa-model');
/**
* Add basemap model
*/
module.exports = Backbone.Model.extend({
defaults: {
tabs: undefined,
contentPane: 'tabs', // [tabs, loading, error]
currentTab: 'xyz' // [xyz, wms, nasa, mapbox, tilejson, nasa]
},
initialize: function (attrs, opts) {
if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
if (!opts.basemapsCollection) throw new Error('basemapsCollection is required');
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
this._basemapsCollection = opts.basemapsCollection;
this._customBaselayersCollection = opts.customBaselayersCollection;
this._currentTab = opts.currentTab;
this._initTabs();
this._initBinds();
},
activeTabModel: function () {
return this.get('tabs').findWhere({ name: this.get('currentTab') });
},
canSaveBasemap: function () {
return this.get('contentPane') === 'tabs' && this._layerToSave();
},
saveBasemap: function () {
var self = this;
this.set('contentPane', 'addingBasemap');
var customBaselayerModel = this._layerToSave();
var attrs = customBaselayerModel.getAttributes();
if (this.activeTabModel().hasAlreadyAddedLayer(this._customBaselayersCollection)) {
// update selected in basemaps collection
this._basemapsCollection.updateSelected(attrs.className);
// update category if needed in basemaps collection,
// this is the case for mapbox basemaps which were stored without category in the editor
this._basemapsCollection.updateCategory(attrs.className, attrs.category);
// update baselayer
this._onBasemapSaved(attrs);
} else {
// Add to customBaselayersCollection before saving, so save URL resolves to the expected endpoint
this._customBaselayersCollection.add(customBaselayerModel);
customBaselayerModel.save({}, {
success: function (mdl, mdlAttrs) {
var options = mdlAttrs.options;
var name = options.name ? options.name : 'Custom basemap ' + mdlAttrs.order;
var className = options.className;
var urlTemplate = options.urlTemplate;
// add in basemaps collection
self._basemapsCollection.add({
id: mdlAttrs.id,
urlTemplate: urlTemplate,
maxZoom: options.minZoom || 21,
minZoom: options.minZoom || 0,
name: name,
className: className,
attribution: options.attribution,
category: options.category,
tms: options.tms,
type: options.type,
val: className,
label: name,
template: function (imgURL) {
return mosaicThumbnail({
imgURL: imgURL
});
}
});
// update baselayer
self._onBasemapSaved(attrs);
},
error: function () {
// Cleanup, remove layer it could not be saved!
self._customBaselayersCollection.remove(customBaselayerModel);
self.set('contentPane', 'addBasemapFailed');
}
});
}
},
_onBasemapSaved: function (layerAttrs) {
// Update baseLayer
this._layerDefinitionsCollection.setBaseLayer(layerAttrs);
this.trigger('saveBasemapDone');
},
_initTabs: function () {
var tabs = new Backbone.Collection([
new XYZModel(),
new MapboxModel(),
new WMSModel(null, {
customBaselayersCollection: this._customBaselayersCollection
}),
new TileJSONModel(),
new NASAModel()
]);
this.set({
tabs: tabs,
currentTab: this._currentTab || tabs.first().get('name')
});
},
_initBinds: function () {
this.get('tabs').each(function (tabModel) {
tabModel.bind('saveBasemap', this.saveBasemap, this);
}, this);
},
_layerToSave: function () {
return this.activeTabModel().get('layer');
}
});

View File

@@ -0,0 +1,106 @@
var CoreView = require('backbone/core-view');
var template = require('./add-basemap.tpl');
var TabPaneView = require('builder/components/tab-pane/tab-pane-view');
var TabPaneCollection = require('builder/components/tab-pane/tab-pane-collection');
var ViewFactory = require('builder/components/view-factory');
var renderLoading = require('builder/components/loading/render-loading');
var ErrorView = require('builder/components/error/error-view');
var _ = require('underscore');
var TabsView = require('./tabs-view');
/**
* Add basemap dialog
*/
module.exports = CoreView.extend({
className: 'Dialog-content Dialog-content--expanded',
events: {
'click .js-ok': 'ok'
},
initialize: function (opts) {
if (!opts.modalModel) throw new TypeError('modalModel is required');
if (!opts.createModel) throw new TypeError('createModel is required');
this._modalModel = opts.modalModel;
this._createModel = opts.createModel;
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this._initViews();
},
_initBinds: function () {
this._createModel.bind('saveBasemapDone', this._modalModel.destroy.bind(this._modalModel), this);
this._createModel.bind('change:contentPane', this._onChangeContentView, this);
this.add_related_model(this._createModel);
},
_onChangeContentView: function () {
var context = this._createModel.get('contentPane');
var paneModel = _.first(this._tabPaneCollection.where({ name: context }));
paneModel.set('selected', true);
},
_initViews: function () {
var self = this;
this._submitButtom = this.$('.js-ok');
this._modalFooter = this.$('.js-Modal-footer');
this._tabPaneCollection = new TabPaneCollection([
{
name: 'tabs',
selected: this._createModel.get('contentPane') === 'tabs',
createContentView: function () {
return new TabsView({
model: self._createModel,
submitButton: self._submitButtom,
modalFooter: self._modalFooter
});
}
}, {
name: 'addingBasemap',
selected: this._createModel.get('contentPane') === 'addingBasemap',
createContentView: function () {
self._disableModalFooter(true);
return ViewFactory.createByHTML(
renderLoading({
title: _t('components.modals.add-basemap.adding-basemap')
})
);
}
}, {
name: 'addBasemapFailed',
selected: this._createModel.get('contentPane') === 'addBasemapFailed',
createContentView: function () {
return new ErrorView({
title: _t('components.modals.add-basemap.add-basemap-error')
});
}
}
]);
var tabPaneView = new TabPaneView({
collection: this._tabPaneCollection
});
this.addView(tabPaneView);
this.$('.js-content-container').append(tabPaneView.render().el);
},
_disableModalFooter: function (disable) {
this._modalFooter.toggleClass('is-disabled', disable);
},
ok: function () {
if (this._createModel.canSaveBasemap()) {
this._createModel.saveBasemap();
}
}
});

View File

@@ -0,0 +1,19 @@
<div class="Modal">
<div class="Modal-header">
<div class="Modal-headerContainer">
<h2 class="CDB-Text CDB-Size-huge is-light u-mainTextColor u-bSpace"><%- _t('components.modals.add-basemap.modal-title') %></h2>
<h3 class="CDB-Text CDB-Size-medium u-altTextColor"><%- _t('components.modals.add-basemap.modal-desc') %></h3>
</div>
</div>
<div class="Modal-container">
<div class="Tab-pane js-content-container">
</div>
</div>
<div class="Modal-footer js-Modal-footer">
<div class="Modal-footerContainer u-flex u-justifyEnd">
<button class="CDB-Button CDB-Button--primary is-disabled js-ok ok">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase"><%- _t('components.modals.add-basemap.add-btn') %></span>
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,18 @@
<div class="u-flex u-alignCenter Modal-basemapContainer">
<h3 class="CDB-Text CDB-Size-large u-mainTextColor Modal-titleBasemap"><%- _t('components.modals.add-basemap.mapbox.insert') %></h3>
<div class="CDB-Text u-flex u-alignCenter ">
<label class="Metadata-label Metadata-label--big CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis"><%- _t('components.modals.add-basemap.mapbox.enter') %></label>
<div class="Form-rowData Form-rowData--longer">
<input type="text" class="CDB-InputText CDB-Text js-url" value="<%- url %>" placeholder="<%- _t('components.modals.add-basemap.xyz.eg') %> https://api.mapbox.com/styles/v1/username/basemap/tiles/256/{z}/{x}/{y}@2x?access_token=access_token">
<div class="XYZPanel-error CDB-InfoTooltip CDB-InfoTooltip--left is-error CDB-Text CDB-Size-medium CDB-InfoTooltip-text js-error <%- lastErrorMsg ? 'is-visible' : '' %>"><%- lastErrorMsg %></div>
</div>
</div>
<div class="u-flex">
<label class="Metadata-label Metadata-label--big CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis"></label>
<div class="Form-rowData Form-rowData--noMinHeight Form-rowData--longer">
<p class="CDB-Text CDB-Size-small Form-rowInfoText--block u-altTextColor">
Learn how to get your Mapbox Style URL <a href="https://www.mapbox.com/help/carto/" target="_blank">here</a>
</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,71 @@
var Backbone = require('backbone');
var _ = require('underscore');
var MapboxView = require('./mapbox-view');
var MapboxToTileLayerFactory = require('./mapbox-to-tile-layer-factory');
/**
* View model for Mapbox tab content.
*/
module.exports = Backbone.Model.extend({
defaults: {
name: 'mapbox',
label: 'Mapbox',
currentView: 'enterURL', //, validatingInputs, valid
lastErrorMsg: '', // set if fails to save
layer: undefined // will be set when valid
},
createView: function (opts) {
if (!opts.submitButton) throw new Error('submitButton is required');
if (!opts.modalFooter) throw new Error('modalFooter is required');
this._submitButton = opts.submitButton;
this._modalFooter = opts.modalFooter;
this.set({
currentView: 'enterURL',
layer: undefined
});
return new MapboxView({
model: this,
submitButton: this._submitButton,
modalFooter: this._modalFooter
});
},
hasAlreadyAddedLayer: function (userLayers) {
var urlTemplate = this.get('layer').get('urlTemplate');
return _.any(userLayers.isCustomCategory(), function (customLayer) {
return customLayer.get('urlTemplate') === urlTemplate;
});
},
validateInputs: function (url) {
this.set({
currentView: 'validatingInputs',
url: url
});
var self = this;
var mf = new MapboxToTileLayerFactory({
url: url
});
mf.createTileLayer({
success: function (tileLayer) {
self.set('layer', tileLayer);
self.trigger('saveBasemap');
},
error: function (errorMsg) {
self.set({
currentView: 'enterURL',
lastErrorMsg: errorMsg
});
}
});
}
});

View File

@@ -0,0 +1,194 @@
/* global Image, location */
var $ = require('jquery');
var _ = require('underscore');
var Backbone = require('backbone');
var CustomBaselayerModel = require('builder/data/custom-baselayer-model');
/**
* Factory to create a CustomBaselayerModel from a given Integration URL for Mapbox.
*/
module.exports = Backbone.Model.extend({
defaults: {
url: ''
},
_MAPBOX: {
version: 4,
https: 'https://dnv9my2eseobd.cloudfront.net',
base: 'https://a.tiles.mapbox.com/'
},
/**
* @param {Object} callbacks
* success {Function} given a new TileLayer object
* error {Function} given an error explanation
*/
createTileLayer: function (callbacks) {
var val = this.get('url');
var url = this._lowerXYZ(val);
var type = 'json';
var subdomains = ['a', 'b', 'c'];
var mapbox_id;
// Detects the URL's type
if (url.indexOf('{x}') < 0 && url.indexOf('tiles.mapbox.com') !== -1) {
mapbox_id = this._getMapBoxMapID(url);
if (mapbox_id) {
type = 'mapbox_id';
url = mapbox_id;
}
} else if (url.indexOf('{x}') !== -1) {
type = 'xyz';
url = url.replace(/\{s\}/g, function () {
return subdomains[Math.floor(Math.random() * 3)];
})
.replace(/\{x\}/g, '0')
.replace(/\{y\}/g, '0')
.replace(/\{z\}/g, '0');
} else if (url && url.indexOf('http') < 0 && url.match(/(.*?)\.(.*)/) != null && url.match(/(.*?)\.(.*)/).length === 3) {
type = 'mapbox_id';
mapbox_id = val;
} else { // If not, check https
url = this._fixHTTPS(url);
}
var self = this;
var image;
if (type === 'mapbox') {
callbacks.success(this._newTileLayer({ tiles: [url] }));
} else if (type === 'xyz') {
image = new Image();
image.onload = function () {
callbacks.success(self._newTileLayer({
tiles: [self._lowerXYZ(val)]
}));
};
image.onerror = function () {
callbacks.error(self._errorToMsg());
};
image.src = url;
} else if (type === 'mapbox_id') {
var base_url = this._MAPBOX.base + 'v' + this._MAPBOX.version + '/' + mapbox_id;
var tile_url = base_url + '/{z}/{x}/{y}.png';
var json_url = base_url + '.json';
// JQuery has a faulty implementation of the getJSON method and doesn't return
// a 404, so we use a timeout. TODO: replace with CORS
var errorTimeout = setTimeout(function () {
callbacks.error(self._errorToMsg());
}, 5000);
$.ajax({
url: json_url,
success: function (data) {
clearTimeout(errorTimeout);
callbacks.success(self._newTileLayer({
tiles: [tile_url],
attribution: data.attribution,
minzoom: data.minzoom,
maxzoom: data.maxzoom,
name: data.name
}));
},
error: function (e) {
clearTimeout(errorTimeout);
callbacks.error(self._errorToMsg(e));
}
});
} else {
callbacks.error(this._errorToMsg());
}
},
_newTileLayer: function (data) {
// Check if the respond is an array
// In that case, get only the first
if (_.isArray(data) && _.size(data) > 0) {
data = _.first(data);
}
var url = data.tiles[0];
var attribution = data.attribution || null;
var layer = new CustomBaselayerModel({
urlTemplate: url,
attribution: attribution,
maxZoom: data.maxzoom || 21,
minZoom: data.minzoom || 0,
name: data.name || '',
category: 'Mapbox',
type: 'Tiled'
});
layer.set('className', layer._generateClassName(url));
return layer;
},
_errorToMsg: function (error) {
if (typeof error === 'object' || !error) {
if (error && error.status && error.status === 401) {
return _t('components.modals.add-basemap.mapbox.error');
} else {
return _t('components.modals.add-basemap.mapbox.invalid');
}
}
return error;
},
_lowerXYZ: function (url) {
return url.replace(/\{S\}/g, '{s}')
.replace(/\{X\}/g, '{x}')
.replace(/\{Y\}/g, '{y}')
.replace(/\{Z\}/g, '{z}');
},
// Extracts the Mapbox MapId from a Mapbox URL
_getMapBoxMapID: function (url) {
// http://d.tiles.mapbox.com/v3/{user}.{map}/3/4/3.png
// http://a.tiles.mapbox.com/v3/{user}.{map}/page.html
// http://a.tiles.mapbox.com/v4/{user}.{map}.*
var reg1 = /https?:\/\/[a-z]?\.?tiles\.mapbox.com\/v(\d)\/([^\/.]*)\.([^\/.]*)/;
// https://tiles.mapbox.com/{user}/edit/{map}?newmap&preset=Streets#3/0.00/-0.09
var reg2 = /https?:\/\/tiles\.mapbox\.com\/(.*?)\/edit\/(.*?)(\?|#)/;
var match = '';
// Check first expresion
match = url.match(reg1);
if (match && match[1] && match[2]) {
return match[2] + '.' + match[3];
}
// Check second expresion
match = url.match(reg2);
if (match && match[1] && match[2]) {
return match[1] + '.' + match[2];
}
},
/**
* return a https url if the current application is loaded from https
*/
_fixHTTPS: function (url, loc) {
loc = loc || location;
// fix the url to https or http
if (url.indexOf('https') !== 0 && loc.protocol === 'https:') {
// search for mapping
var i = url.indexOf('mapbox.com');
if (i !== -1) {
return this._MAPBOX.https + url.substr(i + 'mapbox.com'.length);
}
return url.replace(/http/, 'https');
}
return url;
}
});

View File

@@ -0,0 +1,117 @@
var CoreView = require('backbone/core-view');
var ViewFactory = require('builder/components/view-factory');
var renderLoading = require('builder/components/loading/render-loading');
var enterUrl = require('./enter-url.tpl');
/**
* Represents the Mapbox tab content.
*/
module.exports = CoreView.extend({
events: {
'keydown': '_onKeyDown',
'keyup': '_onKeyUp'
},
initialize: function (opts) {
if (!opts.submitButton) throw new Error('submitButton is required');
if (!opts.modalFooter) throw new Error('modalFooter is required');
this._submitButton = opts.submitButton;
this._modalFooter = opts.modalFooter;
this._onClickBinded = this._onClickOK.bind(this);
this._bindSubmitButton();
this._initBinds();
},
render: function () {
this.clearSubViews();
var view;
switch (this.model.get('currentView')) {
case 'validatingInputs':
this._disableModalFooter(true);
view = ViewFactory.createByHTML(
renderLoading({
title: _t('components.modals.add-basemap.validating')
})
);
break;
case 'enterURL':
default:
this._disableModalFooter(false);
view = ViewFactory.createByHTML(
enterUrl({
url: this.model.get('url'),
lastErrorMsg: this.model.get('lastErrorMsg')
})
);
}
this.addView(view);
this.$el.append(view.render().el);
this._updateOkBtn();
this._onKeyUp();
return this;
},
_initBinds: function () {
this.model.bind('change', this.render, this);
},
_hasValues: function () {
return this._urlVal();
},
_urlVal: function () {
return this.$('.js-url').val();
},
_onClickOK: function (e) {
this.killEvent(e);
if (this._hasValues()) {
var url = this._urlVal();
this.model.validateInputs(url);
}
},
_disableModalFooter: function (disable) {
this._modalFooter.toggleClass('is-disabled', disable);
},
_updateOkBtn: function () {
this._submitButton.find('span').text(_t('components.modals.add-basemap.add-btn'));
},
_onKeyDown: function (e) {
e.stopPropagation();
this.$('.js-error').removeClass('is-visible');
},
_onKeyUp: function (e) {
e && e.stopPropagation();
this._submitButton.toggleClass('is-disabled', !this._hasValues());
},
_bindSubmitButton: function () {
this._submitButton.on('click', this._onClickBinded);
},
_unBindSubmitButton: function () {
this._submitButton.off('click', this._onClickBinded);
},
clean: function () {
this._unBindSubmitButton();
CoreView.prototype.clean.call(this);
}
});

View File

@@ -0,0 +1,107 @@
var _ = require('underscore');
var Backbone = require('backbone');
var moment = require('moment');
var NASAView = require('./nasa-view');
var CustomBaselayerModel = require('builder/data/custom-baselayer-model');
var TYPES = {
day: {
url: 'http://map1.vis.earthdata.nasa.gov/wmts-webmerc/MODIS_Terra_CorrectedReflectance_TrueColor/default/<%- date %>/GoogleMapsCompatible_Level9/{z}/{y}/{x}.jpeg',
limit: '2012-05-01',
default: '2012-05-01',
attribution: '<a href="http://earthdata.nasa.gov/gibs" target="_blank">NASA EOSDIS GIBS</a>',
name: 'NASA Terra',
maxZoom: 9,
minZoom: 1
},
night: {
url: 'http://map1.vis.earthdata.nasa.gov/wmts-webmerc/VIIRS_CityLights_2012/default/<%- date %>/GoogleMapsCompatible_Level8/{z}/{y}/{x}.jpeg',
limit: '2012-05-01',
default: '2012-05-02',
attribution: '<a href="http://earthdata.nasa.gov/gibs" target="_blank">NASA EOSDIS GIBS</a>',
name: 'NASA Earth at night',
maxZoom: 8,
minZoom: 1
}
};
/**
* View model for NASA tab content.
*/
module.exports = Backbone.Model.extend({
defaults: {
name: 'nasa',
label: 'NASA',
layer: undefined, // gets set on dayOrNight/date changes
layerType: 'day',
date: undefined, // for date picker
current: undefined,
format: 'Y-m-d' // YYYY-MM-DD
},
initialize: function () {
this._initBinds();
},
createView: function (opts) {
if (!opts.submitButton) throw new Error('submitButton is required');
this._submitButton = opts.submitButton;
var utc = new Date().getTimezoneOffset();
var today = moment(new Date()).utcOffset(utc).format('YYYY-MM-DD');
var yesterday = moment(new Date()).utcOffset(utc).subtract(1, 'days').format('YYYY-MM-DD');
this.set({
current: today,
date: yesterday
});
return new NASAView({
model: this,
submitButton: this._submitButton
});
},
hasAlreadyAddedLayer: function (userLayers) {
var urlTemplate = this.get('layer').get('urlTemplate');
return _.any(userLayers.isCustomCategory(), function (customLayer) {
return customLayer.get('urlTemplate') === urlTemplate;
});
},
_initBinds: function () {
this.bind('change:date change:layerType', this._onChange, this);
},
_onChange: function () {
var dateStr = this.get('date');
var layerType = this.get('layerType');
var url = _.template(TYPES[layerType].url)({
date: dateStr
});
var name = TYPES[layerType].name;
if (layerType === 'day') {
name = name + ' ' + dateStr;
}
var layer = new CustomBaselayerModel({
urlTemplate: url,
attribution: TYPES[layerType].attribution,
maxZoom: TYPES[layerType].maxZoom,
minZoom: TYPES[layerType].minZoom,
name: name,
category: 'NASA',
type: 'Tiled'
});
layer.set('className', layer._generateClassName(url));
this.set('layer', layer);
}
});

View File

@@ -0,0 +1,106 @@
var $ = require('jquery');
var moment = require('moment');
var CoreView = require('backbone/core-view');
var DatePickerView = require('builder/components/date-picker/date-picker-view');
var EditFieldModel = require('builder/components/date-picker/edit-field-model');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var template = require('./nasa.tpl');
/**
* Represents the NASA tab content
*/
module.exports = CoreView.extend({
options: {
dateFormat: 'YYYY-MM-DD'
},
events: {
'click .js-day': '_onChangeToDay',
'click .js-night': '_onChangeToNight'
},
initialize: function (opts) {
if (!opts.submitButton) throw new Error('submitButton is required');
this._submitButton = opts.submitButton;
this.dateModel = new EditFieldModel({
value: this.model.get('date'),
type: 'date'
});
this._initBinds();
},
render: function () {
this.clearSubViews();
this._updateOkBtn();
this._disableOkBtn(false);
this.$el.html(
template({
layerType: this.model.get('layerType')
})
);
this._renderDatePicker();
return this;
},
_initBinds: function () {
this.model.bind('change:layerType', function () {
this.dateModel.set('readOnly', this.model.get('layerType') === 'night');
this.render();
}, this);
this.dateModel.bind('change:value', function () {
var date = moment(this.dateModel.get('value')).format(this.options.dateFormat);
this.model.set('date', date);
}, this);
this.add_related_model(this.dateModel);
},
_renderDatePicker: function () {
// Date picker
var datepicker = this.datepicker = new DatePickerView({
className: 'DatePicker DatePicker--withBorder',
model: this.dateModel
});
this._$datePicker().html(datepicker.render().el);
this.addView(datepicker);
// Disabled tooltip
if (this.dateModel.get('readOnly')) {
var tooltip = new TipsyTooltipView({
el: this._$datePicker(),
title: function (e) {
return $(this).attr('data-title');
}
});
this.addView(tooltip);
}
},
_onChangeToNight: function () {
this.model.set('layerType', 'night');
},
_onChangeToDay: function () {
this.model.set('layerType', 'day');
},
_$datePicker: function () {
return this.$('.js-datePicker');
},
_updateOkBtn: function () {
this._submitButton.find('span').text(_t('components.modals.add-basemap.add-btn'));
},
_disableOkBtn: function (disable) {
this._submitButton.toggleClass('is-disabled', disable);
}
});

View File

@@ -0,0 +1,22 @@
<div class="u-flex u-alignCenter Modal-basemapContainer">
<h3 class="CDB-Text CDB-Size-large u-mainTextColor Modal-titleBasemap"><%- _t('components.modals.add-basemap.nasa.select') %></h3>
<div class="CDB-Text u-flex u-alignCenter">
<div class="Form-rowData Form-rowData--short Form-rowData--alignLeft">
<div class="Form-rowData Form-rowData--full">
<div class="RadioButton js-day">
<button type="button" class="RadioButton-input <% if (layerType === 'day') { %>is-checked<% } %>"></button>
<label class="Metadata-label Metadata-label--auto CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis" for="nasa-type-day"><%- _t('components.modals.add-basemap.nasa.day') %></label>
</div>
</div>
<div class="Form-rowData Form-rowData--full">
<div class="RadioButton js-night">
<button type="button" class="RadioButton-input <% if (layerType === 'night') { %>is-checked<% } %>"/></button>
<label class="Metadata-label Metadata-label--auto CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis" for="nasa-type-night"><%- _t('components.modals.add-basemap.nasa.night') %></label>
</div>
</div>
</div>
<div class="Form-rowData Form-rowData--short">
<div class="js-datePicker" data-title="<%- _t('components.modals.add-basemap.nasa.cant-select') %>"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,81 @@
var CoreView = require('backbone/core-view');
var ScrollView = require('builder/components/scroll/scroll-view');
var $ = require('jquery');
var template = require('./tabs.tpl');
/**
* View representing the tabs content of the dialog.
*/
module.exports = CoreView.extend({
events: {
'click .js-tabs button': '_onClickTab'
},
attributes: function () {
return {
class: 'Modal-outer'
};
},
initialize: function (opts) {
if (!opts.submitButton) throw new Error('submitButton is required');
if (!opts.modalFooter) throw new Error('modalFooter is required');
this._submitButton = opts.submitButton;
this._modalFooter = opts.modalFooter;
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(template({
model: this.model
}));
this._initViews();
return this;
},
_initViews: function () {
var scrollView = new ScrollView({
className: 'ScrollView ScrollView--withoutMargins',
createContentView: function () {
return this._createTabContentView();
}.bind(this)
});
this.addView(scrollView);
this.$('.js-tab-content').append(scrollView.render().el);
},
_initBinds: function () {
this.model.bind('change:currentTab', this.render, this);
},
_createTabContentView: function () {
if (this._currentTabView) {
this._currentTabView.clean();
}
this._currentTabView = this.model.activeTabModel().createView({
submitButton: this._submitButton,
modalFooter: this._modalFooter
});
this.addView(this._currentTabView);
return this._currentTabView.render();
},
_onClickTab: function (e) {
this.killEvent(e);
var name = $(e.target).closest('button').data('name');
if (name) {
this.model.set('currentTab', name);
} else {
throw new Error('tab name was expected but was empty');
}
}
});

View File

@@ -0,0 +1,12 @@
<div class="Modal-navigation">
<ul class="Modal-navigationInner CDB-Text is-semibold CDB-Size-medium js-tabs">
<% model.get('tabs').each(function(tab) { %>
<li class="CDB-NavMenu-item <%- model.get('currentTab') === tab.get('name') ? 'is-selected' : '' %>">
<button data-name="<%- tab.get('name') %>" class="CDB-NavMenu-link u-upperCase">
<%- tab.get('label') %>
</button>
</li>
<% }) %>
</ul>
</div>
<div class="Modal-inner Modal-inner--with-navigation js-tab-content"></div>

View File

@@ -0,0 +1,12 @@
<div class="u-flex u-alignCenter Modal-basemapContainer">
<h3 class="CDB-Text CDB-Size-large u-mainTextColor Modal-titleBasemap"><%- _t('components.modals.add-basemap.tilejson.insert') %></h3>
<div class="CDB-Text u-flex u-alignCenter">
<label class="Metadata-label Metadata-label--auto CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis"><%- _t('components.modals.add-basemap.xyz.enter') %></label>
<div class="Form-rowData Form-rowData--longer">
<input type="text" class="has-icon CDB-InputText CDB-Text js-url" value="" placeholder="<%- _t('components.modals.add-basemap.xyz.eg') %> http://domain.com/tiles.json?foo=bar">
<i class="CDB-IconFont CDB-IconFont-dribbble Form-inputIcon js-idle"></i>
<i class="Spinner XYZPanel-inputIcon Spinner--formIcon Form-inputIcon js-validating" style="display: none;"></i>
<div class="XYZPanel-error CDB-InfoTooltip CDB-InfoTooltip--left is-error CDB-Text CDB-Size-medium CDB-InfoTooltip-text js-error"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,52 @@
var Backbone = require('backbone');
var CustomBaselayerModel = require('builder/data/custom-baselayer-model');
/**
* Model to representing a TileJSON endpoint
* See https://github.com/mapbox/tilejson-spec/tree/master/2.1.0 for details
*/
module.exports = Backbone.Model.extend({
url: function () {
return this.get('tilejson_url');
},
newTileLayer: function () {
if (!this._isFetched()) throw new Error('no tiles, have fetch been called and returned a successful resultset?');
var url = this._urlTemplate();
var layer = new CustomBaselayerModel({
urlTemplate: url,
attribution: this.get('attribution'),
maxZoom: this.get('maxzoom'),
minZoom: this.get('minzoom'),
name: this._name(),
bounding_boxes: this.get('bounds'),
tms: this.get('scheme') === 'tms',
category: 'TileJSON',
type: 'Tiled'
});
layer.set('className', layer._generateClassName(url));
return layer;
},
setUrl: function (url) {
this.set('tilejson_url', url);
},
_isFetched: function () {
return this.get('tiles').length > 0;
},
_urlTemplate: function () {
return this.get('tiles')[0];
},
_name: function () {
return this.get('name') || this.get('description');
}
});

View File

@@ -0,0 +1,38 @@
var _ = require('underscore');
var Backbone = require('backbone');
var TileJSONView = require('./tilejson-view');
var TileJSONLayerModel = require('./tilejson-layer-model');
/**
* View model for TileJSON tab content.
*/
module.exports = Backbone.Model.extend({
defaults: {
name: 'tilejson',
label: 'TileJSON',
layer: undefined // will be set when valid
},
createView: function (opts) {
if (!opts.submitButton) throw new Error('submitButton is required');
this._submitButton = opts.submitButton;
var tileJSONLayerModel = new TileJSONLayerModel();
return new TileJSONView({
model: this,
submitButton: this._submitButton,
tileJSONLayerModel: tileJSONLayerModel
});
},
hasAlreadyAddedLayer: function (userLayers) {
var urlTemplate = this.get('layer').get('urlTemplate');
return _.any(userLayers.isCustomCategory(), function (customLayer) {
return customLayer.get('urlTemplate') === urlTemplate;
});
}
});

View File

@@ -0,0 +1,120 @@
var _ = require('underscore');
var CoreView = require('backbone/core-view');
var template = require('./enter-url.tpl');
/**
* Represents the TileJSON tab content.
*/
module.exports = CoreView.extend({
events: {
'keydown .js-url': '_onKeydown',
'paste .js-url': '_onPaste'
},
initialize: function (opts) {
if (!opts.submitButton) throw new Error('submitButton is required');
if (!opts.tileJSONLayerModel) throw new Error('tileJSONLayerModel is required');
this._submitButton = opts.submitButton;
this._tileJSONLayerModel = opts.tileJSONLayerModel;
this._lastURL = '';
this._debouncedUpdate = _.debounce(this._update.bind(this), 150);
},
render: function () {
this._updateOkBtn();
this._disableOkBtn(true);
this.$el.html(
template()
);
return this;
},
_onKeydown: function (e) {
e.stopPropagation();
this._debouncedUpdate();
},
_onPaste: function (e) {
e.stopPropagation();
this._debouncedUpdate();
},
_update: function () {
var self = this;
this._disableOkBtn(true);
this._indicateIsValidating(true);
var url = this._urlWithHTTP();
if (url === this._lastURL) {
// Even if triggered nothing really changed so just update UI and return early
this._indicateIsValidating(false);
this._updateError();
return;
}
this._lastURL = url;
this._indicateIsValidating(true);
this._tileJSONLayerModel.setUrl(url);
this._tileJSONLayerModel.fetch({
success: function (mdl) {
if (url === self._lastURL) {
self.model.set('layer', mdl.newTileLayer());
self._disableOkBtn(false);
self._indicateIsValidating(false);
self._updateError();
}
},
error: function () {
if (url === self._lastURL) {
self._indicateIsValidating(false);
// Note that this text can not be longer, or it will exceed available space of the error label.
self._updateError(_t('components.modals.add-basemap.tilejson.invalid'));
}
}
});
},
_updateOkBtn: function () {
this._submitButton.find('span').text(_t('components.modals.add-basemap.add-btn'));
},
_disableOkBtn: function (disable) {
this._submitButton.toggleClass('is-disabled', disable);
},
_updateError: function (msg) {
this.$('.js-error').text(msg)[msg ? 'addClass' : 'removeClass']('is-visible');
},
_indicateIsValidating: function (indicate) {
if (indicate) {
this.$('.js-idle').hide();
this.$('.js-validating').show();
} else {
this.$('.js-validating').hide();
this.$('.js-idle').show();
}
},
// So don't try to be fetched relatively to current URL path later
_urlWithHTTP: function () {
var str = this.$('.js-url').val();
if (str.indexOf('http://') === -1 && str.indexOf('https://') === -1) {
return 'http://' + str;
} else {
return str;
}
}
});

View File

@@ -0,0 +1,16 @@
<div class="u-flex u-alignCenter Modal-basemapContainer">
<h3 class="CDB-Text CDB-Size-large u-mainTextColor Modal-titleBasemap"><%- _t('components.modals.add-basemap.wms.insert') %></h3>
<div class="CDB-Text u-flex u-alignCenter">
<label class="Metadata-label Metadata-label--auto CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis"><%- _t('components.modals.add-basemap.xyz.enter') %></label>
<div class="Form-rowData Form-rowData--longer">
<input type="text" class="has-icon CDB-InputText CDB-Text js-url" value="" placeholder="<%- _t('components.modals.add-basemap.xyz.eg') %> http://openlayers.org/en/v3.5.0/examples/data/ogcsample.xml">
<i class="CDB-IconFont CDB-IconFont-dribbble Form-inputIcon js-idle"></i>
<i class="Spinner XYZPanel-inputIcon XYZPanel-inputIcon--loader Spinner--formIcon Form-inputIcon js-validating" style="display: none;"></i>
<div class="XYZPanel-error CDB-InfoTooltip CDB-InfoTooltip--left is-error CDB-Text CDB-Size-medium CDB-InfoTooltip-text js-error <%- (layersFetched && layers.length === 0) ? 'is-visible' : '' %>">
<% if (layersFetched && layers.length === 0) { %>
<%- _t('components.modals.add-basemap.wms.invalid') %>
<% } %>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,50 @@
var CoreView = require('backbone/core-view');
var template = require('./select-layer.tpl');
var WMSLayersView = require('./wms-layers-view');
/**
* Sub view, to select what layer to use as basemap.
*/
module.exports = CoreView.extend({
events: {
'click .js-back': '_onClickBack'
},
initialize: function (opts) {
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
this._customBaselayersCollection = opts.customBaselayersCollection;
},
render: function () {
this.clearSubViews();
this.$el.html(
template({
searchQuery: this.model.get('searchQuery'),
layersFound: this.model.getLayers(),
layersAvailableCount: this.model.layersAvailableCount()
})
);
this._initViews();
return this;
},
_initViews: function () {
var wmsListView = new WMSLayersView({
model: this.model,
customBaselayersCollection: this._customBaselayersCollection
});
this.addView(wmsListView);
this.$('.js-layers').append(wmsListView.render().el);
},
_onClickBack: function (e) {
this.killEvent(e);
this.model.set('currentView', 'enterURL');
}
});

View File

@@ -0,0 +1,42 @@
<div class="u-inner">
<div class="Filters WMSSSelectLayer-Filter is-relative">
<div class="Filters-inner">
<div class="Filters-row">
<div class="Filters-group">
<div class="Filters-typeItem Filters-typeItem--searchEnabler">
<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>
</div>
<div class="Filters-typeItem Filters-typeItem--searchField">
<form class="Filters-searchForm js-search-form" action="#">
<input class="Filters-searchInput CDB-Text CDB-Size-medium js-search-input" type="text" value="<%- searchQuery %>" placeholder="<%- _t('components.modals.add-basemap.wms.placeholder', { layersFoundCount: layersFound.length, layersFoundCountPluralize: _t('components.modals.add-basemap.wms.tables-pluralize', { smart_count: layersFound.length }), layersAvailableCount: layersAvailableCount, layersAvailableCountPluralize: _t('components.modals.add-basemap.wms.tables-pluralize', { smart_count: layersAvailableCount }) }) %>" />
<button type="button" class="Filters-cleanSearch js-clean-search u-actionTextColor">
<i class="CDB-IconFont CDB-IconFont-close"></i>
</button>
</form>
</div>
<span class="Filters-separator"></span>
</div>
</div>
</div>
<% if (searchQuery && layersFound.length == 0) { %>
<div class="IntermediateInfo">
<div class="LayoutIcon">
<i class="CDB-IconFont CDB-IconFont-defaultUser"></i>
</div>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-bSpace u-secondaryTextColor u-tSpace-xl"><%- _t('components.modals.add-basemap.wms.oh-no') %></h4>
<p class="CDB-Text CDB-Size-medium u-altTextColor"><%- _t('components.modals.add-basemap.wms.unfortunately') %></p>
</div>
<% } else { %>
<div class="js-layers"></div>
<% } %>
</div>
<button class="NavButton Dialog-backBtn js-back">
<i class="CDB-IconFont CDB-IconFont-arrowPrev"></i>
</button>

View File

@@ -0,0 +1,163 @@
var _ = require('underscore');
var Backbone = require('backbone');
var CustomBaselayerModel = require('builder/data/custom-baselayer-model');
/**
* Model for an individual WMS/WMTS layer.
*/
module.exports = Backbone.Model.extend({
defaults: {
state: 'idle' // idle, saving, saveDone, saveFail
},
url: function () {
return this._wmsService.saveLayerURL({
title: this.get('title'),
name: this.get('name'),
layer: this.get('name'),
srs: this.get('srs'),
bounding_boxes: this.get('llbbox'),
type: this.get('type'), // wms/wmts
matrix_sets: this.get('matrix_sets')
});
},
sync: function (method, model, options) {
options = options || {};
options.url = this.url(method.toLowerCase());
options.dataType = 'jsonp';
options.attrs = '_';
method = 'READ';
return Backbone.sync.apply(this, arguments);
},
initialize: function (attrs, opts) {
if (!opts.wmsService) throw new Error('wmsService is required');
this._wmsService = opts.wmsService;
},
canSave: function (customBaselayersCollection) {
return !_.any(customBaselayersCollection.isCustomCategory(), function (customLayer) {
return customLayer.get('name') === this.get('title');
}, this);
},
createProxiedLayerOrCustomBaselayerModel: function () {
this.set('state', 'saving');
this._shouldBeProxied() ? this._createProxiedLayer() : this._newCustomBaselayerModel();
},
_shouldBeProxied: function () {
if (this.get('type') === 'wmts') {
var supportedMatrixSets = this._wmsService.supportedMatrixSets(this.get('matrix_sets') || []);
return supportedMatrixSets.length > 0;
}
return true;
},
_createProxiedLayer: function () {
var self = this;
this.save({}, {
success: function () {
var proxiedBaselayerModel;
try {
proxiedBaselayerModel = self._newProxiedBaselayerModel();
} catch (e) {
}
if (proxiedBaselayerModel) {
self._setCustomBaselayerModel(proxiedBaselayerModel);
} else {
self.set('state', 'saveFail');
}
},
error: function (e) {
self.set('state', 'saveFail');
}
});
},
_newCustomBaselayerModel: function () {
var customBaselayerModel = this._byCustomURL(this._xyzURLTemplate());
customBaselayerModel.set({
name: this.get('title') || this.get('name'),
attribution: this.get('attribution'),
bounding_boxes: this.get('llbbox')
});
this._setCustomBaselayerModel(customBaselayerModel);
return customBaselayerModel;
},
_newProxiedBaselayerModel: function () {
if (!this.get('mapproxy_id')) {
throw new Error('mapproxy_id must be set');
}
var url = this._wmsService.getProxyTilesURL() + '/' + this.get('mapproxy_id') + '/wmts/map/webmercator/{z}/{x}/{y}.png';
var proxiedBaselayerModel = this._generateCustomBaselayerModel(url);
proxiedBaselayerModel.set({
name: this.get('title') || this.get('name'),
attribution: this.get('attribution'),
proxy: true,
bounding_boxes: this.get('bounding_boxes')
});
return proxiedBaselayerModel;
},
_generateCustomBaselayerModel: function (url) {
var layer = new CustomBaselayerModel({
urlTemplate: url,
attribution: null,
maxZoom: 21,
minZoom: 0,
name: '',
category: 'WMS',
tms: false,
type: 'Tiled'
});
layer.set('className', layer._generateClassName(url));
return layer;
},
_byCustomURL: function (url) {
// Minimal test for "valid URL" w/o having to complicate it with regex
if (url && url.indexOf('/') === -1) throw new TypeError('invalid URL');
// Only lowercase the placeholder variables, since the URL may contain case-sensitive data (e.g. API keys and such)
url = url.replace(/\{S\}/g, '{s}')
.replace(/\{X\}/g, '{x}')
.replace(/\{Y\}/g, '{y}')
.replace(/\{Z\}/g, '{z}');
var layer = this._generateCustomBaselayerModel(url);
return layer;
},
_xyzURLTemplate: function () {
var urlTemplate = this.get('url_template') || '';
// Convert the proxy template variables to XYZ format, http://foo.com/bar/%%(z)s/%%(x)s/%%(y)s.png"
return urlTemplate.replace(/%%\((\w)\)s/g, '{$1}');
},
_setCustomBaselayerModel: function (customBaselayerModel) {
this.set({
state: 'saveDone',
customBaselayerModel: customBaselayerModel
});
}
});

View File

@@ -0,0 +1,41 @@
var CoreView = require('backbone/core-view');
var template = require('./wms-layer.tpl');
/**
* View for an individual layer item.
*/
module.exports = CoreView.extend({
tagName: 'li',
className: 'List-row',
events: {
'click .js-add': '_onClickAdd'
},
initialize: function (opts) {
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
this._customBaselayersCollection = opts.customBaselayersCollection;
},
render: function () {
this.$el.html(
template({
model: this.model,
canSave: this.model.canSave(this._customBaselayersCollection)
})
);
return this;
},
_onClickAdd: function (e) {
this.killEvent(e);
if (this.model.canSave(this._customBaselayersCollection)) {
this.model.createProxiedLayerOrCustomBaselayerModel();
}
}
});

View File

@@ -0,0 +1,6 @@
<div class="List-rowItem">
<div class="DefaultTitle <%- canSave ? '': 'is-disabled' %>"><%- model.get('title') || model.get('name') %></div>
<button class="js-add Button Button--secondary Button--secondaryTransparentBkg <%- canSave ? '' : 'is-disabled' %>">
<span>Add this</span>
</button>
</div>

View File

@@ -0,0 +1,46 @@
var _ = require('underscore');
var Backbone = require('backbone');
var WMSLayerModel = require('./wms-layer-model');
module.exports = Backbone.Collection.extend({
model: function (attrs, opts) {
var self = opts.collection;
return new WMSLayerModel(attrs, {
wmsService: self._wmsService
});
},
parse: function (r) {
var layers = [];
if (r.layers) {
layers = _.map(r.layers, function (layer) {
return _.extend({ type: r.type || 'wms' }, layer);
});
}
return layers;
},
sync: function (method, model, options) {
options = options || {};
options.url = this.url(method.toLowerCase());
options.dataType = 'jsonp';
method = 'READ';
return Backbone.sync.apply(this, arguments);
},
url: function () {
return this._wmsService.getFetchLayersURL();
},
initialize: function (models, opts) {
if (!opts.wmsService) throw new Error('wmsService is required');
this._wmsService = opts.wmsService;
}
});

View File

@@ -0,0 +1,38 @@
var WMSLayerView = require('./wms-layer-view');
var CoreView = require('backbone/core-view');
/**
* Sub view, to select what layer to use as basemap.
*/
module.exports = CoreView.extend({
className: 'List',
tagName: 'ul',
initialize: function (opts) {
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
if (!this.model) throw new Error('model is required');
this._customBaselayersCollection = opts.customBaselayersCollection;
},
render: function () {
this.clearSubViews();
this.$el.append.apply(this.$el, this._renderedLayers());
return this;
},
_renderedLayers: function () {
return this.model.getLayers().map(function (layer) {
var view = new WMSLayerView({
model: layer,
customBaselayersCollection: this._customBaselayersCollection
});
this.addView(view);
return view.render().el;
}, this);
}
});

View File

@@ -0,0 +1,128 @@
var Backbone = require('backbone');
var _ = require('underscore');
var WMSView = require('./wms-view');
var WMSLayersCollection = require('./wms-layers-collection');
var WMSService = require('builder/data/wms-service');
/**
* View model for WMS/WMTS tab content.
*/
module.exports = Backbone.Model.extend({
defaults: {
name: 'wms',
label: 'WMS/WMTS',
currentView: 'enterURL', // [fetchingLayers, selectLayer, savingLayer]
layersFetched: false,
layer: undefined // will be set when selected
},
initialize: function (attrs, opts) {
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
this._customBaselayersCollection = opts.customBaselayersCollection;
this.wmsService = new WMSService();
this.wmsLayersCollection = new WMSLayersCollection(null, {
wmsService: this.wmsService
});
this._initBinds();
},
_initBinds: function () {
this.wmsLayersCollection.bind('change:state', this._onLayerStateChange, this);
this.wmsLayersCollection.bind('reset', function () {
this.set({
currentView: this.wmsLayersCollection.length > 0 ? 'selectLayer' : 'enterURL',
layersFetched: true
});
this.trigger('layersFetched');
}, this);
},
createView: function (opts) {
if (!opts.submitButton) throw new Error('submitButton is required');
if (!opts.modalFooter) throw new Error('modalFooter is required');
this._submitButton = opts.submitButton;
this._modalFooter = opts.modalFooter;
this.set({
currentView: 'enterURL',
layersFetched: false
});
return new WMSView({
model: this,
customBaselayersCollection: this._customBaselayersCollection,
submitButton: this._submitButton,
modalFooter: this._modalFooter
});
},
fetchLayers: function (url) {
this.set('currentView', 'fetchingLayers');
this.wmsService.setUrl(url);
this.wmsLayersCollection.fetch({
reset: true
});
},
layersAvailableCount: function () {
return _.difference(
this.wmsLayersCollection.pluck('title'),
this._customBaselayersCollection.pluck('name')
).length;
},
get: function (name) {
if (name === 'layer') {
var customBaselayerModel = this.wmsLayersCollection.find(function (mdl) {
return mdl.get('state') === 'saveDone';
});
return customBaselayerModel && customBaselayerModel.get('customBaselayerModel');
} else {
return Backbone.Model.prototype.get.apply(this, arguments);
}
},
getLayers: function () {
if (this.get('searchQuery')) {
var regExp = new RegExp(this.get('searchQuery'), 'i');
return this.wmsLayersCollection.filter(function (layer) {
return layer.get('name').match(regExp);
}, this);
} else {
return this.wmsLayersCollection;
}
},
hasAlreadyAddedLayer: function () {
// Already added layers are disabled to be saved for each layer
return false;
},
_onLayerStateChange: function (mdl, newState) {
switch (newState) {
case 'saving':
this.set('currentView', 'savingLayer');
break;
case 'saveDone':
this.set('layer', mdl.get('customBaselayerModel'));
this.trigger('saveBasemap');
break;
case 'saveFail':
this.set('currentView', 'saveFail');
break;
default:
this.set('currentView', 'selectLayer');
}
}
});

View File

@@ -0,0 +1,198 @@
var CoreView = require('backbone/core-view');
var _ = require('underscore');
var SelectLayerView = require('./select-layer-view');
var ViewFactory = require('builder/components/view-factory');
var ErrorView = require('builder/components/error/error-view');
var renderLoading = require('builder/components/loading/render-loading');
var enterUrl = require('./enter-url.tpl');
var $ = require('jquery');
/**
* Represents the WMS/WMTS tab category.
* Current state is defined by presence (or lack of) layers
*/
module.exports = CoreView.extend({
events: {
'keydown .js-search-input': '_onKeyDown',
'submit .js-search-form': 'killEvent',
'keydown .js-url': '_onKeydown',
'paste .js-url': '_onPaste',
'click .js-clean-search': '_onCleanSearchClick',
'click .js-search-link': '_submitSearch'
},
initialize: function (opts) {
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
if (!opts.submitButton) throw new Error('submitButton is required');
if (!opts.modalFooter) throw new Error('modalFooter is required');
this._customBaselayersCollection = opts.customBaselayersCollection;
this._submitButton = opts.submitButton;
this._modalFooter = opts.modalFooter;
this._debouncedUpdate = _.debounce(this._update.bind(this), 150);
this._onClickBinded = this._onClickOK.bind(this);
this._bindSubmitButton();
this._initBinds();
},
render: function () {
this.clearSubViews();
this._updateOkBtn();
this._disableOkBtn(true);
var view;
switch (this.model.get('currentView')) {
case 'savingLayer':
this._disableModalFooter(true);
view = ViewFactory.createByHTML(
renderLoading({
title: _t('components.modals.add-basemap.saving')
})
);
break;
case 'selectLayer':
this._disableModalFooter(true);
view = new SelectLayerView({
model: this.model,
customBaselayersCollection: this._customBaselayersCollection
});
break;
case 'saveFail':
this._disableModalFooter(true);
view = new ErrorView({
title: _t('components.modals.add-basemap.add-basemap-error')
});
break;
case 'fetchingLayers':
this._disableModalFooter(true);
view = ViewFactory.createByHTML(
renderLoading({
title: _t('components.modals.add-basemap.fetching')
})
);
break;
case 'enterURL':
default:
this._disableModalFooter(false);
view = ViewFactory.createByHTML(
enterUrl({
layersFetched: this.model.get('layersFetched'),
layers: this.model.wmsLayersCollection
})
);
break;
}
this.addView(view);
this.$el.append(view.render().el);
this.$('.js-search-input').focus();
return this;
},
_showCleanSearchButton: function () {
this.$('.js-clean-search').show();
},
_hideCleanSearchButton: function () {
this.$('.js-clean-search').hide();
},
_initBinds: function () {
this.model.bind('change', this.render, this);
this.model.bind('change', this._onChangeSearchQuery, this);
this.model.bind('layersFetched', this.render, this);
},
_onKeydown: function (e) {
e.stopPropagation();
this._debouncedUpdate();
},
_onPaste: function (e) {
e.stopPropagation();
this._debouncedUpdate();
},
_update: function (e) {
this._disableOkBtn(!this.$('.js-url').val());
this.$('.js-error').removeClass('is-visible'); // resets error state when changed
},
_disableOkBtn: function (disable) {
this._submitButton.toggleClass('is-disabled', disable);
},
_onKeyDown: function (e) {
var enterPressed = (e.keyCode === $.ui.keyCode.ENTER);
if (enterPressed) {
this.killEvent(e);
this._submitSearch();
}
},
_submitSearch: function (e) {
this.killEvent(e);
this.model.set('searchQuery', this.$('.js-search-input').val());
},
_onChangeSearchQuery: function () {
var searchQuery = this.model.get('searchQuery');
if (!searchQuery) {
this._hideCleanSearchButton();
}
},
_onCleanSearchClick: function (e) {
this.killEvent(e);
this.model.set('searchQuery', '');
},
_onClickOK: function (e) {
this.killEvent(e);
var url = this.$('.js-url').val();
if (url) {
this.model.fetchLayers(url);
}
},
_disableModalFooter: function (disable) {
this._modalFooter.toggleClass('is-disabled', disable);
},
_updateOkBtn: function () {
this._submitButton.find('span').text(_t('components.modals.add-basemap.get-layers'));
},
_bindSubmitButton: function () {
this._submitButton.on('click', this._onClickBinded);
},
_unBindSubmitButton: function () {
this._submitButton.off('click', this._onClickBinded);
},
clean: function () {
this._unBindSubmitButton();
CoreView.prototype.clean.call(this);
}
});

View File

@@ -0,0 +1,15 @@
<div class="u-flex u-alignCenter Modal-basemapContainer">
<h3 class="CDB-Text CDB-Size-large u-mainTextColor Modal-titleBasemap"><%- _t('components.modals.add-basemap.xyz.insert') %></h3>
<div class="CDB-Text u-flex u-alignCenter">
<label class="Metadata-label Metadata-label--auto CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis"><%- _t('components.modals.add-basemap.xyz.enter') %></label>
<div class="Form-rowData Form-rowData--longer">
<input type="text" class="has-icon CDB-InputText CDB-Text js-url" value="" placeholder="E.g. https://{s}.carto.com/foobar/{z}/{x}/{y}.png">
<i class="Spinner XYZPanel-inputIcon XYZPanel-inputIcon--loader Spinner--formIcon Form-inputIcon js-validating" style="display: none;"></i>
<div class="Checkbox XYZPanel-inputCheckbox js-tms" data-title="Inverts Y axis numbering for tiles">
<button class="Checkbox-input u-rSpace--m"></button>
<label class="CDB-Text CDB-Size-small is-semibold u-upperCase"><%- _t('components.modals.add-basemap.xyz.tms') %></label>
</div>
<div class="XYZPanel-error CDB-InfoTooltip CDB-InfoTooltip--left is-error CDB-Text CDB-Size-medium CDB-InfoTooltip-text js-error"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,38 @@
var _ = require('underscore');
var Backbone = require('backbone');
var XYZView = require('./xyz-view');
/**
* View model for XYZ tab content.
*/
module.exports = Backbone.Model.extend({
defaults: {
name: 'xyz',
label: 'XYZ',
tms: false,
layer: undefined // will be set when valid
},
createView: function (opts) {
if (!opts.submitButton) throw new Error('submitButton is required');
if (!opts.modalFooter) throw new Error('modalFooter is required');
this._submitButton = opts.submitButton;
this._modalFooter = opts.modalFooter;
return new XYZView({
model: this,
submitButton: this._submitButton,
modalFooter: this._modalFooter
});
},
hasAlreadyAddedLayer: function (userLayers) {
var urlTemplate = this.get('layer').get('urlTemplate');
return _.any(userLayers.isCustomCategory(), function (customLayer) {
return customLayer.get('urlTemplate') === urlTemplate;
});
}
});

View File

@@ -0,0 +1,177 @@
var $ = require('jquery');
var _ = require('underscore');
var CoreView = require('backbone/core-view');
var template = require('./enter-url.tpl');
var CustomBaselayerModel = require('builder/data/custom-baselayer-model');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
/**
* Represents the XYZ tab content.
*/
module.exports = CoreView.extend({
className: 'XYZPanel',
events: {
'click .js-tms': '_changeTMS',
'keydown .js-url': '_onKeydown',
'paste .js-url': '_onPaste'
},
initialize: function (opts) {
if (!opts.submitButton) throw new Error('submitButton is required');
this._submitButton = opts.submitButton;
this._lastCallSeq = 0;
this._debouncedUpdate = _.debounce(this._update.bind(this), 150);
this._initBinds();
},
render: function () {
this.clearSubViews();
this._updateOkBtn();
this._disableOkBtn(true);
this.$el.html(
template()
);
this._initViews();
return this;
},
_initViews: function () {
// Add TMS tooltip
var tooltip = new TipsyTooltipView({
el: this.$('.js-tms'),
title: function () {
return $(this).data('title');
}
});
this.addView(tooltip);
},
_onKeydown: function (e) {
e.stopPropagation();
this._debouncedUpdate();
},
_onPaste: function (e) {
e.stopPropagation();
this._debouncedUpdate();
},
_initBinds: function () {
this.model.bind('change:tms', this._setTMSCheckbox, this);
},
_update: function () {
this._disableOkBtn(true);
this._indicateIsValidating(true);
var layer;
var urlErrorMsg;
var url = this.$('.js-url').val();
var tms = this.model.get('tms');
if (url) {
try {
layer = this._byCustomURL(url, tms);
} catch (e) {
urlErrorMsg = _t('components.modals.add-basemap.xyz.not-valid');
}
}
this.model.set('layer', layer);
if (layer) {
var self = this;
// Make sure only the last call made is the one that defines view change,
// avoids laggy responses to indicate wrong state
var thisCallSeq = ++this._lastCallSeq;
layer.validateTemplateURL({
success: function () {
if (thisCallSeq === self._lastCallSeq) {
self._disableOkBtn(false);
self._indicateIsValidating(false);
self._updateError();
}
},
error: function () {
if (thisCallSeq === self._lastCallSeq) {
self._disableOkBtn(false);
self._indicateIsValidating(false);
self._updateError(_t('components.modals.add-basemap.xyz.couldnt-validate'));
}
}
});
} else if (url) {
this._indicateIsValidating(false);
this._updateError(urlErrorMsg);
} else {
this._indicateIsValidating(false);
this._updateError();
}
},
_changeTMS: function (e) {
this.model.set('tms', !this.model.get('tms'));
this._onKeydown(e);
},
_setTMSCheckbox: function (e) {
this.$('.js-tms .Checkbox-input').toggleClass('is-checked', this.model.get('tms'));
},
_byCustomURL: function (url, tms) {
// Minimal test for "valid URL" w/o having to complicate it with regex
if (url && url.indexOf('/') === -1) throw new TypeError('invalid URL');
// Only lowercase the placeholder variables, since the URL may contain case-sensitive data (e.g. API keys and such)
url = url.replace(/\{S\}/g, '{s}')
.replace(/\{X\}/g, '{x}')
.replace(/\{Y\}/g, '{y}')
.replace(/\{Z\}/g, '{z}');
var layer = new CustomBaselayerModel({
urlTemplate: url,
attribution: null,
maxZoom: 21,
minZoom: 0,
name: '',
tms: tms,
category: 'Custom',
type: 'Tiled'
});
layer.set('className', layer._generateClassName(url));
return layer;
},
_setTMS: function (ev) {
var $checkbox = $(ev.target).closest('.Checkbox');
$checkbox.find('.Checkbox-input').toggleClass('is-checked');
this._update(ev);
},
_updateOkBtn: function () {
this._submitButton.find('span').text(_t('components.modals.add-basemap.add-btn'));
},
_disableOkBtn: function (disable) {
this._submitButton.toggleClass('is-disabled', disable);
},
_updateError: function (msg) {
this.$('.js-error').text(msg)[ msg ? 'addClass' : 'removeClass' ]('is-visible');
},
_indicateIsValidating: function (indicate) {
this.$('.js-validating').toggle(!!indicate);
}
});

View File

@@ -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)
});
}
});

View File

@@ -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();
}
}
});

View File

@@ -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>

View File

@@ -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);
}
});

View File

@@ -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'));
}
}
}
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -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');
}
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'));
}
});

View File

@@ -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;
}
});

View File

@@ -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>

View File

@@ -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'));
}
}
}
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
<% } %>

View File

@@ -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;
}
});

View File

@@ -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);
}
});

View File

@@ -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);
}
});

View File

@@ -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>

View File

@@ -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');
}
});

View File

@@ -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>
<% } %>

View File

@@ -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
});
}
}
});

View File

@@ -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>

View File

@@ -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>
<% } %>
<% } %>

View File

@@ -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')
// }
}
};

View File

@@ -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>

View File

@@ -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>
<% } %>

View File

@@ -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();
}
}
});

View File

@@ -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>
<% } %>

View File

@@ -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');
}
});

Some files were not shown because too many files have changed in this diff Show More