Initial commit
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
const _ = require('underscore');
|
||||
const Polyglot = require('node-polyglot');
|
||||
const CartoNode = require('carto-node');
|
||||
const AccountMainView = require('dashboard/views/account/account-main-view');
|
||||
const ConfigModel = require('dashboard/data/config-model');
|
||||
const UserModel = require('dashboard/data/user-model');
|
||||
const OrganizationModel = require('dashboard/data/organization-model');
|
||||
const AssetsVersionHelper = require('dashboard/helpers/assets-version');
|
||||
|
||||
const Locale = require('locale/index');
|
||||
|
||||
const PACKAGE = require('../../../../package.json');
|
||||
const ACTIVE_LOCALE = 'zh-cn';
|
||||
|
||||
const polyglot = new Polyglot({
|
||||
locale: ACTIVE_LOCALE,
|
||||
phrases: Locale[ACTIVE_LOCALE]
|
||||
});
|
||||
|
||||
require('dashboard/data/backbone/sync-options');
|
||||
|
||||
window._t = polyglot.t.bind(polyglot);
|
||||
window.StaticConfig = window.StaticConfig || {};
|
||||
window.CartoConfig = window.CartoConfig || {};
|
||||
|
||||
const ForbiddenAction = require('builder/data/backbone/network-interceptors/interceptors/forbidden-403');
|
||||
const NetworkResponseInterceptor = require('builder/data/backbone/network-interceptors/interceptor');
|
||||
NetworkResponseInterceptor.addURLPattern('api/v');
|
||||
NetworkResponseInterceptor.addErrorInterceptor(ForbiddenAction());
|
||||
NetworkResponseInterceptor.start();
|
||||
|
||||
document.title = _t('account.title');
|
||||
|
||||
const InitAccount = function () {
|
||||
const client = new CartoNode.AuthenticatedClient();
|
||||
|
||||
const dataLoaded = function (data) {
|
||||
const ASSETS_VERSION = AssetsVersionHelper.getAssetsVersion(PACKAGE.version);
|
||||
const organizationNotifications = data.organization_notifications;
|
||||
const userData = data.user_data || {};
|
||||
|
||||
const configModel = new ConfigModel(
|
||||
_.extend(
|
||||
{ base_url: userData.base_url,
|
||||
url_prefix: userData.base_url },
|
||||
data.config
|
||||
)
|
||||
);
|
||||
|
||||
const userModelOptions = { groups: userData.groups };
|
||||
|
||||
if (userData.organization) {
|
||||
userModelOptions.organization = new OrganizationModel(userData.organization, { configModel });
|
||||
}
|
||||
|
||||
const userModel = new UserModel(
|
||||
_.extend(userData, {
|
||||
auth_username_password_enabled: data.auth_username_password_enabled,
|
||||
can_be_deleted: data.can_be_deleted,
|
||||
can_change_password: data.can_change_password,
|
||||
cant_be_deleted_reason: data.cant_be_deleted_reason,
|
||||
logged_with_google: data.google_sign_in,
|
||||
plan_name: data.plan_name,
|
||||
plan_url: data.plan_url,
|
||||
services: data.services,
|
||||
should_display_old_password: data.should_display_old_password
|
||||
}), userModelOptions
|
||||
);
|
||||
|
||||
new AccountMainView({ // eslint-disable-line no-new
|
||||
el: document.body,
|
||||
userModel: userModel,
|
||||
configModel: configModel,
|
||||
assetsVersion: ASSETS_VERSION,
|
||||
client,
|
||||
organizationNotifications
|
||||
});
|
||||
};
|
||||
|
||||
if (window.CartoConfig && window.CartoConfig.data) {
|
||||
dataLoaded(window.CartoConfig.data);
|
||||
} else {
|
||||
client.getConfig(function (err, response, data) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return err;
|
||||
}
|
||||
|
||||
window.CartoConfig.data = data;
|
||||
dataLoaded(data);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
InitAccount();
|
||||
@@ -0,0 +1,107 @@
|
||||
const Polyglot = require('node-polyglot');
|
||||
const Locale = require('locale/index');
|
||||
|
||||
const ACTIVE_LOCALE = 'zh-cn';
|
||||
const polyglot = new Polyglot({
|
||||
locale: ACTIVE_LOCALE, // Needed for pluralize behaviour
|
||||
phrases: Locale[ACTIVE_LOCALE]
|
||||
});
|
||||
window._t = polyglot.t.bind(polyglot);
|
||||
|
||||
const $ = require('jquery');
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
require('dashboard/data/backbone/sync-options');
|
||||
const UserModel = require('dashboard/data/user-model');
|
||||
const OrganizationModel = require('dashboard/data/organization-model');
|
||||
const ConfigModel = require('dashboard/data/config-model');
|
||||
const DashboardHeaderView = require('dashboard/components/dashboard-header-view');
|
||||
const HeaderViewModel = require('dashboard/views/api-keys/header-view-model');
|
||||
const UpgradeMessageView = require('dashboard/components/upgrade-message-view');
|
||||
const ApiKeysPageView = require('dashboard/views/api-keys/api-keys-page-view');
|
||||
const ApiKeysFormView = require('dashboard/views/api-keys/api-keys-form-view');
|
||||
const StackLayoutView = require('builder/components/stack-layout/stack-layout-view');
|
||||
const UserTablesModel = require('dashboard/data/user-tables-model');
|
||||
const getObjectValue = require('deep-insights/util/get-object-value');
|
||||
|
||||
const ForbiddenAction = require('builder/data/backbone/network-interceptors/interceptors/forbidden-403');
|
||||
const NetworkResponseInterceptor = require('builder/data/backbone/network-interceptors/interceptor');
|
||||
NetworkResponseInterceptor.addURLPattern('api/v3');
|
||||
NetworkResponseInterceptor.addErrorInterceptor(ForbiddenAction());
|
||||
NetworkResponseInterceptor.start();
|
||||
|
||||
const configModel = new ConfigModel(
|
||||
_.defaults(
|
||||
{
|
||||
base_url: window.base_url
|
||||
},
|
||||
window.config
|
||||
)
|
||||
);
|
||||
|
||||
if (window.trackJs) {
|
||||
window.trackJs.configure({
|
||||
userId: window.user_data.username
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point for the new keys, bootstraps all dependency models and application.
|
||||
*/
|
||||
$(function () {
|
||||
const userModel = new UserModel(window.user_data, { configModel });
|
||||
// User has an organization
|
||||
if (window.user_data.organization) {
|
||||
const organization = new OrganizationModel(window.user_data, {
|
||||
currentUserId: window.user_data.id,
|
||||
configModel
|
||||
});
|
||||
organization.owner = new UserModel(getObjectValue(window.user_data, 'organization.owner'));
|
||||
userModel.setOrganization(organization);
|
||||
}
|
||||
|
||||
const headerView = new DashboardHeaderView({
|
||||
el: $('#header'), // pre-rendered in DOM by Rails app
|
||||
model: userModel,
|
||||
configModel: configModel,
|
||||
viewModel: new HeaderViewModel()
|
||||
});
|
||||
headerView.render();
|
||||
|
||||
const upgradeMessage = new UpgradeMessageView({
|
||||
configModel: configModel,
|
||||
userModel: userModel
|
||||
});
|
||||
|
||||
$('#header').after(upgradeMessage.render().el);
|
||||
|
||||
const userTablesModel = new UserTablesModel(null, { userModel });
|
||||
|
||||
// Prefetch user tables for new api key form
|
||||
userTablesModel.fetch();
|
||||
|
||||
// Debug
|
||||
window.userTablesModel = userTablesModel;
|
||||
|
||||
const stackLayoutCollection = new Backbone.Collection([
|
||||
{
|
||||
createStackView: (stackLayoutModel) =>
|
||||
new ApiKeysPageView({ userModel, stackLayoutModel })
|
||||
},
|
||||
{
|
||||
createStackView: (stackLayoutModel, [apiKeyModel, ...other]) =>
|
||||
new ApiKeysFormView({
|
||||
stackLayoutModel,
|
||||
apiKeyModel,
|
||||
userTablesModel,
|
||||
userModel
|
||||
})
|
||||
}
|
||||
]);
|
||||
|
||||
const stackLayout = new StackLayoutView({
|
||||
collection: stackLayoutCollection
|
||||
});
|
||||
|
||||
$('.js-api-keys-new').append(stackLayout.render().el);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Send events to Google Analytics if it is available
|
||||
* - https://developers.google.com/analytics/devguides/collection/analyticsjs/sending-hits
|
||||
*
|
||||
* *Remove this "helper" when dashboard is deprecated
|
||||
*/
|
||||
|
||||
var GAPusher = function (opts) {
|
||||
var ga = window.ga;
|
||||
opts = opts || {};
|
||||
|
||||
if (ga) {
|
||||
ga(opts.eventName || 'send', {
|
||||
hitType: opts.hitType,
|
||||
eventCategory: opts.eventCategory,
|
||||
eventAction: opts.eventAction,
|
||||
eventLabel: opts.eventLabel
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = GAPusher;
|
||||
@@ -0,0 +1,60 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* @extends http://backbonejs.org/#Router With some common functionality in the context of this app.
|
||||
*/
|
||||
var RouterBase = Backbone.Router.extend({
|
||||
|
||||
/**
|
||||
* Placeholder, is replaced by enableAfterMainView().
|
||||
*/
|
||||
navigate: function () {
|
||||
throw new Error('router.enableAfterMainView({ ... }) must be called before you can navigate');
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable router to monitor and manage browser URL and history.
|
||||
* Expected to be called after main view as the function name indicates,
|
||||
*/
|
||||
enableAfterMainView: function () {
|
||||
/**
|
||||
* @override http://backbonejs.org/#Router-navigate Allow
|
||||
* @param fragmentOrUrl {String} Either a fragment (e.g. '/dashboard/datasets') or a full URL
|
||||
* (e.g. http://user.carto.com/dashboard/datasets), the navigate method takes care to route correctly.
|
||||
*/
|
||||
this.navigate = function (fragmentOrUrl, opts) {
|
||||
Backbone.Router.prototype.navigate.call(this, this.normalizeFragmentOrUrl(fragmentOrUrl), opts);
|
||||
};
|
||||
|
||||
Backbone.history.start({
|
||||
pushState: true,
|
||||
root: this.rootPath() + '/' // Yes, this trailing slash is necessary for the router to update the history state properly.
|
||||
});
|
||||
},
|
||||
|
||||
rootPath: function () {
|
||||
throw new Error('implement rootPath in child router (no trailing slash)');
|
||||
},
|
||||
|
||||
/**
|
||||
* Normalise a given fragment or URL for navigation mechanisms to work.
|
||||
* Typically, remove the leading base URL from the given fragment or URL.
|
||||
*
|
||||
* @param {String} fragmentOrUrl
|
||||
* @return {String}
|
||||
*/
|
||||
normalizeFragmentOrUrl: function (fragmentOrUrl) {
|
||||
throw new Error('implement normalizeFragmentOrUrl in child router');
|
||||
}
|
||||
});
|
||||
|
||||
RouterBase.supportTrailingSlashes = function (obj) {
|
||||
return _.reduce(obj, function (res, val, key) {
|
||||
res[key] = val;
|
||||
res[key + '/'] = val;
|
||||
return res;
|
||||
}, {});
|
||||
};
|
||||
|
||||
module.exports = RouterBase;
|
||||
@@ -0,0 +1,140 @@
|
||||
const RouterBase = require('dashboard/common/router-base');
|
||||
const RouterModel = require('dashboard/views/organization/groups-admin/router-model');
|
||||
const GroupHeaderView = require('dashboard/views/organization/groups-admin/group-header/group-header-view');
|
||||
const GroupsIndexView = require('dashboard/views/organization/groups-admin/group-index/group-index-view');
|
||||
const CreateGroupView = require('dashboard/views/organization/groups-admin/create-group/create-group-view');
|
||||
const GroupUsersView = require('dashboard/views/organization/groups-admin/group-users/group-users-view');
|
||||
const EditGroupView = require('dashboard/views/organization/groups-admin/edit-group/edit-group-view');
|
||||
const ViewFactory = require('builder/components/view-factory');
|
||||
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'rootUrl',
|
||||
'groups',
|
||||
'userModel',
|
||||
'flashMessageModel',
|
||||
'modals'
|
||||
];
|
||||
|
||||
/**
|
||||
* Backbone router for organization groups urls.
|
||||
*/
|
||||
module.exports = RouterBase.extend({
|
||||
|
||||
routes: RouterBase.supportTrailingSlashes({
|
||||
'': 'renderGroupsIndex',
|
||||
'new': 'renderCreateGroup',
|
||||
':id': 'renderGroupUsers',
|
||||
':id/edit': 'renderEditGroup',
|
||||
|
||||
// If URL is lacking the trailing slash (e.g. 'http://username.carto.com/organization/groups'), treat it like index
|
||||
'*prefix/groups': 'renderGroupsIndex'
|
||||
}),
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this.rootPath = this._rootUrl.pathname.bind(this._rootUrl);
|
||||
|
||||
this.model = new RouterModel();
|
||||
this.model.createLoadingView('Loading view'); // Until router's history is started
|
||||
this.listenTo(this.model, 'change', this._onChange);
|
||||
},
|
||||
|
||||
normalizeFragmentOrUrl: function (fragmentOrUrl) {
|
||||
return fragmentOrUrl ? fragmentOrUrl.toString().replace(this._rootUrl.toString(), '') : '';
|
||||
},
|
||||
|
||||
isWithinCurrentRoutes: function (url) {
|
||||
return url.indexOf(this._rootUrl.pathname()) !== -1;
|
||||
},
|
||||
|
||||
renderGroupsIndex: function () {
|
||||
this.model.set('view',
|
||||
new GroupsIndexView({
|
||||
newGroupUrl: this._groupUrl.bind(this),
|
||||
groups: this._groups,
|
||||
router: this
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
renderCreateGroup: function () {
|
||||
const group = this._groups.newGroupById();
|
||||
|
||||
this.model.set('view',
|
||||
ViewFactory.createListView([
|
||||
() => this._createGroupHeader(group),
|
||||
|
||||
() => new CreateGroupView({
|
||||
flashMessageModel: this._flashMessageModel,
|
||||
group,
|
||||
onCreated: this._navigateToGroup.bind(this, group)
|
||||
})
|
||||
])
|
||||
);
|
||||
},
|
||||
|
||||
renderGroupUsers: function (id) {
|
||||
this.model.createGroupView(this._groups, id, group => {
|
||||
return ViewFactory.createListView([
|
||||
() => this._createGroupHeader(group, 'group_users'),
|
||||
|
||||
() => new GroupUsersView({
|
||||
group,
|
||||
orgUsers: this._userModel.organization.users,
|
||||
userModel: this._userModel
|
||||
})
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
renderEditGroup: function (id) {
|
||||
this.model.createGroupView(this._groups, id, group => {
|
||||
return ViewFactory.createListView([
|
||||
() => this._createGroupHeader(group, 'edit_group'),
|
||||
|
||||
() => new EditGroupView({
|
||||
group,
|
||||
flashMessageModel: this._flashMessageModel,
|
||||
modals: this._modals,
|
||||
userModel: this._userModel,
|
||||
onSaved: this._navigateToGroup.bind(this, group),
|
||||
onDeleted: this.navigate.bind(this, this._rootUrl, { trigger: true })
|
||||
})
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
_navigateToGroup: function (group) {
|
||||
this.navigate(this._rootUrl.urlToPath(group.id), { trigger: true });
|
||||
},
|
||||
|
||||
_groupUrl: function (group, subpath) {
|
||||
var path = group.id;
|
||||
|
||||
if (subpath) {
|
||||
path += '/' + subpath;
|
||||
}
|
||||
|
||||
return this._rootUrl.urlToPath(path);
|
||||
},
|
||||
|
||||
_createGroupHeader: function (group, current) {
|
||||
var urls = {
|
||||
root: this._rootUrl,
|
||||
users: this._groupUrl(group),
|
||||
edit: this._groupUrl(group, 'edit')
|
||||
};
|
||||
urls.users.isCurrent = current === 'group_users';
|
||||
urls.edit.isCurrent = current === 'edit_group';
|
||||
|
||||
return new GroupHeaderView({ group, urls });
|
||||
},
|
||||
|
||||
_onChange: function () {
|
||||
this._flashMessageModel.hide();
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
const Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* New public table router \o/
|
||||
*
|
||||
* - No more /#/xxx routes
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Router.extend({
|
||||
|
||||
routes: {
|
||||
':id/public/:scenario': 'change'
|
||||
},
|
||||
|
||||
initialize: function (table) {
|
||||
this.table = table;
|
||||
},
|
||||
|
||||
change: function (_id, scenario) {
|
||||
// Check active view, if it is different, change
|
||||
if (scenario != 'table' && scenario != 'map') scenario = 'table'; // eslint-disable-line eqeqeq
|
||||
this.table.workViewMobile.active(scenario);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
/**
|
||||
* Default upload config
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
uploadStates: [
|
||||
'enqueued',
|
||||
'pending',
|
||||
'importing',
|
||||
'uploading',
|
||||
'guessing',
|
||||
'unpacking',
|
||||
'getting',
|
||||
'creating',
|
||||
'complete'
|
||||
],
|
||||
fileExtensions: [
|
||||
'csv',
|
||||
'xls',
|
||||
'xlsx',
|
||||
'zip',
|
||||
'kml',
|
||||
'geojson',
|
||||
'json',
|
||||
'ods',
|
||||
'kmz',
|
||||
'tsv',
|
||||
'gpx',
|
||||
'tar',
|
||||
'gz',
|
||||
'tgz',
|
||||
'osm',
|
||||
'bz2',
|
||||
'tif',
|
||||
'tiff',
|
||||
'txt',
|
||||
'sql',
|
||||
'rar',
|
||||
'carto',
|
||||
'gpkg'
|
||||
],
|
||||
// How big should file be?
|
||||
fileTimesBigger: 3
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
types: [
|
||||
'POINT',
|
||||
'LINESTRING',
|
||||
'POLYGON',
|
||||
'MULTIPOINT',
|
||||
'MULTILINESTRING',
|
||||
'MULTIPOLYGON'
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
const Backbone = require('backbone');
|
||||
const CoreView = require('backbone/core-view');
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'change .js-appPlatformsLegendOption': '_changePlatformValue'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this.model = new Backbone.Model({
|
||||
value: ''
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change:value', this._changeIdPlatformLegends);
|
||||
},
|
||||
|
||||
_changePlatformValue: function (ev) {
|
||||
this.model.set('value', ev.target.value);
|
||||
},
|
||||
|
||||
_changeIdPlatformLegends: function () {
|
||||
const legend = this.options.appPlatforms[this.model.get('value')]['legend'];
|
||||
this.$('.js-appPlatformsLegend').html(legend);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
const IconSelectorView = require('dashboard/components/icon-selector/icon-selector-view');
|
||||
const template = require('./avatar-selector.tpl');
|
||||
|
||||
module.exports = IconSelectorView.extend({
|
||||
options: {
|
||||
acceptedExtensions: ['jpeg', 'jpg', 'png', 'gif'],
|
||||
imageKind: 'orgavatar',
|
||||
imageURLAttribute: 'avatar_url'
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this._destroyFileInput();
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
state: this._model.get('state'),
|
||||
name: this._renderModel.get('name'),
|
||||
inputName: this._renderModel.get('inputName'),
|
||||
avatarURL: this._renderModel.get('avatar_url'),
|
||||
username: this._renderModel.get('username'),
|
||||
avatarAcceptedExtensions: this._formatAcceptedExtensions(this.options.acceptedExtensions)
|
||||
})
|
||||
);
|
||||
|
||||
this._renderFileInput();
|
||||
return this;
|
||||
}
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,75 @@
|
||||
/* global google */
|
||||
const L = require('leaflet');
|
||||
const NEW_YORK = [40.7127837, -74.0059413];
|
||||
|
||||
/**
|
||||
* Creates a default fallback map, to be used when an user doesn't have a public map.
|
||||
*
|
||||
* @param opts {Object} config
|
||||
* el: {HTMLElement} an HTMLElement node where to draw the map
|
||||
*/
|
||||
module.exports = function (opts) {
|
||||
if (opts.basemap.urlTemplate) {
|
||||
const map = L.map(opts.el, {
|
||||
zoomControl: false,
|
||||
minZoom: 6,
|
||||
maxZoom: 6,
|
||||
scrollWheelZoom: false
|
||||
});
|
||||
|
||||
map.setView(NEW_YORK, 6);
|
||||
|
||||
map.dragging.disable();
|
||||
map.touchZoom.disable();
|
||||
map.doubleClickZoom.disable();
|
||||
|
||||
map.attributionControl.setPrefix('');
|
||||
|
||||
let url = opts.basemap.urlTemplate;
|
||||
|
||||
if (window.devicePixelRatio > 1) {
|
||||
url = opts.basemap.urlTemplate2x || url;
|
||||
}
|
||||
|
||||
L.tileLayer(url, {
|
||||
attribution: opts.basemap.attribution
|
||||
}).addTo(map);
|
||||
} else if (opts.basemap.className === 'googlemaps' && google.maps !== undefined) {
|
||||
const map = new google.maps.Map(opts.el, { // eslint-disable-line
|
||||
center: { lat: NEW_YORK[0], lng: NEW_YORK[1] },
|
||||
zoom: 6,
|
||||
draggable: false,
|
||||
scrollwheel: false,
|
||||
panControl: false,
|
||||
zoomControl: false,
|
||||
streetViewControl: false,
|
||||
maxZoom: 6,
|
||||
minZoom: 6,
|
||||
mapTypeControl: false,
|
||||
mapTypeId: opts.basemap.baseType || google.maps.MapTypeId.ROADMAP,
|
||||
styles: [
|
||||
{
|
||||
featureType: 'all',
|
||||
elementType: 'labels',
|
||||
stylers: [
|
||||
{ visibility: 'off' }
|
||||
]
|
||||
},
|
||||
{
|
||||
featureType: 'road',
|
||||
elementType: 'geometry',
|
||||
stylers: [
|
||||
{ visibility: 'off' }
|
||||
]
|
||||
},
|
||||
{
|
||||
featureType: 'administrative',
|
||||
elementType: 'geometry.stroke',
|
||||
stylers: [
|
||||
{ visibility: 'off' }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
const $ = require('jquery');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const logoTemplate = require('./dashboard-header/logo.tpl');
|
||||
const dropdownLinkTemplate = require('./dashboard-header/breadcrumbs/dropdown-link.tpl');
|
||||
const SettingsDropdownView = require('./dashboard-header/settings-dropdown-view');
|
||||
const BreadcrumbsDropdown = require('./dashboard-header/breadcrumbs/dropdown-view');
|
||||
const UserNotificationsView = require('./dashboard-header/notifications/user-notifications-view');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'viewModel',
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Responsible for the header part of the layout.
|
||||
* It's currently pre-rendered server-side, why the header element is required to be given when instantiating the view.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'click .js-breadcrumb-dropdown': '_createBreadcrumbsDropdown',
|
||||
'click .js-settings-dropdown': '_createSettingsDropdown'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
if (!this.options.el) {
|
||||
throw new Error('el element is required');
|
||||
}
|
||||
|
||||
this.router = this.options.router;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this._renderBreadcrumbsDropdownLink();
|
||||
this._renderNotifications();
|
||||
this._renderLogoLink();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this._viewModel.bind('change', this._renderBreadcrumbsDropdownLink, this);
|
||||
this.listenTo(this._viewModel, 'change', this._renderBreadcrumbsDropdownLink);
|
||||
|
||||
if (this.router) {
|
||||
this.listenTo(this.router.model, 'change', this._onRouterChange);
|
||||
}
|
||||
if (this.collection) {
|
||||
this.listenTo(this.collection, 'reset', this._stopLogoAnimation);
|
||||
this.listenTo(this.collection, 'error', this._onCollectionError);
|
||||
}
|
||||
},
|
||||
|
||||
_onCollectionError: function (col, e, opts) {
|
||||
// Old requests can be stopped, so aborted requests are not
|
||||
// considered as an error
|
||||
if (!e || (e && e.statusText !== 'abort')) {
|
||||
this._stopLogoAnimation();
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: Not sure if the changes made here are correct, because of backbone version changes
|
||||
_onRouterChange: function (m, c) {
|
||||
if (m.changed && !m.changed.content_type && this.collection.total_user_entries > 0) {
|
||||
this._startLogoAnimation();
|
||||
}
|
||||
},
|
||||
|
||||
_startLogoAnimation: function () {
|
||||
this.$('.Logo').addClass('is-loading');
|
||||
},
|
||||
|
||||
_stopLogoAnimation: function () {
|
||||
this.$('.Logo').removeClass('is-loading');
|
||||
},
|
||||
|
||||
_renderBreadcrumbsDropdownLink: function () {
|
||||
this.$('.js-breadcrumb-dropdown').html(
|
||||
dropdownLinkTemplate({
|
||||
title: this._viewModel.breadcrumbTitle(),
|
||||
dropdownEnabled: this._viewModel.isBreadcrumbDropdownEnabled()
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_renderNotifications: function () {
|
||||
var userNotifications = new UserNotificationsView({
|
||||
user: this.model,
|
||||
configModel: this._configModel,
|
||||
organizationNotifications: this.options.organizationNotifications
|
||||
});
|
||||
|
||||
this.$('.js-user-notifications').html(userNotifications.render().el);
|
||||
this.addView(userNotifications);
|
||||
},
|
||||
|
||||
_renderLogoLink: function () {
|
||||
this.$('.js-logo').html(
|
||||
logoTemplate({
|
||||
homeUrl: this.model.viewUrl().dashboard(),
|
||||
googleEnabled: this.model.featureEnabled('google_maps')
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_createSettingsDropdown: function (event) {
|
||||
this.killEvent(event);
|
||||
|
||||
this._setupDropdown(new SettingsDropdownView({
|
||||
target: $(event.target),
|
||||
model: this.model, // a user model
|
||||
configModel: this._configModel,
|
||||
horizontalOffset: 15
|
||||
}));
|
||||
},
|
||||
|
||||
_createBreadcrumbsDropdown: function (ev) {
|
||||
if (this._viewModel.isBreadcrumbDropdownEnabled()) {
|
||||
this.killEvent(ev);
|
||||
this._setupDropdown(new BreadcrumbsDropdown({
|
||||
target: $(ev.target),
|
||||
model: this.model,
|
||||
viewModel: this._viewModel,
|
||||
router: this.router, // optional
|
||||
tick: 'center',
|
||||
template: require('dashboard/components/dashboard-header/breadcrumbs/dropdown.tpl'),
|
||||
horizontalOffset: this.options.breadcrumbsDropdownOffset
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
_setupDropdown: function (dropdownView) {
|
||||
this._closeAnyOtherOpenDialogs();
|
||||
this._previousDropDown = dropdownView;
|
||||
this.addView(dropdownView);
|
||||
|
||||
dropdownView.on('onDropdownHidden', function () {
|
||||
dropdownView.clean();
|
||||
}, this);
|
||||
|
||||
dropdownView.render();
|
||||
dropdownView.open();
|
||||
},
|
||||
|
||||
_closeAnyOtherOpenDialogs: function () {
|
||||
// TODO: This is not how it used to work, it used to listen to a global event
|
||||
if (this._previousDropDown) {
|
||||
this._previousDropDown.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
+1
@@ -0,0 +1 @@
|
||||
<button class="Header-navigationBreadcrumbLink <%- dropdownEnabled ? "DropdownLink DropdownLink--white" : "is-disabled" %>"><%- title %></button>
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
const $ = require('jquery');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const navigateThroughRouter = require('builder/helpers/navigate-through-router');
|
||||
const AdminDropdownMenu = require('dashboard/components/dropdown/dropdown-admin-view');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'viewModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* The content of the dropdown menu opened by the link at the end of the breadcrumbs menu, e.g.
|
||||
* username > [Maps]
|
||||
* ______/\____
|
||||
* | |
|
||||
* | this |
|
||||
* |____________|
|
||||
*/
|
||||
|
||||
module.exports = AdminDropdownMenu.extend({
|
||||
className: 'Dropdown BreadcrumbsDropdown',
|
||||
|
||||
events: {
|
||||
'click a': '_navigateToLinksHref'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
AdminDropdownMenu.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var dashboardUrl = this.model.viewUrl().dashboard();
|
||||
var datasetsUrl = dashboardUrl.datasets();
|
||||
var deepInsightsUrl = dashboardUrl.deepInsights();
|
||||
var mapsUrl = dashboardUrl.maps();
|
||||
|
||||
this.$el.html(this.template({
|
||||
avatarUrl: this.model.get('avatar_url'),
|
||||
userName: this.model.get('username'),
|
||||
mapsUrl: mapsUrl,
|
||||
datasetsUrl: datasetsUrl,
|
||||
deepInsightsUrl: deepInsightsUrl,
|
||||
lockedDatasetsUrl: datasetsUrl.lockedItems(),
|
||||
lockedMapsUrl: mapsUrl.lockedItems(),
|
||||
isDeepInsights: this._viewModel.isDisplayingDeepInsights(),
|
||||
isDatasets: this._viewModel.isDisplayingDatasets(),
|
||||
isMaps: this._viewModel.isDisplayingMaps(),
|
||||
isLocked: this._viewModel.isDisplayingLockedItems()
|
||||
}));
|
||||
|
||||
// Necessary to hide dialog on click outside popup, for example.
|
||||
// TODO: Handle this
|
||||
// cdb.god.bind('closeDialogs', this.hide, this);
|
||||
|
||||
// TODO: taken from existing code, how should dropdowns really be added to the DOM?
|
||||
$('body').append(this.el);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_navigateToLinksHref: function () {
|
||||
this.hide(); // Hide must be called before routing for proper deconstruct of dropdown
|
||||
|
||||
if (this.options.router) {
|
||||
navigateThroughRouter.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
<ul class="BreadcrumbsDropdown-list CDB-Text CDB-Size-medium">
|
||||
<li class="BreadcrumbsDropdown-listItem">
|
||||
<span class="BreadcrumbsDropdown-icon u-rSpace--xl">
|
||||
<img class="UserAvatar-img UserAvatar-img--small" src="<%- avatarUrl %>" title="<%- userName %>" alt="<%- userName %>" />
|
||||
</span>
|
||||
<nav class="BreadcrumbsDropdown-options">
|
||||
<a href="<%- mapsUrl %>" class="BreadcrumbsDropdown-optionsItem <%- isMaps && !isDeepInsights && !isLocked ? 'is-selected' : '' %>">Your maps</a>
|
||||
<a href="<%- datasetsUrl %>" class="BreadcrumbsDropdown-optionsItem has-margin <%- isDatasets && !isLocked ? 'is-selected' : '' %>">Your datasets</a>
|
||||
</nav>
|
||||
</li>
|
||||
<li class="BreadcrumbsDropdown-listItem is-dark u-flex">
|
||||
<span class="BreadcrumbsDropdown-lockIcon BreadcrumbsDropdown-icon u-flex u-alignCenter u-justifyCenter u-rSpace--xl">
|
||||
<i class="CDB-IconFont CDB-IconFont-lock"></i>
|
||||
</span>
|
||||
<nav class="BreadcrumbsDropdown-options">
|
||||
<a href="<%- lockedMapsUrl %>" class="BreadcrumbsDropdown-optionsItem <%- isMaps && isLocked ? 'is-selected' : '' %>">Your locked maps</a>
|
||||
<a href="<%- lockedDatasetsUrl %>" class="BreadcrumbsDropdown-optionsItem has-margin <%- isDatasets && isLocked ? 'is-selected' : '' %>">Your locked datasets</a>
|
||||
</nav>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,16 @@
|
||||
<a class="Logo" href="<%- homeUrl %>">
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="92px" height="36px" viewBox="0 0 92 36" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-162.000000, -282.000000)" fill="#FFFFFF">
|
||||
<g transform="translate(162.000000, 282.000000)">
|
||||
<path d="M74,36 C83.9411255,36 92,27.9411255 92,18 C92,8.0588745 83.9411255,0 74,0 C64.0588745,0 56,8.0588745 56,18 C56,27.9411255 64.0588745,36 74,36 Z" id="halo" fill-opacity="0.200000018"></path>
|
||||
<path d="M6.25280899,23.981602 C8.76747566,23.981602 10.220757,22.882802 11.2984713,21.390402 L8.9144367,19.684802 C8.22861851,20.521202 7.52647133,21.078802 6.33445401,21.078802 C4.73421159,21.078802 3.60751029,19.734002 3.60751029,18.012002 L3.60751029,17.979202 C3.60751029,16.306402 4.73421159,14.928802 6.33445401,14.928802 C7.4284973,14.928802 8.1796315,15.470002 8.83279168,16.273602 L11.2168263,14.420402 C10.204428,13.026402 8.70215964,12.042402 6.36711202,12.042402 C2.9053631,12.042402 0.358038428,14.666402 0.358038428,18.012002 L0.358038428,18.044802 C0.358038428,21.472402 2.98700813,23.981602 6.25280899,23.981602 L6.25280899,23.981602 Z M16.732047,23.752002 L20.0468349,23.752002 L20.8632851,21.685602 L25.2884453,21.685602 L26.1048955,23.752002 L29.5013284,23.752002 L24.6352851,12.190002 L21.5817613,12.190002 L16.732047,23.752002 Z M21.7940384,19.209202 L23.0840297,15.962002 L24.357692,19.209202 L21.7940384,19.209202 Z M35.6697093,23.752002 L38.8375361,23.752002 L38.8375361,20.275202 L40.2418305,20.275202 L42.5442201,23.752002 L46.1855881,23.752002 L43.4586443,19.750402 C44.8792677,19.143602 45.810021,17.979202 45.810021,16.208002 L45.810021,16.175202 C45.810021,15.043602 45.4671119,14.174402 44.7976227,13.502002 C44.0301595,12.731202 42.8218132,12.272002 41.0746097,12.272002 L35.6697093,12.272002 L35.6697093,23.752002 Z M38.8375361,17.782402 L38.8375361,15.010802 L40.9276487,15.010802 C41.9727049,15.010802 42.6421941,15.470002 42.6421941,16.388402 L42.6421941,16.421202 C42.6421941,17.257602 42.005363,17.782402 40.9439777,17.782402 L38.8375361,17.782402 Z M55.2605317,23.752002 L58.4283585,23.752002 L58.4283585,15.060002 L61.8574495,15.060002 L61.8574495,12.272002 L51.8477698,12.272002 L51.8477698,15.060002 L55.2605317,15.060002 L55.2605317,23.752002 Z M74,24 C77.3137085,24 80,21.3137085 80,18 C80,14.6862915 77.3137085,12 74,12 C70.6862915,12 68,14.6862915 68,18 C68,21.3137085 70.6862915,24 74,24 Z" id="logotype"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<% if (googleEnabled) { %>
|
||||
<span class="Logo-sub Logo-sub--google"></span>
|
||||
<% } %>
|
||||
</a>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
const $ = require('jquery');
|
||||
const ScrollView = require('builder/components/scroll/scroll-view');
|
||||
const ViewFactory = require('builder/components/view-factory');
|
||||
const AdminDropdownMenu = require('dashboard/components/dropdown/dropdown-admin-view');
|
||||
const template = require('./templates/dropdown-content.tpl');
|
||||
|
||||
/**
|
||||
* User notifications dropdown, rendering notifications
|
||||
* from the collection
|
||||
*/
|
||||
|
||||
module.exports = AdminDropdownMenu.extend({
|
||||
className: 'Dropdown',
|
||||
|
||||
initialize: function (options) {
|
||||
AdminDropdownMenu.prototype.initialize.apply(this, arguments);
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.html(this.template());
|
||||
this._renderDropdown();
|
||||
this._checkScroll();
|
||||
|
||||
$('body').append(this.el);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderDropdown: function () {
|
||||
this.dropdown_content = ViewFactory.createByTemplate(template, {
|
||||
items: this.collection.toJSON(),
|
||||
unreadItems: this.collection.filter(item => !item.get('opened')).length
|
||||
});
|
||||
this.addView(this.dropdown_content);
|
||||
|
||||
this.$('.js-content').html(this.dropdown_content.render().el);
|
||||
},
|
||||
|
||||
_checkScroll: function () {
|
||||
// we need to wait until dropdown has appeared,
|
||||
// then if it is taller than 300px we wrap the content in a ScrollView,
|
||||
// this is a fix for IE11, which needs a fixed height when using flex in a child element
|
||||
setTimeout(function () {
|
||||
if (this.$el.height() >= 300) {
|
||||
var view = new ScrollView({
|
||||
createContentView: function () {
|
||||
return this.dropdown_content;
|
||||
}.bind(this)
|
||||
});
|
||||
this.addView(view);
|
||||
|
||||
this.$el.addClass('Dropdown--withScroll');
|
||||
this.$('.js-content').html(view.render().el);
|
||||
}
|
||||
}.bind(this), 301);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
// TODO: Handle event
|
||||
// cdb.god.bind('closeDialogs', this.hide, this);
|
||||
}
|
||||
});
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
const Backbone = require('backbone');
|
||||
const UserNotificationModel = require('./user-notification-model');
|
||||
const OrganizationNotificationModel = require('./organization-notification-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* User notification default collection, it will
|
||||
* require the user notification model
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
model: function (attrs, options) {
|
||||
return attrs.type === 'org_notification'
|
||||
? new OrganizationNotificationModel(attrs, {
|
||||
...options,
|
||||
configModel: options.collection._configModel
|
||||
})
|
||||
: new UserNotificationModel(attrs);
|
||||
}
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
const UserNotificationModel = require('./user-notification-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userId',
|
||||
'configModel',
|
||||
'apiKey'
|
||||
];
|
||||
|
||||
/**
|
||||
* User notification default model
|
||||
*/
|
||||
|
||||
module.exports = UserNotificationModel.extend({
|
||||
url: function () {
|
||||
return `/api/v3/users/${this._userId}/notifications/${this.id}?api_key=${this._apiKey}`;
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
markAsRead: function () {
|
||||
this.save({
|
||||
notification: {
|
||||
read_at: new Date()
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Hey <strong><%- userName %></strong>, looks like you're about to reach your account limit.
|
||||
<% if (userType === "admin") { %>
|
||||
<a href="mailto:<%- upgradeContactEmail %>">Contact us</a> for upgrading your account.
|
||||
<% } %>
|
||||
<% if (userType === "org") { %>
|
||||
Start thinking about <a href="mailto:<%- upgradeContactEmail %>">contacting your admin</a>.
|
||||
<% } %>
|
||||
<% if (userType === "regular") { %>
|
||||
Start thinking about <a href="<%- upgradeUrl %>?utm_source=Dashboard_Limits_Nearing&utm_medium=referral&utm_campaign=Upgrade_from_Dashboard&utm_content=upgrading%20your%20plan" class ="underline">upgrading your plan</a>.
|
||||
<% } %>
|
||||
<% if (userType === "internal") { %>
|
||||
Feel free to <a href="mailto:<%- upgradeContactEmail %>">contact us</a> for more resources.
|
||||
<% } %>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<ul class="NotificationsDropdown ">
|
||||
<% if (items.length > 0) { %>
|
||||
<% _.each(items, function(item){ %>
|
||||
<% if (!item.opened) { %>
|
||||
<li class="u-flex NotificationsDropdown-item is-new CDB-Text CDB-Size-medium">
|
||||
<i class="u-hintTextColor u-flex u-alignCenter u-justifyCenter NotificationsDropdown-icon CDB-IconFont <%- item.iconFont %> <%- item.severity %>"></i>
|
||||
<p class="NotificationsDropdown-text u-altTextColor"><%= cdb.core.sanitize.html(item.msg) %></p>
|
||||
</li>
|
||||
<% } %>
|
||||
<% }) %>
|
||||
|
||||
<% _.each(items, function(item){ %>
|
||||
<% if (item.opened) { %>
|
||||
<li class="u-flex NotificationsDropdown-item CDB-Text CDB-Size-medium">
|
||||
<i class="u-hintTextColor u-flex u-alignCenter u-justifyCenter NotificationsDropdown-icon CDB-IconFont <%- item.iconFont %>"></i>
|
||||
<p class="NotificationsDropdown-text u-altTextColor"><%= cdb.core.sanitize.html(item.msg) %></p>
|
||||
</li>
|
||||
<% } %>
|
||||
<% }) %>
|
||||
|
||||
<% } else { %>
|
||||
<li class="u-flex u-alignCenter NotificationsDropdown-item NotificationsDropdown-item--no-notifications CDB-Text CDB-Size-medium">
|
||||
<p class="NotificationsDropdown-text u-altTextColor">There are no notifications :)</p>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
+1
@@ -0,0 +1 @@
|
||||
<div class="Dropdown-content Dropdown-content--withScroll js-content"></div>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Hey <strong><%- userName %></strong>, you're over your disk limits.
|
||||
<% if (userType === "admin") { %>
|
||||
<a href="mailto:<%- upgradeContactEmail %>">Contact us</a> for upgrading your account.
|
||||
<% } %>
|
||||
<% if (userType === "org") { %>
|
||||
Start thinking about <a href="mailto:<%- upgradeContactEmail %>">contacting your admin</a>.
|
||||
<% } %>
|
||||
<% if (userType === "regular") { %>
|
||||
Start thinking about <a href="<%- upgradeUrl %>?utm_source=Dashboard_Limits_Nearing&utm_medium=referral&utm_campaign=Upgrade_from_Dashboard&utm_content=upgrading%20your%20plan" class ="underline">upgrading your plan</a>.
|
||||
<% } %>
|
||||
<% if (userType === "internal") { %>
|
||||
Feel free to <a href="mailto:<%- upgradeContactEmail %>">contact us</a> for more resources.
|
||||
<% } %>
|
||||
+1
@@ -0,0 +1 @@
|
||||
Just a reminder, your <strong><%- accountType %></strong> trial will finish the next <%- trialEnd %>. Happy mapping!
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Start your trial to experience the full CARTO.
|
||||
<br/>
|
||||
<a href="<%- upgradeUrl %>">Start now</a>
|
||||
+1
@@ -0,0 +1 @@
|
||||
Welcome to your brand new <strong><%- accountType %></strong> CARTO. Now we love you even more than before ;)
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<i class="UserNotifications-Icon CDB-IconFont CDB-IconFont-alert"></i>
|
||||
|
||||
<% if (notificationsCount > 0) { %>
|
||||
<span class="Badge UserNotifications-badge"><%- notificationsCount %></span>
|
||||
<% } %>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
const Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* User notification default model
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
type: '',
|
||||
message: '',
|
||||
opened: false
|
||||
}
|
||||
});
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
const $ = require('jquery');
|
||||
const moment = require('moment');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const UserNotificationsCollection = require('./notifications-collection');
|
||||
const NotificationsDropdown = require('./dropdown-view');
|
||||
const template = require('./templates/user-notifications.tpl');
|
||||
const dropdownTemplate = require('./templates/dropdown.tpl');
|
||||
const LocalStorage = require('../../../helpers/local-storage');
|
||||
const checkAndBuildOpts = require('../../../../builder/helpers/required-opts');
|
||||
|
||||
const TEMPLATES = {
|
||||
tryTrial: require('./templates/try-trial.tpl'),
|
||||
limitsExceeded: require('./templates/limits-exceeded.tpl'),
|
||||
closeLimits: require('./templates/close-limits.tpl'),
|
||||
upgradedMessage: require('./templates/upgraded-message.tpl'),
|
||||
trialEndsSoon: require('./templates/trial-ends-soon.tpl')
|
||||
};
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'user'
|
||||
];
|
||||
|
||||
/**
|
||||
* User notifactions view used to show alerts from the application
|
||||
*
|
||||
* In storage we will check these attributes, managed by a collection:
|
||||
*
|
||||
* try_trial -> trial_end_at is null && user is not paid user
|
||||
* limits_exceeded -> check table quota size
|
||||
* close_limits -> check table quota size < 80%
|
||||
* upgraded -> check upgraded_at less than one week
|
||||
* trial_ends_soon -> trial_end_at is not null and it is close to be finished
|
||||
* new_dashboard -> new dashboard
|
||||
* notification -> check notification
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
attributes: {
|
||||
href: '#/notifications'
|
||||
},
|
||||
|
||||
tagName: 'a',
|
||||
className: 'UserNotifications',
|
||||
|
||||
events: {
|
||||
'click': '_openNotifications'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this.localStorage = new LocalStorage();
|
||||
this.collection = new UserNotificationsCollection({
|
||||
configModel: options.configModel
|
||||
});
|
||||
this.collection.reset(this._generateCollection(), {
|
||||
userId: this._user.get('id'),
|
||||
apiKey: this._user.get('api_key'),
|
||||
silent: true
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var notificationsCount = this.collection.filter(item => !item.get('opened')).length;
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
notificationsCount: notificationsCount
|
||||
})
|
||||
);
|
||||
|
||||
this.$el.toggleClass('has--alerts', notificationsCount > 0);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this._user.bind('change', this._onUserChange, this);
|
||||
this.collection.bind('reset', this.render, this);
|
||||
this.collection.bind('remove', this.render, this);
|
||||
this.add_related_model(this._user);
|
||||
this.add_related_model(this.collection);
|
||||
},
|
||||
|
||||
_onUserChange: function () {
|
||||
// When api is ready, we will make a valid fetch :)
|
||||
this.collection.reset(this._generateCollection(), {
|
||||
userId: this._user.get('id'),
|
||||
apiKey: this._user.get('api_key')
|
||||
});
|
||||
this.render();
|
||||
},
|
||||
|
||||
// This method will check notifications and create a collection with them
|
||||
// Also it will check if those have been opened or not with Local Storage.
|
||||
_generateCollection: function () {
|
||||
var arr = [];
|
||||
var data = {}; // data
|
||||
var userUrl = this._user.viewUrl();
|
||||
var comHosted = this._configModel.get('cartodb_com_hosted');
|
||||
|
||||
data.isInsideOrg = this._user.isInsideOrg();
|
||||
data.isOrgOwner = this._user.isOrgOwner();
|
||||
data.accountType = this._user.get('account_type').toLowerCase();
|
||||
data.remainingQuota = this._user.get('remaining_byte_quota');
|
||||
data.publicProfileUrl = userUrl.publicProfile();
|
||||
data.bytesQuota = this._user.get('quota_in_bytes');
|
||||
data.userType = 'regular';
|
||||
data.upgradeUrl = window.upgrade_url || '';
|
||||
data.upgradeContactEmail = this._user.upgradeContactEmail();
|
||||
data.trialEnd = this._user.get('trial_ends_at') && moment(this._user.get('trial_ends_at')).format('YYYY-MM-DD');
|
||||
data.userName = this._user.get('name') || this._user.get('username');
|
||||
|
||||
// Get user type
|
||||
if (data.isInsideOrg && !data.isOrgOwner) {
|
||||
data.userType = 'org';
|
||||
} else if (data.isOrgOwner) {
|
||||
data.userType = 'admin';
|
||||
} else if (data.accountType === 'internal' || data.accountType === 'partner' || data.accountType === 'ambassador') {
|
||||
data.userType = 'internal';
|
||||
}
|
||||
|
||||
// try_trial -> trial_end_at is null && user is not paid user
|
||||
if (!comHosted && !data.isInsideOrg && data.accountType === 'free' && this._user.get('table_count') > 0) {
|
||||
arr.push({
|
||||
iconFont: 'CDB-IconFont-gift',
|
||||
severity: 'NotificationsDropdown-itemIcon--positive',
|
||||
type: 'try_trial',
|
||||
msg: TEMPLATES.tryTrial(data),
|
||||
opened: this.localStorage.get('notification.try_trial')
|
||||
});
|
||||
} else {
|
||||
this.localStorage.remove('notification.try_trial');
|
||||
}
|
||||
|
||||
// limits_exceeded -> check table quota size
|
||||
if (!comHosted && data.bytesQuota > 0 && data.remainingQuota <= 0) {
|
||||
arr.push({
|
||||
iconFont: 'CDB-IconFont-barometer',
|
||||
severity: 'NotificationsDropdown-itemIcon--negative',
|
||||
type: 'limits_exceeded',
|
||||
msg: TEMPLATES.limitsExceeded(data),
|
||||
opened: this.localStorage.get('notification.limits_exceeded')
|
||||
});
|
||||
} else {
|
||||
this.localStorage.remove('notification.limits_exceeded');
|
||||
}
|
||||
|
||||
// close_limits -> check table quota size < 80%
|
||||
if (!comHosted && data.bytesQuota > 0 && ((data.remainingQuota * 100) / data.bytesQuota) < 20) {
|
||||
arr.push({
|
||||
iconFont: 'CDB-IconFont-barometer',
|
||||
severity: 'NotificationsDropdown-itemIcon--alert',
|
||||
type: 'close_limits',
|
||||
msg: TEMPLATES.closeLimits(data),
|
||||
opened: this.localStorage.get('notification.close_limits')
|
||||
});
|
||||
} else {
|
||||
this.localStorage.remove('notification.close_limits');
|
||||
}
|
||||
|
||||
// upgraded -> check upgraded_at less than ... one week?
|
||||
if (!comHosted && this._user.get('show_upgraded_message')) {
|
||||
arr.push({
|
||||
iconFont: 'CDB-IconFont-heartFill',
|
||||
severity: 'NotificationsDropdown-itemIcon--positive',
|
||||
type: 'upgraded_message',
|
||||
msg: TEMPLATES.upgradedMessage(data),
|
||||
opened: this.localStorage.get('notification.upgraded_message')
|
||||
});
|
||||
} else {
|
||||
this.localStorage.remove('notification.upgraded_message');
|
||||
}
|
||||
|
||||
// trial_ends_soon -> show_trial_reminder flag
|
||||
if (this._user.get('show_trial_reminder')) {
|
||||
arr.push({
|
||||
iconFont: 'CDB-IconFont-clock',
|
||||
severity: 'NotificationsDropdown-itemIcon--alert',
|
||||
type: 'trial_ends_soon',
|
||||
msg: TEMPLATES.trialEndsSoon(data),
|
||||
opened: this.localStorage.get('notification.trial_ends_soon')
|
||||
});
|
||||
} else {
|
||||
this.localStorage.remove('notification.trial_ends_soon');
|
||||
}
|
||||
|
||||
const organizationNotifications = window.organization_notifications || this.options.organizationNotifications;
|
||||
|
||||
if (organizationNotifications) {
|
||||
for (var n = 0; n < organizationNotifications.length; n++) {
|
||||
var notification = organizationNotifications[n];
|
||||
var icon = notification.icon ? ('CDB-IconFont-' + notification.icon) : 'CDB-IconFont-alert';
|
||||
|
||||
arr.push({
|
||||
iconFont: icon,
|
||||
severity: 'NotificationsDropdown-itemIcon--alert',
|
||||
id: notification.id,
|
||||
msg: notification.html_body,
|
||||
read_at: notification.read_at,
|
||||
type: 'org_notification'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
},
|
||||
|
||||
_openNotifications: function (event) {
|
||||
if (event) this.killEvent(event);
|
||||
|
||||
if (this.notification) {
|
||||
this.notification.hide();
|
||||
delete this.notification;
|
||||
return this;
|
||||
}
|
||||
|
||||
var view = this.notification = new NotificationsDropdown({
|
||||
target: this.$el,
|
||||
collection: this.collection,
|
||||
horizontal_offset: 5,
|
||||
vertical_offset: -5,
|
||||
template: dropdownTemplate
|
||||
});
|
||||
|
||||
$(view.options.target).unbind('click', view._handleClick);
|
||||
this._closeAnyOtherOpenDialogs();
|
||||
|
||||
view.on('onDropdownHidden', () => this._onDropdownHidden(view));
|
||||
|
||||
view.render();
|
||||
view.open();
|
||||
|
||||
this.addView(view);
|
||||
},
|
||||
|
||||
_onDropdownHidden: function (view) {
|
||||
// All notifications have been seen, opened -> true
|
||||
this.collection.each(notification => {
|
||||
const notificationType = notification.get('type');
|
||||
|
||||
if (notificationType === 'org_notification') {
|
||||
notification.markAsRead();
|
||||
} else if (notificationType) {
|
||||
notification.set('opened', true);
|
||||
this.localStorage.set({
|
||||
[`notification.${notificationType}`]: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Clean collection because all notifications should
|
||||
// removed from the collection
|
||||
this.collection.reset();
|
||||
|
||||
// Clean dropdown
|
||||
view.clean();
|
||||
// Remove it from subviews
|
||||
this.removeView(view);
|
||||
// Remove count
|
||||
this.$el.removeClass('has--alerts');
|
||||
// No local notification set
|
||||
delete this.notification;
|
||||
},
|
||||
|
||||
_closeAnyOtherOpenDialogs: function () {
|
||||
// cdb.god.trigger("closeDialogs"); TODO: handle event
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
<div class="u-inner Header-inner">
|
||||
<div class="Header-navigation">
|
||||
<ul class="Header-navigationList">
|
||||
<li class="js-logo"></li>
|
||||
<li>
|
||||
<ul class="Header-navigationBreadcrumb">
|
||||
<% if (organizationName) { %>
|
||||
<li class="Header-navigationBreadcrumbItem CDB-Text CDB-Size-large"><p class="Header-navigationBreadcrumbParagraph"><%= organizationName %></p> /</li>
|
||||
<% } %>
|
||||
<li class="Header-navigationBreadcrumbItem CDB-Text CDB-Size-large">
|
||||
<p class="Header-navigationBreadcrumbParagraph"><a href="<%= homeUrl %>" class="Header-navigationBreadcrumbLink"><%= nameOrUsername %></a></p> /
|
||||
</li>
|
||||
<li class="Header-navigationBreadcrumbItem js-breadcrumb-dropdown CDB-Text CDB-Size-large"></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="Header-settings">
|
||||
<ul class="Header-settingsList CDB-Text CDB-Size-medium">
|
||||
<% if (!isCartoDBHosted) { %>
|
||||
<li class="Header-settingsItem">
|
||||
<a target="_blank" href="https://carto.com/learn/guides" class="CDB-Text is-semibold Header-settingsLink Header-settingsLink--dashboard">Guides</a>
|
||||
</li>
|
||||
<li class="Header-settingsItem">
|
||||
<a target="_blank" href="https://carto.com/developers" class="CDB-Text is-semibold Header-settingsLink Header-settingsLink--dashboard">Developers</a>
|
||||
</li>
|
||||
<% } %>
|
||||
<li class="Header-settingsItem Header-settingsItemNotifications js-user-notifications">
|
||||
<button class="UserNotifications">
|
||||
<i class="UserNotifications-Icon CDB-IconFont CDB-IconFont-Alert"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li class="Header-settingsItem Header-settingsItem--avatar">
|
||||
<button class="UserAvatar js-settings-dropdown">
|
||||
<img src="<%= avatar %>" class="UserAvatar-img UserAvatar-img--medium">
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const DropdownAdminView = require('dashboard/components/dropdown/dropdown-admin-view');
|
||||
const Utils = require('builder/helpers/utils');
|
||||
const template = require('./settings-dropdown.tpl');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'model',
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* The content of the dropdown menu opened by the user avatar in the top-right of the header, e.g.:
|
||||
* Explore, Learn, ♞
|
||||
* ______/\____
|
||||
* | |
|
||||
* | this |
|
||||
* |____________|
|
||||
*/
|
||||
|
||||
module.exports = DropdownAdminView.extend({
|
||||
className: 'Dropdown',
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
DropdownAdminView.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
shortDisplayName: function (user) {
|
||||
// This changes should also be done in Central, ./app/assets/javascripts/dashboard/users/views/user_avatar.js
|
||||
var accountTypeDisplayName = user.get('account_type_display_name');
|
||||
var displayName = _.isUndefined(accountTypeDisplayName) ? user.get('account_type') : accountTypeDisplayName;
|
||||
|
||||
if (_.isUndefined(displayName)) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
displayName = displayName.toLowerCase();
|
||||
|
||||
if (displayName === 'organization user') {
|
||||
return 'org. user';
|
||||
} else {
|
||||
return displayName.replace(/lump-sum/gi, '- A')
|
||||
.replace(/academic/gi, 'aca.')
|
||||
.replace(/ - Monthly/i, ' - M')
|
||||
.replace(/ - Annual/i, ' - A')
|
||||
.replace(/Non-Profit/i, 'NP')
|
||||
.replace(/On-premises/i, 'OP')
|
||||
.replace(/Internal use engine/i, 'engine')
|
||||
.replace(/Lite/i, 'L')
|
||||
.replace(/Cloud Engine &/i, 'C. Engine &')
|
||||
.replace(/& Enterprise Builder/i, '& E. Builder')
|
||||
.replace(/CARTO for /i, '')
|
||||
.replace(/CARTO /i, '');
|
||||
}
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var user = this.model;
|
||||
var usedDataBytes = user.get('db_size_in_bytes');
|
||||
var quotaInBytes = user.get('quota_in_bytes');
|
||||
var usedDataPct = Math.round(usedDataBytes / quotaInBytes * 100);
|
||||
var progressBarClass = '';
|
||||
|
||||
if (usedDataPct > 80 && usedDataPct < 90) {
|
||||
progressBarClass = 'is--inAlert';
|
||||
} else if (usedDataPct > 89) {
|
||||
progressBarClass = 'is--inDanger';
|
||||
}
|
||||
|
||||
var accountType = this.shortDisplayName(user);
|
||||
|
||||
var userUrl = this.model.viewUrl();
|
||||
var upgradeUrl = window.upgrade_url || this._configModel.get('upgrade_url') || '';
|
||||
|
||||
this.$el.html(template({
|
||||
name: user.fullName() || user.get('username'),
|
||||
email: user.get('email'),
|
||||
accountType: accountType,
|
||||
isOrgAdmin: user.isOrgAdmin(),
|
||||
usedDataStr: Utils.readablizeBytes(usedDataBytes),
|
||||
usedDataPct: usedDataPct,
|
||||
progressBarClass: progressBarClass,
|
||||
availableDataStr: Utils.readablizeBytes(quotaInBytes),
|
||||
showUpgradeLink: upgradeUrl && (user.isOrgOwner() || !user.isInsideOrg()) && !this._configModel.get('cartodb_com_hosted'),
|
||||
upgradeUrl: upgradeUrl,
|
||||
publicProfileUrl: userUrl.publicProfile(),
|
||||
apiKeysUrl: userUrl.apiKeys(),
|
||||
organizationUrl: userUrl.organization(),
|
||||
accountProfileUrl: userUrl.accountProfile(),
|
||||
logoutUrl: userUrl.logout(),
|
||||
isViewer: user.isViewer(),
|
||||
isBuilder: user.isBuilder(),
|
||||
orgDisplayEmail: user.isInsideOrg() ? user.organization.display_email : null,
|
||||
engineEnabled: user.get('actions').engine_enabled,
|
||||
mobileAppsEnabled: user.get('actions').mobile_sdk_enabled
|
||||
}));
|
||||
|
||||
// Necessary to hide dialog on click outside popup, for example.
|
||||
// TODO: Handle closeDialogs
|
||||
// cdb.god.bind('closeDialogs', this.hide, this);
|
||||
|
||||
// TODO: taken from existing code, how should dropdowns really be added to the DOM?
|
||||
$('body').append(this.el);
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
<ul class="SettingsDropdown">
|
||||
<li>
|
||||
<div class="SettingsDropdown-sameline">
|
||||
<p class="CDB-Text CDB-Size-medium"><%- name %></p>
|
||||
<p class="SettingsDropdown-accountType CDB-Text CDB-Size-small u-altTextColor u-upperCase"><%- accountType %></p>
|
||||
</div>
|
||||
<p class="CDB-Text CDB-Size-medium u-altTextColor u-tSpace u-ellipsis">
|
||||
<%- email %>
|
||||
</p>
|
||||
</li>
|
||||
<li class="u-tSpace-xl">
|
||||
<p class="SettingsDropdown-userRole">
|
||||
<% if (isViewer) { %>
|
||||
<span class="UserRoleIndicator Viewer CDB-Text CDB-Size-small is-semibold u-altTextColor">VIEWER</span>
|
||||
<% if (orgDisplayEmail) { %>
|
||||
<a href="mailto:<%- orgDisplayEmail %>" class="CDB-Text CDB-Size-small">Become a Builder</a>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<% if (isBuilder) { %>
|
||||
<span class="UserRoleIndicator Builder CDB-Text CDB-Size-small is-semibold u-altTextColor">BUILDER</span>
|
||||
<% } %>
|
||||
</p>
|
||||
</li>
|
||||
<li class="u-tSpace-xl">
|
||||
<% if (showUpgradeLink) { %>
|
||||
<a href="<%- upgradeUrl %>" class="SettingsDropdown-itemLink">
|
||||
<% } %>
|
||||
|
||||
<div class="SettingsDropdown-sameline u-bSpace CDB-Text CDB-Size-medium u-altTextColor">
|
||||
<p class="DefaultDescription"><%- usedDataStr %> of <%- availableDataStr %> used</p>
|
||||
<% if (showUpgradeLink) { %>
|
||||
<p class="SettingsDropdown-itemLinkText u-actionTextColor">Upgrade</p>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="SettingsDropdown-progressBar <%- progressBarClass %>">
|
||||
<div class="progress-bar">
|
||||
<span class="bar-2" style="width: <%- usedDataPct %>%"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (showUpgradeLink) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="BreadcrumbsDropdown-listItem is-dark CDB-Text CDB-Size-medium">
|
||||
<ul>
|
||||
<li class="u-bSpace--m"><a href="<%- publicProfileUrl %>">View your public profile</a></li>
|
||||
<li class="u-bSpace--m"><a href="<%- accountProfileUrl %>">Your account</a></li>
|
||||
<% if (isOrgAdmin) { %>
|
||||
<li class="u-bSpace--m"><a href="<%- organizationUrl %>">Your organization</a></li>
|
||||
<% } %>
|
||||
<% if (engineEnabled || mobileAppsEnabled) { %>
|
||||
<li class="u-bSpace--m"><a href="<%- apiKeysUrl %>">Your API keys</a></li>
|
||||
<% } %>
|
||||
<li><a href="<%- logoutUrl %>">Close session</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,42 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./user-support.tpl');
|
||||
const checkAndBuildOpts = require('../../../builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* View to render the user support link in the header.
|
||||
* Expected to be created from existing DOM element.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
userType: this._getUserType()
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_getUserType: function () {
|
||||
var accountType = this._userModel.get('account_type').toLowerCase();
|
||||
|
||||
if (this._userModel.isInsideOrg()) {
|
||||
return 'org';
|
||||
} else if (accountType === 'internal' || accountType === 'partner' || accountType === 'ambassador') {
|
||||
return 'internal';
|
||||
} else if (accountType !== 'free') {
|
||||
return 'client';
|
||||
} else {
|
||||
return 'regular';
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
<% if (userType === 'org') { %>
|
||||
<a href="mailto:enterprise-support@carto.com" class="Header-navigationLink u-hideOnMobile">Support</a>
|
||||
<% } else if (userType === 'client' || userType === 'internal') { %>
|
||||
<a href="mailto:support@carto.com" class="Header-navigationLink u-hideOnMobile">Support</a>
|
||||
<% } else { %>
|
||||
<a href="http://gis.stackexchange.com/questions/tagged/carto" class="Header-navigationLink u-hideOnMobile">Support</a>
|
||||
<% } %>
|
||||
@@ -0,0 +1,97 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./delete-account.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'modalModel',
|
||||
'userModel',
|
||||
'client'
|
||||
];
|
||||
|
||||
/**
|
||||
* When user wants to delete his own account
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'click .js-ok': '_onClickDelete',
|
||||
'click .js-cancel': '_closeDialog',
|
||||
'submit .js-form': '_closeDialog'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
CoreView.prototype.initialize.apply(this);
|
||||
|
||||
this._onError = options.onError;
|
||||
this._error = '';
|
||||
|
||||
this._isLoading = false;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
passwordNeeded: !!this._userModel.get('needs_password_confirmation'),
|
||||
isLoading: this._isLoading,
|
||||
error: this._error
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_onClickDelete: function (event) {
|
||||
this.killEvent(event);
|
||||
|
||||
const params = {
|
||||
deletion_password_confirmation: this.$('#deletion_password_confirmation').val()
|
||||
};
|
||||
|
||||
this._isLoading = true;
|
||||
this._error = '';
|
||||
|
||||
this.render();
|
||||
|
||||
this._client.deleteUser(params, (errors, response, data) => {
|
||||
this._isLoading = false;
|
||||
this.render();
|
||||
|
||||
if (errors) {
|
||||
this._handleError(data, errors);
|
||||
} else {
|
||||
this._onSuccess(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_handleError: function (data, errors) {
|
||||
if (this._onError) {
|
||||
this._closeDialog();
|
||||
this._onError(data, errors);
|
||||
} else {
|
||||
const jsonData = data && data.responseJSON || {};
|
||||
|
||||
this._error = jsonData.message;
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
_setHref: function (href) {
|
||||
window.location.href = href;
|
||||
},
|
||||
|
||||
_onSuccess: function (data) {
|
||||
this._setHref(data.logout_url);
|
||||
this._closeDialog();
|
||||
},
|
||||
|
||||
_onFormError: function (data, errors) {
|
||||
this._onError(data, errors);
|
||||
this._closeDialog();
|
||||
},
|
||||
|
||||
_closeDialog: function () {
|
||||
this._modalModel.destroy();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
<form accept-charset="UTF-8" class="js-form">
|
||||
<div class="CDB-Text Dialog-header u-inner">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--negative">
|
||||
<i class="CDB-IconFont CDB-IconFont-defaultUser"></i>
|
||||
</div>
|
||||
<p class="Dialog-headerTitle">You are about to delete your account.</p>
|
||||
<p class="Dialog-headerText">
|
||||
Remember, once you delete your account there is no going back.<br/>
|
||||
All your maps, data and work will be lost. Are you sure you want to proceed?<br/>
|
||||
<% if (passwordNeeded) { %>
|
||||
In any case, you need to type your password.
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<% if (passwordNeeded) { %>
|
||||
<div class="CDB-Text Dialog-body">
|
||||
<div class="Form-row Form-row--centered has-label">
|
||||
<div class="Form-rowLabel">
|
||||
<label class="Form-label">Your password</label>
|
||||
</div>
|
||||
<div class="Form-rowData">
|
||||
<input
|
||||
type="password"
|
||||
id="deletion_password_confirmation"
|
||||
name="deletion_password_confirmation"
|
||||
class="CDB-InputText CDB-Text Form-input Form-input--long <%- isLoading ? 'is-disabled' : '' %>"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (error) { %>
|
||||
<p class="CDB-Text CDB-Size-medium u-errorTextColor u-flex u-justifyCenter"><%- error %></p>
|
||||
<% } %>
|
||||
|
||||
<div class="Dialog-footer u-inner">
|
||||
<button type="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 type="submit" class="CDB-Button CDB-Button--error js-ok">
|
||||
<% if (isLoading) { %>
|
||||
<div class="CDB-LoaderIcon CDB-LoaderIcon--small u-iBlock">
|
||||
<svg class="CDB-LoaderIcon-spinner" viewBox="0 0 50 50">
|
||||
<circle class="CDB-LoaderIcon-path" cx="25" cy="25" r="20" fill="none"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Yes, delete my account</span>
|
||||
<% } %>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,81 @@
|
||||
var $ = require('jquery');
|
||||
var DropdownBaseView = require('./dropdown-base-view');
|
||||
|
||||
module.exports = DropdownBaseView.extend({
|
||||
|
||||
show: function () {
|
||||
var dfd = $.Deferred();
|
||||
var self = this;
|
||||
// sometimes this dialog is child of a node that is removed
|
||||
// for that reason we link again DOM events just in case
|
||||
this.delegateEvents();
|
||||
this.$el
|
||||
.css({
|
||||
marginTop: self.options.verticalPosition === 'down' ? '-10px' : '10px',
|
||||
opacity: 0,
|
||||
display: 'block'
|
||||
})
|
||||
.animate({
|
||||
margin: '0',
|
||||
opacity: 1
|
||||
}, {
|
||||
'duration': this.options.speedIn,
|
||||
'complete': function () {
|
||||
dfd.resolve();
|
||||
}
|
||||
});
|
||||
this.trigger('onDropdownShown', this.el);
|
||||
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
/**
|
||||
* open the dialog at x, y
|
||||
*/
|
||||
openAt: function (x, y) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
this.$el.css({
|
||||
top: y,
|
||||
left: x,
|
||||
width: this.options.width
|
||||
})
|
||||
.addClass(
|
||||
(this.options.verticalPosition === 'up' ? 'vertical_top' : 'vertical_bottom') + ' ' +
|
||||
(this.options.horizontalPosition === 'right' ? 'horizontal_right' : 'horizontal_left') + ' ' +
|
||||
// Add tick class
|
||||
'tick_' + this.options.tick
|
||||
);
|
||||
|
||||
this.modelView.set({open: true});
|
||||
|
||||
// Show
|
||||
$.when(this.show()).done(function () {
|
||||
dfd.resolve();
|
||||
});
|
||||
// xabel: I've add the deferred to make it easily testable
|
||||
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
hide: function (done) {
|
||||
// don't attempt to hide the dropdown if it's already hidden
|
||||
if (!this.isOpen) { done && done(); return; }
|
||||
|
||||
var self = this;
|
||||
|
||||
this.$el.animate({
|
||||
marginTop: self.options.verticalPosition === 'down' ? '10px' : '-10px',
|
||||
opacity: 0
|
||||
}, this.options.speedOut, function () {
|
||||
// Remove selected class
|
||||
$(self.options.target).removeClass('selected');
|
||||
|
||||
// And hide it
|
||||
self.$el.hide();
|
||||
done && done();
|
||||
|
||||
self.trigger('onDropdownHidden', self.el);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
var $ = require('jquery');
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
|
||||
var DEFAULTS = {
|
||||
width: 160,
|
||||
speedIn: 150,
|
||||
speedOut: 300,
|
||||
verticalPosition: 'down',
|
||||
horizontalPosition: 'right',
|
||||
tick: 'right',
|
||||
verticalOffset: 0,
|
||||
horizontalOffset: 0
|
||||
};
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'dropdown',
|
||||
|
||||
initialize: function (options) {
|
||||
_.bindAll(this, 'open', 'hide', '_handleClick', '_keydown', '_onDocumentClick');
|
||||
|
||||
this.options = {};
|
||||
// Extend options
|
||||
_.defaults(this.options, options, DEFAULTS);
|
||||
|
||||
if (options.template) {
|
||||
this.template = options.template;
|
||||
}
|
||||
|
||||
// Bind to target
|
||||
$(options.target).on('click', this._handleClick);
|
||||
$(document).on('keydown', this._keydown);
|
||||
$(document).on('click', this._onDocumentClick);
|
||||
|
||||
this.modelView = new Backbone.Model({
|
||||
open: false
|
||||
});
|
||||
|
||||
this.modelView.on('change:open', function (model, isOpen) {
|
||||
isOpen ? this.hide() : this.open();
|
||||
}, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
// Render
|
||||
var $el = this.$el;
|
||||
$el
|
||||
.html(this.template && this.template(this.options))
|
||||
.css({
|
||||
width: this.options.width
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
_handleClick: function (event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
var isOpen = this.modelView.get('open');
|
||||
this.modelView.set('open', !isOpen);
|
||||
},
|
||||
|
||||
_onDocumentClick: function (e) {
|
||||
var $el = $(e.target);
|
||||
var $target = $(this.options.target);
|
||||
var isTarget = $el.get(0) === $target.get(0);
|
||||
if (!isTarget && $el.closest('.Dropdown').length === 0) {
|
||||
this.modelView.set({open: false}, {silent: true});
|
||||
this.hide();
|
||||
}
|
||||
},
|
||||
|
||||
_keydown: function (event) {
|
||||
if (event.keyCode === 27) {
|
||||
this.modelView.set('open', false);
|
||||
}
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.$el.hide();
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.$el.css({
|
||||
display: 'block',
|
||||
opacity: 1
|
||||
});
|
||||
},
|
||||
|
||||
open: function (event, target) {
|
||||
// Target
|
||||
var $target = target && $(target) || this.options.target;
|
||||
this.options.target = $target;
|
||||
|
||||
// Positionate
|
||||
var targetPos = $target[this.options.position || 'offset']();
|
||||
var targetWidth = $target.outerWidth();
|
||||
var targetHeight = $target.outerHeight();
|
||||
var elementWidth = this.$el.outerWidth();
|
||||
var elementHeight = this.$el.outerHeight();
|
||||
var verticalPosition = this.options.verticalPosition;
|
||||
var verticalOffset = this.options.verticalOffset;
|
||||
var horizontalPosition = this.options.horizontalPosition;
|
||||
var horizontalOffset = this.options.horizontalOffset;
|
||||
|
||||
this.$el.css({
|
||||
top: targetPos.top + parseInt((verticalPosition === 'up') ? (-elementHeight - 10 - verticalOffset) : (targetHeight + 10 - verticalOffset)),
|
||||
left: targetPos.left + parseInt((horizontalPosition === 'left') ? (horizontalOffset - 15) : (targetWidth - elementWidth + 15 - horizontalOffset))
|
||||
})
|
||||
.addClass(
|
||||
// Add vertical and horizontal position class
|
||||
(verticalPosition === 'up' ? 'vertical_top' : 'vertical_bottom') +
|
||||
' ' +
|
||||
(horizontalPosition === 'right' ? 'horizontal_right' : 'horizontal_left') +
|
||||
' ' +
|
||||
// Add tick class
|
||||
'tick_' + this.options.tick
|
||||
);
|
||||
|
||||
this.show();
|
||||
},
|
||||
|
||||
isOpen: function () {
|
||||
return this.modelView.get('open');
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
const target = $(this.options.target);
|
||||
this.options.target && target.off('click', this._handleClick);
|
||||
$(document).off('keydown', this._keydown);
|
||||
$(document).off('click', this._onDocumentClick);
|
||||
CoreView.prototype.clean.apply(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="IntermediateInfo">
|
||||
<div class="LayoutIcon LayoutIcon--negative">
|
||||
<i class="CDB-IconFont CDB-IconFont-cockroach"></i>
|
||||
</div>
|
||||
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m u-tSpace-xl">Oouch! There has been an error</h4>
|
||||
<% if (msg) { %>
|
||||
<p class="CDB-Text CDB-Size-medium u-altTextColor"><%= msg %></p>
|
||||
<% } %>
|
||||
<p class="CDB-Text CDB-Size-medium u-altTextColor">If the problem persists contact us at <a class="js-mail-link" href="mailto:support@carto.com">support@carto.com</a>.</p>
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./flash-message.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'model'
|
||||
];
|
||||
|
||||
/**
|
||||
* View for a flash message to be displayed at the header.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.toggle(this.model.shouldDisplay());
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
str: this.model.get('msg'),
|
||||
type: this.model.get('type')
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="FlashMessage FlashMessage--<%- type %> CDB-Text">
|
||||
<div class="u-inner">
|
||||
<p class="FlashMessage-info"><%- str %></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./footer.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Decide what support block app should show
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
tagName: 'footer',
|
||||
|
||||
className: function () {
|
||||
let classes = 'CDB-Text CDB-FontSize-medium Footer';
|
||||
|
||||
if (this.options && this.options.light) {
|
||||
classes += ' Footer--light';
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
onpremiseVersion: this._configModel.get('onpremise_version'),
|
||||
isHosted: this._configModel.get('cartodb_com_hosted')
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="u-inner Footer-inner">
|
||||
<ul class="Footer-list Footer-list--primary">
|
||||
<% if (!isHosted) { %>
|
||||
<li class="Footer-listItem CDB-Text CDB-Size-medium"><a href="https://carto.com/learn/guides/">Guides</a></li>
|
||||
<li class="Footer-listItem CDB-Text CDB-Size-medium"><a href="https://carto.com/developers">Developers</a></li>
|
||||
<% } %>
|
||||
<% if (onpremiseVersion && onpremiseVersion !== "") { %>
|
||||
<li class="Footer-listItem CDB-Text CDB-Size-medium">Version: <%= onpremiseVersion %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
|
||||
<ul class="Footer-list Footer-list--secondary">
|
||||
<% if (onpremiseVersion && onpremiseVersion !== "") { %>
|
||||
<li class="Footer-listItem CDB-Text CDB-Size-medium"><a href="mailto:onpremise-support@carto.com">Support</a></li>
|
||||
<% } else { %>
|
||||
<li class="Footer-listItem CDB-Text CDB-Size-medium"><a href="mailto:support@carto.com">Support</a></li>
|
||||
<% } %>
|
||||
<li class="Footer-listItem CDB-Text CDB-Size-medium"><a href="mailto:contact@carto.com">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const template = require('./multi-checkbox.tpl');
|
||||
const EditorHelpers = require('builder/components/form-components/helpers/editor');
|
||||
|
||||
Backbone.Form.editors.MultiCheckbox = Backbone.Form.editors.Base.extend({
|
||||
events: {
|
||||
'click .js-checkbox': '_onCheckboxClick'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
Backbone.Form.editors.Base.prototype.initialize.call(this, opts);
|
||||
EditorHelpers.setOptions(this, opts);
|
||||
|
||||
this._initViews();
|
||||
},
|
||||
|
||||
validate: function () {
|
||||
if (this.options.optional) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const requiredError = {
|
||||
type: 'required',
|
||||
message: 'Required'
|
||||
};
|
||||
|
||||
return this._hasValue() ? null : requiredError;
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
disabled: this.options.editorAttrs.disabled,
|
||||
inputs: this.options.inputs,
|
||||
values: this.value
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_onCheckboxClick: function (event) {
|
||||
const { name, checked } = event.target;
|
||||
const newValue = { ...this.getValue(), [name]: checked };
|
||||
|
||||
this.setValue(newValue);
|
||||
this.trigger('change', this);
|
||||
},
|
||||
|
||||
_hasValue: function () {
|
||||
return _.some(_.values(this.value));
|
||||
}
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<div class="FormAccount-rowData">
|
||||
<% inputs.forEach(function (input) { %>
|
||||
<div
|
||||
class="ApiKeys-MultiCheckbox u-iblock CDB-Text CDB-Size-medium"
|
||||
data-name="<%- input.name %>"
|
||||
>
|
||||
<input
|
||||
class="CDB-Checkbox js-checkbox"
|
||||
type="checkbox"
|
||||
id="<%- input.name %>"
|
||||
name="<%- input.name %>"
|
||||
<%- values[input.name] ? 'checked' : '' %>
|
||||
<%- disabled ? 'disabled' : '' %>
|
||||
>
|
||||
<span class="u-iBlock CDB-Checkbox-face"></span>
|
||||
<label class="u-secondaryTextColor u-iBlock u-lSpace u-rSpace" for="<%- input.name %>"><%- input.label || input.name %></label>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
@@ -0,0 +1,2 @@
|
||||
require('builder/components/form-components/index');
|
||||
require('./editors/multi-checkbox/multi-checkbox');
|
||||
@@ -0,0 +1,122 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./icon-selector.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
require('filestyle');
|
||||
|
||||
var AssetModel = require('dashboard/data/asset-model.js');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'renderModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Change and preview new mobile app icon
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
options: {
|
||||
acceptedExtensions: ['jpeg', 'jpg', 'png', 'gif'],
|
||||
imageKind: 'mobileAppIcon',
|
||||
imageURLAttribute: 'icon_url'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this._model = new Backbone.Model({ state: 'idle' });
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this._destroyFileInput();
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
state: this._model.get('state'),
|
||||
name: this._renderModel.get('name'),
|
||||
inputName: this._renderModel.get('inputName'),
|
||||
iconURL: this._renderModel.get('icon_url'),
|
||||
iconAcceptedExtensions: this._formatAcceptedExtensions(this.options.acceptedExtensions)
|
||||
})
|
||||
);
|
||||
this._renderFileInput();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
_.bindAll(this, '_onInputChange', '_onSuccess', '_onError');
|
||||
this.listenTo(this._model, 'change', this.render);
|
||||
},
|
||||
|
||||
_destroyFileInput: function () {
|
||||
var $file = this.$(':file');
|
||||
$file.unbind('change', this._onInputChange, this);
|
||||
$file.filestyle('destroy');
|
||||
},
|
||||
|
||||
_renderFileInput: function () {
|
||||
var $file = this.$(':file');
|
||||
var opts = { buttonText: 'Choose image' };
|
||||
|
||||
// If we set disabled, no mather if it is true
|
||||
// or false, it turns into disabled
|
||||
if (this._model.get('state') === 'loading') {
|
||||
opts.disabled = true;
|
||||
}
|
||||
|
||||
$file.filestyle(opts);
|
||||
$file.bind('change', this._onInputChange);
|
||||
},
|
||||
|
||||
_onInputChange: function () {
|
||||
var file = this.$(':file').prop('files');
|
||||
var iconUpload = new AssetModel(
|
||||
null, {
|
||||
userId: this._renderModel.get('id'),
|
||||
configModel: this._configModel
|
||||
}
|
||||
);
|
||||
|
||||
iconUpload.save({
|
||||
kind: this.options.imageKind,
|
||||
filename: file
|
||||
}, {
|
||||
success: this._onSuccess,
|
||||
error: this._onError
|
||||
});
|
||||
|
||||
// If we move "loading" state before starting the upload,
|
||||
// it would trigger a new render and "remove" file value :S
|
||||
this._model.set('state', 'loading');
|
||||
},
|
||||
|
||||
_onSuccess: function (model, data) {
|
||||
this._renderModel.set(this.options.imageURLAttribute, data.public_url);
|
||||
this._model.set('state', 'success');
|
||||
},
|
||||
|
||||
_onError: function () {
|
||||
this._model.set('state', 'error');
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._destroyFileInput();
|
||||
CoreView.prototype.clean.apply(this);
|
||||
},
|
||||
|
||||
_formatAcceptedExtensions: function (acceptedExtensions) {
|
||||
var formattedExtensions = [];
|
||||
|
||||
for (var i = 0; i < acceptedExtensions.length; i++) {
|
||||
formattedExtensions[i] = 'image/' + acceptedExtensions[i];
|
||||
}
|
||||
|
||||
return formattedExtensions.join(',');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="js-iconSelector">
|
||||
<div class="FormAccount-rowLabel">
|
||||
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor">App icon</label>
|
||||
</div>
|
||||
<div class="FormAccount-rowData FormAccount-avatar">
|
||||
<div class="FormAccount-avatarPreview">
|
||||
<% if (iconURL == null) { %>
|
||||
<div class="FormAccount-inputIcon--noIcon">No icon</div>
|
||||
<% } else { %>
|
||||
<img src="<%- iconURL %>" title="<%- name %>" alt="<%- name %>" class="FormAccount-avatarPreviewImage" />
|
||||
<% } %>
|
||||
<% if ( state === "loading" ) { %>
|
||||
<div class="FormAccount-avatarPreviewLoader">
|
||||
<div class="Spinner FormAccount-avatarPreviewSpinner"></div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<input class="js-fileIcon" type="file" value="Choose image" accept="<%- iconAcceptedExtensions %>" />
|
||||
<input class="js-inputIcon" id="mobile_app_icon_url" name="<%- inputName %>" type="hidden" value="<%- iconURL %>" />
|
||||
<div class="FormAccount-rowInfo FormAccount-rowInfo--marginLeft">
|
||||
<% if (state === "error") { %>
|
||||
<p class="FormAccount-rowInfoText FormAccount-rowInfoText--error FormAccount-rowInfoText--maxWidth">There was an error uploading the icon. Check the height and size (max 1MB) of the image</p>
|
||||
<% } else { %>
|
||||
<p class="FormAccount-rowInfoText FormAccount-rowInfoText--smaller">Recommended images should be 128x128 pixels of size</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./mamufas-import-dialog.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
/**
|
||||
* Dialog for drop actions using mamufas
|
||||
*
|
||||
*/
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'modalModel'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'Dialog-contentWrapper MamufasDialog',
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$('.Dialog-content').addClass('Dialog-content--expanded');
|
||||
this.$el.append(template());
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="CDB-Text Dialog-header MamufasDialog-header">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
|
||||
<i class="CDB-IconFont CDB-IconFont-step"></i>
|
||||
</div>
|
||||
<p class="Dialog-headerTitle">Drag and drop your data to this window</p>
|
||||
<p class="Dialog-headerText">Drop your file to connect a new dataset.</p>
|
||||
</div>
|
||||
<div class="Dialog-body Dialog-body--expanded MamufasDialog-body">
|
||||
<div class="MamufasDialog-dropZone">
|
||||
<i class="CDB-IconFont CDB-IconFont-addDocument MamufasDialog-dropZoneIcon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="CDB-Text Dialog-footer Dialog-footer--expanded MamufasDialog-footer">
|
||||
<p class="MamufasDialog-footerInfo">
|
||||
<i class="CDB-IconFont CDB-IconFont-info MamufasDialog-footerInfoIcon"></i>
|
||||
Files like CSV, GPX, XLS and many more are supported
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,132 @@
|
||||
// require('dragster');
|
||||
const Dropzone = require('dropzone');
|
||||
const Backbone = require('backbone');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
|
||||
const MamufasDialog = require('./mamufas-import-dialog-view');
|
||||
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Big mamufas to import files
|
||||
* using drag and drop
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this._modals = new ModalsServiceModel();
|
||||
this.model = new Backbone.Model({ visible: false });
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return this;
|
||||
},
|
||||
|
||||
_createDragster: function () {
|
||||
if (this.dragster) {
|
||||
this._destroyDragster();
|
||||
}
|
||||
this.dragster = new Dragster(this.$el[0]); // eslint-disable-line
|
||||
},
|
||||
|
||||
_createDropzone: function () {
|
||||
if (this.dropzone) {
|
||||
this._destroyDropzone();
|
||||
}
|
||||
this.dropzone = new Dropzone(this.$el[0], {
|
||||
url: ':)',
|
||||
autoProcessQueue: false,
|
||||
previewsContainer: false
|
||||
});
|
||||
},
|
||||
|
||||
_destroyDragster: function () {
|
||||
if (this.dragster) {
|
||||
this.dragster.removeListeners();
|
||||
this.dragster.reset();
|
||||
delete this.dragster;
|
||||
}
|
||||
},
|
||||
|
||||
_destroyDropzone: function () {
|
||||
if (this.dropzone) {
|
||||
this.dropzone.destroy();
|
||||
delete this.dropzone;
|
||||
}
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
let mamufasDialog;
|
||||
let modalModel;
|
||||
|
||||
this.$el.on('dragster:enter', () => {
|
||||
modalModel = this._modals.create(function (model) {
|
||||
mamufasDialog = new MamufasDialog({
|
||||
modalModel: model
|
||||
});
|
||||
|
||||
return mamufasDialog;
|
||||
});
|
||||
|
||||
this.trigger('dialogOpened');
|
||||
});
|
||||
|
||||
this.$el.on('dragster:leave', e => {
|
||||
modalModel.destroy();
|
||||
this.trigger('dialogClosed');
|
||||
});
|
||||
|
||||
this.dropzone.on('drop', event => {
|
||||
this.dragster.dragleave(event);
|
||||
modalModel.destroy();
|
||||
this.dropzone.removeFile(event);
|
||||
|
||||
let files = event.dataTransfer.files;
|
||||
|
||||
if (files && files.length > 0) {
|
||||
if (files.length === 1) {
|
||||
files = files[0];
|
||||
}
|
||||
|
||||
this.trigger('fileDropped', files, this);
|
||||
}
|
||||
|
||||
this.trigger('dialogClosed');
|
||||
});
|
||||
},
|
||||
|
||||
_removeBinds: function () {
|
||||
this.$el.off('dragster:enter');
|
||||
this.$el.off('dragster:leave');
|
||||
},
|
||||
|
||||
enable: function () {
|
||||
if (!this.model.get('visible')) {
|
||||
this._createDragster();
|
||||
this._createDropzone();
|
||||
this._initBinds();
|
||||
this.model.set('visible', true);
|
||||
}
|
||||
},
|
||||
|
||||
disable: function () {
|
||||
if (this.model.get('visible')) {
|
||||
this._removeBinds();
|
||||
this._destroyDragster();
|
||||
this._destroyDropzone();
|
||||
this.model.set('visible', false);
|
||||
}
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._removeBinds();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
var $ = require('jquery');
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var TEMPLATES = {
|
||||
// Using <%= %> instead of <%- %> because if not / characters (for example) will be escaped
|
||||
regular: '<%- protocol %>://<%= mapsApiResource %>/api/v1/map/static/named/<%- tpl %>/<%- width %>/<%- height %>.png<%= authTokens %>',
|
||||
cdn: '<%- protocol %>://<%- cdn %>/<%- username %>/api/v1/map/static/named/<%- tpl %>/<%- width %>/<%- height %>.png<%= authTokens %>'
|
||||
};
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'config',
|
||||
'visId',
|
||||
'mapsApiResource',
|
||||
'username'
|
||||
];
|
||||
|
||||
/**
|
||||
* MapCard previews
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
options: {
|
||||
width: 300,
|
||||
height: 170,
|
||||
privacy: 'PUBLIC',
|
||||
className: '',
|
||||
authTokens: []
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
load: function () {
|
||||
this._startLoader();
|
||||
this._loadFromVisId();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_generateImageTemplate: function () {
|
||||
return 'tpl_' + this._visId.replace(/-/g, '_');
|
||||
},
|
||||
|
||||
_loadFromVisId: function () {
|
||||
var protocol = this._isHTTPS() ? 'https' : 'http';
|
||||
var cdnConfig = this._config.get('cdn_url');
|
||||
var template = _.template(cdnConfig ? TEMPLATES['cdn'] : TEMPLATES['regular']);
|
||||
|
||||
var options = {
|
||||
protocol: protocol,
|
||||
username: this._username,
|
||||
mapsApiResource: this._mapsApiResource,
|
||||
tpl: this._generateImageTemplate(),
|
||||
width: this.options.width,
|
||||
height: this.options.height,
|
||||
authTokens: this._generateAuthTokensParams()
|
||||
};
|
||||
|
||||
if (cdnConfig) {
|
||||
options = _.extend(options, {
|
||||
cdn: cdnConfig[protocol]
|
||||
});
|
||||
}
|
||||
|
||||
var url = template(options);
|
||||
|
||||
this._loadImage({}, url);
|
||||
},
|
||||
|
||||
_generateAuthTokensParams: function () {
|
||||
var authTokens = this.options.authTokens;
|
||||
if (authTokens && authTokens.length > 0) {
|
||||
return '?' + _.map(authTokens, function (t) { return 'auth_token=' + t; }).join('&');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
_isHTTPS: function () {
|
||||
return location.protocol.indexOf('https') === 0;
|
||||
},
|
||||
|
||||
loadURL: function (url) {
|
||||
var $img = $('<img class="MapCard-preview" src="' + url + '" />');
|
||||
this.$el.append($img);
|
||||
|
||||
if (this.options.className) {
|
||||
$img.addClass(this.options.className);
|
||||
}
|
||||
|
||||
$img.fadeIn(250);
|
||||
},
|
||||
|
||||
showError: function () {
|
||||
this._onError();
|
||||
},
|
||||
|
||||
_startLoader: function () {
|
||||
this.$el.addClass('is-loading');
|
||||
},
|
||||
|
||||
_stopLoader: function () {
|
||||
this.$el.removeClass('is-loading');
|
||||
},
|
||||
|
||||
_onSuccess: function (url) {
|
||||
this._stopLoader();
|
||||
this.loadURL(url);
|
||||
this.trigger('loaded', url);
|
||||
},
|
||||
|
||||
_onError: function () {
|
||||
this._stopLoader();
|
||||
this.$el.addClass('has-error');
|
||||
var $error = $('<div class="MapCard-error" />');
|
||||
this.$el.append($error);
|
||||
$error.fadeIn(250);
|
||||
this.trigger('error');
|
||||
},
|
||||
|
||||
_loadImage: function (error, url) {
|
||||
var self = this;
|
||||
var img = new Image();
|
||||
|
||||
img.onerror = function () {
|
||||
self._onError(error);
|
||||
};
|
||||
|
||||
img.onload = function () {
|
||||
self._onSuccess(url);
|
||||
};
|
||||
|
||||
try {
|
||||
img.src = url;
|
||||
} catch (err) {
|
||||
this._onError(err);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
var $ = require('jquery');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var IndustriesDropdown = require('./user-industries/dropdown-view');
|
||||
|
||||
/**
|
||||
* View to render the user settings section in the header.
|
||||
* Expected to be created from existing DOM element.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'click .js-dropdown-target': '_createDropdown'
|
||||
},
|
||||
|
||||
_createDropdown: function (event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
var $target = $(event.target);
|
||||
|
||||
var view = new IndustriesDropdown({
|
||||
target: $target,
|
||||
verticalOffset: -10,
|
||||
horizontalOffset: $target.width() - 100,
|
||||
horizontalPosition: 'left',
|
||||
tick: 'center'
|
||||
});
|
||||
|
||||
view.render();
|
||||
|
||||
view.on('onDropdownHidden', function () {
|
||||
view.clean();
|
||||
}, this);
|
||||
|
||||
view.open();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
var $ = require('jquery');
|
||||
var DropdownAdminView = require('dashboard/components/dropdown/dropdown-admin-view');
|
||||
var template = require('./dropdown.tpl');
|
||||
|
||||
/**
|
||||
* The content of the dropdown menu opened by the industries link in the header, e.g.:
|
||||
* CartoDB, Industries, Explore, Pricing
|
||||
* ______/\____
|
||||
* | |
|
||||
* | this |
|
||||
* |____________|
|
||||
*/
|
||||
module.exports = DropdownAdminView.extend({
|
||||
className: 'CDB-Text Dropdown Dropdown--public',
|
||||
|
||||
render: function () {
|
||||
this.$el.html(template());
|
||||
|
||||
// TODO: taken from existing code, how should dropdowns really be added to the DOM?
|
||||
$('body').append(this.el);
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
<ul class="SettingsDropdown CDB-Size-medium">
|
||||
<li class="SettingsDropdown-item SettingsDropdown-item--public">
|
||||
<p><a class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public" href="https://carto.com/solutions/banking-and-finance/">Banking and Finance</a></p>
|
||||
<p><a class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public" href="https://carto.com/solutions/business-intelligence-and-analytics/">BI and Analytics</a></p>
|
||||
<p><a class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public" href="https://carto.com/solutions/government/">Government</a></p>
|
||||
<p><a class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public" href="https://carto.com/solutions/real-estate/">Real Estate</a></p>
|
||||
<p><a class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public" href="https://carto.com/solutions/web-mobile/">Web Development</a></p>
|
||||
<p><a class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public" href="https://carto.com/solutions/journalism/">Journalism</a></p>
|
||||
<p><a class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public" href="https://carto.com/solutions/natural-resources/">Natural Resources</a></p>
|
||||
<p><a class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public" href="https://carto.com/solutions/earth-observation-and-space/">Earth Observation</a></p>
|
||||
<p><a class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public" href="https://carto.com/solutions/non-profits/">Non-profits</a></p>
|
||||
<p><a class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public" href="https://carto.com/solutions/education-and-research/">Education</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,47 @@
|
||||
var $ = require('jquery');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var SettingsDropdownView = require('./user-settings/dropdown-view');
|
||||
var userSettingsTemplate = require('./user-settings.tpl');
|
||||
|
||||
/**
|
||||
* View to render the user settings section in the header.
|
||||
* Expected to be created from existing DOM element.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click .js-dropdown-target': '_createDropdown'
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var dashboardUrl = this.model.viewUrl().dashboard();
|
||||
var datasetsUrl = dashboardUrl.datasets();
|
||||
var mapsUrl = dashboardUrl.maps();
|
||||
|
||||
this.$el.html(
|
||||
userSettingsTemplate({
|
||||
avatarUrl: this.model.get('avatar_url'),
|
||||
mapsUrl: mapsUrl,
|
||||
datasetsUrl: datasetsUrl
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_createDropdown: function (event) {
|
||||
var view = new SettingsDropdownView({
|
||||
target: $(event.target),
|
||||
model: this.model, // user
|
||||
horizontalOffset: 18
|
||||
});
|
||||
view.render();
|
||||
|
||||
view.on('onDropdownHidden', function () {
|
||||
view.clean();
|
||||
}, this);
|
||||
|
||||
view.open();
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<li class="Header-settingsItem u-hideOnTablet">
|
||||
<a href="<%- mapsUrl %>" class="Header-settingsLink Header-settingsLink--public">Maps</a>
|
||||
</li>
|
||||
|
||||
<li class="Header-settingsItem u-hideOnTablet">
|
||||
<a href="<%- datasetsUrl %>" class="Header-settingsLink Header-settingsLink--public">Datasets</a>
|
||||
</li>
|
||||
|
||||
<li class="Header-settingsItem Header-settingsItem--avatar">
|
||||
<button class="UserAvatar js-dropdown-target">
|
||||
<img src="<%- avatarUrl %>" class="UserAvatar-img UserAvatar-img--medium js-user-avatar-img" />
|
||||
</button>
|
||||
</li>
|
||||
@@ -0,0 +1,36 @@
|
||||
var $ = require('jquery');
|
||||
var template = require('./dropdown.tpl');
|
||||
var DropdownAdminView = require('dashboard/components/dropdown/dropdown-admin-view');
|
||||
|
||||
/**
|
||||
* The content of the dropdown menu opened by the user avatar in the top-right of the header, e.g.:
|
||||
* Explore, Learn, ♞
|
||||
* ______/\____
|
||||
* | |
|
||||
* | this |
|
||||
* |____________|
|
||||
*/
|
||||
|
||||
module.exports = DropdownAdminView.extend({
|
||||
className: 'CDB-Text Dropdown',
|
||||
|
||||
render: function () {
|
||||
var user = this.model;
|
||||
var userUrl = user.viewUrl();
|
||||
|
||||
this.$el.html(template({
|
||||
name: user.fullName() || user.get('username'),
|
||||
email: user.get('email'),
|
||||
isOrgOwner: user.isOrgOwner(),
|
||||
dashboardUrl: userUrl.dashboard(),
|
||||
publicProfileUrl: userUrl.publicProfile(),
|
||||
accountProfileUrl: userUrl.accountProfile(),
|
||||
logoutUrl: userUrl.logout()
|
||||
}));
|
||||
|
||||
// TODO: taken from existing code, how should dropdowns really be added to the DOM?
|
||||
$('body').append(this.el);
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
<ul class="SettingsDropdown">
|
||||
<li>
|
||||
<div class="SettingsDropdown-sameline">
|
||||
<p class="CDB-Text CDB-Size-medium"><%- name %></p>
|
||||
</div>
|
||||
<p class="CDB-Text CDB-Size-medium u-altTextColor u-tSpace u-ellipsis">
|
||||
<%- email %>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="BreadcrumbsDropdown-listItem is-dark CDB-Text CDB-Size-medium">
|
||||
<ul>
|
||||
<li class="u-bSpace--m"><a href="<%- dashboardUrl %>">Your dashboard</a></li>
|
||||
<li class="u-bSpace--m"><a href="<%- publicProfileUrl %>">Your public profile</a></li>
|
||||
<li class="u-bSpace--m"><a href="<%- accountProfileUrl %>">Account settings</a></li>
|
||||
<li><a href="<%- logoutUrl %>">Close session</a></li>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<div class="Dialog-body Dialog-body--share Dialog-body--withoutBorder Dialog-body--expanded">
|
||||
<div class="u-inner">
|
||||
<%= htmlToWrap %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,257 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const Utils = require('builder/helpers/utils');
|
||||
const PaginationModel = require('builder/components/pagination/pagination-model');
|
||||
const template = require('./paged-search.tpl');
|
||||
const pagedSearchDialogWrapperTemplate = require('./paged-search-dialog-wrapper.tpl');
|
||||
const errorTemplate = require('dashboard/views/data-library/content/error-template.tpl');
|
||||
const loadingView = require('builder/components/loading/render-loading');
|
||||
const noResultsView = require('builder/components/no-results/render-no-results.js');
|
||||
const TabPane = require('dashboard/components/tabpane/tabpane');
|
||||
const ViewFactory = require('builder/components/view-factory');
|
||||
const PaginationView = require('builder/components/pagination/pagination-view');
|
||||
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'collection',
|
||||
'pagedSearchModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* View to render a searchable/pageable collection.
|
||||
* Also allows to filter/search list.
|
||||
* Set {isUsedInDialog: true} in view opts if intended to be used in a dialog, to have proper classes to position views
|
||||
* properly.
|
||||
*
|
||||
* - collection is a collection which has a PagedSearchModel.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click .js-search-link': '_onSearchClick',
|
||||
'click .js-clean-search': '_onCleanSearchClick',
|
||||
'keydown .js-search-input': '_onKeyDown',
|
||||
'submit .js-search-form': 'killEvent'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this.options.noResults = this.options.noResults || {};
|
||||
|
||||
const params = this._pagedSearchModel;
|
||||
this.paginationModel = new PaginationModel({
|
||||
current_page: params.get('page'),
|
||||
total_count: this._collection.totalCount() || 0,
|
||||
per_page: params.get('per_page')
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
this._pagedSearchModel.fetch(this._collection);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._collection, 'fetching', function () {
|
||||
this._toggleCleanSearchBtn();
|
||||
this._activatePane('loading');
|
||||
});
|
||||
|
||||
this.listenTo(this._collection, 'error', function (e) {
|
||||
// Old requests can be stopped, so aborted requests are not
|
||||
// considered as an error
|
||||
if (!e || (e && e.statusText !== 'abort')) {
|
||||
this._activatePane('error');
|
||||
}
|
||||
this._toggleCleanSearchBtn();
|
||||
});
|
||||
|
||||
this.listenTo(this._collection, 'sync', function (collection) {
|
||||
this.paginationModel.set({
|
||||
total_count: this._collection.totalCount(),
|
||||
current_page: this._pagedSearchModel.get('page')
|
||||
});
|
||||
this._activatePane(this._collection.totalCount() > 0 ? 'list' : 'no_results');
|
||||
this._toggleCleanSearchBtn();
|
||||
});
|
||||
|
||||
this.listenTo(this.paginationModel, 'change:current_page', function (model, newPage) {
|
||||
this._pagedSearchModel.set('page', newPage);
|
||||
this._pagedSearchModel.fetch(this._collection);
|
||||
});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this._renderContent(
|
||||
template({
|
||||
thinFilters: this.options.thinFilters || false,
|
||||
q: this._pagedSearchModel.get('q')
|
||||
})
|
||||
);
|
||||
|
||||
this._initViews();
|
||||
this._$cleanSearchBtn().hide();
|
||||
this._renderExtraFilters();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderExtraFilters: function () {
|
||||
if (this.options.filtersExtrasView) {
|
||||
this.$('.js-filters').append(this.options.filtersExtrasView.render().el);
|
||||
}
|
||||
},
|
||||
|
||||
_renderContent: function (html) {
|
||||
if (this.options.isUsedInDialog) {
|
||||
html = pagedSearchDialogWrapperTemplate({
|
||||
htmlToWrap: html
|
||||
});
|
||||
}
|
||||
this.$el.html(html);
|
||||
|
||||
// Needs to be called after $el html changed:
|
||||
if (this.options.isUsedInDialog) {
|
||||
this.$el.addClass('Dialog-expandedSubContent');
|
||||
this._$tabPane().addClass('Dialog-bodyInnerExpandedWithSubFooter');
|
||||
}
|
||||
},
|
||||
|
||||
_toggleCleanSearchBtn: function () {
|
||||
this._$cleanSearchBtn().toggle(!!this._pagedSearchModel.get('q'));
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
this._panes = new TabPane({
|
||||
el: this._$tabPane()
|
||||
});
|
||||
|
||||
this.addView(this._panes);
|
||||
|
||||
this._panes.addTab('list',
|
||||
ViewFactory.createListView([
|
||||
() => this._createListView(),
|
||||
|
||||
() => new PaginationView({
|
||||
className: 'CDB-Text CDB-Size-medium Pagination Pagination--shareList',
|
||||
model: this.paginationModel
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
this._panes.addTab('error',
|
||||
ViewFactory.createByHTML(errorTemplate({
|
||||
msg: ''
|
||||
})).render()
|
||||
);
|
||||
|
||||
this._panes.addTab('no_results',
|
||||
ViewFactory.createByHTML(noResultsView({
|
||||
icon: this.options.noResults.icon || 'CDB-IconFont-defaultUser',
|
||||
title: this.options.noResults.title || 'Oh! No results',
|
||||
msg: this.options.noResults.msg || 'Unfortunately we could not find anything with these parameters'
|
||||
})).render()
|
||||
);
|
||||
|
||||
this._panes.addTab('loading',
|
||||
ViewFactory.createByHTML(loadingView({
|
||||
title: 'Searching'
|
||||
})).render()
|
||||
);
|
||||
|
||||
if (this._pagedSearchModel.get('q')) {
|
||||
this._focusSearchInput();
|
||||
}
|
||||
|
||||
this._activatePane(this._chooseActivePaneName(this._collection.totalCount()));
|
||||
},
|
||||
|
||||
_createListView: function () {
|
||||
var view = this.options.createListView();
|
||||
if (view instanceof CoreView) {
|
||||
return view;
|
||||
} else {
|
||||
console.error('createListView function must return a view');
|
||||
// fallback for view to not fail miserably
|
||||
return new CoreView();
|
||||
}
|
||||
},
|
||||
|
||||
_activatePane: function (name) {
|
||||
// Only change active pane if the panes is actually initialized
|
||||
if (this._panes && this._panes.size() > 0) {
|
||||
// explicit render required, since tabpane doesn't do it
|
||||
this._panes.active(name).render();
|
||||
}
|
||||
},
|
||||
|
||||
_chooseActivePaneName: function (totalCount) {
|
||||
if (totalCount === 0) {
|
||||
return 'no_results';
|
||||
} else if (totalCount > 0) {
|
||||
return 'list';
|
||||
} else {
|
||||
return 'loading';
|
||||
}
|
||||
},
|
||||
|
||||
_focusSearchInput: function () {
|
||||
// also selects the current search str on the focus
|
||||
this._$searchInput().focus().val(this._$searchInput().val());
|
||||
},
|
||||
|
||||
_onSearchClick: function (ev) {
|
||||
this.killEvent(ev);
|
||||
this._$searchInput().focus();
|
||||
},
|
||||
|
||||
_onCleanSearchClick: function (ev) {
|
||||
this.killEvent(ev);
|
||||
this._cleanSearch();
|
||||
},
|
||||
|
||||
_onKeyDown: function (ev) {
|
||||
var enterPressed = (ev.key === 'Enter');
|
||||
var escapePressed = (ev.key === 'Escape');
|
||||
if (enterPressed) {
|
||||
this.killEvent(ev);
|
||||
this._submitSearch();
|
||||
} else if (escapePressed) {
|
||||
this.killEvent(ev);
|
||||
if (this._pagedSearchModel.get('q')) {
|
||||
this._cleanSearch();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_submitSearch: function (e) {
|
||||
this._makeNewSearch(Utils.stripHTML(this._$searchInput().val().trim()));
|
||||
},
|
||||
|
||||
_cleanSearch: function () {
|
||||
this._$searchInput().val('');
|
||||
this._makeNewSearch();
|
||||
},
|
||||
|
||||
_makeNewSearch: function (query) {
|
||||
this._pagedSearchModel.set({
|
||||
q: query,
|
||||
page: 1
|
||||
});
|
||||
this._pagedSearchModel.fetch(this._collection);
|
||||
},
|
||||
|
||||
_$searchInput: function () {
|
||||
return this.$('.js-search-input');
|
||||
},
|
||||
|
||||
_$cleanSearchBtn: function () {
|
||||
return this.$('.js-clean-search');
|
||||
},
|
||||
|
||||
_$tabPane: function () {
|
||||
return this.$('.js-tab-pane');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="Filters is-relative <% if (thinFilters) { %>Filters--thin<% } %>">
|
||||
<div class="Filters-inner">
|
||||
<div class="Filters-row js-filters">
|
||||
<div class="Filters-typeItem Filters-typeItem--searchEnabler">
|
||||
<p class="Filters-searchLink js-search-link u-alignCenter CDB-Text CDB-Size-medium">
|
||||
<i class="Filters-searchLinkIcon CDB-IconFont CDB-IconFont-lens"></i> Search
|
||||
</p>
|
||||
</div>
|
||||
<div class="Filters-typeItem Filters-typeItem--searchField">
|
||||
<form class="Filters-searchForm js-search-form" action="#">
|
||||
<input class="Filters-searchInput CDB-Text CDB-Size-medium js-search-input" type="text" value="<%- q %>" placeholder="Search by username or email" />
|
||||
<button type="button" class="Filters-cleanSearch js-clean-search u-actionTextColor">
|
||||
<i class="CDB-IconFont CDB-IconFont-close"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="Filters-separator"></span>
|
||||
</div>
|
||||
<div class="js-tab-pane"></div>
|
||||
@@ -0,0 +1,53 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./pages-subheader.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const bytesToSize = require('dashboard/helpers/bytes-to-size');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel',
|
||||
'configModel'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
const usedDataBytes = this._userModel.get('db_size_in_bytes');
|
||||
const quotaInBytes = this._userModel.get('quota_in_bytes');
|
||||
const usedDataPct = Math.round(usedDataBytes / quotaInBytes * 100);
|
||||
let progressBarClass = '';
|
||||
|
||||
if (usedDataPct > 80 && usedDataPct < 90) {
|
||||
progressBarClass = 'caution';
|
||||
} else if (usedDataPct > 89) {
|
||||
progressBarClass = 'danger';
|
||||
}
|
||||
|
||||
this.$el.html(template({
|
||||
isCartoDBHosted: this._configModel.get('cartodb_com_hosted'),
|
||||
usedDataStr: bytesToSize(usedDataBytes).toString(2),
|
||||
usedDataPct: usedDataPct,
|
||||
progressBarClass: progressBarClass,
|
||||
availableDataStr: bytesToSize(quotaInBytes).toString(2),
|
||||
profileUrl: this._userModel.viewUrl().accountProfile().pathname(),
|
||||
accountUrl: this._userModel.viewUrl().accountSettings().pathname(),
|
||||
apiKeysUrl: this._userModel.viewUrl().apiKeys().pathname(),
|
||||
connectedAppsUrl: this._userModel.viewUrl().connectedApps().pathname(),
|
||||
isInsideOrg: this._userModel.isInsideOrg(),
|
||||
planUrl: this._userModel.get('plan_url'),
|
||||
isOrgAdmin: this._userModel.isOrgAdmin(),
|
||||
organizationUrl: this._userModel.viewUrl().organization().pathname(),
|
||||
isOrgOwner: this._userModel.isOrgOwner(),
|
||||
upgradeContactEmail: this._userModel.upgradeContactEmail(),
|
||||
path: this.getPath()
|
||||
}));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getPath: function () {
|
||||
return window.location.pathname;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
<div class="SideMenu-type">
|
||||
<ul class="SideMenu-list">
|
||||
<li class="SideMenu-typeItem"><a href="<%= profileUrl %>" class="SideMenu-typeLink <% if (path === profileUrl) { %>is-selected<% } %>">个人资料</a></li>
|
||||
<li class="SideMenu-typeItem"><a href="<%= accountUrl %>" class="SideMenu-typeLink <% if (path === accountUrl) { %>is-selected<% } %>">账户</a></li>
|
||||
<li class="SideMenu-typeItem"><a href="<%= connectedAppsUrl %>" class="SideMenu-typeLink <% if (path === connectedAppsUrl) { %>is-selected<% } %>">APP应用</a></li>
|
||||
<% if (isOrgAdmin) { %>
|
||||
<li class="SideMenu-typeItem"><a href="<%= organizationUrl %>" class="SideMenu-typeLink <% if (path === organizationUrl) { %>is-selected<% } %>">组织设置</a></li>
|
||||
<% } %>
|
||||
|
||||
<span class="SideMenu-separator"></span>
|
||||
|
||||
<li class="SideMenu-typeItem"><a href="<%= apiKeysUrl %>" class="SideMenu-typeLink <% if (path === apiKeysUrl) { %>is-selected<% } %>">开发设置</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
const _ = require('underscore');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./password-confirmation.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'modalModel',
|
||||
'onPasswordTyped'
|
||||
];
|
||||
|
||||
/**
|
||||
* Password Confirmation Modal
|
||||
*
|
||||
* Modal used for password validated forms, so
|
||||
* the user needs to type the password in to
|
||||
* save form changes
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'click .js-ok': '_onConfirm',
|
||||
'click .js-cancel': '_closeDialog',
|
||||
'keydown #password-confirmation-form': '_onEnterPressed',
|
||||
'input .js-password': '_toggleConfirmButton'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
this._isConfirmDisabled = true;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
isConfirmDisabled: this._isConfirmDisabled,
|
||||
updatePassword: this.options.updatePassword
|
||||
})
|
||||
);
|
||||
|
||||
this._okButton = this.$('.js-ok');
|
||||
return this;
|
||||
},
|
||||
|
||||
_toggleConfirmButton: function (event) {
|
||||
const passwordInput = event.target;
|
||||
this._isConfirmDisabled = _.isEmpty(passwordInput.value);
|
||||
this._okButton.toggleClass('is-disabled', this._isConfirmDisabled);
|
||||
},
|
||||
|
||||
_onConfirm: function (event) {
|
||||
this.killEvent(event);
|
||||
|
||||
if (this._isConfirmDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordValue = this.$('#password-confirmation').val();
|
||||
this._onPasswordTyped && this._onPasswordTyped(passwordValue);
|
||||
this._closeDialog();
|
||||
},
|
||||
|
||||
_onEnterPressed: function (event) {
|
||||
if (event.keyCode !== 13) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._onConfirm(event);
|
||||
},
|
||||
|
||||
_closeDialog: function () {
|
||||
this._modalModel.destroy();
|
||||
}
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<div class="CDB-Text Dialog-header u-inner">
|
||||
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
|
||||
<i class="CDB-IconFont CDB-IconFont-defaultUser"></i>
|
||||
</div>
|
||||
<h2 class="Dialog-headerTitle">
|
||||
<%= _t('components.modals.password-confirmation.modal-title') %>
|
||||
</h2>
|
||||
<p class="Dialog-headerText">
|
||||
<%= _t('components.modals.password-confirmation.modal-description') %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="CDB-Text Dialog-body">
|
||||
<form id="password-confirmation-form" method="POST" class="Form-row Form-row--centered has-label">
|
||||
<div class="Form-rowLabel">
|
||||
<% if (updatePassword) {%>
|
||||
<label class="Form-label" for="password-confirmation"><%= _t('components.modals.password-confirmation.form.old-password-label') %></label>
|
||||
<% } else { %>
|
||||
<label class="Form-label" for="password-confirmation"><%= _t('components.modals.password-confirmation.form.password-label') %></label>
|
||||
<% }%>
|
||||
</div>
|
||||
<div class="Form-rowData">
|
||||
<input type="password" id="password-confirmation" name="password_confirmation" class="CDB-InputText CDB-Text Form-input Form-input--long js-password" value="" autofocus/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="Dialog-footer u-inner">
|
||||
<button type="button" class="CDB-Button CDB-Button--secondary js-cancel">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">
|
||||
<%= _t('components.modals.password-confirmation.actions.cancel') %>
|
||||
</span>
|
||||
</button>
|
||||
<button class="CDB-Button CDB-Button--primary js-ok<%= isConfirmDisabled ? ' is-disabled' : ''%>">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">
|
||||
<%= _t('components.modals.password-confirmation.actions.confirm') %>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
const DashboardHeaderView = require('./dashboard-header-view');
|
||||
const template = require('dashboard/components/dashboard-header/private-header.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'viewModel',
|
||||
'configModel'
|
||||
];
|
||||
|
||||
module.exports = DashboardHeaderView.extend({
|
||||
className: 'Header CDB-Text',
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this.router = this.options.router;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
DashboardHeaderView.prototype._initBinds.apply(this);
|
||||
this.model.bind('change', this.render, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
const hasOrganization = this.model.isInsideOrg();
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
organizationName: hasOrganization && this.model.organization.get('name'),
|
||||
nameOrUsername: this.model.nameOrUsername(),
|
||||
avatar: this.model.get('avatar_url'),
|
||||
homeUrl: this.model.viewUrl().dashboard(),
|
||||
isCartoDBHosted: this._configModel.get('cartodb_com_hosted')
|
||||
})
|
||||
);
|
||||
|
||||
this._renderBreadcrumbsDropdownLink();
|
||||
this._renderNotifications();
|
||||
this._renderLogoLink();
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const ServiceOauth = require('dashboard/data/service-oauth-model');
|
||||
const ServiceValidToken = require('dashboard/data/service-valid-token-model');
|
||||
const DisconnectDialog = require('dashboard/views/account/service-disconnect-dialog/service-disconnect-dialog-view');
|
||||
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const template = require('./service-item.tpl');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* OAuth service item view
|
||||
* Connect or disconnect from a service
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
_WINDOW_INTERVAL: 1000,
|
||||
|
||||
className: 'FormAccount-row',
|
||||
|
||||
events: {
|
||||
'click .js-connect': '_connect',
|
||||
'click .js-disconnect': '_disconnect'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this._initBinds();
|
||||
|
||||
this._modals = new ModalsServiceModel();
|
||||
|
||||
if (this.model.get('connected')) {
|
||||
this._checkToken();
|
||||
}
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change:state change:connected', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template(this.model.attributes)
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_connect: function (e) {
|
||||
if (this.model.get('state') === 'loading') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.set('state', 'loading');
|
||||
|
||||
const service = new ServiceOauth(null, {
|
||||
datasourceName: this.model.get('name'),
|
||||
configModel: this._configModel
|
||||
});
|
||||
|
||||
service.fetch({
|
||||
success: (model, response) => {
|
||||
if (response.success && response.url) {
|
||||
this._openWindow(response.url);
|
||||
}
|
||||
},
|
||||
error: () => this._setErrorState()
|
||||
});
|
||||
},
|
||||
|
||||
_disconnect: function () {
|
||||
if (this.model.get('state') === 'loading') return;
|
||||
|
||||
this._modals.create(modalModel => new DisconnectDialog({
|
||||
serviceModel: this.model,
|
||||
configModel: this._configModel,
|
||||
modalModel
|
||||
}));
|
||||
},
|
||||
|
||||
_checkToken: function (successCallback, errorCallback) {
|
||||
const validToken = new ServiceValidToken(
|
||||
{ datasource: this.model.get('name') },
|
||||
{ configModel: this._configModel }
|
||||
);
|
||||
|
||||
validToken.fetch({
|
||||
success: (model, response) => {
|
||||
this.model.set('connected', response.oauth_valid);
|
||||
|
||||
if (response.oauth_valid) {
|
||||
successCallback && successCallback();
|
||||
} else {
|
||||
errorCallback && errorCallback();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
errorCallback && errorCallback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_setErrorState: function () {
|
||||
this.model.set('state', 'error');
|
||||
},
|
||||
|
||||
_reloadWindow: function () {
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
_openWindow: function (url) {
|
||||
const popupWindow = window.open(url, null, 'menubar=no,toolbar=no,width=600,height=495');
|
||||
|
||||
const checkConnectionInterval = window.setInterval(() => {
|
||||
if (popupWindow && popupWindow.closed) {
|
||||
// Check valid token to see if user has connected or not.
|
||||
this._checkToken(this._reloadWindow, this._setErrorState.bind(this));
|
||||
clearInterval(checkConnectionInterval);
|
||||
} if (!popupWindow) {
|
||||
// Show error directly
|
||||
this._setErrorState();
|
||||
clearInterval(checkConnectionInterval);
|
||||
}
|
||||
}, this._WINDOW_INTERVAL);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
<div class="FormAccount-rowLabel">
|
||||
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor FormAccount-label"><%- title %></label>
|
||||
</div>
|
||||
<div class="FormAccount-rowData">
|
||||
<% if (connected) { %>
|
||||
<input class="CDB-InputText CDB-Text FormAccount-input FormAccount-input--med is-disabled" readonly value="Connected" />
|
||||
<% } else { %>
|
||||
<% if (state === "loading") { %>
|
||||
<button type="button" class="CDB-Size-medium FormAccount-link is-disabled">Connecting...</button>
|
||||
<% } else { %>
|
||||
<button type="button" class="CDB-Size-medium FormAccount-link js-connect">
|
||||
<i class="ServiceIcon ServiceIcon--<%- name %>"></i>Connect
|
||||
</button>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<div class="FormAccount-rowInfo FormAccount-rowInfo--marginLeft FormAccount-rowInfoText--multipleLines">
|
||||
<p class="FormAccount-rowInfoText <%- state === "error" ? 'FormAccount-rowInfoText--error' : '' %>">
|
||||
<% if (connected) { %>
|
||||
<% if (state === "error") { %>
|
||||
Ooops! There was an error, please <button type="button" class="FormAccount-link js-disconnect">try it again</button>
|
||||
or <a class="FormAccount-link" href="mailto:support@carto.com">contact us</a> if the problem persists
|
||||
<% } else if (state === "loading") { %>
|
||||
Disconnecting...
|
||||
<% } else { %>
|
||||
<button type="button" class="CDB-Size-medium FormAccount-link js-disconnect">Disconnect</button>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<% if (state === "error") { %>
|
||||
There was an error, please be sure your pop-up blocker is disabled and try again or
|
||||
<a class="FormAccount-link" href="mailto:support@carto.com">contact us</a> if the problem persists
|
||||
<% } %>
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./support-view/support-banner.tpl');
|
||||
const checkAndBuildOpts = require('../../builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Decide what support block app should show
|
||||
*
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'SupportBanner',
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
userType: this._getUserType(),
|
||||
orgDisplayEmail: this._getOrgAdminEmail(),
|
||||
isViewer: this._userModel.isViewer()
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_getUserType: function () {
|
||||
var accountType = this._userModel.get('account_type').toLowerCase();
|
||||
|
||||
// Get user type
|
||||
if (this._userModel.isOrgOwner()) {
|
||||
return 'org_admin';
|
||||
} else if (this._userModel.isInsideOrg()) {
|
||||
return 'org';
|
||||
} else if (accountType === 'internal' || accountType === 'partner' || accountType === 'ambassador') {
|
||||
return 'internal';
|
||||
} else if (accountType !== 'free') {
|
||||
return 'client';
|
||||
} else {
|
||||
return 'regular';
|
||||
}
|
||||
},
|
||||
|
||||
_getOrgAdminEmail: function () {
|
||||
if (this._userModel.isInsideOrg()) {
|
||||
return this._userModel.organization && this._userModel.organization.display_email;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
<div class="u-inner">
|
||||
<div class="SupportBanner-inner">
|
||||
<div class="SupportBanner-info">
|
||||
<h4 class="CDB-Text CDB-Size-large u-secondaryTextColor u-bSpace">
|
||||
<% if (userType === 'org_admin' || userType === 'client') { %>
|
||||
As a paying customer, you have access to our dedicated support.
|
||||
<% } else if (isViewer) { %>
|
||||
Contact the <a href="mailto:<%- orgDisplayEmail %>">organization administrator</a> to become a builder.
|
||||
<% } else if (userType === 'org') { %>
|
||||
Contact the <a href="mailto:<%- orgDisplayEmail %>">organization administrator</a> for support.
|
||||
<% } else if (userType === "internal") { %>
|
||||
You are part of CARTO, you deserve outstanding support.
|
||||
<% } else { %>
|
||||
For all technical questions, contact our community support forum.
|
||||
<% } %>
|
||||
</h4>
|
||||
<p class="CDB-Text CDB-Size-medium u-altTextColor">
|
||||
<% if (isViewer) { %>
|
||||
You will be able to create your own maps!
|
||||
<% } else if (userType === 'org' || userType === 'org_admin' || userType === 'client') { %>
|
||||
Remember that there is a lot of information in our <a href="http://gis.stackexchange.com/questions/tagged/carto" target="_blank">community forums</a>.
|
||||
<% } else if (userType === "internal") { %>
|
||||
Don't forget to share your knowledge in our <a href="http://gis.stackexchange.com/questions/tagged/carto" target="_blank">community forums</a>.
|
||||
<% } else { %>
|
||||
If you experience any problems with the CARTO service, feel free to <a href="mailto:support@carto.com">contact us</a>.
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
<% if (userType === 'org_admin') { %>
|
||||
<a href="mailto:enterprise-support@carto.com" class="SupportBanner-link CDB-Button CDB-Button--secondary">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">Contact support</span>
|
||||
</a>
|
||||
<% } else if (userType === 'org') { %>
|
||||
<a href="mailto:<%- orgDisplayEmail %>" class="SupportBanner-link CDB-Button CDB-Button--secondary">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">Contact administrator</span>
|
||||
</a>
|
||||
<% } else if (userType === 'client' || userType === 'internal') { %>
|
||||
<a href="mailto:support@carto.com" class="SupportBanner-link CDB-Button CDB-Button--secondary">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">Contact us</span>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<a href="http://gis.stackexchange.com/questions/tagged/carto" class="SupportBanner-link CDB-Button CDB-Button--secondary" target="_blank">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">Community support</span>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<ul class="ApiKeysForm-tablesList CDB-Text CDB-Size-medium js-list"><%= rows %></ul>
|
||||
@@ -0,0 +1,6 @@
|
||||
<li class="ApiKeysForm-grantsTable-item u-ellipsis">
|
||||
<span class="u-ellipsis u-rSpace" title="<%- tableName %>">
|
||||
<%- tableName %>
|
||||
</span>
|
||||
<div data-fields="<%- tableName %>"></div>
|
||||
</li>
|
||||
@@ -0,0 +1,3 @@
|
||||
<li class="ApiKeys-list-loader">
|
||||
<div class="Spinner"></div>
|
||||
</li>
|
||||
@@ -0,0 +1,8 @@
|
||||
<li class="ApiKeys-list-placeholder">
|
||||
<div class="LayoutIcon">
|
||||
<i class="CDB-IconFont CDB-IconFont-lens"></i>
|
||||
</div>
|
||||
<div class="CDB-Text CDB-Size-medium u-mainTextColor u-secondaryTextColor u-tSpace-xl">
|
||||
<%- message %>
|
||||
</div>
|
||||
</li>
|
||||
@@ -0,0 +1,125 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const template = require('./table-grants.tpl');
|
||||
const formTemplate = require('./table-grants-form.tpl');
|
||||
const itemTemplate = require('./table-grants-item.tpl');
|
||||
const loadingTemplate = require('./table-grants-loader.tpl');
|
||||
const placeholderTemplate = require('./table-grants-placeholder.tpl');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'apiKeyModel',
|
||||
'userTablesModel'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'ApiKeysForm-grantsTable',
|
||||
|
||||
events: {
|
||||
'input .js-search': '_onSearchChanged'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
if (this._apiKeyModel.isPublic()) {
|
||||
this._userTablesModel.fetchPublicDatasets();
|
||||
}
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(template({
|
||||
showSearch: !this._apiKeyModel.id
|
||||
}));
|
||||
|
||||
this._renderFormView();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._userTablesModel.getStateModel(), 'change:status', this._renderFormView);
|
||||
},
|
||||
|
||||
_renderFormView: function () {
|
||||
this._formView && this._formView.off('change', this._onFormViewChanged, this);
|
||||
let view = loadingTemplate();
|
||||
|
||||
if (this._userTablesModel.isFetched()) {
|
||||
if (this._userTablesModel.isEmpty()) {
|
||||
const message = this._userTablesModel.hasQuery() ? '0 datasets found' : 'There are no datasets';
|
||||
view = placeholderTemplate({ message });
|
||||
} else {
|
||||
view = this._createFormView().render().el;
|
||||
}
|
||||
}
|
||||
|
||||
this.$('.js-datasets-list').html(view);
|
||||
},
|
||||
|
||||
_createFormView: function () {
|
||||
const tables = this._getTables();
|
||||
|
||||
const multiCheckbox = {
|
||||
type: 'MultiCheckbox',
|
||||
title: false,
|
||||
inputs: [
|
||||
{ name: 'select', label: 'Select' },
|
||||
{ name: 'insert', label: 'Insert' },
|
||||
{ name: 'update', label: 'Update' },
|
||||
{ name: 'delete', label: 'Delete' }
|
||||
],
|
||||
editorAttrs: {
|
||||
disabled: !!this._apiKeyModel.id
|
||||
}
|
||||
};
|
||||
|
||||
const schema = _.mapObject(tables, () => multiCheckbox);
|
||||
|
||||
const data = _.mapObject(tables, (value, tableName) => {
|
||||
const apiKeyTables = this._apiKeyModel.get('tables');
|
||||
|
||||
if (apiKeyTables.hasOwnProperty(tableName) && apiKeyTables[tableName]) {
|
||||
return apiKeyTables[tableName].permissions;
|
||||
}
|
||||
|
||||
return value.permissions;
|
||||
});
|
||||
|
||||
this._formView = new Backbone.Form({ data, schema, template: this._generateFormMarkup });
|
||||
this._formView.on('change', this._onFormViewChanged, this);
|
||||
|
||||
return this._formView;
|
||||
},
|
||||
|
||||
_generateFormMarkup: function () {
|
||||
const rows = Object.keys(this.data).map((tableName) => itemTemplate({ tableName })).join('');
|
||||
return formTemplate({ rows });
|
||||
},
|
||||
|
||||
_getTables: function () {
|
||||
return !this._apiKeyModel.id || this._apiKeyModel.isPublic()
|
||||
? this._userTablesModel.attributes
|
||||
: this._apiKeyModel.get('tables');
|
||||
},
|
||||
|
||||
_onFormViewChanged: function (form) {
|
||||
const persistedTables = this._apiKeyModel.get('tables');
|
||||
|
||||
const formTables = _.mapObject(form.getValue(), (value, key) => ({ permissions: value }));
|
||||
|
||||
this._apiKeyModel.set({
|
||||
tables: {
|
||||
...persistedTables,
|
||||
...formTables
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_onSearchChanged: _.debounce(function (event) {
|
||||
this._userTablesModel.setQuery(event.target.value);
|
||||
}, 500, this)
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="CDB-Text Editor-formInner">
|
||||
<div class="CDB-Box-modal ApiKeysForm-box-modal">
|
||||
<% if (showSearch) { %>
|
||||
<div class="CDB-Box-modalHeader">
|
||||
<div class="CDB-Box-modalHeaderItem">
|
||||
<input type="text" name="text" placeholder="Search by name" class="CDB-InputTextPlain CDB-Text js-search">
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<ul class="js-datasets-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,91 @@
|
||||
const _ = require('underscore');
|
||||
const CoreView = require('backbone/core-view');
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
tagName: 'tr',
|
||||
|
||||
initialize: function () {
|
||||
this.model.bind('change', this.render, this);
|
||||
this.model.bind('destroy', this.clean, this);
|
||||
this.model.bind('remove', this.clean, this);
|
||||
this.model.bind('change', this.triggerChange, this);
|
||||
this.model.bind('sync', this.triggerSync, this);
|
||||
this.model.bind('error', this.triggerError, this);
|
||||
|
||||
this.add_related_model(this.model);
|
||||
this.order = this.options.order;
|
||||
},
|
||||
|
||||
triggerChange: function () {
|
||||
this.trigger('changeRow');
|
||||
},
|
||||
|
||||
triggerSync: function () {
|
||||
this.trigger('syncRow');
|
||||
},
|
||||
|
||||
triggerError: function () {
|
||||
this.trigger('errorRow');
|
||||
},
|
||||
|
||||
valueView: function (colName, value) {
|
||||
return value;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var self = this;
|
||||
var row = this.model;
|
||||
|
||||
var tr = '';
|
||||
|
||||
var tdIndex = 0;
|
||||
var td;
|
||||
if (this.options.row_header) {
|
||||
td = '<td class="rowHeader" data-x="' + tdIndex + '">';
|
||||
} else {
|
||||
td = '<td class="EmptyRowHeader" data-x="' + tdIndex + '">';
|
||||
}
|
||||
var v = self.valueView('', '');
|
||||
if (v.html) {
|
||||
v = v[0].outerHTML;
|
||||
}
|
||||
td += v;
|
||||
td += '</td>';
|
||||
tdIndex++;
|
||||
tr += td;
|
||||
|
||||
var attrs = this.order || _.keys(row.attributes);
|
||||
var tds = '';
|
||||
var row_attrs = row.attributes;
|
||||
for (var i = 0, len = attrs.length; i < len; ++i) {
|
||||
var key = attrs[i];
|
||||
var value = row_attrs[key];
|
||||
if (value !== undefined) {
|
||||
td = '<td id="cell_' + row.id + '_' + key + '" data-x="' + tdIndex + '">';
|
||||
v = self.valueView(key, value);
|
||||
if (v.html) {
|
||||
v = v[0].outerHTML;
|
||||
}
|
||||
td += v;
|
||||
td += '</td>';
|
||||
tdIndex++;
|
||||
tds += td;
|
||||
}
|
||||
}
|
||||
tr += tds;
|
||||
this.$el.html(tr).attr('id', 'row_' + row.id);
|
||||
return this;
|
||||
},
|
||||
|
||||
getCell: function (x) {
|
||||
if (this.options.row_header) {
|
||||
++x;
|
||||
}
|
||||
return this.$('td:eq(' + x + ')');
|
||||
},
|
||||
|
||||
getTableView: function () {
|
||||
return this.tableView;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,287 @@
|
||||
const $ = require('jquery');
|
||||
const _ = require('underscore');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const RowView = require('dashboard/components/table/row-view');
|
||||
|
||||
/**
|
||||
* render a table
|
||||
* this widget needs two data sources
|
||||
* - the table model which contains information about the table (columns and so on). See TableProperties
|
||||
* - the model with the data itself (TableData)
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
tagName: 'table',
|
||||
rowView: RowView,
|
||||
|
||||
events: {
|
||||
'click td': '_cellClick',
|
||||
'dblclick td': '_cellDblClick'
|
||||
},
|
||||
|
||||
default_options: {
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
var self = this;
|
||||
_.defaults(this.options, this.default_options);
|
||||
this.dataModel = this.options.dataModel;
|
||||
this.rowViews = [];
|
||||
|
||||
// binding
|
||||
this.setDataSource(this.dataModel);
|
||||
this.model.bind('change', this.render, this);
|
||||
this.model.bind('change:dataSource', this.setDataSource, this);
|
||||
|
||||
// assert the rows are removed when table is removed
|
||||
this.bind('clean', this.clear_rows, this);
|
||||
|
||||
// prepare for cleaning
|
||||
this.add_related_model(this.dataModel);
|
||||
this.add_related_model(this.model);
|
||||
|
||||
// we need to use custom signals to make the tableview aware of a row being deleted,
|
||||
// because when you delete a point from the map view, sometimes it isn't on the dataModel
|
||||
// collection, so its destroy doesn't bubble throught there.
|
||||
// Also, the only non-custom way to acknowledge that a row has been correctly deleted from a server is with
|
||||
// a sync, that doesn't bubble through the table
|
||||
this.model.bind('removing:row', function () {
|
||||
self.rowsBeingDeleted = self.rowsBeingDeleted ? self.rowsBeingDeleted + 1 : 1;
|
||||
self.rowDestroying();
|
||||
});
|
||||
this.model.bind('remove:row', function () {
|
||||
if (self.rowsBeingDeleted > 0) {
|
||||
self.rowsBeingDeleted--;
|
||||
self.rowDestroyed();
|
||||
if (self.dataModel.length == 0) { // eslint-disable-line eqeqeq
|
||||
self.emptyTable();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
headerView: function (column) {
|
||||
return column[0];
|
||||
},
|
||||
|
||||
setDataSource: function (dm) {
|
||||
if (this.dataModel) {
|
||||
this.dataModel.unbind(null, null, this);
|
||||
}
|
||||
this.dataModel = dm;
|
||||
this.dataModel.bind('reset', this._renderRows, this);
|
||||
this.dataModel.bind('error', this._renderRows, this);
|
||||
this.dataModel.bind('add', this.addRow, this);
|
||||
},
|
||||
|
||||
_renderHeader: function () {
|
||||
var self = this;
|
||||
var thead = $('<thead>');
|
||||
var tr = $('<tr>');
|
||||
if (this.options.row_header) {
|
||||
tr.append($('<th>').append(self.headerView(['', 'header'])));
|
||||
} else {
|
||||
tr.append($('<th>').append(self.headerView(['', 'header'])));
|
||||
}
|
||||
_(this.model.get('schema')).each(function (col) {
|
||||
tr.append($('<th>').append(self.headerView(col)));
|
||||
});
|
||||
thead.append(tr);
|
||||
return thead;
|
||||
},
|
||||
|
||||
/**
|
||||
* remove all rows
|
||||
*/
|
||||
clear_rows: function () {
|
||||
this.$('tfoot').remove();
|
||||
this.$('tr.noRows').remove();
|
||||
|
||||
// unbind rows before cleaning them when all are gonna be removed
|
||||
var rowView = null;
|
||||
while ((rowView = this.rowViews.pop())) {
|
||||
// this is a hack to avoid all the elements are removed one by one
|
||||
rowView.unbind(null, null, this);
|
||||
// each element removes itself from rowViews
|
||||
rowView.clean();
|
||||
}
|
||||
// clean all the html at the same time
|
||||
this.rowViews = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* add rows
|
||||
*/
|
||||
addRow: function (row, collection, options) {
|
||||
var self = this;
|
||||
var tr = new self.rowView({ // eslint-disable-line new-cap
|
||||
model: row,
|
||||
order: this.model.columnNames(),
|
||||
row_header: this.options.row_header
|
||||
});
|
||||
tr.tableView = this;
|
||||
|
||||
tr.bind('clean', function () {
|
||||
var idx = _.indexOf(self.rowViews, tr);
|
||||
self.rowViews.splice(idx, 1);
|
||||
// update index
|
||||
for (var i = idx; i < self.rowViews.length; ++i) {
|
||||
self.rowViews[i].$el.attr('data-y', i);
|
||||
}
|
||||
}, this);
|
||||
tr.bind('changeRow', this.rowChanged, this);
|
||||
tr.bind('saved', this.rowSynched, this);
|
||||
tr.bind('errorSaving', this.rowFailed, this);
|
||||
tr.bind('saving', this.rowSaving, this);
|
||||
this.retrigger('saving', tr);
|
||||
|
||||
tr.render();
|
||||
if (options && options.index !== undefined && options.index != self.rowViews.length) { // eslint-disable-line eqeqeq
|
||||
tr.$el.insertBefore(self.rowViews[options.index].$el);
|
||||
self.rowViews.splice(options.index, 0, tr);
|
||||
// tr.$el.attr('data-y', options.index);
|
||||
// change others view data-y attribute
|
||||
for (var i = options.index; i < self.rowViews.length; ++i) {
|
||||
self.rowViews[i].$el.attr('data-y', i);
|
||||
}
|
||||
} else {
|
||||
// at the end
|
||||
tr.$el.attr('data-y', self.rowViews.length);
|
||||
self.$el.append(tr.el);
|
||||
self.rowViews.push(tr);
|
||||
}
|
||||
|
||||
this.trigger('createRow');
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback executed when a row change
|
||||
* @method rowChanged
|
||||
* @abstract
|
||||
*/
|
||||
rowChanged: function () {},
|
||||
|
||||
/**
|
||||
* Callback executed when a row is sync
|
||||
* @method rowSynched
|
||||
* @abstract
|
||||
*/
|
||||
rowSynched: function () {},
|
||||
|
||||
/**
|
||||
* Callback executed when a row fails to reach the server
|
||||
* @method rowFailed
|
||||
* @abstract
|
||||
*/
|
||||
rowFailed: function () {},
|
||||
|
||||
/**
|
||||
* Callback executed when a row send a POST to the server
|
||||
* @abstract
|
||||
*/
|
||||
rowSaving: function () {},
|
||||
|
||||
/**
|
||||
* Callback executed when a row is being destroyed
|
||||
* @method rowDestroyed
|
||||
* @abstract
|
||||
*/
|
||||
rowDestroying: function () {},
|
||||
|
||||
/**
|
||||
* Callback executed when a row gets destroyed
|
||||
* @method rowDestroyed
|
||||
* @abstract
|
||||
*/
|
||||
rowDestroyed: function () {},
|
||||
|
||||
/**
|
||||
* Callback executed when a row gets destroyed and the table data is empty
|
||||
* @method emptyTable
|
||||
* @abstract
|
||||
*/
|
||||
emptyTable: function () {},
|
||||
|
||||
/**
|
||||
* Checks if the table is empty
|
||||
* @method isEmptyTable
|
||||
* @returns boolean
|
||||
*/
|
||||
isEmptyTable: function () {
|
||||
return (this.dataModel.length === 0 && this.dataModel.fetched);
|
||||
},
|
||||
|
||||
/**
|
||||
* render only data rows
|
||||
*/
|
||||
_renderRows: function () {
|
||||
this.clear_rows();
|
||||
if (!this.isEmptyTable()) {
|
||||
if (this.dataModel.fetched) {
|
||||
var self = this;
|
||||
|
||||
this.dataModel.each(function (row) {
|
||||
self.addRow(row);
|
||||
});
|
||||
} else {
|
||||
this._renderLoading();
|
||||
}
|
||||
} else {
|
||||
this._renderEmpty();
|
||||
}
|
||||
},
|
||||
|
||||
_renderLoading: function () {
|
||||
},
|
||||
|
||||
_renderEmpty: function () {
|
||||
},
|
||||
|
||||
/**
|
||||
* Method for the children to redefine with the table behaviour when it has no rows.
|
||||
* @method addEmptyTableInfo
|
||||
* @abstract
|
||||
*/
|
||||
addEmptyTableInfo: function () {
|
||||
// #to be overwrite by descendant classes
|
||||
},
|
||||
|
||||
/**
|
||||
* render table
|
||||
*/
|
||||
render: function () {
|
||||
var self = this;
|
||||
|
||||
// render header
|
||||
self.$el.html(self._renderHeader());
|
||||
|
||||
// render data
|
||||
self._renderRows();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* return jquery cell element of cell x,y
|
||||
*/
|
||||
getCell: function (x, y) {
|
||||
if (this.options.row_header) {
|
||||
++y;
|
||||
}
|
||||
return this.rowViews[y].getCell(x);
|
||||
},
|
||||
|
||||
_cellClick: function (e, evtName) {
|
||||
evtName = evtName || 'cellClick';
|
||||
e.preventDefault();
|
||||
var cell = $(e.currentTarget || e.target);
|
||||
var x = parseInt(cell.attr('data-x'), 10);
|
||||
var y = parseInt(cell.parent().attr('data-y'), 10);
|
||||
this.trigger(evtName, e, cell, x, y);
|
||||
},
|
||||
|
||||
_cellDblClick: function (e) {
|
||||
this._cellClick(e, 'cellDblClick');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
const _ = require('underscore');
|
||||
const CoreView = require('backbone/core-view');
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
initialize: function () {
|
||||
this.tabs = {};
|
||||
this.activeTab = null;
|
||||
this.activePane = null;
|
||||
},
|
||||
|
||||
addTab: function (name, view, options) {
|
||||
options = options || { active: true };
|
||||
if (this.tabs[name] !== undefined) {
|
||||
console.debug(name + 'already added');
|
||||
} else {
|
||||
this.tabs[name] = view.cid;
|
||||
this.addView(view);
|
||||
if (options.after !== undefined) {
|
||||
var e = this.$el.children()[options.after];
|
||||
view.$el.insertAfter(e);
|
||||
} else if (options.prepend) {
|
||||
this.$el.prepend(view.el);
|
||||
} else {
|
||||
this.$el.append(view.el);
|
||||
}
|
||||
this.trigger('tabAdded', name, view);
|
||||
if (options.active) {
|
||||
this.active(name);
|
||||
} else {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getPreviousPane: function () {
|
||||
var tabs = _.toArray(this.tabs);
|
||||
var panes = _.toArray(this._subviews);
|
||||
|
||||
var i = _.indexOf(tabs, this.activePane.cid) - 1;
|
||||
if (i < 0) i = panes.length - 1;
|
||||
|
||||
return panes[i];
|
||||
},
|
||||
|
||||
getNextPane: function () {
|
||||
var tabs = _.toArray(this.tabs);
|
||||
var panes = _.toArray(this._subviews);
|
||||
|
||||
var i = 1 + _.indexOf(tabs, this.activePane.cid);
|
||||
if (i > panes.length - 1) i = 0;
|
||||
|
||||
return panes[i];
|
||||
},
|
||||
|
||||
getPane: function (name) {
|
||||
var vid = this.tabs[name];
|
||||
return this._subviews[vid];
|
||||
},
|
||||
|
||||
getActivePane: function () {
|
||||
return this.activePane;
|
||||
},
|
||||
|
||||
size: function () {
|
||||
return _.size(this.tabs);
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this.removeTabs();
|
||||
CoreView.prototype.clean.call(this);
|
||||
},
|
||||
|
||||
removeTab: function (name) {
|
||||
if (this.tabs[name] !== undefined) {
|
||||
var vid = this.tabs[name];
|
||||
this._subviews[vid].clean();
|
||||
delete this.tabs[name];
|
||||
|
||||
if (this.activeTab == name) { // eslint-disable-line eqeqeq
|
||||
this.activeTab = null;
|
||||
}
|
||||
|
||||
if (_.size(this.tabs)) {
|
||||
this.active(_.keys(this.tabs)[0]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeTabs: function () {
|
||||
for (var name in this.tabs) {
|
||||
var vid = this.tabs[name];
|
||||
this._subviews[vid].clean();
|
||||
delete this.tabs[name];
|
||||
}
|
||||
this.activeTab = null;
|
||||
},
|
||||
|
||||
active: function (name) {
|
||||
const vid = this.tabs[name];
|
||||
|
||||
if (vid !== undefined) {
|
||||
if (this.activeTab !== name) {
|
||||
var v = this._subviews[vid];
|
||||
|
||||
if (this.activeTab) {
|
||||
var vid_old = this._subviews[this.tabs[this.activeTab]];
|
||||
|
||||
vid_old.hide();
|
||||
this.trigger('tabDisabled', this.activeTab, vid_old);
|
||||
this.trigger('tabDisabled:' + this.activeTab, vid_old);
|
||||
if (vid_old.deactivated) {
|
||||
vid_old.deactivated();
|
||||
}
|
||||
}
|
||||
|
||||
v.show();
|
||||
if (v.activated) {
|
||||
v.activated();
|
||||
}
|
||||
|
||||
this.activeTab = name;
|
||||
this.activePane = v;
|
||||
|
||||
this.trigger('tabEnabled', name, v);
|
||||
this.trigger('tabEnabled:' + name, v);
|
||||
}
|
||||
|
||||
return this.activePane;
|
||||
}
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return this;
|
||||
},
|
||||
|
||||
each: function (fn) {
|
||||
_.each(this.tabs, (cid, tab) => {
|
||||
fn(tab, this.getPane(tab));
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
const CoreView = require('backbone/core-view');
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click': '_click'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
_.bindAll(this, 'activate');
|
||||
this.preventDefault = false;
|
||||
},
|
||||
|
||||
activate: function (name) {
|
||||
this.$('a').removeClass('selected');
|
||||
this.$('a[href$="#' + ((this.options.slash) ? '/' : '') + name + '"]').addClass('selected');
|
||||
},
|
||||
|
||||
desactivate: function (name) {
|
||||
this.$('a[href$="#' + ((this.options.slash) ? '/' : '') + name + '"]').removeClass('selected');
|
||||
},
|
||||
|
||||
disable: function (name) {
|
||||
this.$('a[href$="#' + ((this.options.slash) ? '/' : '') + name + '"]').addClass('disabled');
|
||||
},
|
||||
|
||||
enable: function (name) {
|
||||
this.$('a[href$="#' + ((this.options.slash) ? '/' : '') + name + '"]').removeClass('disabled');
|
||||
},
|
||||
|
||||
getTab: function (name) {
|
||||
return this.$('a[href$="#' + ((this.options.slash) ? '/' : '') + name + '"]');
|
||||
},
|
||||
|
||||
disableAll: function () {
|
||||
this.$('a').addClass('disabled');
|
||||
},
|
||||
|
||||
removeDisabled: function () {
|
||||
this.$('.disabled').parent().remove();
|
||||
},
|
||||
|
||||
_click: function (e) {
|
||||
if (e && this.preventDefault) e.preventDefault();
|
||||
|
||||
var t = $(e.target).closest('a');
|
||||
const href = t.attr('href');
|
||||
|
||||
if (!t.hasClass('disabled') && href) {
|
||||
var name = href.replace('#/', '#').split('#')[1];
|
||||
this.trigger('click', name);
|
||||
}
|
||||
},
|
||||
|
||||
linkToPanel: function (panel) {
|
||||
this.preventDefault = true;
|
||||
panel.bind('tabEnabled', this.activate, this);
|
||||
this.bind('click', panel.active, panel);
|
||||
}
|
||||
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./trial-notification.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel',
|
||||
'upgradeUrl',
|
||||
'trialDays'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this.userAccount = this._userModel.get('account_type');
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
upgradeUrl: this._upgradeUrl,
|
||||
trialDays: this._trialDays
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="CDB-Text FlashMessage FlashMessage--main">
|
||||
<div class="u-inner">
|
||||
<div class="FlashMessage FlashMessage-info FlashMessage--main u-flex u-justifyCenter u-alignCenter">
|
||||
<p class="FlashMessage--text"><%= _t('common.trial_notification.views.trial_notification.message', { trial_days: trialDays }) %> <a href="<%= upgradeUrl %>" class="FlashMessage--text"><%= _t('common.trial_notification.views.trial_notification.add_payment') %></a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./upgrade-message.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'userModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Upgrade message for settings pages
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var upgradeUrl = this._configModel.get('upgrade_url');
|
||||
var canUpgrade = upgradeUrl && !this._configModel.get('cartodb_com_hosted') && (!this._userModel.isInsideOrg() || this._userModel.isOrgOwner());
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
canUpgrade: canUpgrade,
|
||||
closeToLimits: this._userModel.isCloseToLimits(),
|
||||
upgradeableWithoutContactingSales: !this._userModel.isEnterprise(),
|
||||
quotaPer: (this._userModel.get('remaining_byte_quota') * 100) / this._userModel.get('quota_in_bytes'),
|
||||
upgradeUrl: upgradeUrl,
|
||||
showTrial: this._userModel.canStartTrial()
|
||||
})
|
||||
);
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._userModel, 'change', this.render);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
<% if (closeToLimits && canUpgrade) { %>
|
||||
<div class="UpgradeElement">
|
||||
<div class="u-inner u-flex u-alignCenter u-justifyCenter">
|
||||
<div class="UpgradeElement-info">
|
||||
<p class="UpgradeElement-infoText u-ellipsLongText">
|
||||
<% if (quotaPer <= 0) { %>
|
||||
You have reached your limits.
|
||||
<% } else { %>
|
||||
You're reaching your account limits.
|
||||
<% } %>
|
||||
|
||||
<% if (upgradeableWithoutContactingSales) { %>
|
||||
Upgrade your account to boost your quota.
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<% if (upgradeableWithoutContactingSales) { %>
|
||||
|
||||
<a href="<%- upgradeUrl %>" class="UpgradeElement-infoText">
|
||||
<span>Upgrade your plan.</span>
|
||||
</a>
|
||||
<% } else { %>
|
||||
|
||||
<a href="mailto:sales@carto.com" class="UpgradeElement-infoText">
|
||||
<span>Talk to Sales.</span>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
@@ -0,0 +1,19 @@
|
||||
var DropdownAdminView = require('dashboard/components/dropdown/dropdown-admin-view');
|
||||
|
||||
/**
|
||||
* The content of the dropdown menu opened by the link at the end of the breadcrumbs menu, e.g.
|
||||
* username / Maps v
|
||||
* ______/\____
|
||||
* | |
|
||||
* | this |
|
||||
* |____________|
|
||||
*/
|
||||
module.exports = DropdownAdminView.extend({
|
||||
className: 'Dropdown',
|
||||
|
||||
hide: function () {
|
||||
this.$el.css({
|
||||
opacity: 0
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
const $ = require('jquery');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const BreadcrumbDropdown = require('./dropdown-view.js');
|
||||
|
||||
/**
|
||||
* View to render the user info section.
|
||||
* Expected to be created from existing DOM element.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click .js-breadcrumb-dropdown-target': '_createBreadcrumbDropdown'
|
||||
},
|
||||
|
||||
_previousDropDown: null,
|
||||
|
||||
_createBreadcrumbDropdown: function (ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (this._previousDropDown) {
|
||||
this._previousDropDown.open();
|
||||
return;
|
||||
}
|
||||
|
||||
var dropdown = new BreadcrumbDropdown({
|
||||
target: $(ev.target),
|
||||
el: $('.js-breadcrumb-dropdown-content'),
|
||||
horizontal_offset: 3, // to match the dropdown indicator/arrow
|
||||
horizontal_position: 'right',
|
||||
tick: 'right'
|
||||
});
|
||||
|
||||
this._previousDropDown = dropdown;
|
||||
this.addView(dropdown);
|
||||
dropdown.on('onDropdownHidden', function () {
|
||||
dropdown.clean();
|
||||
}, this);
|
||||
|
||||
dropdown.render();
|
||||
dropdown.open();
|
||||
},
|
||||
|
||||
_closeAnyOtherOpenDialogs: function () {
|
||||
if (this._previousDropDown) {
|
||||
this._previousDropDown.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click .js-Navmenu-editLink--more': '_onClickMoreLink'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.$metaList = this.$('.js-PublicMap-metaList--mobile');
|
||||
this.$moreLink = this.$('.js-Navmenu-editLink--more');
|
||||
|
||||
this.model.on('change:active', this._toggleMeta, this);
|
||||
},
|
||||
|
||||
_onClickMoreLink: function (e) {
|
||||
this.model.set('active', !this.model.get('active'));
|
||||
},
|
||||
|
||||
_toggleMeta: function () {
|
||||
if (this.model.get('active')) {
|
||||
this.$moreLink.html('Less info');
|
||||
this.$metaList.slideDown(250);
|
||||
} else {
|
||||
this.$moreLink.html('More info');
|
||||
this.$metaList.slideUp(250);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const template = require('./vendor-scripts.tpl');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'userModel',
|
||||
'assetsVersion'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
assetsVersion: this._assetsVersion,
|
||||
googleTagManagerId: this._configModel.get('google_tag_manager_id'),
|
||||
hubspotIds: this._configModel.get('hubspot_ids'),
|
||||
hubspotToken: this._configModel.get('hubspot_token'),
|
||||
intercomAppId: this._configModel.get('intercom_app_id'),
|
||||
intercomEnabled: !!this._userModel.featureEnabled('intercom'),
|
||||
trackjsAppKey: this._configModel.get('trackjs_app_key'),
|
||||
trackjsCustomer: this._configModel.get('trackjs_customer'),
|
||||
trackjsEnabled: !!this._configModel.get('trackjs_enabled'),
|
||||
fullstoryEnabled: !!this._configModel.get('fullstory_enabled'),
|
||||
fullstoryOrg: this._configModel.get('fullstoryOrg'),
|
||||
userEmail: this._userModel.get('email'),
|
||||
userName: this._userModel.get('username'),
|
||||
userId: this._userModel.get('id'),
|
||||
userAccountType: this._userModel.get('account_type'),
|
||||
userCreatedAtInSeconds: Date.parse(this._userModel.get('created_at')) / 1000,
|
||||
userJobRole: this._userModel.get('job_role'),
|
||||
userInTrialPeriod: this._userModel.get('show_trial_reminder')
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
<% if (googleTagManagerId) { %>
|
||||
<!-- Google Tag Manager -->
|
||||
<% // Tags for GTM are being included in _google_tag_manager.html.erb for Rails templates.
|
||||
// So if you change anything here, please make sure that you change it there too. %>
|
||||
|
||||
<script>
|
||||
dataLayer = [{
|
||||
'userId': '<%= userId %>',
|
||||
'userAccountType': '<%= userAccountType %>',
|
||||
'userSignUpDate': '<%= userCreatedAtInSeconds %>',
|
||||
'userJobRole': '<%= userJobRole %>',
|
||||
'userInTrialPeriod': '<%= userInTrialPeriod %>'
|
||||
}];
|
||||
</script>
|
||||
|
||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','<%= googleTagManagerId %>');</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
<% } %>
|
||||
|
||||
<% if (trackjsEnabled) { %>
|
||||
<script type='text/javascript'>
|
||||
window._trackJs = {
|
||||
enabled: true,
|
||||
application: '<%= trackjsAppKey %>',
|
||||
version: '<%= assetsVersion %>',
|
||||
userId: '<%= userName %>',
|
||||
token: '<%= trackjsCustomer %>'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script
|
||||
type='text/javascript'
|
||||
src='//d2zah9y47r7bi2.cloudfront.net/releases/current/tracker.js'
|
||||
data-token='<%= trackjsCustomer %>'>
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
<% if (intercomEnabled) { %>
|
||||
<script type='text/javascript'>
|
||||
window.intercomSettings = {
|
||||
app_id: '<%= intercomAppId %>',
|
||||
email: '<%= userEmail %>'
|
||||
};
|
||||
</script>
|
||||
<script type='text/javascript'>
|
||||
(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/<%= intercomAppId %>';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})();
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
<% if (fullstoryEnabled) { %>
|
||||
<script type='text/javascript'>
|
||||
window['_fs_debug'] = false;
|
||||
window['_fs_host'] = 'www.fullstory.com';
|
||||
window['_fs_org'] = '<%= fullstoryOrg %>';
|
||||
window['_fs_namespace'] = 'FS';
|
||||
(function(m,n,e,t,l,o,g,y){
|
||||
if (e in m && m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].'); return;}
|
||||
g=m[e]=function(a,b){g.q?g.q.push([a,b]):g._api(a,b);};g.q=[];
|
||||
o=n.createElement(t);o.async=1;o.src='https://'+_fs_host+'/s/fs.js';
|
||||
y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y);
|
||||
g.identify=function(i,v){g(l,{uid:i});if(v)g(l,v)};g.setUserVars=function(v){g(l,v)};
|
||||
g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)};
|
||||
g.clearUserCookie=function(c,d,i){if(!c || document.cookie.match('fs_uid=[`;`]*`[`;`]*`[`;`]*`')){
|
||||
d=n.domain;while(1){n.cookie='fs_uid=;domain='+d+
|
||||
';path=/;expires='+new Date(0).toUTCString();i=d.indexOf('.');if(i<0)break;d=d.slice(i+1)}}};
|
||||
})(window,document,window['_fs_namespace'],'script','user');
|
||||
FS.clearUserCookie();
|
||||
FS.identify('<%= userName %>', {
|
||||
displayName: '<%= userName %>',
|
||||
email: '<%= userEmail %>'
|
||||
});
|
||||
</script>
|
||||
<% } %>
|
||||
@@ -0,0 +1,20 @@
|
||||
const $ = require('jquery');
|
||||
const ConfirmationView = require('./confirmation/confirmation-view');
|
||||
|
||||
const ForbiddenAction = require('builder/data/backbone/network-interceptors/interceptors/forbidden-403');
|
||||
const NetworkResponseInterceptor = require('builder/data/backbone/network-interceptors/interceptor');
|
||||
NetworkResponseInterceptor.addURLPattern('api/v1');
|
||||
NetworkResponseInterceptor.addErrorInterceptor(ForbiddenAction());
|
||||
NetworkResponseInterceptor.start();
|
||||
|
||||
$(function () {
|
||||
const { userCreationId, username, customHosted, userURL } = window;
|
||||
|
||||
const confirmation = new ConfirmationView({ // eslint-disable-line
|
||||
el: '.js-info',
|
||||
userCreationId,
|
||||
username,
|
||||
customHosted,
|
||||
userURL
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
<div class="js-info">
|
||||
<h1 class="Sessions-title u-tspace-m">
|
||||
<% if (state === "success") { %>
|
||||
Your account is ready
|
||||
<% } else if (state === "failure") { %>
|
||||
Oops! There was a problem
|
||||
<% } else { %>
|
||||
Your account is being created
|
||||
<% } %>
|
||||
</h1>
|
||||
<p class="Sessions-description">
|
||||
<% if (state === "success") { %>
|
||||
<% if (googleSignup) { %>
|
||||
You will be redirected to your dashboard in a moment...
|
||||
<% } else if (!requiresValidationEmail) { %>
|
||||
You will be redirected to the login page in a moment...
|
||||
<% } else { %>
|
||||
Check your email inbox and validate your email.
|
||||
<% }%>
|
||||
<% } else if (state === "failure") { %>
|
||||
Unfortunately there was a problem creating your account. <% if (!customHosted) { %>Please, <a href="mailto:support@carto.com?subject=User creation error: <%- userCreationId %>">contact us</a><% } %>.
|
||||
<% } else { %>
|
||||
It will take us some time, just a few seconds.
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,87 @@
|
||||
const Backbone = require('backbone');
|
||||
|
||||
const POLL_TIMER = 2000; // Interval time between poll checkings
|
||||
const TIMER_MULTIPLY = 2.5; // Multiply interval for this number
|
||||
const MAX_TRIES = 30; // Max tries until interval change
|
||||
const STATES = {
|
||||
success: 'success',
|
||||
failure: 'failure'
|
||||
};
|
||||
|
||||
/**
|
||||
* User creation model
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
email: '',
|
||||
google_sign_in: false,
|
||||
requires_validation_email: false,
|
||||
state: '',
|
||||
username: ''
|
||||
},
|
||||
|
||||
url: function (method) {
|
||||
return `/api/v1/user_creations/${this.id}`;
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('change:state', this._checkState, this);
|
||||
},
|
||||
|
||||
_checkState: function () {
|
||||
if (this.hasFinished() || this.hasFailed()) {
|
||||
this.destroyCheck();
|
||||
}
|
||||
},
|
||||
|
||||
pollCheck: function () {
|
||||
if (this.pollTimer) return;
|
||||
let tries = 0;
|
||||
|
||||
const request = () => {
|
||||
this.destroyCheck();
|
||||
this.fetch();
|
||||
|
||||
tries += 1;
|
||||
|
||||
// Multiply polling timer by a number when a max
|
||||
// of tries have been reached
|
||||
const multiply = tries > MAX_TRIES ? TIMER_MULTIPLY : 1;
|
||||
|
||||
this.pollTimer = setInterval(request, POLL_TIMER * multiply);
|
||||
};
|
||||
|
||||
this.pollTimer = setInterval(request, POLL_TIMER);
|
||||
|
||||
// Start doing a fetch
|
||||
request();
|
||||
},
|
||||
|
||||
destroyCheck: function () {
|
||||
clearInterval(this.pollTimer);
|
||||
delete this.pollTimer;
|
||||
},
|
||||
|
||||
hasUsedGoogle: function () {
|
||||
return this.get('google_sign_in');
|
||||
},
|
||||
|
||||
requiresValidationEmail: function () {
|
||||
return this.get('requires_validation_email');
|
||||
},
|
||||
|
||||
hasFinished: function () {
|
||||
return this.get('state') === STATES.success;
|
||||
},
|
||||
|
||||
hasFailed: function () {
|
||||
return this.get('state') === STATES.failure;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
const CoreView = require('backbone/core-view');
|
||||
const ConfirmationModel = require('./confirmation-model');
|
||||
const template = require('./confirmation-info.tpl');
|
||||
|
||||
/**
|
||||
* Confirmation view
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function (opts) {
|
||||
if (!opts.userCreationId) {
|
||||
throw new Error('user creation id is needed to check its state');
|
||||
}
|
||||
|
||||
this.model = new ConfirmationModel({
|
||||
id: opts.userCreationId
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
|
||||
this.model.pollCheck();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
googleSignup: this.model.get('google_sign_in'),
|
||||
requiresValidationEmail: this.model.requiresValidationEmail(),
|
||||
userCreationId: this.model.get('id'),
|
||||
state: this.model.get('state'),
|
||||
customHosted: this.options.customHosted
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change:state', function () {
|
||||
this._setLogo();
|
||||
this.render();
|
||||
|
||||
if (this.model.hasFinished() && (this.model.hasUsedGoogle() || !this.model.requiresValidationEmail())) {
|
||||
this._goToUserURL();
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
// Instead of rendering logo each time and f**k the animation
|
||||
// we toggle the 'is-loading' class when process has finished
|
||||
_setLogo: function () {
|
||||
// Loading state
|
||||
this.$('.js-logo').toggleClass('is-loading', !this.model.hasFailed() && !this.model.hasFinished());
|
||||
|
||||
// Remove unnecessary notification, if needed
|
||||
if (this.model.hasFailed()) {
|
||||
this.$('.js-successNotification').remove();
|
||||
} else if (this.model.hasFinished()) {
|
||||
this.$('.js-errorNotification').remove();
|
||||
}
|
||||
|
||||
// Show notification if it is failed or finished
|
||||
if (this.model.hasFailed() || this.model.hasFinished()) {
|
||||
this.$('.js-notification').show();
|
||||
}
|
||||
},
|
||||
|
||||
_goToUserURL: function () {
|
||||
if (this.options.userURL) {
|
||||
window.location.href = this.options.userURL;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
const Backbone = require('backbone');
|
||||
const Polyglot = require('node-polyglot');
|
||||
const _ = require('underscore');
|
||||
const $ = require('jquery');
|
||||
require('dashboard/data/backbone/sync-options');
|
||||
|
||||
const Locale = require('../locale/index');
|
||||
const AuthenticatedUser = require('dashboard/data/authenticated-user-model');
|
||||
const UserModel = require('dashboard/data/user-model');
|
||||
const ConfigModel = require('dashboard/data/config-model');
|
||||
const UserSettingsView = require('dashboard/components/navbar/user-settings-view');
|
||||
const UserIndustriesView = require('dashboard/components/navbar/user-industries-view');
|
||||
const UserInfoView = require('dashboard/components/user-info/user-info-view');
|
||||
const DataLibraryView = require('dashboard/views/data-library/data-library-view');
|
||||
|
||||
//const ACTIVE_LOCALE = window.ACTIVE_LOCALE || 'en';
|
||||
|
||||
var ACTIVE_LOCALE = 'zh-cn';
|
||||
if (ACTIVE_LOCALE !== 'en') {
|
||||
require('moment/locale/' + ACTIVE_LOCALE);
|
||||
}
|
||||
|
||||
const polyglot = new Polyglot({
|
||||
locale: ACTIVE_LOCALE, // Needed for pluralize behaviour
|
||||
phrases: Locale[ACTIVE_LOCALE]
|
||||
});
|
||||
window._t = polyglot.t.bind(polyglot);
|
||||
|
||||
const ForbiddenAction = require('builder/data/backbone/network-interceptors/interceptors/forbidden-403');
|
||||
const NetworkResponseInterceptor = require('builder/data/backbone/network-interceptors/interceptor');
|
||||
NetworkResponseInterceptor.addURLPattern('api/v');
|
||||
|
||||
const configModel = new ConfigModel(
|
||||
_.defaults(
|
||||
{
|
||||
base_url: window.base_url,
|
||||
dataset_base_url: window.dataset_base_url
|
||||
},
|
||||
window.config
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Entry point for data-library index
|
||||
*/
|
||||
|
||||
$(function () {
|
||||
let userModel;
|
||||
const authenticatedUser = new AuthenticatedUser({
|
||||
host: `${configModel.get('common_data_user')}.${configModel.get('account_host')}`
|
||||
});
|
||||
|
||||
authenticatedUser.sync = Backbone.withCORS;
|
||||
|
||||
authenticatedUser.on('change', function (model) {
|
||||
if (model.get('username')) {
|
||||
NetworkResponseInterceptor.addErrorInterceptor(ForbiddenAction(model.get('username')));
|
||||
NetworkResponseInterceptor.start();
|
||||
|
||||
userModel = new UserModel(model.attributes, {
|
||||
configModel: configModel
|
||||
});
|
||||
|
||||
const userSettingsView = new UserSettingsView({
|
||||
el: $('.js-user-settings'),
|
||||
model: userModel
|
||||
});
|
||||
userSettingsView.render();
|
||||
}
|
||||
});
|
||||
|
||||
authenticatedUser.fetch();
|
||||
|
||||
const userIndustriesView = new UserIndustriesView({ // eslint-disable-line no-unused-vars
|
||||
el: $('.js-user-industries')
|
||||
});
|
||||
|
||||
var userInfoView = new UserInfoView({
|
||||
el: $('.js-user-info')
|
||||
});
|
||||
userInfoView.render();
|
||||
|
||||
const dataLibraryView = new DataLibraryView({ configModel });
|
||||
$('.js-data_library').append(dataLibraryView.render().el);
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
const _ = require('underscore');
|
||||
const Backbone = require('backbone');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel'
|
||||
];
|
||||
|
||||
const GRANT_TYPES = {
|
||||
APIS: 'apis',
|
||||
DATABASE: 'database'
|
||||
};
|
||||
|
||||
const TYPES = {
|
||||
MASTER: 'master',
|
||||
DEFAULT: 'default',
|
||||
REGULAR: 'regular'
|
||||
};
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
name: '',
|
||||
token: '',
|
||||
apis: {
|
||||
maps: false,
|
||||
sql: false
|
||||
},
|
||||
datasets: {
|
||||
create: false,
|
||||
listing: false
|
||||
},
|
||||
tables: []
|
||||
},
|
||||
|
||||
initialize: function (attributes, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
regenerate: function () {
|
||||
const options = {
|
||||
url: `${this.url()}/token/regenerate`,
|
||||
type: 'POST',
|
||||
success: (data) => this.set(data)
|
||||
};
|
||||
|
||||
return this.sync(null, this, options);
|
||||
},
|
||||
|
||||
parse: function (data, options) {
|
||||
const schemaName = options.userModel.getSchema();
|
||||
const { grants, ...attrs } = data;
|
||||
const apis = this._parseApiGrants(grants);
|
||||
const tables = this._parseTableGrants(grants);
|
||||
|
||||
const datasets = {
|
||||
create: this._parseDatabaseSchemas(grants, schemaName),
|
||||
listing: this._parseDatabaseGrants(grants)
|
||||
};
|
||||
|
||||
return {
|
||||
...attrs,
|
||||
apis,
|
||||
tables,
|
||||
datasets,
|
||||
id: attrs.name
|
||||
};
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
// Extract apis and tables properties to not include in JSON
|
||||
const { apis, tables, datasets, ...attrs } = this.attributes;
|
||||
|
||||
const grants = [
|
||||
{ type: GRANT_TYPES.APIS, apis: this.getApiGrants() },
|
||||
{
|
||||
type: GRANT_TYPES.DATABASE,
|
||||
...this.getDatabaseGrants()
|
||||
}
|
||||
];
|
||||
|
||||
return { ...attrs, grants };
|
||||
},
|
||||
|
||||
isPublic: function () {
|
||||
return this.get('type') === TYPES.DEFAULT;
|
||||
},
|
||||
|
||||
getApiGrants: function () {
|
||||
const apis = this.get('apis');
|
||||
return Object.keys(apis).filter(name => apis[name]);
|
||||
},
|
||||
|
||||
getDatabaseGrants: function () {
|
||||
const grants = {
|
||||
tables: this.getTablesGrants(),
|
||||
schemas: this.getDatabaseSchemas()
|
||||
};
|
||||
|
||||
if (this.get('datasets').listing) {
|
||||
grants.table_metadata = [];
|
||||
}
|
||||
|
||||
return grants;
|
||||
},
|
||||
|
||||
getDatabaseSchemas: function () {
|
||||
const schemas = [];
|
||||
|
||||
if (this.get('datasets').create) {
|
||||
schemas.push({
|
||||
name: this._userModel.getSchema(),
|
||||
permissions: ['create']
|
||||
});
|
||||
}
|
||||
|
||||
return schemas;
|
||||
},
|
||||
|
||||
getTablesGrants: function () {
|
||||
const tables = _.map(this.get('tables'), (table, tableName) => ({
|
||||
name: tableName,
|
||||
schema: this._userModel.getSchema(),
|
||||
permissions: Object.keys(table.permissions).filter(name => table.permissions[name])
|
||||
}));
|
||||
|
||||
return _.filter(tables, table => table.permissions.length > 0);
|
||||
},
|
||||
|
||||
_parseApiGrants: function (grants) {
|
||||
const apis = _.find(grants, grant => grant.type === GRANT_TYPES.APIS).apis;
|
||||
const apisObj = this._arrayToObj(apis);
|
||||
|
||||
return { ...this.defaults.apis, ...apisObj };
|
||||
},
|
||||
|
||||
_parseTableGrants: function (grants) {
|
||||
const tables = _.find(grants, grant => grant.type === GRANT_TYPES.DATABASE).tables;
|
||||
const tablesObj = tables.reduce((total, table) => {
|
||||
const permissions = this._arrayToObj(table.permissions);
|
||||
return { ...total, [table.name]: { ...table, permissions } };
|
||||
}, {});
|
||||
|
||||
return tablesObj;
|
||||
},
|
||||
|
||||
_parseDatabaseGrants: function (grants) {
|
||||
return !!_.find(grants, grant => grant.type === GRANT_TYPES.DATABASE).table_metadata;
|
||||
},
|
||||
|
||||
_parseDatabaseSchemas: function (grants, schemaName) {
|
||||
const schemas = _.find(grants, grant => grant.type === GRANT_TYPES.DATABASE).schemas;
|
||||
const schema = _.find(schemas, schema => schema.name === schemaName);
|
||||
return !!(schema && schema.permissions && schema.permissions.indexOf('create') > -1);
|
||||
},
|
||||
|
||||
_arrayToObj: function (arr) {
|
||||
return arr.reduce((total, item) => ({ ...total, [item]: true }), {});
|
||||
},
|
||||
|
||||
hasPermissionsSelected: function () {
|
||||
return _.some(this.getTablesGrants().map(table => !_.isEmpty(table.permissions)));
|
||||
},
|
||||
|
||||
urlRoot: function () {
|
||||
return `${this._userModel.get('base_url')}/api/v3/api_keys`;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
MASTER: 'master',
|
||||
DEFAULT: 'default',
|
||||
REGULAR: 'regular'
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
const $ = require('jquery');
|
||||
const Backbone = require('backbone');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
const ApiKeyModel = require('dashboard/data/api-key-model');
|
||||
const apiKeysCollectionTypes = require('dashboard/data/api-keys-collection-types');
|
||||
const PaginationModel = require('builder/components/pagination/pagination-model');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'userModel'
|
||||
];
|
||||
|
||||
const STATUS = {
|
||||
fetching: 'fetching',
|
||||
fetched: 'fetched',
|
||||
errored: 'errored'
|
||||
};
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
defaults: {
|
||||
status: STATUS.fetching
|
||||
},
|
||||
|
||||
initialize: function (models, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this._type = options.type || [apiKeysCollectionTypes.REGULAR];
|
||||
this._paginationModel = new PaginationModel({
|
||||
per_page: 5,
|
||||
current_page: 1
|
||||
});
|
||||
|
||||
this.on('sync', this._onCollectionSync);
|
||||
this.on('error', () => { this.status = STATUS.errored; });
|
||||
this.listenTo(this._paginationModel, 'change:current_page', this.fetch);
|
||||
},
|
||||
|
||||
url: function () {
|
||||
const urlParams = {
|
||||
per_page: this._paginationModel.get('per_page'),
|
||||
page: this._paginationModel.get('current_page'),
|
||||
type: this._type
|
||||
};
|
||||
|
||||
return `${this._userModel.get('base_url')}/api/v3/api_keys?${$.param(urlParams)}`;
|
||||
},
|
||||
|
||||
fetch: function () {
|
||||
const options = {
|
||||
headers: {
|
||||
'Authorization': `Basic ${this._userModel.getAuthToken()}`
|
||||
}
|
||||
};
|
||||
|
||||
Backbone.Collection.prototype.fetch.call(this, options);
|
||||
},
|
||||
|
||||
model: function (attrs, opts) {
|
||||
const options = { ...opts, userModel: opts.collection._userModel };
|
||||
|
||||
return new ApiKeyModel(attrs, options);
|
||||
},
|
||||
|
||||
parse: function ({ result, ...stats }) {
|
||||
this._stats = stats;
|
||||
|
||||
return result.map(key => ({ ...key, id: key.name })); // We are using the name as an unique id
|
||||
},
|
||||
|
||||
_onCollectionSync: function () {
|
||||
this.status = STATUS.fetched;
|
||||
|
||||
this._paginationModel.set({
|
||||
total_count: this._getTotalPages()
|
||||
});
|
||||
},
|
||||
|
||||
_getTotalPages: function (attribute) {
|
||||
return (this._stats && this._stats.total) || 0;
|
||||
},
|
||||
|
||||
getPaginationModel: function () {
|
||||
return this._paginationModel;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
const Backbone = require('backbone');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
/**
|
||||
* Model that let user upload files
|
||||
* to our endpoints
|
||||
*/
|
||||
|
||||
require('backbone-model-file-upload');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel',
|
||||
'userId'
|
||||
];
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
url: function (method) {
|
||||
var version = this._configModel.urlVersion('asset', method);
|
||||
return `/api/${version}/users/${this._userId}/assets`;
|
||||
},
|
||||
|
||||
fileAttribute: 'filename',
|
||||
|
||||
initialize: function (attributes, options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
}
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user