Initial commit
This commit is contained in:
+34
@@ -0,0 +1,34 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./background-polling-header-title.tpl');
|
||||
|
||||
/**
|
||||
* Background polling header title view
|
||||
*
|
||||
* It will contain only the title
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
tagName: 'h3',
|
||||
className: 'CDB-Text CDB-Size-large u-lSpace--xl',
|
||||
|
||||
initialize: function () {
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
imports: this.model.getTotalImports(),
|
||||
totalPollings: this.model.getTotalPollings()
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change analysisAdded analysisRemoved importAdded importRemoved geocodingAdded geocodingRemoved', this.render, this);
|
||||
}
|
||||
|
||||
});
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<% if (totalPollings === 1) { %>
|
||||
<% if (imports > 0) { %>
|
||||
Connecting
|
||||
<% } %>
|
||||
dataset...
|
||||
<% } else { %>
|
||||
Working...
|
||||
<% } %>
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
const $ = require('jquery');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const BackgroundPollingHeaderTitleView = require('dashboard/views/dashboard/background-polling/background-polling-header-title/background-polling-header-title-view');
|
||||
const template = require('./background-polling-header.tpl');
|
||||
|
||||
/**
|
||||
* Background polling header view
|
||||
*
|
||||
* It will contain:
|
||||
* - Badge
|
||||
* - Title
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'BackgroundPolling-header',
|
||||
|
||||
initialize: function () {
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(template());
|
||||
this._initViews();
|
||||
this._updateBadges();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change importAdded importRemoved geocodingAdded geocodingRemoved', this._updateBadges);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
const headerTitle = new BackgroundPollingHeaderTitleView({
|
||||
model: this.model
|
||||
});
|
||||
this.$el.append(headerTitle.render().el);
|
||||
this.addView(headerTitle);
|
||||
},
|
||||
|
||||
_updateBadges: function () {
|
||||
const failed = this.model.getTotalFailedItems();
|
||||
|
||||
if (this.$('.BackgroundPolling-headerBadgeCount').length === 0 && failed > 0) {
|
||||
const $span = $('<span>').addClass('BackgroundPolling-headerBadgeCount Badge Badge--negative CDB-Text CDB-Size-small').text(failed);
|
||||
this.$('.BackgroundPolling-headerBadge')
|
||||
.append($span)
|
||||
.addClass('has-failures');
|
||||
} else if (this.$('.BackgroundPolling-headerBadgeCount').length > 0 && failed > 0) {
|
||||
this.$('.BackgroundPolling-headerBadgeCount').text(failed);
|
||||
} else if (failed === 0) {
|
||||
this.$('.BackgroundPolling-headerBadgeCount').remove();
|
||||
this.$('.BackgroundPolling-headerBadge').removeClass('has-failures');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<div class="BackgroundPolling-headerBadge LayoutIcon">
|
||||
<i class="CDB-IconFont CDB-IconFont-cloud BackgroundPolling-headerBadgeIcon js-icon"></i>
|
||||
</div>
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const ImportItemView = require('dashboard/views/dashboard/imports/background-import-item/background-import-item-view');
|
||||
const ImportLimitItemView = require('builder/components/background-importer/background-import-limit-view');
|
||||
const ImportsModel = require('dashboard/data/imports-model');
|
||||
const BackgroundPollingModel = require('dashboard/data/background-polling/background-polling-model');
|
||||
const BackgroundPollingHeaderView = require('dashboard/views/dashboard/background-polling/background-polling-header/background-polling-header-view');
|
||||
const template = require('./background-polling.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'userModel',
|
||||
'createVis'
|
||||
];
|
||||
|
||||
/**
|
||||
* Background polling view
|
||||
*
|
||||
* It will pool all polling operations that happens
|
||||
* in Cartodb, as in imports and geocodings
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'BackgroundPolling',
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this._vis = options.vis;
|
||||
|
||||
if (!this.model) {
|
||||
this.model = new BackgroundPollingModel({}, {
|
||||
userModel: this._userModel
|
||||
});
|
||||
}
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(template());
|
||||
this._initViews();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'importAdded', this._addImport);
|
||||
this.listenTo(this.model, 'importAdded importRemoved', this._checkPollingsSize);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
const backgroundPollingHeaderView = new BackgroundPollingHeaderView({
|
||||
model: this.model
|
||||
});
|
||||
|
||||
this.$el.prepend(backgroundPollingHeaderView.render().el);
|
||||
this.addView(backgroundPollingHeaderView);
|
||||
},
|
||||
|
||||
_checkPollingsSize: function () {
|
||||
if (this.model.getTotalPollings() > 0) {
|
||||
this.show();
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
},
|
||||
|
||||
_addImport: function (model) {
|
||||
const importItem = new ImportItemView({
|
||||
showSuccessDetailsButton: this.model.get('showSuccessDetailsButton'),
|
||||
model,
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
|
||||
importItem.bind('remove', function (mdl) {
|
||||
this.model.removeImportItem(mdl);
|
||||
}, this);
|
||||
|
||||
this.$('.js-list').prepend(importItem.render().el);
|
||||
this.addView(importItem);
|
||||
|
||||
this.enable();
|
||||
},
|
||||
|
||||
_addDataset: function (d) {
|
||||
if (d) {
|
||||
this._addImportsItem(d);
|
||||
}
|
||||
},
|
||||
|
||||
_onDroppedFile: function (files) {
|
||||
if (files) {
|
||||
this._addImportsItem({
|
||||
type: 'file',
|
||||
value: files,
|
||||
create_vis: files._createVis || this._createVis
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_addImportsItem: function (uploadData) {
|
||||
if (this.model.canAddImport()) {
|
||||
this._removeLimitItem();
|
||||
} else {
|
||||
this._addLimitItem();
|
||||
return false;
|
||||
}
|
||||
|
||||
const imp = new ImportsModel({}, {
|
||||
upload: uploadData,
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
this.model.addImportItem(imp);
|
||||
},
|
||||
|
||||
// Limits view
|
||||
|
||||
_addLimitItem: function () {
|
||||
if (!this._importLimit) {
|
||||
const view = new ImportLimitItemView({
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
this.$('.js-list').prepend(view.render().el);
|
||||
this.addView(view);
|
||||
this._importLimit = view;
|
||||
}
|
||||
},
|
||||
|
||||
_removeLimitItem: function () {
|
||||
var view = this._importLimit;
|
||||
if (view) {
|
||||
view.clean();
|
||||
this.removeView(view);
|
||||
delete this._importLimit;
|
||||
}
|
||||
},
|
||||
|
||||
// Enable background polling checking
|
||||
// ongoing imports
|
||||
enable: function () {
|
||||
this.model.startPollings();
|
||||
},
|
||||
|
||||
// Disable/stop background pollings
|
||||
disable: function () {
|
||||
this.model.stopPollings();
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.$el.addClass('is-visible');
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.$el.removeClass('is-visible');
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this.disable();
|
||||
CoreView.prototype.clean.apply(this);
|
||||
}
|
||||
});
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<div class="BackgroundPolling-body CDB-Text CDB-Size-medium">
|
||||
<ul class="BackgroundPolling-list js-list"></ul>
|
||||
</div>
|
||||
@@ -0,0 +1,247 @@
|
||||
const Backbone = require('backbone');
|
||||
const UploadModel = require('dashboard/data/upload-model');
|
||||
const VisFetchModel = require('builder/data/visualizations-fetch-model');
|
||||
const VisualizationsCollection = require('dashboard/data/visualizations-collection');
|
||||
const CartoTableMetadata = require('dashboard/views/public-dataset/carto-table-metadata');
|
||||
|
||||
const IMPORT = 'import';
|
||||
const DATASETS = 'datasets';
|
||||
const SCRATCH = 'scratch';
|
||||
|
||||
const IMPORT_TWITTER = 'listing.import.twitter';
|
||||
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel',
|
||||
'configModel',
|
||||
'backgroundPollingView'
|
||||
];
|
||||
|
||||
/**
|
||||
* Create dataset model
|
||||
*
|
||||
* - Store the state of the dialog (listing or loading).
|
||||
* - Store the selected datasets for a map creation.
|
||||
* - Store the upload info for a dataset creation.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
type: 'dataset',
|
||||
option: 'listing',
|
||||
listing: 'datasets',
|
||||
contentPane: 'listing' // [import, datasets, scratch]
|
||||
},
|
||||
|
||||
initialize: function (attributes, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this.upload = new UploadModel(
|
||||
{ create_vis: false },
|
||||
{ userModel: this._userModel,
|
||||
configModel: this._configModel }
|
||||
);
|
||||
|
||||
this.selectedDatasets = new Backbone.Collection();
|
||||
this.collection = new VisualizationsCollection(null, { configModel: this._configModel });
|
||||
this.visFetchModel = new VisFetchModel({
|
||||
content_type: 'datasets',
|
||||
library: this.showLibrary()
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
viewsReady: function () {
|
||||
// nothing to do for this use-case
|
||||
this.set('listing', 'import');
|
||||
},
|
||||
|
||||
// For create-listing view
|
||||
showLibrary: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
// For create-listing view
|
||||
showDatasets: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
// For create-listing view
|
||||
canSelect: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
// Get option state (it could be loading or listing)
|
||||
getOption: function () {
|
||||
const option = this.get('option');
|
||||
const states = option.split('.');
|
||||
|
||||
if (states.length > 0) {
|
||||
return states[0];
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
// Get import state (it could be any of the possibilities of the import options, as in scratch, dropbox, etc...)
|
||||
// For create-footer view
|
||||
getImportState: function () {
|
||||
const option = this.get('option');
|
||||
const states = option.split('.');
|
||||
|
||||
if (states.length > 0 && states.length < 4 && states[0] === 'listing' && states[1] === 'import') {
|
||||
return states[2];
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
// For create-footer view
|
||||
showGuessingToggler: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
// For create-footer view
|
||||
showPrivacyToggler: function () {
|
||||
const hiddenDueToDeprecation = this._atTwitterImportPane() && !this._userModel.hasOwnTwitterCredentials();
|
||||
const 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('option') === IMPORT_TWITTER;
|
||||
},
|
||||
|
||||
// For create-footer view
|
||||
startUpload: function () {
|
||||
this._backgroundPollingView._addDataset(this.upload.toJSON());
|
||||
},
|
||||
|
||||
// For create-listing-import view
|
||||
setActiveImportPane: function (option) {
|
||||
if (option && this._atImportPane() && this.getImportState() !== option) {
|
||||
this.set('option', 'listing.import.' + option);
|
||||
}
|
||||
},
|
||||
|
||||
// For create-footer view
|
||||
isMapType: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
// For create-from-scratch view
|
||||
createFromScratch: function () {
|
||||
this.trigger('creatingDataset', 'dataset', this);
|
||||
this.set('contentPane', 'creatingFromScratch');
|
||||
|
||||
var dataset = new CartoTableMetadata(null, { configModel: this._configModel });
|
||||
|
||||
dataset.save({}, {
|
||||
success: m => {
|
||||
this.trigger('datasetCreated', m, this);
|
||||
},
|
||||
error: (m, e) => {
|
||||
this.trigger('datasetError', e, this);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.upload, 'change', function () {
|
||||
this.trigger('change:upload', this);
|
||||
});
|
||||
|
||||
this.listenTo(this.collection, 'change:selected', this._onItemSelected);
|
||||
this.listenTo(this.visFetchModel, 'change', this._fetchCollection);
|
||||
|
||||
this.listenTo(this, 'change:option', this._maybePrefetchDatasets, this);
|
||||
this.listenTo(this, 'change:listing', this._maybePrefetchDatasets, this);
|
||||
},
|
||||
|
||||
_maybePrefetchDatasets: function () {
|
||||
const isDatasets = this.get('listing') === 'datasets';
|
||||
|
||||
// Fetch collection if it was never fetched (and a search is not applied!)
|
||||
if (isDatasets && !this.get('collectionFetched') && !this.visFetchModel.isSearching()) {
|
||||
this.set('collectionFetched', true);
|
||||
this._fetchCollection();
|
||||
}
|
||||
},
|
||||
|
||||
getVisualizationFetchModel: function () {
|
||||
return this.visFetchModel;
|
||||
},
|
||||
|
||||
getTablesCollection: function () {
|
||||
return this.collection;
|
||||
},
|
||||
|
||||
getSelectedDatasetsCollection: function () {
|
||||
return this.selectedDatasets;
|
||||
},
|
||||
|
||||
getUploadModel: function () {
|
||||
return this.upload;
|
||||
},
|
||||
|
||||
canFinish: function () {},
|
||||
|
||||
finish: function () {},
|
||||
|
||||
_selectedItems: function () {
|
||||
return this.collection.where({ selected: true });
|
||||
},
|
||||
|
||||
_fetchCollection: function () {
|
||||
const params = this.visFetchModel.attributes;
|
||||
|
||||
this.collection.options.set({
|
||||
locked: '',
|
||||
q: params.q,
|
||||
page: params.page || 1,
|
||||
tags: params.tag,
|
||||
per_page: this.collection['_TABLES_PER_PAGE'],
|
||||
shared: params.shared,
|
||||
only_liked: params.liked,
|
||||
order: 'updated_at',
|
||||
type: '',
|
||||
types: params.library ? 'remote' : 'table',
|
||||
exclude_raster: true
|
||||
});
|
||||
|
||||
this.collection.fetch();
|
||||
},
|
||||
|
||||
_onItemSelected: function (changedModel) {
|
||||
// Triggers an import immediately
|
||||
if (changedModel.get('type') === 'remote') {
|
||||
// previously located in listings/datasets/remote_datasets_item_view
|
||||
const table = new CartoTableMetadata(changedModel.get('external_source'), { configModel: this._configModel });
|
||||
const data = {
|
||||
type: 'remote',
|
||||
value: changedModel.get('name'),
|
||||
remote_visualization_id: changedModel.get('id'),
|
||||
size: table.get('size'),
|
||||
create_vis: false
|
||||
};
|
||||
|
||||
this._backgroundPollingView._addDataset(data);
|
||||
this.trigger('destroyModal');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,385 @@
|
||||
const Backbone = require('backbone');
|
||||
const _ = require('underscore');
|
||||
const ImportsModel = require('builder/data/background-importer/imports-model');
|
||||
const UploadModel = require('dashboard/data/upload-model');
|
||||
const PermissionModel = require('dashboard/data/permission-model');
|
||||
const VisualizationModel = require('dashboard/data/visualization-model');
|
||||
const VisualizationsCollection = require('dashboard/data/visualizations-collection');
|
||||
const VisFetchModel = require('builder/data/visualizations-fetch-model');
|
||||
const CartoTableMetadata = require('dashboard/views/public-dataset/carto-table-metadata');
|
||||
const TablesCollection = require('builder/data/visualizations-collection');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const IMPORT = 'import';
|
||||
const DATASETS = 'datasets';
|
||||
const SCRATCH = 'scratch';
|
||||
|
||||
const IMPORT_TWITTER = 'listing.import.twitter';
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel',
|
||||
'configModel',
|
||||
'backgroundPollingView'
|
||||
];
|
||||
|
||||
/**
|
||||
* This model will be on charge of create a new map
|
||||
* using user selected datasets, where they can be
|
||||
* already imported datasets or remote (and needed to import)
|
||||
* datasets.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
type: 'map',
|
||||
contentPane: 'listing', // [listing, loading]
|
||||
option: 'listing',
|
||||
currentImport: null,
|
||||
tableIdsArray: [],
|
||||
listing: 'datasets', // [import, datasets, scratch]
|
||||
collectionFetched: false,
|
||||
activeImportPane: 'file'
|
||||
},
|
||||
|
||||
_DEFAULT_MAP_NAME: 'Untitled Map',
|
||||
|
||||
initialize: function (attributes, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this.upload = new UploadModel(
|
||||
{ create_vis: true },
|
||||
{ userModel: this._userModel, configModel: this._configModel }
|
||||
);
|
||||
|
||||
this.selectedDatasets = new TablesCollection(options.selectedItems, { configModel: this._configModel });
|
||||
this.collection = new VisualizationsCollection(null, { configModel: this._configModel });
|
||||
this.vis = new VisualizationModel({ name: 'Untitled map' }, { configModel: this._configModel });
|
||||
this.visFetchModel = new VisFetchModel({
|
||||
content_type: 'datasets',
|
||||
library: this.showLibrary()
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
setSelected: function (datasets) {
|
||||
this.selectedDatasets.reset(datasets);
|
||||
},
|
||||
|
||||
// For entry point, notifies model that depending views are ready for changes (required for custom events)
|
||||
viewsReady: function () {
|
||||
if (this.selectedDatasets.isEmpty()) {
|
||||
this._maybePrefetchDatasets();
|
||||
} else {
|
||||
// Not empty, so start creating map from these preselected items
|
||||
this.createMap();
|
||||
}
|
||||
},
|
||||
|
||||
// For create-listing view
|
||||
showLibrary: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
// For create-listing view
|
||||
showDatasets: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
// For create-listing view
|
||||
canSelect: function (datasetModel) {
|
||||
if (datasetModel.get('selected')) {
|
||||
return true;
|
||||
} else {
|
||||
return this.selectedDatasets.length < this._userModel.getMaxLayers();
|
||||
}
|
||||
},
|
||||
|
||||
canFinish: function () {
|
||||
if (this.get('listing') === 'import') {
|
||||
return this.upload.isValidToUpload();
|
||||
} else if (this.get('listing') === 'datasets') {
|
||||
return this.selectedDatasets.length > 0;
|
||||
}
|
||||
},
|
||||
|
||||
finish: function () {},
|
||||
|
||||
// Get option state (it could be listing or loading)
|
||||
getOption: function () {
|
||||
var option = this.get('option');
|
||||
var states = option.split('.');
|
||||
|
||||
if (states.length > 0) {
|
||||
return states[0];
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
// Get import state (it could be any of the possibilities of the import options, as in scratch, dropbox, etc...)
|
||||
// For create-footer view
|
||||
getImportState: function () {
|
||||
var option = this.get('option');
|
||||
var states = option.split('.');
|
||||
|
||||
if (states.length > 0 && states.length < 4 && states[0] === 'listing' && states[1] === 'import') {
|
||||
return states[2];
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
// For create-footer view
|
||||
showGuessingToggler: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
// For create-footer view
|
||||
showPrivacyToggler: function () {
|
||||
var hiddenDueToDeprecation = this._atTwitterImportPane() && !this._userModel.hasOwnTwitterCredentials();
|
||||
var hasToBeShowed = this._atImportPane() && !hiddenDueToDeprecation;
|
||||
return hasToBeShowed;
|
||||
},
|
||||
|
||||
// For create-listing-import view
|
||||
setActiveImportPane: function (option) {
|
||||
if (option && this._atImportPane() && this.getImportState() !== option) {
|
||||
this.set('option', 'listing.import.' + option);
|
||||
}
|
||||
},
|
||||
|
||||
// For create-footer view
|
||||
isMapType: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
// For create-footer view
|
||||
startUpload: function () {
|
||||
this._backgroundPollingView._addDataset(this.upload.toJSON());
|
||||
},
|
||||
|
||||
createMap: function () {
|
||||
if (this.selectedDatasets.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.set('contentPane', 'loading');
|
||||
this._checkCollection();
|
||||
},
|
||||
|
||||
// For create-from-scratch view
|
||||
createFromScratch: function (configModel) {
|
||||
this.trigger('creatingDataset', 'dataset', this);
|
||||
this.set('contentPane', 'loading');
|
||||
|
||||
var dataset = new CartoTableMetadata(null, { configModel: this._configModel });
|
||||
|
||||
dataset.save({}, {
|
||||
success: m => {
|
||||
this.trigger('datasetCreated', m, this);
|
||||
},
|
||||
error: (m, e) => {
|
||||
this.trigger('datasetError', e, this);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.upload.bind('change', function () {
|
||||
this.trigger('change:upload', this);
|
||||
}, this);
|
||||
|
||||
this.bind('change:option', this._onOptionChange, this);
|
||||
|
||||
this.collection.bind('change:selected', function (changedModel, wasSelected) {
|
||||
this.selectedDatasets[ wasSelected ? 'add' : 'remove' ](changedModel);
|
||||
}, this);
|
||||
|
||||
this.collection.bind('reset', function () {
|
||||
this.selectedDatasets.each(function (model) {
|
||||
var sameModel = this.collection.get(model.id);
|
||||
if (sameModel) {
|
||||
sameModel.set('selected', true);
|
||||
}
|
||||
}, this);
|
||||
}, this);
|
||||
|
||||
this.visFetchModel.bind('change', this._fetchCollection, this);
|
||||
|
||||
if (this.selectedDatasets.isEmpty()) {
|
||||
this.bind('change:option', this._maybePrefetchDatasets, this);
|
||||
this.bind('change:listing', this._maybePrefetchDatasets, this);
|
||||
}
|
||||
},
|
||||
|
||||
_maybePrefetchDatasets: function () {
|
||||
var isDatasets = this.get('listing') === 'datasets';
|
||||
|
||||
// Fetch collection if it was never fetched (and a search is not applied!)
|
||||
if (isDatasets && !this.get('collectionFetched') && !this.visFetchModel.isSearching()) {
|
||||
this._fetchCollection();
|
||||
}
|
||||
},
|
||||
|
||||
_fetchCollection: function () {
|
||||
this.set('collectionFetching', true);
|
||||
var params = this.visFetchModel.attributes;
|
||||
var types;
|
||||
|
||||
if (this.visFetchModel.isSearching()) {
|
||||
// Supporting search in data library and user datasets at the same time
|
||||
types = 'table,remote';
|
||||
} else {
|
||||
types = params.library ? 'remote' : 'table';
|
||||
}
|
||||
|
||||
this.collection.options.set({
|
||||
locked: '',
|
||||
q: params.q,
|
||||
page: params.page || 1,
|
||||
tags: params.tag,
|
||||
per_page: this.collection['_TABLES_PER_PAGE'],
|
||||
shared: params.shared,
|
||||
only_liked: params.liked,
|
||||
order: 'updated_at',
|
||||
type: '',
|
||||
types: types,
|
||||
exclude_raster: true
|
||||
});
|
||||
|
||||
this.collection.fetch({
|
||||
success: function () {
|
||||
this.set({
|
||||
collectionFetching: false,
|
||||
collectionFetched: true
|
||||
});
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
|
||||
getVisualizationFetchModel: function () {
|
||||
return this.visFetchModel;
|
||||
},
|
||||
|
||||
getTablesCollection: function () {
|
||||
return this.collection;
|
||||
},
|
||||
|
||||
getSelectedDatasetsCollection: function () {
|
||||
return this.selectedDatasets;
|
||||
},
|
||||
|
||||
getUploadModel: function () {
|
||||
return this.upload;
|
||||
},
|
||||
|
||||
_selectedItems: function () {
|
||||
return this.selectedDatasets;
|
||||
},
|
||||
|
||||
_checkCollection: function () {
|
||||
if (this.selectedDatasets.length > 0) {
|
||||
this._importDataset(this.selectedDatasets.pop());
|
||||
} else {
|
||||
this.set('currentImport', '');
|
||||
this._createMap();
|
||||
}
|
||||
},
|
||||
|
||||
_importDataset: function (mdl) {
|
||||
var tableIdsArray = _.clone(this.get('tableIdsArray'));
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
var impModel = new ImportsModel({}, {
|
||||
upload: d,
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
this.set('currentImport', _.clone(impModel));
|
||||
this.trigger('importingRemote', this);
|
||||
|
||||
impModel.bind('change:state', function (m) {
|
||||
if (m.hasCompleted()) {
|
||||
var data = m.getImportModel().toJSON();
|
||||
tableIdsArray.push(data.table_name);
|
||||
this.set('tableIdsArray', tableIdsArray);
|
||||
this._checkCollection();
|
||||
this.trigger('importCompleted', this);
|
||||
}
|
||||
if (m.hasFailed()) {
|
||||
this.set('contentPane', 'importFailed');
|
||||
this.trigger('importFailed', this);
|
||||
}
|
||||
}, this);
|
||||
|
||||
// If import model has any errors at the beginning
|
||||
if (impModel.hasFailed()) {
|
||||
this.set('contentPane', 'importFailed');
|
||||
this.trigger('importFailed', this);
|
||||
}
|
||||
} else {
|
||||
var table = mdl.tableMetadata();
|
||||
tableIdsArray.push(table.get('name'));
|
||||
this.set({
|
||||
currentImport: '',
|
||||
tableIdsArray: tableIdsArray
|
||||
});
|
||||
this._checkCollection();
|
||||
}
|
||||
},
|
||||
|
||||
_createMap: function () {
|
||||
const vis = new VisualizationModel({
|
||||
name: this._DEFAULT_MAP_NAME,
|
||||
type: 'derived'
|
||||
}, { configModel: this._configModel });
|
||||
|
||||
vis.permission = new PermissionModel({
|
||||
owner: this._userModel.attributes
|
||||
}, {
|
||||
configModel: this._configModel,
|
||||
userModel: this._userModel
|
||||
});
|
||||
|
||||
this.trigger('creatingMap', this);
|
||||
|
||||
vis.save({
|
||||
tables: this.get('tableIdsArray')
|
||||
}, {
|
||||
success: () => {
|
||||
this._redirectTo(vis.viewUrl(this._userModel).edit().toString());
|
||||
},
|
||||
error: () => {
|
||||
this.trigger('mapError', this);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_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('option') === IMPORT_TWITTER;
|
||||
},
|
||||
|
||||
_redirectTo: function (url) {
|
||||
window.location = url;
|
||||
}
|
||||
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
const Backbone = require('backbone');
|
||||
const batchProcessItems = require('dashboard/helpers/batch-process-items');
|
||||
|
||||
/**
|
||||
* View model for change lock view.
|
||||
* Manages the life cycle states for the change lock view.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
state: 'ConfirmChangeLock',
|
||||
initialLockValue: false,
|
||||
contentType: 'datasets',
|
||||
items: undefined // a Backbone collection
|
||||
},
|
||||
|
||||
initialize: function (attributes) {
|
||||
this.set('items', new Backbone.Collection(attributes.items));
|
||||
|
||||
const lockedStates = this.get('items').chain()
|
||||
.map(item => item.get('locked'))
|
||||
.uniq()
|
||||
.value()
|
||||
.length;
|
||||
|
||||
if (lockedStates > 1) {
|
||||
const errorMsg = 'It is assumed that all items have the same locked state, a user should never be able to ' +
|
||||
'select a mixed item with current UI. If you get an error with this message something is broken';
|
||||
|
||||
if (window.trackJs && window.trackJs.track) {
|
||||
window.trackJs.track(errorMsg);
|
||||
} else {
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
this.set('initialLockValue', this.get('items').at(0).get('locked'));
|
||||
},
|
||||
|
||||
inverseLock: function () {
|
||||
this.set('state', 'ProcessingItems');
|
||||
|
||||
batchProcessItems({
|
||||
howManyInParallel: 5,
|
||||
items: this.get('items').toArray(),
|
||||
processItem: this._lockItem.bind(this, !this.get('initialLockValue')),
|
||||
done: this.set.bind(this, 'state', 'ProcessItemsDone'),
|
||||
fail: this.set.bind(this, 'state', 'ProcessItemsFail')
|
||||
});
|
||||
},
|
||||
|
||||
_lockItem: function (newLockedValue, item, callback) {
|
||||
item.save({ locked: newLockedValue })
|
||||
.done(function () {
|
||||
callback();
|
||||
})
|
||||
.fail(() =>
|
||||
callback('something failed') // eslint-disable-line
|
||||
);
|
||||
}
|
||||
});
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const pluralizeString = require('dashboard/helpers/pluralize');
|
||||
const loadingView = require('builder/components/loading/render-loading');
|
||||
const failTemplate = require('dashboard/components/fail.tpl');
|
||||
const template = require('./change-lock.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'modalModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Lock/unlock datasets/maps dialog.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'click .js-ok': '_ok',
|
||||
'click .js-cancel': 'close'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
this.listenTo(this.model, 'change', function () {
|
||||
if (this.model.get('state') === 'ProcessItemsDone') {
|
||||
this.close();
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this['_render' + this.model.get('state')]());
|
||||
},
|
||||
|
||||
_renderConfirmChangeLock: function () {
|
||||
// An entity can be an User or Organization
|
||||
const itemsCount = this.model.get('items').length;
|
||||
const areLocked = this.model.get('initialLockValue');
|
||||
const viewTemplate = this.options.template || template;
|
||||
|
||||
return viewTemplate({
|
||||
model: this.model,
|
||||
itemsCount: itemsCount,
|
||||
ownerName: this.options.ownerName,
|
||||
isOwner: this.options.isOwner,
|
||||
thisOrTheseStr: itemsCount === 1 ? 'this' : 'these',
|
||||
itOrThemStr: itemsCount === 1 ? 'it' : 'them',
|
||||
areLocked: areLocked,
|
||||
positiveOrNegativeStr: areLocked ? 'positive' : 'alert',
|
||||
lockOrUnlockStr: areLocked ? 'unlock' : 'lock',
|
||||
contentTypePluralized: pluralizeString(
|
||||
this.model.get('contentType') === 'datasets' ? 'dataset' : 'map', // singular
|
||||
this.model.get('contentType'), // plural
|
||||
itemsCount
|
||||
)
|
||||
});
|
||||
},
|
||||
|
||||
_ok: function (e) {
|
||||
this.killEvent(e);
|
||||
this.model.inverseLock();
|
||||
this.render();
|
||||
},
|
||||
|
||||
close: function () {
|
||||
this._modalModel.destroy();
|
||||
},
|
||||
|
||||
_renderProcessingItems: function () {
|
||||
const lockingOrUnlockingStr = this.model.get('initialLockValue') ? 'Unlocking' : 'Locking';
|
||||
return loadingView({
|
||||
title: `${lockingOrUnlockingStr} ${pluralizeString(this.model.get('contentType') === 'datasets' ? 'dataset' : 'map', this.model.get('items').length)}…`
|
||||
});
|
||||
},
|
||||
|
||||
_renderProcessItemsFail: function () {
|
||||
var lockingOrUnlockingStr = this.model.get('initialLockValue') ? 'unlock' : 'lock';
|
||||
|
||||
return failTemplate({
|
||||
msg: 'Failed to ' + lockingOrUnlockingStr + ' all items'
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="Dialog-header u-inner">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--<%- positiveOrNegativeStr %>">
|
||||
<i class="CDB-IconFont <%- areLocked ? 'CDB-IconFont-unlock' : 'CDB-IconFont-lock' %>"></i>
|
||||
<% if (itemsCount > 1) { %>
|
||||
<span class="Badge Badge--<%- positiveOrNegativeStr %> Dialog-headerIconBadge CDB-Text CDB-Size-small "><%- itemsCount %></span>
|
||||
<% } %>
|
||||
</div>
|
||||
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m u-tSpace-xl">
|
||||
You are about to <%- lockOrUnlockStr %> <%- itemsCount %> <%- contentTypePluralized %>.
|
||||
</h3>
|
||||
<p class="CDB-Text CDB-Size-medium u-altTextColor">
|
||||
<% if (areLocked) { %>
|
||||
<%- _t('components.modals.change-lock.description.locked', {
|
||||
thisOrTheseStr: thisOrTheseStr,
|
||||
contentTypePluralized: contentTypePluralized,
|
||||
itOrThemStr: itOrThemStr
|
||||
}) %>
|
||||
<% } else { %>
|
||||
<%- _t('components.modals.change-lock.description.unlocked', {
|
||||
thisOrTheseStr: thisOrTheseStr,
|
||||
contentTypePluralized: contentTypePluralized,
|
||||
itOrThemStr: itOrThemStr
|
||||
}) %>
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="Dialog-footer Dialog-footer--simple u-inner">
|
||||
<button class="CDB-Button CDB-Button--secondary js-cancel">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">cancel</span>
|
||||
</button>
|
||||
<button class="CDB-Button CDB-Button--primary CDB-Button--<%- positiveOrNegativeStr %> u-lSpace--xl js-ok">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Ok, <%- lockOrUnlockStr %></span>
|
||||
</button>
|
||||
</div>
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const TabPane = require('dashboard/components/tabpane/tabpane');
|
||||
const StartView = require('./start-view');
|
||||
const PrivacyOptions = require('./options-collection');
|
||||
const loadingView = require('builder/components/loading/render-loading');
|
||||
const failTemplate = require('dashboard/components/fail.tpl');
|
||||
const ViewFactory = require('builder/components/view-factory');
|
||||
const MapcapsCollection = require('builder/data/mapcaps-collection');
|
||||
const PrivacyWarningView = require('builder/components/modals/privacy-warning/privacy-warning-view');
|
||||
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
|
||||
const VisualizationModel = require('dashboard/data/visualization-model');
|
||||
const ShareView = require('dashboard/views/dashboard/dialogs/share/share-view');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const upgradeErrorTemplate = require('builder/components/background-importer/upgrade-errors.tpl');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel',
|
||||
'visModel',
|
||||
'configModel',
|
||||
'modals',
|
||||
'modalModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Change privacy datasets/maps dialog.
|
||||
*/
|
||||
const ChangePrivacyView = CoreView.extend({
|
||||
events: {
|
||||
'click .ok': 'ok',
|
||||
'click .cancel': 'destroyDialog'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this._privacyOptions = PrivacyOptions.byVisAndUser(this._visModel, this._userModel);
|
||||
this._privacyModal = new ModalsServiceModel();
|
||||
this._initViews();
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return this._panes.getActivePane().render().el;
|
||||
},
|
||||
|
||||
ok: function () {
|
||||
const selectedOption = this._privacyOptions.selectedOption();
|
||||
if (!selectedOption.canSave()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._shouldShowPrivacyWarning(selectedOption.get('privacy'))
|
||||
.then(shouldShowWarning => {
|
||||
this._panes.active('saving');
|
||||
|
||||
if (shouldShowWarning) {
|
||||
this._checkPrivacyChange(
|
||||
selectedOption.get('privacy'),
|
||||
() => this._savePrivacy(selectedOption),
|
||||
() => this._panes.active('start')
|
||||
);
|
||||
} else {
|
||||
this._savePrivacy(selectedOption);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
destroyDialog: function () {
|
||||
this._modalModel.destroy();
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
this._panes = new TabPane({
|
||||
el: this.el
|
||||
});
|
||||
this.addView(this._panes);
|
||||
this._panes.addTab('start',
|
||||
new StartView({
|
||||
privacyOptions: this._privacyOptions,
|
||||
userModel: this._userModel,
|
||||
visModel: this._visModel,
|
||||
configModel: this._configModel
|
||||
})
|
||||
);
|
||||
this._panes.addTab('saving',
|
||||
ViewFactory.createByHTML(loadingView({
|
||||
title: 'Saving privacy…'
|
||||
}))
|
||||
);
|
||||
this._panes.addTab('saveFail',
|
||||
ViewFactory.createByHTML(failTemplate({
|
||||
msg: ''
|
||||
}))
|
||||
);
|
||||
const upgradeUrl = this._visModel._configModel.get('upgrade_url');
|
||||
const userCanUpgrade = upgradeUrl && !this._visModel._configModel.get('cartodb_com_hosted') && (!this._userModel.isInsideOrg() || this._userModel.isOrgOwner());
|
||||
const upgradeHtml = upgradeErrorTemplate({
|
||||
errorCode: 8007,
|
||||
userCanUpgrade: userCanUpgrade,
|
||||
showTrial: this._userModel.canStartTrial(),
|
||||
upgradeUrl: upgradeUrl
|
||||
});
|
||||
this._panes.addTab('upgrade',
|
||||
ViewFactory.createByHTML(upgradeHtml)
|
||||
);
|
||||
this._panes.active('start');
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this._panes.bind('tabEnabled', this.render, this);
|
||||
this._panes.getPane('start').bind('clickedShare', this._openShareDialog, this);
|
||||
},
|
||||
|
||||
_openShareDialog: function () {
|
||||
this._modals.create(modalModel => {
|
||||
// Order matters, close this dialog before appending the share one, for side-effects to work as expected (body.is-inDialog)
|
||||
return new ShareView({
|
||||
configModel: this._configModel,
|
||||
userModel: this._userModel,
|
||||
visModel: this._visModel,
|
||||
modals: this._modals,
|
||||
modalModel,
|
||||
onClose: this._onShareClose
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onShareClose: function () {
|
||||
this._modals.create(modalModel => {
|
||||
return new ChangePrivacyView({
|
||||
visModel: this._visModel,
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel,
|
||||
modals: this._modals,
|
||||
modalModel
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_shouldShowPrivacyWarning: function (privacyState) {
|
||||
if (!this._userModel.canSelectPremiumOptions(this._visModel)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
const isPubliclyAvailable = VisualizationModel.isPubliclyAvailable(privacyState);
|
||||
|
||||
if (this._visModel.isVisualization()) {
|
||||
this._panes.active('saving');
|
||||
|
||||
const mapcapsCollection = new MapcapsCollection(null, {
|
||||
visDefinitionModel: this._visModel
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
mapcapsCollection.fetch({
|
||||
success: () => {
|
||||
resolve(!!mapcapsCollection.length && isPubliclyAvailable);
|
||||
},
|
||||
error: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(isPubliclyAvailable);
|
||||
},
|
||||
|
||||
_savePrivacy: function (privacyOption) {
|
||||
privacyOption.saveToVis(this._visModel, {
|
||||
success: () => {
|
||||
this._modalModel.destroy();
|
||||
},
|
||||
error: (req, resp) => {
|
||||
if (resp.responseText.indexOf('over account public map quota') !== -1) {
|
||||
this._panes.active('upgrade');
|
||||
} else {
|
||||
this._panes.active('saveFail');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_checkPrivacyChange: function (newPrivacyState, confirmCallback, dismissCallback) {
|
||||
this._privacyModal.create(modalModel => {
|
||||
return new PrivacyWarningView({
|
||||
modalModel: modalModel,
|
||||
privacyType: newPrivacyState,
|
||||
type: this._visModel.isVisualization() ? 'visualization' : 'dataset',
|
||||
onConfirm: confirmCallback,
|
||||
onDismiss: dismissCallback
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ChangePrivacyView;
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Default model for a privacy option.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
privacy: 'PUBLIC',
|
||||
disabled: false,
|
||||
selected: false,
|
||||
password: undefined
|
||||
},
|
||||
|
||||
validate: function (attrs) {
|
||||
if (attrs.disabled && attrs.selected) {
|
||||
return 'Option can not be disabled and selected at the same time';
|
||||
}
|
||||
},
|
||||
|
||||
classNames: function () {
|
||||
return _.chain(['disabled', 'selected'])
|
||||
.map(attr => this.attributes[attr] ? 'is-' + attr : undefined)
|
||||
.compact().value().join(' ');
|
||||
},
|
||||
|
||||
canSave: function () {
|
||||
return !this.get('disabled');
|
||||
},
|
||||
|
||||
/**
|
||||
* @param vis {Object} instance of cdb.admin.Visualization
|
||||
* @param callbacks {Object}
|
||||
*/
|
||||
saveToVis: function (vis, callbacks) {
|
||||
return vis.save(this._attrsToSave(), _.extend({ wait: true }, callbacks));
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Object} attrs
|
||||
* @protected
|
||||
*/
|
||||
_attrsToSave: function () {
|
||||
return _.pick(this.attributes, 'privacy', 'password');
|
||||
}
|
||||
});
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
const Backbone = require('backbone');
|
||||
const _ = require('underscore');
|
||||
const OptionModel = require('./option-model');
|
||||
const PasswordOptionModel = require('./password-option-model');
|
||||
|
||||
/**
|
||||
* type property should match the value given from the API.
|
||||
*/
|
||||
const ALL_OPTIONS = [{
|
||||
privacy: 'PUBLIC',
|
||||
illustrationType: 'positive',
|
||||
iconFontType: 'unlock',
|
||||
title: 'Public',
|
||||
desc: '任何人都可以查找和查看.',
|
||||
alwaysEnable: true
|
||||
}, {
|
||||
privacy: 'LINK',
|
||||
illustrationType: 'alert',
|
||||
iconFontType: 'unlock',
|
||||
title: 'Public - With Link',
|
||||
desc: '知道链接的任何人都可以查看,不需要密码.'
|
||||
}, {
|
||||
privacy: 'PASSWORD',
|
||||
illustrationType: 'alert',
|
||||
iconFontType: 'unlockWithEllipsis',
|
||||
title: 'Public - With Password',
|
||||
desc: '拥有密码的任何人都可以查看.'
|
||||
}, {
|
||||
privacy: 'PRIVATE',
|
||||
illustrationType: 'negative',
|
||||
iconFontType: 'lock',
|
||||
title: 'Private',
|
||||
desc: '只有您可以访问.'
|
||||
}];
|
||||
|
||||
/**
|
||||
* Collection that holds the different privacy options.
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: function (attrs, options) {
|
||||
if (attrs.privacy === 'PASSWORD') {
|
||||
return new PasswordOptionModel(attrs, options);
|
||||
} else {
|
||||
return new OptionModel(attrs, options);
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.bind('change:selected', this._deselectLastSelected, this);
|
||||
},
|
||||
|
||||
selectedOption: function () {
|
||||
return this.find(option => option.get('selected'));
|
||||
},
|
||||
|
||||
passwordOption: function () {
|
||||
return this.find(option => option.get('privacy') === 'PASSWORD');
|
||||
},
|
||||
|
||||
_deselectLastSelected: function (m, isSelected) {
|
||||
if (isSelected) {
|
||||
this.each(function (option) {
|
||||
if (option !== m) {
|
||||
option.set({selected: false}, {silent: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}, { // Class properties:
|
||||
|
||||
/**
|
||||
* Get a privacy options collection from a Vis model
|
||||
*
|
||||
* Note that since the user's permissions should change very seldom, it's reasonable to assume they will be static for
|
||||
* the collection's lifecycle, so set them on the models attrs when creating the collection.
|
||||
* collection is created.
|
||||
*
|
||||
* @param vis {Object} instance of cdb.admin.Visualization
|
||||
* @param user {Object} instance of cdb.admin.User
|
||||
* @returns {Object} instance of this collection
|
||||
*/
|
||||
byVisAndUser: function (vis, user) {
|
||||
const canSelectPremiumOptions = user.get('actions')[ vis.isVisualization() ? 'private_maps' : 'private_tables' ];
|
||||
const currentPrivacy = vis.get('privacy');
|
||||
const availableOptions = vis.privacyOptions();
|
||||
|
||||
return new this(
|
||||
_.chain(ALL_OPTIONS)
|
||||
.filter(function (option) {
|
||||
return _.contains(availableOptions, option.privacy);
|
||||
})
|
||||
.map(function (option) {
|
||||
// Set state that depends on vis and user attrs, they should not vary during the lifecycle of this collection
|
||||
return _.defaults({
|
||||
selected: option.privacy === currentPrivacy,
|
||||
disabled: !(option.alwaysEnable || canSelectPremiumOptions)
|
||||
}, option);
|
||||
})
|
||||
.value()
|
||||
);
|
||||
}
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
const _ = require('underscore');
|
||||
const OptionModel = require('./option-model');
|
||||
|
||||
/**
|
||||
* View model for the special privacy option representing a password protected map.
|
||||
* It handles the logic related to the password that needs to be set for the option.
|
||||
*/
|
||||
const PasswordOptionModel = OptionModel.extend({
|
||||
|
||||
initialize: function () {
|
||||
OptionModel.prototype.initialize.apply(this, arguments);
|
||||
|
||||
// Initially a default fake password is set, but if option is selected (like switching option) it's reset
|
||||
this.set('password', PasswordOptionModel.DEFAULT_FAKE_PASSWORD);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override OptionModel.attrsToSave
|
||||
*/
|
||||
_attrsToSave: function () {
|
||||
const attrs = OptionModel.prototype._attrsToSave.call(this);
|
||||
|
||||
if (attrs.password === PasswordOptionModel.DEFAULT_FAKE_PASSWORD) {
|
||||
delete attrs.password;
|
||||
}
|
||||
|
||||
return attrs;
|
||||
},
|
||||
|
||||
canSave: function () {
|
||||
return OptionModel.prototype.canSave.call(this) && !_.isEmpty(this.get('password'));
|
||||
}
|
||||
}, {
|
||||
DEFAULT_FAKE_PASSWORD: '!@#!@#'
|
||||
});
|
||||
|
||||
module.exports = PasswordOptionModel;
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const pluralizeStr = require('dashboard/helpers/pluralize');
|
||||
const template = require('./start-view.tpl');
|
||||
|
||||
const DISABLED_SAVE_CLASS_NAME = 'is-disabled';
|
||||
const SHARED_ENTITIES_SAMPLE_SIZE = 5;
|
||||
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'privacyOptions',
|
||||
'userModel',
|
||||
'visModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* View represent the start screen when opening the privacy dialog.
|
||||
* Display privacy options and possibly a upgrade or share banner depending on user privileges.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click .js-option': '_onClickOption',
|
||||
'click .js-share': '_onClickShare',
|
||||
'keyup .js-password-input': '_onKeyUpPasswordInput'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
// Password might not be available (i.e. for changing privacy of a dataset)
|
||||
const pwdOption = this._privacyOptions.passwordOption();
|
||||
const password = pwdOption ? pwdOption.get('password') : '';
|
||||
const selectedOption = this._privacyOptions.selectedOption();
|
||||
const upgradeUrl = this._configModel.get('upgrade_url');
|
||||
const sharedEntities = this._visModel.permission.getUsersWithAnyPermission();
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
vis: this._visModel,
|
||||
privacyOptions: this._privacyOptions,
|
||||
password,
|
||||
saveBtnClassNames: selectedOption.canSave() ? '' : DISABLED_SAVE_CLASS_NAME,
|
||||
showUpgradeBanner: upgradeUrl && this._privacyOptions.any(o => !!o.get('disabled')),
|
||||
upgradeUrl,
|
||||
showTrial: this._userModel.canStartTrial(),
|
||||
showShareBanner: this._userModel.organization,
|
||||
sharedEntitiesCount: sharedEntities.length,
|
||||
personOrPeopleStr: pluralizeStr('person', 'people', sharedEntities.length),
|
||||
sharedEntitiesSampleCount: SHARED_ENTITIES_SAMPLE_SIZE,
|
||||
sharedEntitiesSample: _.take(sharedEntities, SHARED_ENTITIES_SAMPLE_SIZE),
|
||||
sharedWithOrganization: this._visModel.permission.isSharedWithOrganization()
|
||||
})
|
||||
);
|
||||
|
||||
this.delegateEvents();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._privacyOptions, 'change:selected change:disabled', this.render);
|
||||
this.listenTo(this._privacyOptions, 'change:password', this._onChangePassword);
|
||||
},
|
||||
|
||||
_onClickOption: function (event) {
|
||||
const index = $(event.target).closest('.js-option').data('index');
|
||||
const option = this._privacyOptions.at(index);
|
||||
|
||||
if (!option.get('disabled')) {
|
||||
option.set('selected', true);
|
||||
}
|
||||
|
||||
const pwdOption = this._privacyOptions.passwordOption();
|
||||
|
||||
if ((option === pwdOption) && (!option.get('disabled'))) {
|
||||
this.$('.js-password-input')
|
||||
.val('') // reset any existing input value
|
||||
.focus()
|
||||
.keyup(); // manually trigger a key up event to change password state
|
||||
} else if (pwdOption) { // Password might not be available (i.e. for changing privacy of a dataset)
|
||||
this.$('.js-password-input').val(pwdOption.get('password'));
|
||||
}
|
||||
},
|
||||
|
||||
_onChangePassword: function () {
|
||||
this.$('.ok').toggleClass(DISABLED_SAVE_CLASS_NAME, !this._privacyOptions.selectedOption().canSave());
|
||||
},
|
||||
|
||||
_onKeyUpPasswordInput: function (event) {
|
||||
this._privacyOptions.passwordOption().set('password', event.target.value);
|
||||
},
|
||||
|
||||
_onClickShare: function (event) {
|
||||
this.killEvent(event);
|
||||
this.trigger('clickedShare');
|
||||
}
|
||||
});
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
<div class="CDB-Text Dialog-header u-inner">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
|
||||
<i class="CDB-IconFont CDB-IconFont-unlock"></i>
|
||||
</div>
|
||||
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-bSpace--m u-tSpace-xl"><%- vis.get('name') %> 隐私</h4>
|
||||
<p class="CDB-Text CDB-Size-medium u-altTextColor">
|
||||
尽管我们相信开放数据的力量,但您也可以保护 <%- vis.isVisualization() ? '地图' : '数据集' %>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="CDB-Text Dialog-body u-inner OptionCards">
|
||||
<% privacyOptions.each(function(m, index) { %>
|
||||
<div class="OptionCard OptionCard--blocky <%- m.classNames() %> js-option" data-index="<%- index %>">
|
||||
<div class="OptionCard-icon IllustrationIcon IllustrationIcon--<%- m.get('illustrationType') %>">
|
||||
<i class="CDB-IconFont CDB-IconFont-<%- m.get('iconFontType') %>"></i>
|
||||
</div>
|
||||
<h5 class="OptionCard-title OptionCard-title CDB-Text CDB-Size-large"><%- m.get('title') %></h5>
|
||||
<% if (m.get('privacy') == 'PASSWORD') { %>
|
||||
<% if (m.get('disabled')) { %>
|
||||
<input class="js-password-input Input CDB-Text CDB-Size-medium ChangePrivacy-passwordInput u-altTextColor" placeholder="Type your password here" value="<%- password %>" type="password" disabled/>
|
||||
<% } else { %>
|
||||
<input class="js-password-input Input CDB-Text CDB-Size-medium ChangePrivacy-passwordInput u-altTextColor" placeholder="Type your password here" value="<%- password %>" type="password" />
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<div class="OptionCard-desc CDB-Text CDB-Size-medium u-altTextColor"><%- m.get('desc') %></div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<% if (showUpgradeBanner) { %>
|
||||
<div class="CDB-Text Dialog-body u-inner ChangePrivacy-upgradeBanner">
|
||||
<div class="UpgradeElement ChangePrivacy-upgradeBannerInner">
|
||||
<div class="UpgradeElement-info">
|
||||
<p class="UpgradeElement-infoText u-ellipsLongText">To get advantage of all the privacy options you should upgrade your plan</p>
|
||||
</div>
|
||||
<div class="UpgradeElement-actions">
|
||||
<% if (showTrial) { %>
|
||||
<div class="UpgradeElement-trial">
|
||||
<i class="CDB-IconFont CDB-IconFont-gift UpgradeElement-trialIcon"></i>
|
||||
<p class="UpgradeElement-trialText u-ellipsLongText">14 days Free trial</p>
|
||||
</div>
|
||||
<% } %>
|
||||
<a href="<%- upgradeUrl %>" class="Button Button--secondary UpgradeElement-button ChangePrivacy-upgradeActionButton">
|
||||
<span>upgrade</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (showShareBanner) { %>
|
||||
<% if (sharedEntitiesCount > 0) { %>
|
||||
<div class="CDB-Text Dialog-body u-inner ChangePrivacy-shareBanner Dialog-affectedEntities">
|
||||
<div class="Dialog-affectedEntities">
|
||||
<div class="LayoutIcon ChangePrivacy-shareBannerIcon">
|
||||
<i class="CDB-IconFont CDB-IconFont-people CDB-IconFont--super"></i>
|
||||
<span class="Badge Dialog-headerIconBadge"><%- sharedEntitiesCount %></span>
|
||||
</div>
|
||||
<div class="DefaultParagraph DefaultParagraph--secondary">
|
||||
<% if (sharedWithOrganization) { %>
|
||||
Shared with your whole organization.
|
||||
<% } else { %>
|
||||
Shared with <%- sharedEntitiesCount %> <%- personOrPeopleStr %>.
|
||||
<% } %>
|
||||
<a href="#" class="js-share">Open sharing settings</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="u-flex">
|
||||
<% sharedEntitiesSample.forEach(function(user) { %>
|
||||
<span class="UserAvatar Dialog-sharedEntitiesAvatar u-lSpace--xl">
|
||||
<% if (user.get('avatar_url')) { %>
|
||||
<img class="UserAvatar-img UserAvatar-img--medium" src="<%- user.get('avatar_url') %>" alt="<%- user.get('name') || user.get('username') %>" title="<%- user.get('name') || user.get('username') %>" />
|
||||
<% } else { %>
|
||||
<div class="UserAvatar-img UserAvatar-img--medium UserAvatar-img--no-src" title="<%- user.get('name') || user.get('username') %>"></div>
|
||||
<% } %>
|
||||
</span>
|
||||
<% }); %>
|
||||
<% if (sharedEntitiesCount > sharedEntitiesSampleCount) { %>
|
||||
<div class="UserAvatar Dialog-sharedEntitiesAvatar">
|
||||
<span class="UserAvatar-img UserAvatar-img--medium UserAvatar--moreItems" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="Dialog-body u-inner ChangePrivacy-shareBanner">
|
||||
<div class="LayoutIcon ChangePrivacy-shareBannerIcon">
|
||||
<i class="CDB-IconFont CDB-IconFont-people CDB-IconFont--super"></i>
|
||||
</div>
|
||||
<div class="DefaultParagraph DefaultParagraph--secondary CDB-Text CDB-Size-medium">Team work is always better. <a href="#" class="js-share">Share it with your colleagues</a></div>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<div class="CDB-Text Dialog-footer Dialog-footer--simple u-inner ChangePrivacy-startFooter">
|
||||
<button class="CDB-Button CDB-Button--secondary cancel">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">取消</span>
|
||||
</button>
|
||||
<button class="ok u-lSpace--xl CDB-Button CDB-Button--primary <%- saveBtnClassNames %>">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">保存</span>
|
||||
</button>
|
||||
</div>
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./dialog-view.tpl');
|
||||
const FooterView = require('./footer/create-footer-view');
|
||||
const NavigationView = require('builder/components/modals/add-layer/content/navigation-view');
|
||||
const ListingView = require('builder/components/modals/add-layer/content/listing-view');
|
||||
const TabPaneView = require('builder/components/tab-pane/tab-pane-view');
|
||||
const TabPaneCollection = require('builder/components/tab-pane/tab-pane-collection');
|
||||
const ViewFactory = require('builder/components/view-factory');
|
||||
const renderLoading = require('builder/components/loading/render-loading');
|
||||
const ErrorDetailsView = require('builder/components/background-importer/error-details-view');
|
||||
const CreateMapModel = require('dashboard/views/dashboard/create-map-model');
|
||||
const CreateDatasetModel = require('dashboard/views/dashboard/create-dataset-model');
|
||||
const VisualizationModel = require('dashboard/data/visualization-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const DEFAULT_VIS_NAME = 'Untitled map';
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'modalModel',
|
||||
'createModel',
|
||||
'configModel',
|
||||
'userModel',
|
||||
'pollingModel',
|
||||
'routerModel',
|
||||
'modalModel',
|
||||
'mamufasView'
|
||||
];
|
||||
|
||||
/**
|
||||
* Create map/dataset dialog, typically used from editor
|
||||
*/
|
||||
const CreateDialogView = CoreView.extend({
|
||||
className: 'Dialog-content Dialog-content--expanded',
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
this._initModels();
|
||||
this._initBinds();
|
||||
|
||||
// Stop pollings and prevent import modal to appear
|
||||
this._mamufasView.disable();
|
||||
this._pollingModel.stopPollings();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.html(template());
|
||||
|
||||
this._initViews();
|
||||
this._createModel.viewsReady();
|
||||
},
|
||||
|
||||
_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, 'change:contentPane', this._onChangeContentView);
|
||||
this.listenTo(this._createModel, 'destroyModal', () => this._modalModel.destroy());
|
||||
this.listenTo(this._pollingModel, 'importByUploadData', this._modalModel.destroy.bind(this._modalModel));
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
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: () => {
|
||||
return new ListingView({
|
||||
createModel: this._createModel,
|
||||
configModel: this._configModel,
|
||||
userModel: this._userModel,
|
||||
privacyModel: this._privacyModel,
|
||||
guessingModel: this._guessingModel
|
||||
});
|
||||
}
|
||||
}, {
|
||||
name: 'creatingFromScratch',
|
||||
selected: this._createModel.get('contentPane') === 'creatingFromScratch',
|
||||
createContentView: () => {
|
||||
return ViewFactory.createByHTML(
|
||||
renderLoading({
|
||||
title: _t('components.modals.add-layer.create-loading-title')
|
||||
})
|
||||
);
|
||||
}
|
||||
}, {
|
||||
name: 'loading',
|
||||
selected: this._createModel.get('contentPane') === 'loading',
|
||||
createContentView: () => {
|
||||
return ViewFactory.createByHTML(
|
||||
renderLoading({
|
||||
title: _t('components.modals.create-dialog.creating-map')
|
||||
})
|
||||
);
|
||||
}
|
||||
}, {
|
||||
name: 'importFailed',
|
||||
selected: this._createModel.get('contentPane') === 'importFailed',
|
||||
createContentView: () => {
|
||||
const currentImport = this._createModel.get('currentImport');
|
||||
this._createModel.set('currentImport', null);
|
||||
|
||||
return new ErrorDetailsView({
|
||||
error: currentImport && currentImport.getError(),
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
}
|
||||
}, {
|
||||
name: 'datasetQuotaExceeded',
|
||||
selected: this._createModel.get('contentPane') === 'datasetQuotaExceeded',
|
||||
createContentView: () => {
|
||||
return new ErrorDetailsView({
|
||||
error: { errorCode: 8002 },
|
||||
userModel: this._userModel,
|
||||
configModel: this._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: this._privacyModel,
|
||||
guessingModel: this._guessingModel
|
||||
});
|
||||
|
||||
this._footerView.on('destroyModal', () => this._modalModel.destroy());
|
||||
|
||||
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' || paneModelName === 'creatingFromScratch' || paneModelName === 'importFailed') {
|
||||
var hiddenStyle = {
|
||||
visibility: 'hidden',
|
||||
opacity: '0'
|
||||
};
|
||||
this._footerView.$el.css(hiddenStyle);
|
||||
this._navigationView.hide();
|
||||
}
|
||||
|
||||
if (paneModelName !== 'listing') {
|
||||
this._navigationView.hide();
|
||||
}
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._mamufasView.enable();
|
||||
this._pollingModel.startPollings();
|
||||
|
||||
CoreView.prototype.clean.apply(this, arguments);
|
||||
}
|
||||
}, {
|
||||
setViewProperties: function (opts) {
|
||||
const mapModel = new CreateMapModel({}, {
|
||||
userModel: opts.userModel,
|
||||
configModel: opts.configModel,
|
||||
backgroundPollingModel: opts.pollingModel,
|
||||
backgroundPollingView: opts.pollingView
|
||||
});
|
||||
|
||||
_.extend(this, {
|
||||
configModel: opts.configModel,
|
||||
userModel: opts.userModel,
|
||||
pollingModel: opts.pollingModel,
|
||||
pollingView: opts.pollingView,
|
||||
routerModel: opts.routerModel,
|
||||
mapModel
|
||||
});
|
||||
},
|
||||
|
||||
addProperties: function (properties) {
|
||||
_.extend(this, properties);
|
||||
},
|
||||
|
||||
openDialog: function (dialogDependencies, dialogOpts) {
|
||||
let createModel;
|
||||
|
||||
if (dialogOpts.type === 'dataset') {
|
||||
createModel = new CreateDatasetModel({}, {
|
||||
userModel: this.userModel,
|
||||
configModel: this.configModel,
|
||||
backgroundPollingView: this.pollingView
|
||||
});
|
||||
} else {
|
||||
this.mapModel.set({
|
||||
listing: 'datasets',
|
||||
collectionFetched: false
|
||||
});
|
||||
this.mapModel.setSelected(dialogOpts.selectedItems);
|
||||
createModel = this.mapModel;
|
||||
}
|
||||
|
||||
const dialogView = new CreateDialogView({
|
||||
modalModel: dialogDependencies.modalModel,
|
||||
configModel: this.configModel,
|
||||
createModel,
|
||||
userModel: this.userModel,
|
||||
pollingModel: this.pollingModel,
|
||||
routerModel: this.routerModel,
|
||||
mamufasView: this.mamufasView,
|
||||
el: dialogOpts.viewElement
|
||||
});
|
||||
|
||||
createModel.bind('datasetError', function (resp) {
|
||||
if (resp.responseText.indexOf('You have reached your table quota') !== -1) {
|
||||
createModel.set('contentPane', 'datasetQuotaExceeded');
|
||||
}
|
||||
});
|
||||
|
||||
createModel.bind('datasetCreated', tableMetadata => {
|
||||
let vis;
|
||||
|
||||
if (this.routerModel.model.isDatasets()) {
|
||||
vis = new VisualizationModel({ type: 'table' }, { configModel: this.configModel });
|
||||
vis.permission.owner = this.userModel;
|
||||
vis.set('table', tableMetadata.toJSON());
|
||||
window.location = vis.viewUrl(this.userModel).edit();
|
||||
} else {
|
||||
vis = new VisualizationModel({ name: DEFAULT_VIS_NAME }, { configModel: this.configModel });
|
||||
vis.permission.owner = this.userModel;
|
||||
vis.save({
|
||||
tables: [ tableMetadata.get('id') ]
|
||||
}, {
|
||||
success: m => {
|
||||
window.location = vis.viewUrl(this.userModel).edit();
|
||||
},
|
||||
error: function (e) {
|
||||
dialogDependencies.modalModel.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return dialogView;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = CreateDialogView;
|
||||
+10
@@ -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">新建数据集</h2>
|
||||
<h3 class="CDB-Text CDB-Size-medium u-altTextColor">选择合适的数据集或新建</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>
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
const Backbone = require('backbone');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const GuessingTogglerView = require('builder/components/modals/add-layer/footer/guessing-toggler-view');
|
||||
const PrivacyTogglerView = require('builder/components/modals/add-layer/footer/privacy-toggler-view');
|
||||
const template = require('./dialog-footer.tpl');
|
||||
const GAPusher = require('dashboard/common/analytics-pusher');
|
||||
|
||||
/**
|
||||
* Create footer view
|
||||
*
|
||||
* It will show possible choices depending the
|
||||
* selected option and the state of the main model
|
||||
*
|
||||
*/
|
||||
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'userModel',
|
||||
'createModel'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click .js-templates': '_goToTemplates',
|
||||
'click .js-create_map': '_createMap',
|
||||
'click .js-connect': '_connectDataset'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this._guessingModel = new Backbone.Model({ guessing: true });
|
||||
this._privacyModel = new Backbone.Model({
|
||||
privacy: this._userModel.canCreatePrivateDatasets() ? 'PRIVATE' : 'PUBLIC'
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
const userCanUpgrade = window.upgrade_url && !this._configModel.get('cartodb_com_hosted') && (!this._userModel.isInsideOrg() || this._userModel.isOrgOwner());
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
isMapType: this._createModel.isMapType(),
|
||||
option: this._createModel.getOption(),
|
||||
listingState: this._createModel.get('listing'),
|
||||
isLibrary: this._createModel.visFetchModel.get('library'),
|
||||
importState: this._createModel.getImportState(),
|
||||
isUploadValid: this._createModel.upload.isValidToUpload(),
|
||||
selectedDatasetsCount: this._createModel.selectedDatasets.length,
|
||||
maxSelectedDatasets: this._userModel.getMaxLayers(),
|
||||
mapTemplate: this._createModel.get('mapTemplate'),
|
||||
userCanUpgrade: userCanUpgrade,
|
||||
upgradeUrl: window.upgrade_url,
|
||||
currentUrl: window.location.href
|
||||
})
|
||||
);
|
||||
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._createModel, 'change:upload', this.render);
|
||||
this.listenTo(this._createModel, 'change:option', this.render);
|
||||
this.listenTo(this._createModel, 'change:listing', this.render);
|
||||
this.listenTo(this._createModel.selectedDatasets, 'all', this.render);
|
||||
this.listenTo(this._createModel.visFetchModel, 'change:library', this.render);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
this.guessingTogglerView = new GuessingTogglerView({
|
||||
guessingModel: this._guessingModel,
|
||||
createModel: this._createModel,
|
||||
configModel: this._configModel,
|
||||
privacyModel: this._privacyModel,
|
||||
userModel: this._userModel
|
||||
});
|
||||
this.$('.js-footer-info').append(this.guessingTogglerView.render().el);
|
||||
this.addView(this.guessingTogglerView);
|
||||
|
||||
this.privacyTogglerView = new PrivacyTogglerView({
|
||||
privacyModel: this._privacyModel,
|
||||
userModel: this._userModel,
|
||||
createModel: this._createModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
this.$('.js-footerActions').prepend(this.privacyTogglerView.render().el);
|
||||
this.addView(this.privacyTogglerView);
|
||||
},
|
||||
|
||||
_connectDataset: function () {
|
||||
if (this._createModel.upload.isValidToUpload()) {
|
||||
// Setting privacy for new import if toggler is enabled
|
||||
if (this._createModel.showPrivacyToggler()) {
|
||||
this._createModel.upload.set('privacy', this._privacyModel.get('privacy'));
|
||||
}
|
||||
// Set proper guessing values before starting the upload
|
||||
this._createModel.upload.setGuessing(this._guessingModel.get('guessing'));
|
||||
this._createModel.startUpload();
|
||||
this.trigger('destroyModal');
|
||||
}
|
||||
},
|
||||
|
||||
_goToTemplates: function (e) {
|
||||
if (e) e.preventDefault();
|
||||
this._createModel.set('option', 'templates');
|
||||
},
|
||||
|
||||
_createMap: function () {
|
||||
GAPusher({
|
||||
eventName: 'send',
|
||||
hitType: 'event',
|
||||
eventCategory: 'Create Map',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Add dataset modal'
|
||||
});
|
||||
|
||||
const selectedDatasets = this._createModel.getSelectedDatasetsCollection();
|
||||
if (selectedDatasets.length > 0 && selectedDatasets.length <= this._userModel.getMaxLayers()) {
|
||||
this._createModel.createMap();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<% if (option !== "loading") { %>
|
||||
<div class="CreateDialog-footerShadow"></div>
|
||||
<div class="CreateDialog-footerLine"></div>
|
||||
|
||||
<div class="CreateDialog-footerInner u-flex u-alignCenter u-justifySpace">
|
||||
<% if (option === 'listing') { %>
|
||||
|
||||
<% if (listingState === "datasets") { %>
|
||||
<% if (isLibrary && !isMapType) { %>
|
||||
<div class="CDB-Text CDB-Size-medium u-altTextColor u-flex u-alignCenter">
|
||||
<i class="CDB-IconFont CDB-IconFont-info CreateDialog-footerInfoIcon HighlightIcon HighlightIcon--warning"></i> Once you click over one of these items it will be imported to your account.
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="CDB-Text CDB-Size-medium u-altTextColor u-flex u-alignCenter">
|
||||
<% if (selectedDatasetsCount < maxSelectedDatasets) { %>
|
||||
<%- selectedDatasetsCount %> dataset<%- selectedDatasetsCount !== 1 ? 's' : '' %> 选择
|
||||
<% } else { %>
|
||||
You have reached the max layers for a new map (<%- maxSelectedDatasets %> max)
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="CreateDialog-footerActions">
|
||||
<% if (selectedDatasetsCount === maxSelectedDatasets && userCanUpgrade) { %>
|
||||
<a class="Button Button--main CreateDialog-footerActionsButton is-separated js-upgrade" href="<%- upgradeUrl %>"><span>升级</span></a>
|
||||
<% } %>
|
||||
<button class="CDB-Button CDB-Button--primary CreateDialog-footerActionsButton js-create_map track-onboarding--createMap <%- selectedDatasetsCount === 0 ? 'is-disabled' : '' %>">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">创建地图</span>
|
||||
</button>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<% if (listingState === "import") { %>
|
||||
<% if (importState === 'scratch') { %>
|
||||
<% if (isMapType) { %>
|
||||
<div class="CreateDialog-footerInfo">
|
||||
<i class="CDB-IconFont CDB-IconFont-info CreateDialog-footerInfoIcon HighlightIcon HighlightIcon--warning"></i>New on CARTO? Start with one of <a href="#/templates" class="js-templates">our templates</a>.
|
||||
</div>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<div class="js-footer-info CreateDialog-footerInfo"></div>
|
||||
<div class="CreateDialog-footerActions js-footerActions">
|
||||
<button class="CDB-Button CDB-Button--primary CreateDialog-footerActionsButton <% if (!isUploadValid) { %>is-disabled<% } %> js-connect">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">连接数据集</span>
|
||||
</button>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const batchProcessItems = require('dashboard/helpers/batch-process-items');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'contentType'
|
||||
];
|
||||
|
||||
/**
|
||||
* View model for delete items view.
|
||||
* Manages the states changes for the delete items view.
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
initialize: function (models, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
state: function () {
|
||||
return this._state;
|
||||
},
|
||||
|
||||
errorMessage: function () {
|
||||
return this._errorMessage;
|
||||
},
|
||||
|
||||
setState: function (newState) {
|
||||
this._state = newState;
|
||||
this.trigger('change');
|
||||
this.trigger(newState);
|
||||
},
|
||||
|
||||
isDeletingDatasets: function () {
|
||||
return this._contentType === 'datasets';
|
||||
},
|
||||
|
||||
loadPrerequisites: function () {
|
||||
const setStateToConfirmDeletion = this.setState.bind(this, 'ConfirmDeletion');
|
||||
|
||||
if (this.isDeletingDatasets()) {
|
||||
this.setState('LoadingPrerequisites');
|
||||
|
||||
batchProcessItems({
|
||||
howManyInParallel: 5,
|
||||
items: this.toArray(),
|
||||
processItem: this._loadPrerequisitesForModel,
|
||||
done: setStateToConfirmDeletion,
|
||||
fail: this.setState.bind(this, 'LoadPrerequisitesFail')
|
||||
});
|
||||
} else {
|
||||
setStateToConfirmDeletion();
|
||||
}
|
||||
},
|
||||
|
||||
affectedEntities: function () {
|
||||
return this.chain()
|
||||
.map(function (m) {
|
||||
return m.sharedWithEntities();
|
||||
})
|
||||
.flatten().compact().value();
|
||||
},
|
||||
|
||||
affectedVisData: function () {
|
||||
const visData = this.chain()
|
||||
.map(function (m) {
|
||||
const metadata = m.tableMetadata();
|
||||
|
||||
return []
|
||||
.concat(metadata.get('dependent_visualizations'))
|
||||
.concat(metadata.get('non_dependent_visualizations'));
|
||||
})
|
||||
.flatten().compact().value();
|
||||
|
||||
return _.uniq(visData, function (metadata) {
|
||||
return metadata.id;
|
||||
});
|
||||
},
|
||||
|
||||
deleteItems: function () {
|
||||
this.setState('DeletingItems');
|
||||
|
||||
// INFO: Don't put more than 1 delete in parallel because this lead to a
|
||||
// race condition in the derived map deletion (if any)
|
||||
batchProcessItems({
|
||||
howManyInParallel: 1,
|
||||
items: this.toArray(),
|
||||
processItem: this._deleteItem,
|
||||
done: this.setState.bind(this, 'DeleteItemsDone'),
|
||||
fail: this._deletionFailed.bind(this)
|
||||
});
|
||||
},
|
||||
|
||||
_deletionFailed: function (error) {
|
||||
this._errorMessage = error;
|
||||
this.setState('DeleteItemsFail');
|
||||
},
|
||||
|
||||
_loadPrerequisitesForModel: function (m, callback) {
|
||||
const metadata = m.tableMetadata();
|
||||
|
||||
// TODO: extract to be included in fetch call instead? modifying global state is not very nice
|
||||
metadata.no_data_fetch = true;
|
||||
|
||||
metadata.fetch({
|
||||
wait: true, // TODO: from old code (delete_dialog), why is it necessary?
|
||||
success: function () {
|
||||
callback();
|
||||
},
|
||||
error: function (model, jqXHR) {
|
||||
callback(jqXHR.responseText);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_deleteItem: function (item, callback) {
|
||||
item.destroy({ wait: true })
|
||||
.done(callback)
|
||||
.fail(function (response) {
|
||||
let errorMessage;
|
||||
|
||||
try {
|
||||
errorMessage = JSON.parse(response.responseText).errors.join('; ');
|
||||
} catch (e) {
|
||||
errorMessage = 'something failed';
|
||||
}
|
||||
|
||||
callback(errorMessage);
|
||||
});
|
||||
}
|
||||
});
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
const $ = require('jquery');
|
||||
const moment = require('moment');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const pluralizeString = require('dashboard/helpers/pluralize');
|
||||
const loadingView = require('builder/components/loading/render-loading');
|
||||
const failTemplate = require('dashboard/components/fail.tpl');
|
||||
const template = require('./delete-items.tpl');
|
||||
const VisualizationModel = require('dashboard/data/visualization-model');
|
||||
const MapCardPreview = require('dashboard/components/mapcard-preview-view');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const AFFECTED_ENTITIES_SAMPLE_COUNT = 3;
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'viewModel',
|
||||
'userModel',
|
||||
'configModel',
|
||||
'modalModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Delete items dialog
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'click .ok': 'ok',
|
||||
'click .cancel': 'close'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this._viewModel.loadPrerequisites();
|
||||
this.listenTo(this._viewModel, 'change', function () {
|
||||
if (this._viewModel.state() === 'DeleteItemsDone') {
|
||||
this.close();
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
this.add_related_model(this._viewModel);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.render_content());
|
||||
this._loadMapPreviews();
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @implements cdb.ui.common.Dialog.prototype.render_content
|
||||
*/
|
||||
render_content: function () {
|
||||
return this['_render' + this._viewModel.state()]();
|
||||
},
|
||||
|
||||
_renderLoadingPrerequisites: function () {
|
||||
return loadingView({
|
||||
title: `Checking what consequences deleting the selected ${this._pluralizedContentType()} would have...`
|
||||
});
|
||||
},
|
||||
|
||||
_renderLoadPrerequisitesFail: function () {
|
||||
return failTemplate({
|
||||
msg: 'Failed to check consequences of deleting the selected ' + this._pluralizedContentType()
|
||||
});
|
||||
},
|
||||
|
||||
_renderConfirmDeletion: function () {
|
||||
// An entity can be an User or Organization
|
||||
const affectedEntities = this._viewModel.affectedEntities();
|
||||
const affectedVisData = this._viewModel.affectedVisData();
|
||||
|
||||
return template({
|
||||
firstItemName: this._getFirstItemName(),
|
||||
selectedCount: this._viewModel.length,
|
||||
isDatasets: this._viewModel.isDeletingDatasets(),
|
||||
pluralizedContentType: this._pluralizedContentType(),
|
||||
affectedEntitiesCount: affectedEntities.length,
|
||||
affectedEntitiesSample: affectedEntities.slice(0, AFFECTED_ENTITIES_SAMPLE_COUNT),
|
||||
affectedEntitiesSampleCount: AFFECTED_ENTITIES_SAMPLE_COUNT,
|
||||
affectedVisCount: affectedVisData.length,
|
||||
pluralizedMaps: pluralizeString('map', affectedVisData.length),
|
||||
affectedVisVisibleCount: affectedVisData.length,
|
||||
visibleAffectedVis: this._prepareVisibleAffectedVisForTemplate(affectedVisData)
|
||||
});
|
||||
},
|
||||
|
||||
_prepareVisibleAffectedVisForTemplate: function (visibleAffectedVisData) {
|
||||
return visibleAffectedVisData.map(function (visData) {
|
||||
const vis = new VisualizationModel(visData, { configModel: this._configModel });
|
||||
const owner = vis.permission.owner;
|
||||
|
||||
return {
|
||||
visId: vis.get('id'),
|
||||
name: vis.get('name'),
|
||||
url: vis.viewUrl(this._userModel).edit(),
|
||||
owner: owner,
|
||||
ownerName: owner.get('username'),
|
||||
isOwner: vis.permission.isOwner(this._userModel),
|
||||
showPermissionIndicator: !vis.permission.hasWriteAccess(this._userModel),
|
||||
timeDiff: moment(vis.get('updated_at')).fromNow(),
|
||||
authTokens: vis.get('auth_tokens').join(';')
|
||||
};
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* @overrides BaseDialog.prototype.ok
|
||||
*/
|
||||
ok: function () {
|
||||
this._viewModel.deleteItems();
|
||||
this.render();
|
||||
},
|
||||
|
||||
close: function () {
|
||||
this._modalModel.destroy();
|
||||
},
|
||||
|
||||
_loadMapPreviews: function () {
|
||||
const currentView = this;
|
||||
|
||||
this.$el.find('.MapCard').each(function () {
|
||||
var username = $(this).data('visOwnerName');
|
||||
var mapCardPreview = new MapCardPreview({
|
||||
config: currentView._configModel,
|
||||
el: $(this).find('.js-header'),
|
||||
width: 298,
|
||||
height: 130,
|
||||
mapsApiResource: currentView._configModel.getMapsResourceName(username),
|
||||
visId: $(this).data('visId'),
|
||||
username: username,
|
||||
authTokens: $(this).data('visAuthTokens').split(';')
|
||||
}).load();
|
||||
|
||||
currentView.addView(mapCardPreview);
|
||||
});
|
||||
},
|
||||
|
||||
_renderDeletingItems: function () {
|
||||
return loadingView({
|
||||
title: `Deleting the selected ${this._pluralizedContentType()}...`
|
||||
});
|
||||
},
|
||||
|
||||
_renderDeleteItemsFail: function () {
|
||||
let message = this._viewModel.errorMessage().replace(/\n/g, '<br>');
|
||||
|
||||
if (message === 'something failed') {
|
||||
message = '';
|
||||
}
|
||||
return failTemplate({
|
||||
msg: `Failed to delete the selected ${this._pluralizedContentType()}. ${message}`
|
||||
});
|
||||
},
|
||||
|
||||
_pluralizedContentType: function () {
|
||||
return pluralizeString(
|
||||
this._viewModel.isDeletingDatasets() ? 'dataset' : 'map',
|
||||
this._viewModel.length
|
||||
);
|
||||
},
|
||||
|
||||
_getFirstItemName: function () {
|
||||
if (!this.options.viewModel) return;
|
||||
|
||||
var firstItem = this.options.viewModel.at(0);
|
||||
|
||||
if (firstItem) {
|
||||
return firstItem.get('name');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
<div class="CDB-Text Dialog-header u-inner">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--negative">
|
||||
<i class="CDB-IconFont CDB-IconFont-trash"></i>
|
||||
<span class="Badge Badge--negative Dialog-headerIconBadge CDB-Text CDB-Size-small"><%- selectedCount %></span>
|
||||
</div>
|
||||
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m u-tSpace-xl">
|
||||
<% if (selectedCount > 1) { %>
|
||||
你准备删除<%- pluralizedContentType %> <%- selectedCount %>.
|
||||
<% } else { %>
|
||||
你准备删除<%- pluralizedContentType %> <%- firstItemName %>.
|
||||
<% } %>
|
||||
</h4>
|
||||
<p class="CDB-Text CDB-Size-medium u-altTextColor">
|
||||
<% if (affectedVisCount > 0) { %>
|
||||
这样做将意味着 <strong><%- affectedVisCount %> <%- pluralizedMaps %></strong> 发生改变.
|
||||
<% } %>
|
||||
删除 <%- pluralizedContentType %> 将不能被恢复, 请仔细确定.
|
||||
</p>
|
||||
<% if (isDatasets) { %>
|
||||
<p class="CDB-Text CDB-Size-medium u-altTextColor">建议在删除之前将数据导出.</p>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (affectedVisCount > 0) { %>
|
||||
<ul class="Dialog-body MapsList MapsList--centerItems u-pt--0 u-pb--20 u-border-bottom">
|
||||
<% visibleAffectedVis.forEach(function(vis) { %>
|
||||
<li class="MapsList-item">
|
||||
<div class="MapCard" data-vis-id="<%- vis.visId %>" data-vis-owner-name="<%- vis.ownerName %>" data-vis-auth-tokens="<%- vis.authTokens %>">
|
||||
<a href="<%- vis.url %>" target="_blank" class="MapCard-header MapCard-header--compact js-header">
|
||||
<div class="MapCard-loader"></div>
|
||||
</a>
|
||||
<div class="MapCard-content MapCard-content--compact">
|
||||
<div class="MapCard-contentBody">
|
||||
<div class="MapCard-contentBodyRow MapCard-contentBodyRow--flex">
|
||||
<h3 class="CDB-Text CDB-Size-medium u-bSpace u-ellipsis u-actionTextColor">
|
||||
<a href="<%- vis.url %>" target="_blank" title="<%- vis.name %>"><%- vis.name %></a>
|
||||
</h3>
|
||||
<% if (vis.showPermissionIndicator) { %>
|
||||
<span class="CDB-Text PermissionIndicator"></span>
|
||||
<% } %>
|
||||
</div>
|
||||
<p class="MapCard-contentBodyTimeDiff DefaultTimeDiff CDB-Text CDB-Size-small u-altTextColor">
|
||||
<%- vis.timeDiff %>
|
||||
<% if (!vis.isOwner) { %>
|
||||
by <span class="UserAvatar">
|
||||
<img class="UserAvatar-img UserAvatar-img--smaller" src="<%- vis.owner.get('avatar_url') %>" alt="<%- vis.owner.nameOrUsername() %>" title="<%- vis.owner.nameOrUsername() %>" />
|
||||
</span>
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (affectedEntitiesCount > 0) { %>
|
||||
<div class="Dialog-body Dialog-affectedEntities">
|
||||
<p class="DefaultParagraph CDB-Text CDB-Size u-altTextColor">Some users will lose access to your <%- pluralizedContentType %></p>
|
||||
<div class="u-flex">
|
||||
<% affectedEntitiesSample.forEach(function(user) { %>
|
||||
<span class="UserAvatar is-in-list">
|
||||
<% if (user.get('avatar_url')) { %>
|
||||
<img class="UserAvatar-img UserAvatar-img--medium" src="<%- user.get('avatar_url') %>" alt="<%- user.get('name') || user.get('username') %>" title="<%- user.get('name') || user.get('username') %>" />
|
||||
<% } else { %>
|
||||
<div class="UserAvatar-img UserAvatar-img--medium UserAvatar-img--no-src" title="<%- user.get('name') || user.get('username') %>"></div>
|
||||
<% } %>
|
||||
</span>
|
||||
<% }); %>
|
||||
<% if (affectedEntitiesCount > affectedEntitiesSampleCount) { %>
|
||||
<div class="UserAvatar is-in-list">
|
||||
<span class="UserAvatar-img UserAvatar-img--medium UserAvatar--moreItems" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="Dialog-footer Dialog-footer--simple u-inner">
|
||||
<div class="Dialog-footerContent MapsList-footer">
|
||||
<button class="CDB-Button CDB-Button--secondary cancel">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">取消</span>
|
||||
</button>
|
||||
<button class="u-lSpace--xl CDB-Button CDB-Button--error ok">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">确认删除</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
const _ = require('underscore');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const loadingView = require('builder/components/loading/render-loading');
|
||||
const failTemplate = require('dashboard/components/fail.tpl');
|
||||
const ErrorDetailsView = require('builder/components/background-importer/error-details-view');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel',
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Dialog to manage duplication process of a cdb.admin.Visualization object.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
if (!this.model) throw new Error('model is required (cdb.admin.Visualization)');
|
||||
|
||||
this._duplicateMap();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(loadingView({
|
||||
title: 'Duplicating your map'
|
||||
}));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderCatchedError: function (error) {
|
||||
const view = new ErrorDetailsView({
|
||||
error,
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
|
||||
this.$el.html(view.render().el);
|
||||
},
|
||||
|
||||
_renderUnknownError: function () {
|
||||
this.$el.html(failTemplate({
|
||||
msg: "Sorry, something went wrong, but we're not sure why."
|
||||
}));
|
||||
},
|
||||
|
||||
_duplicateMap: function (newName) {
|
||||
const newMapName = this.model.get('name') + ' copy';
|
||||
|
||||
this.model.copy(
|
||||
{ name: newMapName },
|
||||
{
|
||||
success: newVis => {
|
||||
this._redirectTo(
|
||||
newVis.viewUrl(this._userModel).edit().toString()
|
||||
);
|
||||
},
|
||||
error: (req, resp) => {
|
||||
if (resp && resp.responseText.indexOf('over account public map quota') !== -1) {
|
||||
this._renderCatchedError({ errorCode: 8007 });
|
||||
} else {
|
||||
this._showError(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
_showError: function (model) {
|
||||
try {
|
||||
const err = _.clone(model.attributes, model.attributes.get_error_text);
|
||||
this._renderCatchedError(_.extend(err, model.attributes.get_error_text));
|
||||
} catch (err) {
|
||||
this._renderUnknownError();
|
||||
}
|
||||
},
|
||||
|
||||
_redirectTo: function (url) {
|
||||
window.location = url;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="Dialog-stickyFooter">
|
||||
<div class="Dialog-footer ChangePrivacy-shareFooter u-inner">
|
||||
<button class="cancel CDB-Button CDB-Button--secondary">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">cancel</span>
|
||||
</button>
|
||||
<button class="ok CDB-Button CDB-Button--primary">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Save settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="CDB-Text Dialog-header Dialog-header--expanded u-inner">
|
||||
<button class="Dialog-backBtn js-back u-actionTextColor">
|
||||
<i class="CDB-IconFont CDB-IconFont-arrowPrev"></i>
|
||||
</button>
|
||||
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
|
||||
<i class="CDB-IconFont CDB-IconFont-unlock"></i>
|
||||
</div>
|
||||
<p class="Dialog-headerTitle u-ellipsLongText"><%- name %> privacy</p>
|
||||
<p class="Dialog-headerText">Select your colleagues you want to give access in the list below</p>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
const ShareViewContent = require('builder/components/modals/publish/share/share-view');
|
||||
|
||||
const DashboardShareViewContent = ShareViewContent.extend({
|
||||
_onSave: function () {},
|
||||
|
||||
saveACLPermissions: function () {
|
||||
var permission = this._visDefinitionModel.getPermissionModel();
|
||||
permission.overwriteAcl(this._sharePermissionModel);
|
||||
|
||||
return permission.save()
|
||||
.fail(this._searchPaginationView.showError.bind(this._searchPaginationView));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = DashboardShareViewContent;
|
||||
@@ -0,0 +1,77 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const DashboardShareViewContent = require('dashboard/views/dashboard/dialogs/share/share-view-content');
|
||||
const shareHeaderTemplate = require('./share-header.tpl');
|
||||
const shareFooterTemplate = require('./share-footer.tpl');
|
||||
const ViewFactory = require('builder/components/view-factory');
|
||||
const loadingView = require('builder/components/loading/render-loading');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'userModel',
|
||||
'visModel',
|
||||
'modalModel',
|
||||
'modals',
|
||||
'onClose'
|
||||
];
|
||||
|
||||
/**
|
||||
* Dialog to share item with other users in organization.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'Dialog-content content is-newContent Dialog-content--expanded',
|
||||
|
||||
events: {
|
||||
'click .js-back': 'cancel',
|
||||
'click .cancel': 'cancel',
|
||||
'click .ok': 'ok'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this._organization = this._userModel.organization;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$('.content').addClass('Dialog-content--expanded');
|
||||
|
||||
this.dashboardShareViewContent = new DashboardShareViewContent({
|
||||
className: 'Dialog-expandedSubContent',
|
||||
configModel: this._configModel,
|
||||
currentUserId: this._userModel.get('id'),
|
||||
organization: this._userModel.organization,
|
||||
visDefinitionModel: this._visModel
|
||||
});
|
||||
|
||||
this.$el.append([
|
||||
shareHeaderTemplate({
|
||||
name: this._visModel.get('name')
|
||||
}),
|
||||
this.dashboardShareViewContent.render().el,
|
||||
shareFooterTemplate()
|
||||
]);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
cancel: function () {
|
||||
this._modalModel.destroy();
|
||||
this._onClose();
|
||||
},
|
||||
|
||||
// @implements cdb.ui.common.Dialog.prototype.ok
|
||||
ok: function () {
|
||||
const modalModel = this._modals.create(function (modalModel) {
|
||||
return ViewFactory.createByHTML(loadingView({}));
|
||||
});
|
||||
|
||||
this.dashboardShareViewContent.saveACLPermissions()
|
||||
.done(() => {
|
||||
modalModel.destroy();
|
||||
this._modalModel.destroy();
|
||||
this._onClose();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const UploadConfig = require('builder/config/upload-config');
|
||||
const ErrorDetailsView = require('builder/components/background-importer/error-details-view');
|
||||
const WarningsDetailsView = require('builder/components/background-importer/warnings-details-view');
|
||||
const TwitterImportDetailsView = require('builder/components/background-importer/twitter-import-details-view');
|
||||
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
|
||||
const template = require('./background-import-item.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel',
|
||||
'configModel',
|
||||
'showSuccessDetailsButton'
|
||||
];
|
||||
|
||||
/**
|
||||
* Import item within background importer
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'ImportItem',
|
||||
tagName: 'li',
|
||||
|
||||
events: {
|
||||
'click .js-abort': '_removeItem',
|
||||
'click .js-show_error': '_showImportError',
|
||||
'click .js-show_warnings': '_showImportWarnings',
|
||||
'click .js-show_stats': '_showImportStats',
|
||||
'click .js-close': '_removeItem'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this._modals = new ModalsServiceModel();
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
const upload = this.model.get('upload');
|
||||
const importModel = this.model.get('import');
|
||||
|
||||
let templateData = {
|
||||
name: '',
|
||||
state: this.model.get('state'),
|
||||
progress: '',
|
||||
service: '',
|
||||
step: this.model.get('step'),
|
||||
url: '',
|
||||
failed: this.model.hasFailed(),
|
||||
completed: this.model.hasCompleted(),
|
||||
warnings: this.model.getWarnings(),
|
||||
showSuccessDetailsButton: this._showSuccessDetailsButton,
|
||||
tables_created_count: importModel.tables_created_count
|
||||
};
|
||||
|
||||
// URL
|
||||
if (templateData.state === 'complete') {
|
||||
const vis = this.model.importedVis();
|
||||
if (vis) {
|
||||
templateData.url = encodeURI(vis.viewUrl(this._userModel).edit());
|
||||
}
|
||||
}
|
||||
|
||||
// Name
|
||||
if (upload.type) {
|
||||
if (upload.type === 'file') {
|
||||
if (upload.value.length > 1) {
|
||||
templateData.name = upload.value.length + ' files';
|
||||
} else {
|
||||
templateData.name = upload.value.name;
|
||||
}
|
||||
}
|
||||
if (upload.type === 'url' || upload.type === 'remote') {
|
||||
templateData.name = upload.value;
|
||||
}
|
||||
if (upload.type === 'service') {
|
||||
templateData.name = upload.value && upload.value.filename || '';
|
||||
}
|
||||
if (upload.service_name === 'twitter_search') {
|
||||
templateData.name = 'Twitter import';
|
||||
}
|
||||
if (upload.type === 'sql') {
|
||||
templateData.name = 'SQL';
|
||||
}
|
||||
if (upload.type === 'duplication') {
|
||||
templateData.name = upload.table_name || upload.value;
|
||||
}
|
||||
} else {
|
||||
templateData.name = importModel.display_name || importModel.item_queue_id || 'import';
|
||||
}
|
||||
|
||||
// Service
|
||||
templateData.service = upload.service_name;
|
||||
|
||||
// Progress
|
||||
if (this.model.get('step') === 'upload') {
|
||||
templateData.progress = this.model.get('upload').progress;
|
||||
} else {
|
||||
templateData.progress = (UploadConfig.uploadStates.indexOf(templateData.state) / UploadConfig.uploadStates.length) * 100;
|
||||
}
|
||||
|
||||
this.$el.html(template(templateData));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.listenTo(this.model, 'remove', this.clean);
|
||||
},
|
||||
|
||||
_removeItem: function () {
|
||||
this.trigger('remove', this.model, this);
|
||||
this.model.pause();
|
||||
this.clean();
|
||||
},
|
||||
|
||||
_showImportStats: function () {
|
||||
this._modals.create(modalModel => {
|
||||
return new TwitterImportDetailsView({
|
||||
userModel: this._userModel,
|
||||
model: this.model,
|
||||
modalModel: this._modalModel
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_showImportError: function () {
|
||||
this._modals.create(() => {
|
||||
return new ErrorDetailsView({
|
||||
error: this.model.getError(),
|
||||
userModel: this._userModel,
|
||||
configModel: this._configModel
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_showImportWarnings: function () {
|
||||
this._modals.create(modalModel => {
|
||||
return new WarningsDetailsView({
|
||||
warnings: this.model.getWarnings(),
|
||||
userModel: this._userModel
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<% if (failed) { %>
|
||||
<div class="ImportItem-text is-failed" title="<%- name %>">
|
||||
Ouch! Error connecting <%- name %> <% if (service) { %> from <%- service %> <% } %>
|
||||
</div>
|
||||
<button class="CDB-Button CDB-Button--secondary CDB-Button--small js-show_error"><span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small">SHOW</span></button>
|
||||
<button class="CDB-Shape js-close">
|
||||
<div class="CDB-Shape-close is-blue is-large"></div>
|
||||
</button>
|
||||
<% } else if (completed && !warnings) { %>
|
||||
<div class="ImportItem-text is-completed" title="<%- name %>">
|
||||
<%- name %> <% if (service && service != "twitter_search") { %> from <%- service %> <% } %> completed!
|
||||
</div>
|
||||
<% if (showSuccessDetailsButton) { %>
|
||||
<% if (service && service === "twitter_search") { %>
|
||||
<button class="CDB-Button CDB-Button--secondary CDB-Button--small js-show_stats"><span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small">SHOW</span></button>
|
||||
<% } else if (tables_created_count === 1) { %>
|
||||
|
||||
<a href="<%- url %>" class="CDB-Button CDB-Button--secondary CDB-Button--small js-show"><span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small">SHOW</span></a>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<button class="CDB-Shape js-close">
|
||||
<div class="CDB-Shape-close is-blue is-large"></div>
|
||||
</button>
|
||||
<% } else if (completed && warnings) { %>
|
||||
<div class="ImportItem-text has-warnings" title="<%- name %>">
|
||||
Some warnings were produced for <%- name %> <% if (service) { %> from <%- service %> <% } %>
|
||||
</div>
|
||||
<button class="CDB-Button CDB-Button--secondary CDB-Button--small js-show_warnings"><span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small">SHOW</span></button>
|
||||
<button class="CDB-Shape js-close">
|
||||
<div class="CDB-Shape-close is-blue is-large"></div>
|
||||
</button>
|
||||
<% } else { %>
|
||||
<div class="ImportItem-text" title="<%- name %>">
|
||||
<span class="ImportItem-textState"><%- state %></span> <%- name %> <% if (service && service != "twitter_search") { %> from <%- service %> <% } %>
|
||||
</div>
|
||||
<div class="ImportItem-progress">
|
||||
<div class="progress-bar">
|
||||
<span class="bar-2" style="width:<%- progress %>%"></span>
|
||||
</div>
|
||||
</div>
|
||||
<% if (state === "uploading" && step === "upload") { %>
|
||||
<button class="ImportItem-closeButton js-abort">
|
||||
<i class="CDB-IconFont CDB-IconFont-close ImportItem-closeButtonIcon"></i>
|
||||
</button>
|
||||
<% } %>
|
||||
<% } %>
|
||||
Reference in New Issue
Block a user