Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
@@ -0,0 +1,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);
}
});
@@ -0,0 +1,8 @@
<% if (totalPollings === 1) { %>
<% if (imports > 0) { %>
Connecting
<% } %>
dataset...
<% } else { %>
Working...
<% } %>
@@ -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');
}
}
});
@@ -0,0 +1,3 @@
<div class="BackgroundPolling-headerBadge LayoutIcon">
<i class="CDB-IconFont CDB-IconFont-cloud BackgroundPolling-headerBadgeIcon js-icon"></i>
</div>
@@ -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);
}
});
@@ -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;
}
});
@@ -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
);
}
});
@@ -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>
@@ -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;
@@ -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');
}
});
@@ -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()
);
}
});
@@ -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;
@@ -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');
}
});
@@ -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>
@@ -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;
@@ -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>
@@ -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();
}
}
});
@@ -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>
<% } %>
@@ -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);
});
}
});
@@ -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');
}
}
});
@@ -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>
@@ -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();
});
}
});
@@ -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
});
});
}
});
@@ -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>
<% } %>
<% } %>