Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
@@ -0,0 +1,45 @@
const CoreView = require('backbone/core-view');
const template = require('./delete-organization-user.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'modalModel',
'configModel',
'passwordNeeded',
'organizationUser',
'authenticityToken'
];
module.exports = CoreView.extend({
events: {
'click .js-cancel': '_closeDialog',
'submit .js-form': '_closeDialog'
},
options: {
authenticityToken: '',
organizationUser: {}
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
CoreView.prototype.initialize.apply(this);
},
render: function () {
this.$el.html(template({
username: this._organizationUser.get('username'),
formAction: `${this._configModel.prefixUrl()}/organization/users/${this._organizationUser.get('username')}`,
authenticityToken: this._authenticityToken,
passwordNeeded: this._passwordNeeded
}));
return this;
},
_closeDialog: function () {
if (this._modalModel) {
this._modalModel.destroy();
}
}
});
@@ -0,0 +1,41 @@
<form accept-charset="UTF-8" action="<%- formAction %>" method="post" class="js-form">
<input name="utf8" type="hidden" value="&#x2713;" />
<input name="authenticity_token" type="hidden" value="<%- authenticityToken %>" />
<input name="_method" type="hidden" value="delete" />
<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 <%- username %>'s account.</p>
<p class="Dialog-headerText">
By deleting this account all <%- username %>'s maps and datasets will be lost,
but extra credits will be reassigned to your user.
<% if (passwordNeeded) { %>
Type your password, please.
<% } %>
</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="password_confirmation" class="CDB-InputText CDB-Text Form-input Form-input--long" value=""/>
</div>
</div>
</div>
<% } %>
<div class="CDB-Text Dialog-footer u-inner">
<button class="CDB-Button CDB-Button--secondary Dialog-footerBtn js-cancel" type="button">
<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">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Yes, delete <%- username %> account</span>
</button>
</div>
</form>
@@ -0,0 +1,40 @@
const CoreView = require('backbone/core-view');
const template = require('./delete-organization.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'userModel',
'modalModel',
'authenticityToken'
];
/**
* When an organization owner wants to delete the full organization
*
*/
module.exports = CoreView.extend({
options: {
authenticityToken: ''
},
events: {
'click .js-cancel': '_closeDialog',
'submit .js-form': '_closeDialog'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
return this.$el.html(template({
formAction: `${this._userModel.get('base_url')}/organization`,
authenticityToken: this._authenticityToken,
passwordNeeded: !!this._userModel.get('needs_password_confirmation')
}));
},
_closeDialog: function () {
this._modalModel.destroy();
}
});
@@ -0,0 +1,41 @@
<form accept-charset="UTF-8" action="<%- formAction %>" method="post" class="js-form">
<input name="utf8" type="hidden" value="&#x2713;" />
<input name="authenticity_token" type="hidden" value="<%- authenticityToken %>" />
<input name="_method" type="hidden" value="delete" />
<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 organization.</p>
<p class="Dialog-headerText">
You will remove this organization and all its users (including this account)<br/>
and it will not be possible to recover its information (including tables and data) after this deletion.<br/>
<% if (passwordNeeded) { %>
If you want to proceed, type your password:<br/>
<% } %>
</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" name="deletion_password_confirmation" class="CDB-InputText CDB-Text Form-input Form-input--long" value=""/>
</div>
</div>
</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">Cancel</span>
</button>
<button type="submit" class="CDB-Button CDB-Button--error js-ok">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Yes, delete the organization</span>
</button>
</div>
</form>
@@ -0,0 +1,160 @@
const _ = require('underscore');
const $ = require('jquery');
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const PagedSearchView = require('dashboard/components/paged-search/paged-search-view');
const PagedSearchModel = require('dashboard/data/paged-search-model');
const PasswordValidatedForm = require('dashboard/helpers/password-validated-form');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const template = require('./add-group-users-view.tpl');
const loadingView = require('builder/components/loading/render-loading.js');
const requestErrorTemplate = require('dashboard/views/data-library/content/request-error-template.tpl');
const responseParser = require('dashboard/helpers/response-parser');
const errorTemplate = require('dashboard/views/data-library/content/error-template.tpl');
const GroupUsersListView = require('dashboard/views/organization/groups-admin/group-users-list/group-users-list-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'group',
'orgUsers',
'userModel',
'modalModel'
];
/**
* Dialog to add custom basemap to current map.
*/
module.exports = CoreView.extend({
events: {
'click .ok': 'ok'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this.model = new Backbone.Model();
// Include current user in fetch results
this._orgUsers.excludeCurrentUser(false);
this._modals = new ModalsServiceModel();
this._initBinds();
this._initViews();
},
clean: function () {
// restore org users
this._orgUsers.restoreExcludeCurrentUser();
CoreView.prototype.clean.apply(this);
},
/**
* @override cdb.ui.common.Dialog.prototype.render
*/
render: function () {
this.$el.html(this.render_content());
this.$el.addClass('Dialog-contentWrapper');
this._onChangeSelected();
return this;
},
/**
* @implements cdb.ui.common.Dialog.prototype.render_content
*/
render_content: function () {
switch (this.model.get('state')) {
case 'saving':
return loadingView({
title: 'Adding users to group'
});
case 'passwordConfirmationFail':
return requestErrorTemplate({
msg: this.model.get('failMessage')
});
case 'saveFail':
return errorTemplate({
msg: ''
});
default:
const $content = $(template());
$content.find('.js-dlg-body').replaceWith(this._PagedSearchView.render().el);
return $content;
}
},
ok: function () {
const selectedUsers = this._selectedUsers();
if (!selectedUsers.length) return;
if (!this._userModel.needsPasswordConfirmation()) {
this.model.set('state', 'saving');
return this._addUsers();
}
PasswordValidatedForm.showPasswordModal({
modalService: this._modals,
onPasswordTyped: password => {
this.model.set('state', 'saving');
this._addUsers(password);
}
});
},
_initViews: function () {
this._PagedSearchView = new PagedSearchView({
isUsedInDialog: true,
pagedSearchModel: new PagedSearchModel({
per_page: 50,
order: 'username'
}),
collection: this._orgUsers,
createListView: this._createUsersListView.bind(this)
});
this.addView(this._PagedSearchView);
},
_createUsersListView: function () {
return new GroupUsersListView({
users: this._orgUsers
});
},
_initBinds: function () {
this.listenTo(this._orgUsers, 'change:selected', this._onChangeSelected);
this.listenTo(this.model, 'change:state', this.render);
},
_onChangeSelected: function () {
this.$('.ok').toggleClass('is-disabled', this._selectedUsers().length === 0);
},
_selectedUsers: function () {
return this._orgUsers.where({ selected: true });
},
_addUsers: function (password) {
const selectedUsers = this._selectedUsers();
const ids = _.pluck(selectedUsers, 'id');
this._group.users.addInBatch(ids, password)
.done(() => {
this._group.users.add(selectedUsers);
this._modalModel.destroy();
})
.fail(response => {
const errors = responseParser(response) || '';
if (errors.indexOf('Confirmation password') > -1) {
return this.model.set({
state: 'passwordConfirmationFail',
failMessage: errors || ''
});
}
this.model.set({ state: 'saveFail' });
});
}
});
@@ -0,0 +1,23 @@
<div class="CDB-Text Dialog-content Dialog-content--expanded">
<div class="Dialog-header Dialog-header--expanded CreateDialog-header">
<ul class="CreateDialog-headerSteps">
<li class="CreateDialog-headerStep CreateDialog-headerStep--single">
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
<i class="CDB-IconFont CDB-IconFont-boss"></i>
</div>
<p class="Dialog-headerTitle">Add users to this group</p>
<p class="Dialog-headerText">When sharing a dataset or map with a group all the users within that group will get the same permissions.</p>
</li>
</ul>
</div>
<div class="CDB-Text js-dlg-body"></div>
<div class="Dialog-stickyFooter">
<div class="Dialog-footer ChangePrivacy-shareFooter u-inner">
<div></div>
<button class="CDB-Button CDB-Button--primary ok is-disabled">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Add users</span>
</button>
</div>
</div>
</div>
@@ -0,0 +1,96 @@
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const loadingView = require('builder/components/loading/render-loading');
const createGroupTemplate = require('./create-group.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'group',
'onCreated',
'flashMessageModel'
];
/**
* View to create a new group for an organization.
*/
module.exports = CoreView.extend({
tagName: 'form',
events: {
'click .js-create': '_onClickCreate',
'keyup .js-name': '_onChangeName'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this.model = new Backbone.Model();
this._initBinds();
},
render: function () {
if (this.model.get('isLoading')) {
this.$el.html(
loadingView({
title: 'Creating group'
})
);
} else {
this.$el.html(createGroupTemplate());
}
return this;
},
_initBinds: function () {
this.listenTo(this.model, 'change:isLoading', this.render);
},
_onClickCreate: function (ev) {
this.killEvent(ev);
const name = this._name();
if (name) {
this.model.set('isLoading', true);
this._group.save(
{ display_name: name },
{
wait: true,
success: this._onCreated,
error: this._showErrors.bind(this)
}
);
}
},
_showErrors: function (message, response, request) {
this.model.set('isLoading', false);
let flashMessage = 'Could not create group for some unknown reason, please try again';
let jsonData;
try {
jsonData = response && JSON.parse(response.responseText);
} catch (e) {
jsonData = {};
}
if (jsonData && jsonData.errors) {
flashMessage = jsonData.errors.join('. ');
}
this._flashMessageModel.show(flashMessage, 'error');
},
_onChangeName: function () {
this._flashMessageModel.hide();
this.$('.js-create').toggleClass('is-disabled', this._name().length === 0);
},
_name: function () {
return this.$('.js-name').val();
}
});
@@ -0,0 +1,16 @@
<div class="FormAccount-separator"></div>
<div class="FormAccount-row">
<div class="FormAccount-rowLabel">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor" for="group-name">Group name</label>
</div>
<div class="FormAccount-rowData">
<input type="text" class="CDB-InputText CDB-Text js-name" id="group-name" size="30" maxlength="28" placeholder="Enter a name of group" />
<div class="FormAccount-rowInfo FormAccount-rowInfo--marginLeft">
</div>
</div>
</div>
<div class="FormAccount-footer u-justifyEnd">
<button class="CDB-Button CDB-Button--primary is-disabled js-create">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Create group</span>
</button>
</div>
@@ -0,0 +1,139 @@
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const loadingView = require('builder/components/loading/render-loading');
const PasswordValidatedForm = require('dashboard/helpers/password-validated-form');
const template = require('./edit-group.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'group',
'userModel',
'flashMessageModel',
'modals',
'onSaved',
'onDeleted'
];
/**
* View to edit an organization group.
*/
module.exports = CoreView.extend({
tagName: 'form',
events: {
'click .js-delete': '_onClickDelete',
'click .js-save': '_onClickSave',
'submit form': '_onClickSave',
'keyup .js-name': '_onChangeName'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this.model = new Backbone.Model();
this._initBinds();
},
render: function () {
if (this.model.get('isLoading')) {
this.$el.html(
loadingView({
title: this.model.get('loadingText')
})
);
} else {
this.$el.html(
template({ displayName: this._group.get('display_name') })
);
}
return this;
},
_initBinds: function () {
this.listenTo(this.model, 'change:isLoading', this.render);
},
_onClickSave: function (ev) {
this.killEvent(ev);
const name = this._name();
if (name && name !== this._group.get('display_name')) {
this._setLoading('Saving changes');
this._group.save(
{ display_name: name },
{
wait: true,
success: this._onSaved,
error: this._showErrors.bind(this)
});
}
},
_onClickDelete: function (ev) {
this.killEvent(ev);
if (!this._userModel.needsPasswordConfirmation()) {
return this._destroyGroup();
}
PasswordValidatedForm.showPasswordModal({
modalService: this._modals,
onPasswordTyped: password => this._destroyGroup(password)
});
},
_destroyGroup: function (password) {
this._setLoading('Deleting group');
this._group.destroy({
wait: true,
data: JSON.stringify({
...this._group.attributes,
password_confirmation: password
}),
contentType: 'application/json; charset=utf-8',
success: this._onDeleted,
error: this._showErrors.bind(this)
});
},
_setLoading: function (msg) {
this._flashMessageModel.hide();
this.model.set({
isLoading: !!msg,
loadingText: msg
});
},
_showErrors: function (message, response, request) {
this._setLoading('');
let flashMessage = 'Could not update group for some unknown reason, please try again';
let jsonData;
try {
jsonData = response && JSON.parse(response.responseText);
} catch (e) {
jsonData = {};
}
if (jsonData && jsonData.errors) {
flashMessage = jsonData.errors.join('. ');
}
this._flashMessageModel.show(flashMessage, 'error');
},
_onChangeName: function () {
this.$('.js-save').toggleClass('is-disabled', this._name().length === 0);
this._flashMessageModel.hide();
},
_name: function () {
return this.$('.js-name').val();
}
});
@@ -0,0 +1,21 @@
<div class="FormAccount-separator"></div>
<div class="FormAccount-row">
<div class="FormAccount-rowLabel">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor" for="group-name">Group name</label>
</div>
<div class="FormAccount-rowData">
<input type="text" class="CDB-InputText CDB-Text FormAccount-input FormAccount-input--med js-name" id="group-name" size="30" maxlength="28" value="<%- displayName %>" placeholder="Enter a name of group" />
<div class="FormAccount-rowInfo FormAccount-rowInfo--marginLeft">
</div>
</div>
</div>
<div class="FormAccount-footer">
<button type="submit" class="CDB-Button CDB-Button--error js-delete">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Delete this group</span>
</button>
<button type="submit" class="CDB-Button CDB-Button--primary js-save">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Save changes</span>
</button>
</div>
@@ -0,0 +1,124 @@
const _ = require('underscore');
const CoreView = require('backbone/core-view');
const AddGroupUsersView = require('dashboard/views/organization/groups-admin/add-group-users/add-group-users-view');
const template = require('./add-or-remove-group-users-filters-extra.tpl');
const ViewFactory = require('builder/components/view-factory');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const PasswordValidatedForm = require('dashboard/helpers/password-validated-form');
const loadingView = require('builder/components/loading/render-loading.js');
const requestErrorTemplate = require('dashboard/views/data-library/content/request-error-template.tpl');
const errorTemplate = require('dashboard/views/data-library/content/error-template.tpl');
const responseParser = require('dashboard/helpers/response-parser');
const randomQuote = require('builder/components/loading/random-quote');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'group',
'orgUsers',
'userModel'
];
/**
* View for the add/remove button in the filters part.
*/
module.exports = CoreView.extend({
className: 'Filters-group',
events: {
'click .js-add-users': '_onClickAddUsers',
'click .js-rm-users': '_onClickRemoveUsers'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._modals = new ModalsServiceModel();
this.listenTo(this._group.users, 'change:selected', this._onChangeSelectedUser);
this.listenTo(this._group.users, 'add remove reset', this.render);
},
render: function () {
this.$el.html(template());
return this;
},
_onClickAddUsers: function (event) {
this.killEvent(event);
this._openAddGroupsUsersDialog();
},
_openAddGroupsUsersDialog: function () {
this._modals.create(modalModel => {
return new AddGroupUsersView({
group: this._group,
orgUsers: this._orgUsers,
modalModel,
userModel: this._userModel
});
});
},
_onChangeSelectedUser: function () {
const hasSelectedUsers = this._selectedUsers().length > 0;
this.$('.js-add-users').toggle(!hasSelectedUsers);
this.$('.js-rm-users').toggle(hasSelectedUsers);
},
_onClickRemoveUsers: function (ev) {
this.killEvent(ev);
if (!this._userModel.needsPasswordConfirmation()) {
return this._removeUsers();
}
PasswordValidatedForm.showPasswordModal({
modalService: this._modals,
onPasswordTyped: password => this._removeUsers(password)
});
},
_removeUsers: function (password) {
const selectedUsers = this._selectedUsers();
if (selectedUsers.length > 0) {
const userIds = _.pluck(selectedUsers, 'id');
const modalModel = this._modals.create(() => {
return this._createLoadingView();
});
this._group.users.removeInBatch(userIds, password)
.always(function () {
modalModel.destroy();
})
.fail(response => {
modalModel.destroy();
const errors = responseParser(response) || '';
if (errors.indexOf('Confirmation password') > -1) {
return this._modals.create(function (modalModel) {
return ViewFactory.createByHTML(requestErrorTemplate({ msg: errors }));
});
}
this._modals.create(function (modalModel) {
return ViewFactory.createByHTML(errorTemplate({ msg: errors }));
});
});
}
},
_createLoadingView: function () {
return ViewFactory.createByHTML(loadingView({
title: 'Removing users',
descHTML: randomQuote()
}));
},
_selectedUsers: function () {
return this._group.users.where({ selected: true });
}
});
@@ -0,0 +1,6 @@
<button class="CDB-Button CDB-Button--primary js-add-users">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Add users</span>
</button>
<button class="CDB-Button CDB-Button--error js-rm-users" style="display: none;">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Remove from group</span>
</button>
@@ -0,0 +1,101 @@
const _ = require('underscore');
const CoreView = require('backbone/core-view');
const ViewFactory = require('builder/components/view-factory');
const template = require('./empty-group-filters-extra.tpl');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const PasswordValidatedForm = require('dashboard/helpers/password-validated-form');
const loadingView = require('builder/components/loading/render-loading.js');
const requestErrorTemplate = require('dashboard/views/data-library/content/request-error-template.tpl');
const errorTemplate = require('dashboard/views/data-library/content/error-template.tpl');
const responseParser = require('dashboard/helpers/response-parser');
const randomQuote = require('builder/components/loading/random-quote');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'groupUsers',
'orgUsers',
'userModel'
];
/**
* View for the add users button and state.
*/
module.exports = CoreView.extend({
className: 'Filters-group',
events: {
'click .js-add-users': '_onClickAddUsers'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._modals = new ModalsServiceModel();
// Init binds
this.listenTo(this._orgUsers, 'change:selected', this._onChangeSelectedUser);
},
render: function () {
this.$el.html(template());
return this;
},
_onChangeSelectedUser: function () {
this.$('.js-add-users').toggleClass('is-disabled', this._selectedUsers().length === 0);
},
_onClickAddUsers: function (ev) {
this.killEvent(ev);
if (!this._userModel.needsPasswordConfirmation()) {
return this._addUsers();
}
PasswordValidatedForm.showPasswordModal({
modalService: this._modals,
onPasswordTyped: password => this._addUsers(password)
});
},
_addUsers: function (password) {
const selectedUsers = this._selectedUsers();
if (selectedUsers.length > 0) {
const userIds = _.pluck(selectedUsers, 'id');
const modalModel = this._modals.create(modalModel => {
return this._createLoadingView();
});
this._groupUsers.addInBatch(userIds, password)
.always(() => modalModel.destroy())
.fail(response => {
const errors = responseParser(response) || '';
if (errors.indexOf('Confirmation password') > -1) {
return this._modals.create(function (modalModel) {
return ViewFactory.createByHTML(requestErrorTemplate({ msg: errors }));
});
}
this._modals.create(function (modalModel) {
return ViewFactory.createByHTML(errorTemplate({ msg: errors }));
});
});
}
},
_createLoadingView: function () {
return ViewFactory.createByHTML(loadingView({
title: 'Adding users',
descHTML: randomQuote()
}));
},
_selectedUsers: function () {
return this._orgUsers.where({ selected: true });
}
});
@@ -0,0 +1,3 @@
<button class="CDB-Button CDB-Button--primary is-disabled js-add-users">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Add users</span>
</button>
@@ -0,0 +1,65 @@
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const pluralizeString = require('dashboard/helpers/pluralize');
const template = require('./group-header.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'group',
'urls'
];
/**
* Header view when looking at details of a specific group.
*/
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initBinds();
},
_initBinds: function () {
this.listenTo(this._group, 'change:display_name', this.render);
this.listenTo(this._group.users, 'reset add remove', this.render);
},
render: function () {
this._$orgSubheader().hide();
const isNewGroup = this._group.isNew();
const templateData = {
backUrl: this._urls.root,
title: this._group.get('display_name') || 'Create new group',
isNewGroup: isNewGroup,
usersUrl: false
};
if (isNewGroup) {
templateData.editUrl = window.location;
templateData.editUrl.isCurrent = true;
} else {
templateData.editUrl = this._urls.edit;
templateData.usersUrl = this._urls.users;
const usersCount = this._group.users.length;
templateData.usersLabel = usersCount === 0 ? 'Users' : `${usersCount} ${pluralizeString('User', 'Users', usersCount)}`;
if (!this._urls.users.isCurrent) {
templateData.backUrl = this._urls.users;
}
}
this.$el.html(template(templateData));
return this;
},
_$orgSubheader: function () {
return $('.js-org-subheader');
},
clean: function () {
this._$orgSubheader().show();
CoreView.prototype.clean.call(this);
}
});
@@ -0,0 +1,33 @@
<div class="Filters is-relative">
<span class="Filters-separator"></span>
<div class="Filters-inner">
<div class="Filters-row">
<ul class="Filters-group CDB-Text CDB-Size-medium">
<li class="u-flex u-alignCenter">
<a href="<%- backUrl %>" class="u-actionTextColor u-flex u-alignCenter">
<i class="CDB-IconFont CDB-IconFont-arrowPrev u-rSpace--xl"></i>
</a>
</li>
<li class="Filters-typeItem">
<div class="FormAccount-title">
<p class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor"><%- title %></p>
</div>
</li>
</ul>
<ul class="Filters-group CDB-Text CDB-Size-medium">
<% if (usersUrl) { %>
<li class="Filters-typeItem">
<a href="<%- usersUrl %>" class="CDB-Text CDB-Size-medium u-mainTextColor Filters-typeLink <%- usersUrl.isCurrent ? 'is-selected' : '' %>">
<%- usersLabel %>
</a>
</li>
<% } %>
<li class="Filters-typeItem">
<a href="<%- editUrl %>" class="CDB-Text CDB-Size-medium u-mainTextColor Filters-typeLink <%- editUrl.isCurrent ? 'is-selected' : '' %>">
Settings
</a>
</li>
</ul>
</div>
</div>
</div>
@@ -0,0 +1,3 @@
<a href="<%- createGroupUrl %>" class="CDB-Button CDB-Button--primary">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Create new group</span>
</a>
@@ -0,0 +1,66 @@
const CoreView = require('backbone/core-view');
const GroupsListView = require('dashboard/views/organization/groups-admin/groups-list/groups-list-view');
const groupsIndexFiltersExtraTemplate = require('./group-index-filters-extra.tpl');
const PagedSearchView = require('dashboard/components/paged-search/paged-search-view');
const PagedSearchModel = require('dashboard/data/paged-search-model');
const ViewFactory = require('builder/components/view-factory');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'groups',
'router',
'newGroupUrl'
];
/**
* Index view of groups to list groups of an organization
*/
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
this.clearSubViews();
const pagedSearchView = new PagedSearchView({
pagedSearchModel: new PagedSearchModel({
fetch_users: true,
fetch_shared_maps_count: true,
fetch_shared_tables_count: true
}),
collection: this._groups,
createListView: this._createGroupsView.bind(this),
thinFilters: true,
filtersExtrasView: this._createFiltersExtraView(),
noResults: {
icon: 'CDB-IconFont-group',
title: 'You have not created any groups yet',
msg: 'Creating groups enables you to visualize and search for user members assigned to a business group or team in your organization.'
}
});
this.addView(pagedSearchView);
this.$el.empty();
this.$el.append(pagedSearchView.render().el);
return this;
},
_createGroupsView: function () {
return new GroupsListView({
groups: this._groups,
newGroupUrl: this._newGroupUrl
});
},
_createFiltersExtraView: function () {
return ViewFactory.createByTemplate(groupsIndexFiltersExtraTemplate, {
createGroupUrl: this._router._rootUrl.urlToPath('new')
}, {
className: 'Filters-group'
});
}
});
@@ -0,0 +1,54 @@
const CoreView = require('backbone/core-view');
const template = require('./group-user.tpl');
const pluralizeStr = require('dashboard/helpers/pluralize');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'model'
];
/**
* View of a single group user.
*/
module.exports = CoreView.extend({
tagName: 'li',
className: 'OrganizationList-user is-selectable',
events: {
'click': '_onClick'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initBinds();
},
_initBinds: function () {
this.listenTo(this.model, 'change:selected', this._onChangeSelected);
},
render: function () {
this.$el.html(
template({
avatarUrl: this.model.get('avatar_url'),
username: this.model.get('username'),
email: this.model.get('email'),
maps_count: pluralizeStr.prefixWithCount('map', 'maps', this.model.get('all_visualization_count')),
table_count: pluralizeStr.prefixWithCount('dataset', 'datasets', this.model.get('table_count'))
})
);
return this;
},
_onChangeSelected: function (model, isSelected) {
this.$el.toggleClass('is-selected', !!isSelected);
},
_onClick: function (ev) {
this.killEvent(ev);
this.model.set('selected', !this.model.get('selected'));
}
});
@@ -0,0 +1,25 @@
<a class="CDB-Text OrganizationList-userLink">
<div class="OrganizationList-userAvatar UserAvatar">
<img src="<%- avatarUrl %>" alt="<%- username %>" src="<%- username %>" class="UserAvatar-img UserAvatar-img--medium-large OrganizationList-userAvatar--img" />
</div>
<div class="OrganizationList-userInfo">
<div class="OrganizationList-userInfoName">
<h3 class="CDB-Size-medium OrganizationList-userInfoTitle u-ellipsLongText" title="<%- username %>">
<%- username %>
</h3>
<h4 class="CDB-Text CDB-Size-small u-ellipis u-secondaryTextColor" title="<%- email %>">
<%- email %>
</h4>
</div>
<div class="OrganizationList-userInfoData CDB-Text CDB-Size-small u-altTextColor u-alignCenter">
<p class="u-flex u-alignCenter">
<i class="CDB-IconFont CDB-IconFont-map OrganizationList-userInfoData--paragraph-icon"></i>
<%- maps_count %>
</p>
<p class="u-flex u-alignCenter u-lSpace--xl">
<i class="CDB-IconFont CDB-IconFont-rows OrganizationList-userInfoData--paragraph-icon"></i>
<%- table_count %>
</p>
</div>
</div>
</a>
@@ -0,0 +1,43 @@
const CoreView = require('backbone/core-view');
const GroupUserView = require('dashboard/views/organization/groups-admin/group-user/group-user-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'users'
];
/**
* View of group users.
*/
module.exports = CoreView.extend({
tagName: 'ul',
className: 'OrganizationList',
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
// init binds
this.listenTo(this._users, 'reset add remove', this.render);
},
render: function () {
this.clearSubViews();
this._renderUsers();
return this;
},
_renderUsers: function () {
this._users.each(this._createUserView, this);
},
_createUserView: function (user) {
const view = new GroupUserView({
model: user
});
this.addView(view);
this.$el.append(view.render().el);
}
});
@@ -0,0 +1,144 @@
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const PagedSearchView = require('dashboard/components/paged-search/paged-search-view');
const PagedSearchModel = require('dashboard/data/paged-search-model');
const ViewFactory = require('builder/components/view-factory');
const loadingTemplate = require('builder/components/loading/loading.tpl');
const randomQuote = require('builder/components/loading/random-quote');
const AddRemoveFiltersExtraView = require('dashboard/views/organization/groups-admin/filters/add-or-remove-group-users-filters-extra-view');
const EmptyGroupFiltersExtraView = require('dashboard/views/organization/groups-admin/filters/empty-group-filters-extra-view');
const GroupUsersListView = require('dashboard/views/organization/groups-admin/group-users-list/group-users-list-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'group',
'orgUsers',
'userModel'
];
/**
* View to manage users of a group
* It basically has two states, each which relies on its own collection:
* - Empty group: i.e. no users, show organization users and allow to add users directly by selecting
* - Group users: allow to add or remove users from group.
*/
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._groupUsers = this._group.users;
this._hasPrefetchedGroupUsers = false;
this._orgUsers.excludeCurrentUser(false);
this.model = new Backbone.Model({
hasPrefetchedGroupUsers: false,
lastRendered: null //, 'groupUsers', 'empty'
});
// Init binds
this.listenTo(this._groupUsers, 'sync', this._onResetGroupUsers);
// Pre-fetch to know what view to render
this._groupUsers.fetch({
success: () => {
this.model.set('hasPrefetchedGroupUsers', true);
this.render();
}
});
},
render: function () {
this.clearSubViews();
this.$el.empty();
let view;
if (this.model.get('hasPrefetchedGroupUsers')) {
view = this._groupUsers.totalCount() > 0
? this._createViewForGroupUsers()
: this._createViewForEmptyGroup();
} else {
view = this._createInitialPreloadingView();
}
this.addView(view);
this.$el.append(view.render().el);
return this;
},
clean: function () {
this._orgUsers.restoreExcludeCurrentUser();
CoreView.prototype.clean.apply(this);
},
_createInitialPreloadingView: function () {
return ViewFactory.createByTemplate(loadingTemplate, {
title: 'Getting users',
descHTML: randomQuote()
});
},
_createViewForGroupUsers: function () {
this.model.set('lastRendered', 'groupUsers');
const filtersExtrasView = new AddRemoveFiltersExtraView({
group: this._group,
orgUsers: this._orgUsers,
userModel: this._userModel
});
this.addView(filtersExtrasView);
return new PagedSearchView({
pagedSearchModel: new PagedSearchModel(),
collection: this._groupUsers,
createListView: this._createGroupUsersListView.bind(this, this._groupUsers),
thinFilters: true,
filtersExtrasView
});
},
_createViewForEmptyGroup: function () {
this.model.set('lastRendered', 'empty');
const filtersExtrasView = new EmptyGroupFiltersExtraView({
groupUsers: this._groupUsers,
orgUsers: this._orgUsers,
userModel: this._userModel
});
this.addView(filtersExtrasView);
return new PagedSearchView({
pagedSearchModel: new PagedSearchModel(),
collection: this._orgUsers,
createListView: this._createGroupUsersListView.bind(this, this._orgUsers),
thinFilters: true,
filtersExtrasView
});
},
_createGroupUsersListView: function (usersCollection) {
return new GroupUsersListView({
users: usersCollection
});
},
_onResetGroupUsers: function () {
// just this.render() is not enough, because each sub-view re-renders its view on state changes,
// Instead, only re-render when hitting the edge-cases after a rest
const lastRendered = this.model.get('lastRendered');
const totalGroupUsersCount = this._groupUsers.totalCount();
if (lastRendered === 'empty') {
// scenario: added at least one user, so group is no longer empty => change to group users view to add users
if (totalGroupUsersCount > 0) {
this.render();
}
} else if (lastRendered === 'groupUsers') {
// scenario: removed last group user(s), so the group is now "empty" => change to org users view to add users
if (totalGroupUsersCount === 0) {
this.render();
}
}
}
});
@@ -0,0 +1,40 @@
const CoreView = require('backbone/core-view');
const template = require('./group.tpl');
const pluralizeStr = require('dashboard/helpers/pluralize');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'model',
'url'
];
/**
* View for an individual group.
*/
module.exports = CoreView.extend({
tagName: 'li',
className: 'OrganizationList-user',
_PREVIEW_COUNT: 3,
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
const sharedMapsCount = this.model.get('shared_maps_count');
const sharedDatasetsCount = this.model.get('shared_tables_count');
this.$el.html(
template({
displayName: this.model.get('display_name'),
sharedMapsCount: pluralizeStr('1 shared map', `${sharedMapsCount} shared maps`, sharedMapsCount),
sharedDatasetsCount: pluralizeStr('1 shared dataset', `${sharedDatasetsCount} shared datasets`, sharedDatasetsCount),
url: this._url,
previewUsers: this.model.users.toArray().slice(0, this._PREVIEW_COUNT),
usersCount: Math.max(this.model.users.length - this._PREVIEW_COUNT, 0)
})
);
return this;
}
});
@@ -0,0 +1,26 @@
<a class="CDB-Text OrganizationList-userLink" href="<%- url %>">
<div class="OrganizationList-userInfo">
<div class="OrganizationList-userInfoName">
<h3 class="CDB-Size-medium OrganizationList-userInfoTitle u-ellipsLongText" title="<%- displayName %>">
<%- displayName %>
</h3>
<h4 class="OrganizationList-userInfoSubtitle u-ellipsLongText">
<%- sharedMapsCount %> &bull; <%- sharedDatasetsCount %>
</h4>
</div>
</div>
<div class="OrganizationList-userInfoData">
<% previewUsers.forEach(function(u) { %>
<span class="UserAvatar is-in-list">
<img class="UserAvatar-img UserAvatar-img--medium" src="<%- u.get('avatar_url') %>" title="<%- u.nameOrUsername() %>" />
</span>
<% }) %>
<% if (usersCount > 0) { %>
<span class="UserAvatar is-in-list">
<span class="UserAvatar-img UserAvatar-img--medium UserAvatar-img--textReplacement">
+<%- usersCount %>
</span>
</span>
<% } %>
</div>
</a>
@@ -0,0 +1,41 @@
const CoreView = require('backbone/core-view');
const GroupView = require('dashboard/views/organization/groups-admin/group-view/group-view');
const ViewFactory = require('builder/components/view-factory');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'groups',
'newGroupUrl'
];
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
this.clearSubViews();
this.$el.empty();
this._renderGroupsView();
return this;
},
_renderGroupsView: function () {
const view = ViewFactory.createListView(this._createGroupViews(), {
tagName: 'ul',
className: 'OrganizationList'
});
this.addView(view);
this.$el.append(view.render().el);
},
_createGroupViews: function () {
return this._groups.map(model => {
return () => new GroupView({
model,
url: this._newGroupUrl(model)
});
});
}
});
@@ -0,0 +1,55 @@
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const navigateThroughRouter = require('builder/helpers/navigate-through-router');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'routerModel'
];
/**
* Controller view, managing view state of the groups entry point
*/
module.exports = CoreView.extend({
events: {
'click': '_onClick'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(this._contentView().render().el);
return this;
},
_initBinds: function () {
this.listenTo(this._routerModel.model, 'change:view', this._onChangeView);
},
_onChangeView: function (model) {
model.previous('view').clean();
this.render();
},
_contentView: function () {
return this._routerModel.model.get('view');
},
_onClick: function (event) {
if (!this._isEventTriggeredOutsideOf(event, 'a')) {
const url = $(event.target).closest('a').attr('href');
if (this._routerModel.isWithinCurrentRoutes(url)) {
navigateThroughRouter.apply(this, arguments);
}
}
},
_isEventTriggeredOutsideOf: function (ev, selector) {
return $(ev.target).closest(selector).length === 0;
}
});
@@ -0,0 +1,56 @@
const Backbone = require('backbone');
const ViewFactory = require('builder/components/view-factory');
const loadingTemplate = require('builder/components/loading/loading.tpl');
const errorTemplate = require('dashboard/views/data-library/content/error-template.tpl');
const randomQuote = require('builder/components/loading/random-quote');
/**
* Model representing the router state
*/
module.exports = Backbone.Model.extend({
defaults: {
view: ''
},
createGroupView: function (groups, id, fetchedCallback) {
const group = groups.newGroupById(id);
fetchedCallback = fetchedCallback.bind(this, group);
const setFetchedCallbackView = () => {
this.set('view', fetchedCallback());
};
if (group.get('display_name')) {
setFetchedCallbackView();
} else {
// No display name == model not fetched yet, so show loading msg meanwhile
this.createLoadingView('Loading group details');
group.fetch({
data: {
fetch_users: true
},
success: function () {
groups.add(group);
setFetchedCallbackView();
},
error: this.createErrorView.bind(this)
});
}
},
createLoadingView: function (msg) {
this.set('view', ViewFactory.createByTemplate(loadingTemplate, {
title: msg,
descHTML: randomQuote()
}));
},
createErrorView: function () {
// Generic error view for now
this.set('view', ViewFactory.createByTemplate(errorTemplate, {
msg: ''
}));
}
});
@@ -0,0 +1,16 @@
const Backbone = require('backbone');
/**
* Header view model to handle state for dashboard header view.
*/
module.exports = Backbone.Model.extend({
breadcrumbTitle: () => 'Configuration',
isBreadcrumbDropdownEnabled: () => false,
isDisplayingDatasets: () => false,
isDisplayingMaps: () => false,
isDisplayingLockedItems: () => false
});
@@ -0,0 +1,2 @@
<div class="js-dialogIconPicker">
</div>
@@ -0,0 +1,68 @@
const CoreView = require('backbone/core-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const OrganizationIconsView = require('../icons/organization-icons-view');
const template = require('./icon-picker-dialog.tpl');
const REQUIRED_OPTS = [
'orgId',
'configModel'
];
module.exports = CoreView.extend({
className: 'Dialog Modal IconPickerDialog',
events: {
'click .js-addIcon': '_onAddIconClicked'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this.$('.content').addClass('Dialog-content--expanded');
this._initViews();
return this;
},
_initViews: function () {
this.icon_picker = new OrganizationIconsView({
el: this.$('.js-dialogIconPicker'),
orgId: this._orgId,
configModel: this._configModel
});
this.addView(this.icon_picker);
this.icon_picker.model.on('change:isProcessRunning', this._onIsProcessRunningChanged, this);
},
_onIsProcessRunningChanged: function () {
var running = this.icon_picker.model.get('isProcessRunning');
if (running) {
this.$el.css('pointer-events', 'none');
} else {
this.$el.css('pointer-events', 'auto');
}
},
_onAddIconClicked: function (event) {
this.killEvent(event);
this._hideErrorMessage();
this.$('.js-inputFile').trigger('click');
},
_hideErrorMessage: function () {
this._hide('.js-errorMessage');
},
_hide: function (selector) {
this.$(selector).addClass('is-hidden');
}
});
@@ -0,0 +1,18 @@
<div class="Modal-header">
<div class="Modal-headerContainer">
<h2 class="CDB-Text CDB-Size-huge is-light u-bSpace">Organization icons</h2>
<h3 class="CDB-Text CDB-Size-medium u-altTextColor"><a data-type="upload_file" href="#/upload" class="js-addIcon">Upload organization icons</a></h3>
</div>
</div>
<div class="Modal-container IconModal">
<div class="Tab-pane">
<div class="Modal-inner">
<div class="Dialog-body Dialog-body--expanded Dialog-body--create Dialog-body--withoutBorder">
<div class="u-inner">
<div class="js-dialogIconPicker"></div>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,3 @@
<li class="CDB-NavMenu-item is-disabled js-your-icons<%- pane === 'your_icons' ? ' is-selected' : '' %>">
<button data-type="your_icons" class="CDB-NavMenu-link js-item u-upperCase">Organization icons</button>
</li>
@@ -0,0 +1,36 @@
const CoreView = require('backbone/core-view');
const template = require('./delete-icons-dialog.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'modalModel',
'onSubmit'
];
module.exports = CoreView.extend({
events: {
'click .js-submit': '_onSubmitClicked',
'click .js-cancel': '_closeDialog'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._numOfIcons = this.options.numOfIcons || 0;
},
render: function () {
this.$el.html(template({
numOfIcons: this._numOfIcons
}));
},
_onSubmitClicked: function () {
this._onSubmit();
this._closeDialog();
},
_closeDialog: function () {
this._modalModel.destroy();
}
});
@@ -0,0 +1,40 @@
<div class="CDB-Text Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--negative">
<i class="CDB-IconFont CDB-IconFont-trash"></i>
<% if (numOfIcons > 0) { %>
<span class="Badge Badge--negative Dialog-headerIconBadge CDB-Text CDB-Size-small"><%- numOfIcons %></span>
<% } %>
</div>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m u-tSpace-xl">
You are deleting
<% if (numOfIcons <= 0) { %>
icons
<% } else if (numOfIcons === 1) { %>
1 icon
<% } else { %>
<%- numOfIcons %> icons
<% } %>
from your organization.
</h4>
<p class="CDB-Text CDB-Size-medium u-altTextColor">
Once icons are deleted they cannot be recovered. Any maps within the organization that use
<% if (numOfIcons === 1) { %>
this icon
<% } else { %>
these icons
<% } %>
will be adversely affected.
</p>
<p class="CDB-Text CDB-Size-medium u-altTextColor">Please be sure before proceeding.</p>
</div>
<div class="Dialog-footer Dialog-footer--simple u-inner">
<div class="Dialog-footerContent">
<button class="CDB-Button CDB-Button--secondary js-cancel">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Cancel</span>
</button>
<button class="u-lSpace--xl CDB-Button CDB-Button--error js-submit">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Ok, delete</span>
</button>
</div>
</div>
@@ -0,0 +1,26 @@
const Backbone = require('backbone');
const IconModel = require('./organization-icon-model');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'orgId',
'configModel'
];
module.exports = Backbone.Collection.extend({
initialize: function (models, options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
model: function (attrs, opts) {
const options = { ...opts, configModel: opts.collection._configModel };
return new IconModel(attrs, options);
},
url: function (method) {
const version = this._configModel.urlVersion('organization-assets', method);
return `/api/${version}/organization/${this._orgId}/assets`;
}
});
@@ -0,0 +1,30 @@
const Backbone = require('backbone');
const _ = require('underscore');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'configModel'
];
module.exports = Backbone.Model.extend({
defaults: {
selected: false,
visible: false,
deleted: false
},
fileAttribute: 'resource',
initialize: function (models, options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
save: function (attrs, options = {}) {
attrs = attrs || _.clone(this.attributes);
attrs = _.omit(attrs, ['selected', 'visible', 'deleted']);
options.data = JSON.stringify(attrs);
return Backbone.Model.prototype.save.call(this, attrs, options);
}
});
@@ -0,0 +1,48 @@
const _ = require('underscore');
const CoreView = require('backbone/core-view');
const template = require('./organization-icon.tpl');
module.exports = CoreView.extend({
tagName: 'li',
className: 'IconList-item IconList-item--small',
events: {
'click': '_onClick'
},
initialize: function (options) {
if (_.isUndefined(options.model)) {
throw new Error('An organization icon model is mandatory.');
}
this._initBinds();
},
render: function () {
this.$el.html(template({
url: this.model.get('public_url')
}));
return this;
},
_initBinds: function () {
this.listenTo(this.model, 'change:selected', this._onSelectedChanged);
this.listenTo(this.model, 'change:deleted', this._onDeletedChanged);
},
_onClick: function () {
this.model.set('selected', !this.model.get('selected'));
},
_onSelectedChanged: function () {
this.$el.toggleClass('is-selected', this.model.get('selected'));
},
_onDeletedChanged: function () {
if (this.model.get('deleted')) {
this.$el.remove();
}
}
});
@@ -0,0 +1,3 @@
<div class="IconItem-icon js-asset">
<img height="24" src="<%- url %>" alt="icon" crossorigin="anonymous" />
</div>
@@ -0,0 +1,349 @@
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const _ = require('underscore');
const IconCollection = require('./organization-icon-collection');
const IconView = require('./organization-icon-view');
const DeleteIconsDialog = require('./delete-icons-dialog-view');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const template = require('./organization-icons.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'orgId',
'configModel'
];
module.exports = CoreView.extend({
events: {
'click .js-addIcon': '_onAddIconClicked',
'click .js-selectAllIcons': '_onSelectAllIconsClicked',
'click .js-deselectAllIcons': '_onDeselectAllIconsClicked',
'click .js-deleteIcons': '_onDeleteIconsClicked',
'change .js-inputFile': '_onFileSelected'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._modals = new ModalsServiceModel();
this.model = new Backbone.Model({
isProcessRunning: false
});
this._iconCollection = new IconCollection(null, {
orgId: this._orgId,
configModel: this._configModel
});
this._numOfUploadingProcesses = 0;
this._numOfDeletingProcesses = 0;
this._fetchErrorMessage = 'Error fetching organization icons. Please refresh the page.';
this._runningMessage = '';
this.render();
this._fetchAllIcons();
this._initBinds();
},
render: function () {
this.$el.html(template());
return this;
},
_initBinds: function () {
this.listenTo(this._iconCollection, 'change:selected', this._refreshActions);
this.listenTo(this.model, 'change:isProcessRunning', this._onProcessRunningChanged);
},
_fetchAllIcons: function () {
this._iconCollection.fetch({
success: this._renderAllIcons.bind(this),
error: this._onFetchIconsError.bind(this)
});
},
_renderAllIcons: function () {
_.each(this._iconCollection.models, this._addIconElement, this);
},
_onFetchIconsError: function () {
this._showErrorMessage(this._fetchErrorMessage);
},
_renderIcon: function (iconModel) {
var iconView = new IconView({
model: iconModel
});
iconView.render();
iconModel.set('visible', true);
this.$('.js-items').append(iconView.$el);
},
_addIconElement: function (iconModel) {
this._renderIcon(iconModel);
},
_onAddIconClicked: function (event) {
this.killEvent(event);
this._hideErrorMessage();
this.$('.js-inputFile').trigger('click');
},
_parseResponseText: function (response) {
if (response && response.responseText) {
try {
var text = JSON.parse(response.responseText);
if (text && text.errors && typeof text.errors === 'string') {
return text.errors;
}
} catch (exc) {
// Swallow
}
}
return '';
},
_getSelectedFiles: function () {
return this.$('.js-inputFile').prop('files');
},
_onFileSelected: function () {
var files = this._getSelectedFiles();
_.each(files, function (file) {
this._iconCollection.create({
kind: 'organization_asset',
resource: file
}, {
beforeSend: this._beforeIconUpload.bind(this),
success: this._onIconUploaded.bind(this),
error: this._onIconUploadError.bind(this),
complete: this._onIconUploadComplete.bind(this)
});
}, this);
},
_beforeIconUpload: function () {
this._numOfUploadingProcesses++;
if (this._numOfUploadingProcesses > 0) {
this._runningMessage = 'Uploading icons...';
this.model.set('isProcessRunning', true);
}
},
_onIconUploaded: function (iconModel) {
this._resetFileSelection();
this._addIconElement(iconModel);
},
_onIconUploadError: function (model, response) {
var errorText = this._parseResponseText(response);
this._resetFileSelection();
this._showErrorMessage(this._uploadErrorMessage(errorText));
},
_onIconUploadComplete: function () {
this._numOfUploadingProcesses--;
if (this._numOfUploadingProcesses <= 0) {
this.model.set('isProcessRunning', false);
}
},
_resetFileSelection: function () {
this.$('.js-inputFile').val('');
},
_show: function (selector) {
this.$(selector).removeClass('is-hidden');
},
_hide: function (selector) {
this.$(selector).addClass('is-hidden');
},
_refreshActions: function () {
if (this.model.get('isProcessRunning')) {
return;
}
var limit = Math.min(this._iconCollection.length);
var numOfSelectedIcons = this._getNumberOfSelectedIcons();
var iconText = (numOfSelectedIcons === 1 ? '1 icon selected' : '' + numOfSelectedIcons + ' icons selected');
this.$('.js-iconMainLabel').text(iconText);
if (numOfSelectedIcons === 0) {
this.$('.js-iconMainLabel').text('');
this._hide('.js-iconMainLabel, .js-selectAllIcons, .js-deselectAllIcons, .js-deleteIcons');
this._show('.js-iconsInfo');
} else if (numOfSelectedIcons < limit) {
this._show('.js-iconMainLabel, .js-selectAllIcons, .js-deleteIcons');
this._hide('.js-deselectAllIcons, .js-iconsInfo');
} else {
this._show('.js-iconMainLabel, .js-deselectAllIcons, .js-deleteIcons');
this._hide('.js-selectAllIcons, .js-iconsInfo');
}
if (numOfSelectedIcons > 1) {
this.$('.js-deleteIcons a').text('Delete icons...');
} else if (numOfSelectedIcons === 1) {
this.$('.js-deleteIcons a').text('Delete icon...');
}
},
_hideActions: function () {
this._hide('.js-selectAllIcons, .js-deselectAllIcons, .js-deleteIcons, .js-iconsInfo');
},
_onDeselectAllIconsClicked: function (event) {
event.preventDefault();
this._iconCollection.each(function (icon) {
icon.set('selected', false);
});
},
_onSelectAllIconsClicked: function (event) {
event.preventDefault();
this._iconCollection.each(function (icon) {
if (icon.get('visible')) {
icon.set('selected', true);
}
});
},
_onDeleteIconsClicked: function (event) {
event.preventDefault();
this._openDeleteIconsDialog();
},
_openDeleteIconsDialog: function (event) {
this.killEvent(event);
this._modals.create(modalModel =>
new DeleteIconsDialog({
modalModel,
numOfIcons: this._getNumberOfSelectedIcons(),
onSubmit: this._deleteIcons.bind(this)
})
);
},
_deleteIcons: function () {
this._hideErrorMessage();
var iconsToDelete = this._iconCollection.where({ selected: true });
_.each(iconsToDelete, function (icon) {
icon.destroy({
beforeSend: this._beforeIconDelete.bind(this),
success: this._onIconDeleted.bind(this),
error: this._onIconDeleteError.bind(this),
complete: this._onIconDeleteComplete.bind(this)
});
}, this);
},
_getNumberOfSelectedIcons: function () {
return this._iconCollection.where({ selected: true }).length;
},
_beforeIconDelete: function () {
this._numOfDeletingProcesses++;
if (this._numOfDeletingProcesses > 0) {
this._runningMessage = 'Deleting icons...';
this.model.set('isProcessRunning', true);
}
},
_onIconDeleted: function (icon) {
icon.set('deleted', true);
this._addExtraIcon();
},
_onIconDeleteError: function (icon, response) {
var errorText = this._parseResponseText(response);
// Even if API throws error the icon has been already removed from the collection.
// We must add it again
this._iconCollection.add(icon);
this._resetSelection();
this._showErrorMessage(this._deleteErrorMessage(errorText));
},
_onIconDeleteComplete: function () {
this._numOfDeletingProcesses--;
if (this._numOfDeletingProcesses <= 0) {
this.model.set('isProcessRunning', false);
}
},
_showSpinner: function (enable) {
if (enable) {
this._hide('.js-plusSign');
this._show('.js-spinner');
} else {
this._show('.js-plusSign');
this._hide('.js-spinner');
}
},
_showErrorMessage: function (message) {
this.$('.js-errorMessage label').text(message);
this._show('.js-errorMessage');
},
_hideErrorMessage: function () {
this._hide('.js-errorMessage');
},
_resetSelection: function () {
this._iconCollection.each(function (icon) {
icon.set('selected', false);
});
this._refreshActions();
},
_addExtraIcon: function () {
var iconAdded = false;
this._iconCollection.each(function (icon) {
if (!icon.get('visible') && !iconAdded) {
this._addIconElement(icon);
iconAdded = true;
}
}, this);
},
_onProcessRunningChanged: function () {
var running = this.model.get('isProcessRunning');
this._showSpinner(running);
if (running) {
this.$el.css('pointer-events', 'none');
this._hideActions();
this.$('.js-runningInfo').text(this._runningMessage);
this._show('.js-runningInfo');
} else {
this.$('.js-runningInfo').text('');
this._hide('.js-runningInfo');
this._refreshActions();
this._iconCollection.trigger('refreshCollection', { cid: this.cid });
this.$el.css('pointer-events', 'auto');
}
},
_uploadErrorMessage: function (errorText) {
var message = 'Error uploading your image. ';
if (errorText) {
message += '[ ' + errorText + ' ]. ';
}
message += 'Please try again.';
return message;
},
_deleteErrorMessage: function (errorText) {
var message = 'Error deleting your image. ';
if (errorText) {
message += '[ ' + errorText + ' ]. ';
}
message += 'Please try again.';
return message;
}
});
@@ -0,0 +1,30 @@
<div class="FormAccount-rowLabel IconActions">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor u-rSpace--xl js-iconMainLabel is-hidden"></label>
<label class="CDB-Text CDB-Size-small u-rSpace--xl u-altTextColor js-iconsInfo">These icons can be used as markers.</label>
<label class="CDB-Text CDB-Size-small u-rSpace--xl u-altTextColor js-runningInfo is-hidden"></label>
<label class="CDB-Text CDB-Size-medium u-rSpace--xl js-selectAllIcons is-hidden"><a href="#/select_all">Select all</a></label>
<label class="CDB-Text CDB-Size-medium u-rSpace--xl js-deselectAllIcons is-hidden"><a href="#/deselect_all">Deselect all</a></label>
<label class="CDB-Text CDB-Size-medium js-deleteIcons is-hidden"><a class="IconText is--critical" href="#/delete_icons">Delete icons...</a></label>
</div>
<div class="FormAccount-rowData js-asset-icons">
<ul class="IconList js-items">
<li class="IconList-item IconList-item--small IconList-item--dashed js-addIcon">
<div class="IconItem-icon js-plusSign">
<svg width="11px" height="11px" viewBox="15 15 11 11" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<polygon id="Mas" stroke="none" fill="#1785FB" fill-rule="evenodd" points="15 20 15 21 20 21 20 26 21 26 21 21 26 21 26 20 21 20 21 15 20 15 20 20"></polygon>
</svg>
</div>
<div class="CDB-LoaderIcon is-blue js-spinner is-hidden">
<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>
</li>
</ul>
</div>
<div class="FormAccount-rowData js-errorMessage is-hidden">
<label class="CDB-Text CDB-Size-medium IconText is--critical"></label>
</div>
<input id="iconfile" class="js-inputFile" type="file" value="Choose icon" accept="image/jpeg,image/jpg,image/gif,image/png,image/svg+xml" tabindex="-1" style="position: absolute; clip: rect(0px 0px 0px 0px); opacity: 0;" multiple>
@@ -0,0 +1,143 @@
const Backbone = require('backbone');
const IconPickerView = require('./icons/organization-icons-view');
const OrganizationIconCollection = require('./icons/organization-icon-collection');
const IconView = require('./icons/organization-icon-view');
const IconPickerDialog = require('./icon-picker-dialog/icon-picker-dialog-view');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const template = require('./organization-icon-picker.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'orgId',
'configModel'
];
module.exports = IconPickerView.extend({
events: IconPickerView.extendEvents({
'click .js-viewAllIcons': '_onViewAllIconsClicked'
}),
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._maxIcons = 23;
this._modals = new ModalsServiceModel();
this.model = new Backbone.Model({
isProcessRunning: false
});
this._iconCollection = new OrganizationIconCollection(null, {
orgId: this._orgId,
configModel: this._configModel
});
this._numOfUploadingProcesses = 0;
this._numOfDeletingProcesses = 0;
this._fetchErrorMessage = 'Error fetching organization icons. Please refresh the page.';
this._runningMessage = '';
this.teplate = template;
this.render();
this._fetchAllIcons();
this._initBinds();
},
render: function () {
this.$el.html(template());
return this;
},
_initBinds: function () {
this.listenTo(this._iconCollection, 'change:selected', this._refreshActions);
this.listenTo(this._iconCollection, 'refreshCollection', this._refreshCollection);
this.listenTo(this.model, 'change:isProcessRunning', this._onProcessRunningChanged);
},
_refreshCollection: function (data) {
if (data.cid && this.cid !== data.cid) {
this.render();
this._fetchAllIcons();
}
},
_renderIcon: function (iconModel) {
if (iconModel.get('index') < this._maxIcons) {
var iconView = new IconView({
model: iconModel
});
iconView.render();
iconModel.set('visible', true);
this.$('.js-items').append(iconView.$el);
}
},
_addIconElement: function (iconModel) {
iconModel.set('index', this._getIconIndex(iconModel));
this._renderIcon(iconModel);
this._refreshActions();
},
_refreshActions: function () {
if (this.model.get('isProcessRunning')) {
return;
}
var limit = Math.min(this._maxIcons, this._iconCollection.length);
var numOfSelectedIcons = this._getNumberOfSelectedIcons();
var iconText = (numOfSelectedIcons === 1 ? '1 icon selected' : '' + numOfSelectedIcons + ' icons selected');
this.$('.js-iconMainLabel').text(iconText);
if (numOfSelectedIcons === 0) {
this.$('.js-iconMainLabel').text('Icons');
this._hide('.js-selectAllIcons, .js-deselectAllIcons, .js-deleteIcons');
this._show('.js-iconsInfo');
} else if (numOfSelectedIcons < limit) {
this._show('.js-selectAllIcons, .js-deleteIcons');
this._hide('.js-deselectAllIcons, .js-iconsInfo');
} else {
this._hide('.js-selectAllIcons, .js-iconsInfo');
this._show('.js-deselectAllIcons, .js-deleteIcons');
}
if (numOfSelectedIcons > 1) {
this.$('.js-deleteIcons a').text('Delete icons...');
} else if (numOfSelectedIcons === 1) {
this.$('.js-deleteIcons a').text('Delete icon...');
}
if (this._iconCollection.length > this._maxIcons) {
this._show('.js-viewAllIcons');
} else {
this._hide('.js-viewAllIcons');
}
},
_hideActions: function () {
this._hide('.js-selectAllIcons, .js-deselectAllIcons, .js-deleteIcons, .js-iconsInfo, .js-viewAllIcons');
},
_getIconIndex: function (icon) {
return this._iconCollection.indexOf(icon);
},
_addExtraIcon: function () {
var iconAdded = false;
this._iconCollection.each(function (icon) {
var index = this._getIconIndex(icon);
if (index < this._maxIcons && !icon.get('visible') && !iconAdded) {
this._addIconElement(icon);
iconAdded = true;
}
}, this);
},
_onViewAllIconsClicked: function (event) {
this.killEvent(event);
this._modals.create(modalModel =>
new IconPickerDialog({
orgId: this._orgId,
configModel: this._configModel
})
);
}
});
@@ -0,0 +1,31 @@
<div class="FormAccount-rowLabel">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor u-rSpace--xl js-iconMainLabel">Icons</label>
<label class="CDB-Text CDB-Size-medium u-rSpace--xl js-viewAllIcons is-hidden"><a href="#/view_icons">View all icons</a></label>
<label class="CDB-Text CDB-Size-small u-rSpace--xl u-altTextColor js-iconsInfo">These icons can be used as markers.</label>
<label class="CDB-Text CDB-Size-small u-rSpace--xl u-altTextColor js-runningInfo is-hidden"></label>
<label class="CDB-Text CDB-Size-medium u-rSpace--xl js-selectAllIcons is-hidden"><a href="#/select_all">Select all</a></label>
<label class="CDB-Text CDB-Size-medium u-rSpace--xl js-deselectAllIcons is-hidden"><a href="#/deselect_all">Deselect all</a></label>
<label class="CDB-Text CDB-Size-medium js-deleteIcons is-hidden"><a class="IconText is--critical" href="#/delete_icons">Delete icons...</a></label>
</div>
<div class="FormAccount-rowData js-asset-icons">
<ul class="IconList js-items">
<li class="IconList-item IconList-item--small IconList-item--dashed js-addIcon">
<div class="IconItem-icon js-plusSign">
<svg width="11px" height="11px" viewBox="15 15 11 11" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<polygon id="Mas" stroke="none" fill="#1785FB" fill-rule="evenodd" points="15 20 15 21 20 21 20 26 21 26 21 21 26 21 26 20 21 20 21 15 20 15 20 20"></polygon>
</svg>
</div>
<div class="CDB-LoaderIcon is-blue js-spinner is-hidden">
<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>
</li>
</ul>
</div>
<div class="FormAccount-rowData js-errorMessage is-hidden">
<label class="CDB-Text CDB-Size-medium IconText is--critical"></label>
</div>
<input id="iconfile" class="js-inputFile" type="file" value="Choose icon" accept="image/jpeg,image/jpg,image/gif,image/png,image/svg+xml" tabindex="-1" style="position: absolute; clip: rect(0px 0px 0px 0px); opacity: 0;" multiple>
@@ -0,0 +1,103 @@
const CoreView = require('backbone/core-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const loadingView = require('builder/components/loading/render-loading');
const OrganizationInviteModel = require('dashboard/data/organization-invite-model');
const TabPane = require('dashboard/components/tabpane/tabpane');
const errorTemplate = require('dashboard/views/data-library/content/error-template.tpl');
const InviteUsersFormView = require('./invite-users-form-view');
const ViewFactory = require('builder/components/view-factory');
const template = require('./invite-users-dialog.tpl');
require('jquery-ui');
require('tagit');
const REQUIRED_OPTS = [
'modalModel',
'organization',
'organizationUsers',
'configModel'
];
/**
* Invite users dialog
*
* - Send invites via email.
* - Shouldn't send emails to already enabled users.
*
*/
module.exports = CoreView.extend({
className: 'InviteUsers',
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this.model = new OrganizationInviteModel({}, {
configModel: options.configModel,
organizationId: this._organization.id,
enable_organization_signup: false
});
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this.$('.content').addClass('Dialog-content--expanded');
this._initViews();
return this;
},
_initViews: function () {
// Panes
this._panes = new TabPane({
el: this.$('.js-content')
});
// Create form
this._form = new InviteUsersFormView({
model: this.model,
organization: this._organization,
organizationUsers: this._organizationUsers,
el: this.$('.js-form')
});
this._form.bind('onSubmit', this._sendInvites, this);
this._panes.addTab('form', this._form.render());
// Create loading
this._panes.addTab('loading', ViewFactory.createByHTML(loadingView({
title: 'Sending invites...'
})).render());
// Create error
this._panes.addTab('error', ViewFactory.createByHTML(errorTemplate({
msg: 'Sorry, something went wrong.'
})).render());
this._panes.active('form');
},
_sendInvites: function () {
this._panes.active('loading');
this.model.save(null, {
success: () => this._modalModel.destroy(),
error: (model, error) => {
try {
const message = JSON.parse(error.responseText).errors.users_emails[0];
this._form.showSubmitError(message);
this._panes.active('form');
} catch (e) {
this._panes.active('error');
}
}
});
}
});
@@ -0,0 +1,12 @@
<div class="CDB-Text Dialog-header Dialog-header--expanded">
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
<i class="CDB-IconFont CDB-IconFont-boss"></i>
</div>
<h3 class="Dialog-headerTitle">Invite users to your Organization</h3>
<p class="Dialog-headerText">New user will become part of your CARTO organization account.</p>
</div>
<div class="CDB-Text Dialog-expandedSubContent js-content">
<div class="InviteUsers-form">
<div class="js-form"></div>
</div>
</div>
@@ -0,0 +1,12 @@
<div class="FlashMessage js-signInMessage">
<div class='u-inner'>
<div class="FlashMessage-content js-flashNotice">
<p class='FlashMessage-info FlashMessage-info--larger'>If you want to invite users you need to enable the sign in page for your organization.</p>
<button class='FlashMessage--button js-enableSignInButton'><span>Enable sign in</span></button>
</div>
<div class="FlashMessage-content js-flashSuccess">
<p class='FlashMessage-info FlashMessage-info--larger'>You have enabled the sign in page successfully!</p>
</div>
</div>
</div>
@@ -0,0 +1,146 @@
const _ = require('underscore');
const CoreView = require('backbone/core-view');
const Utils = require('builder/helpers/utils');
const template = require('./invite-users-form.tpl');
const flashTemplate = require('./invite-users-flash-message.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'organization',
'organizationUsers'
];
/**
* Form view for invite users dialog
*
*/
module.exports = CoreView.extend({
events: {
'submit .js-invitesForm': '_onSubmit',
'keyup .js-welcomeText': '_onTextareaChange',
'click .js-enableSignInButton': '_signInEnabledMessage'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initBinds();
},
render: function () {
this.$el.html(
template({
welcomeText: this.model.get('welcome_text'),
signupEnabled: this._organization.get('signup_enabled'),
viewerEnabled: this._organization.get('viewer_seats') > 0
})
);
this._toggleEmailError(false);
this._initViews();
return this;
},
_renderFlashMessage: function () {
this.$('.js-signInMessageContainer').html(flashTemplate());
this.$('.js-flashSuccess').hide();
},
_initBinds: function () {
this.listenTo(this.model, 'change', this._onChange);
},
_initViews: function () {
const organizationUsersEmail = this._organizationUsers.pluck('email');
this.$('.js-tagsList').tagit({
allowSpaces: true,
placeholderText: this.$('.js-tags').data('title'),
onBlur: () => this.$('.js-tags').removeClass('is-focus'),
onFocus: () => this.$('.js-tags').addClass('is-focus'),
beforeTagAdded: (event, ui) => {
var value = ui.tagLabel;
this._removeSubmitError();
// It is an email
if (!Utils.isValidEmail(value)) {
return false;
}
// It is already in the organization
if (_.contains(organizationUsersEmail, value)) {
this._toggleEmailError(true);
return false;
} else {
this._toggleEmailError(false);
}
},
beforeTagRemoved: () => this._removeSubmitError(),
afterTagRemoved: () => this._updateUsers(),
afterTagAdded: () => this._updateUsers(),
onSubmitTags: (event) => {
event.preventDefault();
this._onSubmit();
return false;
}
});
if (!this._organization.get('signup_enabled')) {
this._renderFlashMessage();
}
},
_onTextareaChange: function () {
this.model.set('welcome_text', this.$('.js-welcomeText').val());
},
_updateUsers: function () {
this.model.set('users_emails', this.$('.js-tagsList').tagit('assignedTags'));
},
_onSubmit: function (event) {
event && this.killEvent(event);
const emails = this.model.get('users_emails');
if (emails.length > 0) {
this.model.set('welcome_text', this.$('.js-welcomeText').val());
this.model.set('viewer', this.$('[name=viewer]').get(0).checked);
this.trigger('onSubmit', this);
}
},
_signInEnabledMessage: function () {
this.$('.js-signInMessage').addClass('FlashMessage--success');
this.$('.js-flashNotice').hide();
this.$('.js-flashSuccess').show();
this.model.set('enable_organization_signup', true);
},
_toggleEmailError: function (visible) {
this.$('.js-emailError')[ visible ? 'show' : 'hide' ]();
},
showSubmitError: function (msg) {
const $serverError = this.$('.js-serverError');
if ($serverError.length === 0) {
this.$('.js-emailError').after('<p class="Form-rowInfoText Form-rowInfoText--error Form-rowInfoText--multipleLines js-serverError">' + msg + '</p>');
}
},
_removeSubmitError: function () {
this.$('.js-serverError').remove();
},
_onChange: function () {
const users = this.model.get('users_emails');
const welcomeText = this.model.get('welcome_text');
this.$('.js-submit').toggleClass('is-disabled', users.length === 0 || !welcomeText);
}
});
@@ -0,0 +1,50 @@
<div class="js-signInMessageContainer"></div>
<form class="Form InviteUsers-formContent js-invitesForm">
<div class="Form-row Form-row--centered has-label">
<div class="Form-rowLabel">
<label class="Form-label">Users email</label>
</div>
<div class="Form-rowData Form-rowData--longer">
<div class="Form-tags js-tags" data-title="Separate emails by commas">
<ul class="Form-tagsList js-tagsList"></ul>
</div>
</div>
<div class="Form-rowInfo">
<p class="Form-rowInfoText Form-rowInfoText--error Form-rowInfoText--multipleLines js-emailError">That email is already used in this organization</p>
</div>
</div>
<div class="Form-row Form-row--centered has-label">
<div class="Form-rowLabel">
<label class="Form-label">Welcome text</label>
</div>
<div class="Form-rowData Form-rowData--longer">
<textarea class="CDB-Textarea Form-input Form-input--longer Form-textarea js-welcomeText" placeholder="Send them a welcome text"><%- welcomeText %></textarea>
</div>
<div class="Form-rowInfo"></div>
</div>
<div class="Form-row Form-row--centered has-label">
<div class="Form-rowLabel">
<label class="Form-label">Viewer user</label>
</div>
<div class="Form-rowData Form-rowData--longer InviteUsers-userType">
<div class="Toggler js-toggler InviteUsers-userTypeToggler <% if (!viewerEnabled) { %>is-disabled<% } %>"
<% if (!viewerEnabled) { %>title="You can't send viewer invitations because you don't have viewer seats"<% } %>>
<input type="checkbox" id="InviteUsers-userType" name="viewer" class="js-input" <% if (!viewerEnabled) { %>disabled="disabled"<% } %>/>
<label for="InviteUsers-userType"></label>
</div>
<div class="Form-label">Viewer users can only read, not write, data and maps</div>
</div>
</div>
<div class="InviteUsers-formFooterSticky">
<div class="Dialog-footer InviteUsers-formFooter">
<div><%/* placeholder for flex layout */%></div>
<button type="submit" class="CDB-Button CDB-Button--primary is-disabled js-submit">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Invite users</span>
</button>
</div>
</div>
</form>
@@ -0,0 +1,48 @@
const CoreView = require('backbone/core-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const loadingView = require('builder/components/loading/render-loading');
const template = require('./delete-notification-dialog.tpl');
var REQUIRED_OPTS = [
'userModel',
'modalModel',
'authenticityToken',
'notificationId'
];
module.exports = CoreView.extend({
events: {
'click .js-submit': '_onSubmit',
'click .js-cancel': '_closeDialog'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
const { authenticityToken, notificationId } = this.options;
return this.$el.html(template({
formAction: `${this._userModel.get('base_url')}/organization/notifications/${notificationId}`,
authenticityToken
}));
},
_onSubmit: function (event) {
event.preventDefault();
this.$('form').submit();
this.$('.Modal-inner').hide();
this.$('.Modal-inner').after(loadingView({
title: 'Removing...'
}));
},
_closeDialog: function (event) {
event && event.preventDefault();
this._modalModel.destroy();
}
});
@@ -0,0 +1,32 @@
<div class="u-flex u-justifyCenter">
<div class="Modal-inner Modal-inner--grid u-flex u-justifyCenter">
<div class="Modal-icon">
<svg width="24px" height="25px" viewbox="521 436 24 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M524.5,440 L540.5,440 L540.5,460 L524.5,460 L524.5,440 Z M528.5,437 L536.5,437 L536.5,440 L528.5,440 L528.5,437 Z M522,440 L544,440 L522,440 Z M528.5,443.5 L528.5,455.5 L528.5,443.5 Z M532.5,443.5 L532.5,455.5 L532.5,443.5 Z M536.5,443.5 L536.5,455.5 L536.5,443.5 Z" id="Shape" stroke="#F19243" stroke-width="1" fill="none"/>
</svg>
</div>
<div>
<form accept-charset="UTF-8" action="<%- formAction %>" method="post">
<input name="utf8" type="hidden" value="&#x2713;" />
<input name="authenticity_token" type="hidden" value="<%- authenticityToken %>" />
<input name="_method" type="hidden" value="delete" />
<h2 class=" CDB-Text CDB-Size-huge is-light u-bSpace--xl">You are about to remove a notification</h2>
<p class="CDB-Text CDB-Size-large u-altTextColor">Are you sure you want to remove it?</p>
<ul class="Modal-listActions u-flex u-alignCenter">
<li class="Modal-listActionsitem">
<button class="CDB-Button CDB-Button--secondary CDB-Button--big js-cancel">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">Cancel</span>
</button>
</li>
<li class="Modal-listActionsitem">
<button class="CDB-Button CDB-Button--primary CDB-Button--big js-submit">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase">Ok, remove it</span>
</button>
</li>
</ul>
</form>
</div>
</div>
</div>
@@ -0,0 +1,99 @@
const CoreView = require('backbone/core-view');
const markdown = require('markdown');
const $ = require('jquery');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const RemoveNotificationDialogView = require('./delete-notification-dialog-view');
const SendButtonView = require('./send-button-view');
const REQUIRED_OPTS = [
'userModel'
];
module.exports = CoreView.extend({
events: {
'click .js-resend': '_onClickResend',
'click .js-remove': '_onClickRemove',
'keydown .js-textarea': '_onTextareaKeydown',
'input .js-textarea': '_updateCounter',
'propertychange .js-textarea': '_updateCounter'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
this.$textarea = this.$('#carto_notification_body');
this._modals = new ModalsServiceModel();
this._initViews();
this._initBinds();
this._updateCounter();
return this;
},
_initViews: function () {
this.sendButton = new SendButtonView({
needsPasswordConfirmation: this._userModel.get('needs_password_confirmation'),
modals: this._modals
});
this.$('.js-send').html(this.sendButton.render().el);
this.addView(this.sendButton);
},
_initBinds: function () {
this.listenTo(this.sendButton, 'submitForm', this._submitForm);
},
_submitForm: function () {
if (navigator.userAgent.toLowerCase().indexOf('firefox') !== -1) {
this.$('form').clone().appendTo('body').submit(); // FF only
} else {
this.$('form').submit();
}
},
_updateCounter: function () {
const textLength = $(markdown.toHTML(this.$textarea.val())).text().length;
this.sendButton.updateCounter(textLength);
},
_onClickResend: function (event) {
const $notificationItem = $(event.target).closest('.js-NotificationsList-item');
const title = $notificationItem.find('.js-html_body').data('body');
const recipients = $notificationItem.find('.js-recipients').data('recipients');
this.$textarea.val(title);
this.$('input[name="carto_notification[recipients]"]').prop('checked', false);
this.$('input[name="carto_notification[recipients]"][value="' + recipients + '"]').prop('checked', true);
$('body').animate({
scrollTop: 0
});
this._updateCounter();
},
_onTextareaKeydown: function (event) {
if (event.key === 'Enter' && event.metaKey) {
this.sendButton.onUpdate();
}
},
_onClickRemove: function (event) {
const $target = $(event.target);
this._modals.create((modalModel) => (
new RemoveNotificationDialogView({
userModel: this._userModel,
modalModel,
authenticityToken: this.options.authenticityToken,
notificationId: $target.data('id')
})
));
}
});
@@ -0,0 +1,94 @@
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const Backbone = require('backbone');
const PasswordValidatedForm = require('dashboard/helpers/password-validated-form');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const template = require('./send-button.tpl');
const REQUIRED_OPTS = [
'modals',
'needsPasswordConfirmation'
];
const MAX_NOTIFICATION_LENGTH = 140;
module.exports = CoreView.extend({
className: 'FormAccount-rowData',
events: {
'click .js-button': 'onUpdate'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this.model = new Backbone.Model({
status: 'idle',
counter: 0
});
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(template({
isLoading: this._isLoading(),
isDisabled: this._isDisabled(),
isNegative: this._isNegative(),
counter: MAX_NOTIFICATION_LENGTH - this.model.get('counter')
}));
return this;
},
updateCounter: function (strLen) {
this.model.set('counter', strLen);
},
_initBinds: function () {
this.model.bind('change', this.render, this);
},
_isNegative: function () {
return this.model.get('counter') > MAX_NOTIFICATION_LENGTH;
},
_isDisabled: function () {
return (this.model.get('counter') === 0) || this._isNegative() || this._isLoading();
},
_isLoading: function () {
return this.model.get('status') === 'loading';
},
_submit: function () {
this.trigger('submitForm');
},
onUpdate: function (event) {
this.killEvent(event);
const form = $('#new_carto_notification');
if (this._isDisabled()) return false;
if (!this._needsPasswordConfirmation) {
this.model.set({ status: 'loading' });
return this._submit();
}
PasswordValidatedForm.showPasswordModal({
modalService: this._modals,
onPasswordTyped: password => {
PasswordValidatedForm.addPasswordToForm(form, password, {
doNotSubmit: true
});
this.model.set({ status: 'loading' });
this._submit();
}
});
}
});
@@ -0,0 +1,16 @@
<p class="Md-counter CDB-Text CDB-Size-medium u-secondaryTextColor u-rSpace--xl <% if (isNegative) { %>Md-counter--negative<% } %>"><%= counter %></p>
<button class="OrganizationNotifications-button CDB-Button CDB-Button--primary <% if (isDisabled) { %>is-disabled<% } %> js-button js-save">
<div class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase u-flex">
<% 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>
Sending
<% } else { %>
Send
<% } %>
</div>
</button>
@@ -0,0 +1,14 @@
const CoreView = require('backbone/core-view');
/* Organization user form */
module.exports = CoreView.extend({
events: {
'submit': '_validateFormSubmit'
},
_validateFormSubmit: function (event) {
if (this.$('.js-userQuotaError').length > 0) {
return false;
}
}
});
@@ -0,0 +1,50 @@
const $ = require('jquery');
const CoreView = require('backbone/core-view');
/* Progress quota bar for organization users input */
module.exports = CoreView.extend({
events: {
'keyup .js-assignedSize': '_onKeyup'
},
initialize: function () {
this._initBinds();
this._initViews();
},
_initBinds: function () {
this.listenTo(this.model, 'change:quota_in_bytes', this._onQuotaChange);
},
_initViews: function () {
const quotaInMb = Math.max(Math.floor(this.model.get('quota_in_bytes') / 1024 / 1024).toFixed(0), 1);
this.$('.js-assignedSize').val(quotaInMb);
},
_onQuotaChange: function () {
this.$('.js-assignedSize').val(Math.max(this.model.assignedQuotaInRoundedMb(), 1));
},
_onKeyup: function () {
const modifiedQuotaInBytes = Math.max(Math.floor(this.$('.js-assignedSize').val() * 1048576), 1048576);
const assignedPer = (modifiedQuotaInBytes * 100) / this.model.organization.get('available_quota_for_user');
const errorMessage = '<p class="FormAccount-rowInfoText FormAccount-rowInfoText--error js-userQuotaError">Invalid quota, insert a valid one.</p>';
if (!isNaN(assignedPer)) {
this.model.set('quota_in_bytes', modifiedQuotaInBytes);
}
if (isNaN(assignedPer) || (this.model.get('quota_in_bytes') > this.model.organization.get('available_quota_for_user')) || (this.model.get('quota_in_bytes') < this.model.get('db_size_in_bytes'))) {
this.$('.js-assignedSize').addClass('has-error');
if (this.$('.js-userQuotaError').length === 0) {
$('.js-userQuotaSliderInput').append(errorMessage);
}
} else {
this.$('.js-assignedSize').removeClass('has-error');
this.$('.js-userQuotaError').remove();
}
}
});
@@ -0,0 +1,85 @@
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
const MAX_QUOTA_BITS = 104857600; // 100Mb
/* Progress quota bar for organization users */
module.exports = CoreView.extend({
events: {
'hover .js-quotaProgressSlider': '_showTooltip'
},
initialize: function () {
this._initBinds();
this._initViews();
},
_initBinds: function () {
this.listenTo(this.model, 'change:quota_in_bytes', this._onQuotaChange);
},
_initViews: function () {
const userMinQuota = MAX_QUOTA_BITS / this.model.organization.get('available_quota_for_user');
// Init slider
this.$('.js-slider').slider({
max: 100,
min: userMinQuota,
step: 1,
value: this.model.assignedQuotaPercentage(),
range: 'min',
orientation: 'horizontal',
slide: this._onSlideChange.bind(this)
});
$('.ui-slider-handle').addClass('js-quotaProgressSlider');
},
_onSlideChange: function (ev, ui) {
const userQuota = Math.max(Math.floor((this.model.organization.get('available_quota_for_user') * ui.value) / 100), 1);
const assignedPer = (userQuota * 100) / this.model.organization.get('available_quota_for_user');
if (ui.value >= this.model.usedQuotaPercentage()) {
this.$('.ui-slider-range').css('width', `${assignedPer}%`);
this.$('.js-quotaProgressSlider').css('left', `${assignedPer}%`);
const tooltipLeft = $('.js-quotaProgressSlider').offset().left -
($('.js-orgUserQuotaTooltip').outerWidth() / 2) +
($('.js-quotaProgressSlider').outerWidth() / 2);
$('.js-orgUserQuotaTooltip').css('left', tooltipLeft);
this.model.set('quota_in_bytes', userQuota);
} else {
return false;
}
},
_onQuotaChange: function () {
if (this.model.assignedQuotaPercentage() >= this.model.usedQuotaPercentage()) {
const assignedQuotaPercentage = Math.min(this.model.assignedQuotaPercentage(), 100);
$('.ui-slider-range').css('width', `${assignedQuotaPercentage}%`);
$('.js-quotaProgressSlider').css('left', `${assignedQuotaPercentage}%`);
}
},
_showTooltip: function () {
const viewModel = this.model;
this.addView(
new TipsyTooltipView({
el: $('.js-quotaProgressSlider'),
className: 'js-orgUserQuotaTooltip',
title: function () {
const quotaPercentage = viewModel.assignedQuotaPercentage().toFixed(0);
return `${quotaPercentage}% of the available org quota`;
}
})
);
}
});
@@ -0,0 +1,39 @@
const CoreView = require('backbone/core-view');
const UserQuotaSlider = require('./organization-user-quota-slider');
const UserQuotaSliderInput = require('./organization-user-quota-slider-input');
/* Progress quota bar for organization users */
module.exports = CoreView.extend({
initialize: function () {
this._initBinds();
this._initViews();
},
_initBinds: function () {
this.listenTo(this.model, 'change:quota_in_bytes', this._onQuotaChange);
},
_initViews: function () {
// Quota slider
const userQuotaSlider = new UserQuotaSlider({
el: this.$('.js-userQuotaSlider'),
model: this.model
});
this.addView(userQuotaSlider);
// Quota slider input
const userQuotaSliderInput = new UserQuotaSliderInput({
el: this.$('.js-userQuotaSliderInput'),
model: this.model
});
this.addView(userQuotaSliderInput);
},
_onQuotaChange: function (model, quota) {
this.$('#user_quota').val(quota);
}
});
@@ -0,0 +1,4 @@
<ul class="DropdownList DropdownList--list CDB-Text CDB-Size-medium">
<li class="DropdownList-item DropdownList-item--verticalPadding"><a href="<%- createUrl %>">Create User</a></li>
<li class="DropdownList-item DropdownList-item--verticalPadding"><a href="#" class="js-inviteUser">Invite User</a></li>
</ul>
@@ -0,0 +1,35 @@
const CoreView = require('backbone/core-view');
const Utils = require('builder/helpers/utils');
const pluralize = require('dashboard/helpers/pluralize');
const template = require('./organization-user.tpl');
module.exports = CoreView.extend({
className: 'OrganizationList-user',
tagName: 'li',
render: function () {
this.$el.html(
template({
totalPer: this.options.totalPer,
userPer: this.options.userPer,
usedPer: this.options.usedPer,
isOwner: this.options.isOwner,
isAdmin: this.options.isAdmin,
isViewer: this.options.isViewer,
editable: this.options.editable,
url: this.options.url,
sizeOnBytes: Utils.readablizeBytes(this.model.get('db_size_in_bytes')),
quotaInBytes: Utils.readablizeBytes(this.model.get('quota_in_bytes')),
avatarUrl: this.model.get('avatar_url'),
username: this.model.get('username'),
user_email: this.model.get('email'),
table_count: pluralize.prefixWithCount('Dataset', 'Datasets', this.model.get('table_count')),
maps_count: pluralize.prefixWithCount('Map', 'Maps', this.model.get('all_visualization_count'))
})
);
return this;
}
});
@@ -0,0 +1,43 @@
<% if (editable) { %>
<a class="OrganizationList-userLink" href="<%- url %>">
<% } else { %>
<span class="OrganizationList-userLink is-disabled">
<% } %>
<div class="OrganizationList-userAvatar UserAvatar">
<img src="<%- avatarUrl %>" alt="<%- username %>" src="<%- username %>" class="UserAvatar-img UserAvatar-img--medium-large OrganizationList-userAvatar--img" />
</div>
<div class="OrganizationList-userInfo">
<div class="OrganizationList-userInfoName">
<div class="u-flex u-alignCenter">
<h3 class="CDB-Text u-ellipsLongText CDB-Size-medium is-semibold u-rSpace" title="<%- username %>"><%- username %></h3>
<% if (isOwner) { %>
<span class="UserRoleIndicator UserRoleIndicator--filled is-green u-lSpace">OWNER</span>
<% } else if (isAdmin) { %>
<span class="UserRoleIndicator UserRoleIndicator--filled is-grey u-lSpace">ADMIN</span>
<% } %>
<span class="UserRoleIndicator u-altTextColor u-lSpace">
<% if (isViewer) { %>
VIEWER
<% } else { %>
BUILDER
<% } %>
</span>
</div>
<h4 class="OrganizationList-userInfoSubtitle u-ellipsLongText" title="<%- user_email %>"><%- user_email %></h4>
</div>
<ul class="OrganizationList-userInfoData u-upperCase u-altTextColor">
<li class="CDB-Text CDB-Size-small u-rSpace--xl u-flex u-alignCenter">
<i class="CDB-IconFont CDB-IconFont-map CDB-Size-medium u-rSpace"></i>
<span><%- maps_count %></span>
</li>
<li class="CDB-Text CDB-Size-small u-flex u-alignCenter">
<i class="CDB-IconFont CDB-IconFont-rows CDB-Size-medium u-rSpace"></i>
<span><%- table_count %></span>
</li>
</ul>
</div>
<% if (editable) { %>
</a>
<% } else { %>
</span>
<% } %>
@@ -0,0 +1,37 @@
const CoreView = require('backbone/core-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const template = require('./organization-users-footer.tpl');
const REQUIRED_OPTS = [
'configModel'
];
module.exports = CoreView.extend({
className: 'Form-footer',
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initBinds();
},
render: function () {
this.$el.html(
template({
seats: this.model.get('seats'),
users: this.options.organizationUsers.totalCount(),
newUserUrl: this.model.viewUrl().create(),
upgradeUrl: window.upgrade_url,
customHosted: this._configModel.isHosted()
})
);
return this;
},
_initBinds: function () {
this.listenTo(this.model, 'change:total_users', this.render);
this.listenTo(this.options.organizationUsers, 'sync', this.render);
}
});
@@ -0,0 +1,16 @@
<% if (seats > users) { %>
<% if ((seats - users) < 5) { %>
<p class="CDB-Text Form-footerText">
<i class="CDB-IconFont CDB-IconFont-info Form-footerIcon"></i>
You are near your seats limit. Consider to <a href="<%- upgradeUrl %>">upgrade your account</a>.
</p>
<% } else { %>
<em></em>
<% } %>
<% } else if (!customHosted) { %>
<p class="CDB-Text Form-footerText">
<i class="CDB-IconFont CDB-IconFont-info Form-footerIcon"></i>
<a href="mailto:sales@carto.com">Contact us</a> if you have any questions.
</p>
<a href="<%- upgradeUrl %>" class="CDB-Text Button Button--positive"><span>Upgrade account</span></a>
<% } %>
@@ -0,0 +1,70 @@
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const PaginationView = require('builder/components/pagination/pagination-view.js');
const OrganizationUserView = require('./organization-user-view');
const REQUIRED_OPTS = [
'userModel',
'organization',
'paginationModel'
];
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.empty();
// Users list
const $ul = $('<ul>').addClass('OrganizationList');
this.$el.append($ul);
let totalPer = 0;
this.collection.each(function (user) {
// Calculations to create organization user bars
const userPer = (user.get('quota_in_bytes') * 100) / this._organization.get('quota_in_bytes');
const usedPer = (user.get('db_size_in_bytes') * 100) / this._organization.get('quota_in_bytes');
user.organization = this._organization;
const view = new OrganizationUserView({
model: user,
isOwner: user.isOrgOwner(),
isAdmin: user.isOrgAdmin(),
isViewer: user.get('viewer'),
editable: this._userModel.isOrgOwner() || this._userModel.id === user.id || !user.isOrgAdmin(),
userPer: userPer,
usedPer: usedPer,
totalPer: totalPer,
url: this._organization.viewUrl().edit(user)
});
$ul.append(view.render().el);
this.addView(view);
totalPer = totalPer + userPer;
}, this);
// Paginator
const $paginatorWrapper = $('<div>').addClass('OrganizationList-paginator');
this.$el.append($paginatorWrapper);
const paginationView = new PaginationView({
model: this._paginationModel
});
$paginatorWrapper.append(paginationView.render().el);
this.addView(paginationView);
return this;
},
_initBinds: function () {
this.listenTo(this.collection, 'sync', this.render);
}
});
@@ -0,0 +1,275 @@
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const Utils = require('builder/helpers/utils');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const PaginationModel = require('builder/components/pagination/pagination-model');
const template = require('./organization-users.tpl');
const OrganizationUsersFooterView = require('./organization-users-footer-view');
const DropdownAdminView = require('dashboard/components/dropdown/dropdown-admin-view');
const PagedSearchModel = require('dashboard/data/paged-search-model');
const TabPane = require('dashboard/components/tabpane/tabpane');
const InviteUsersDialogView = require('../invite-users/invite-users-dialog-view');
const addUsersTemplate = require('./add-users.tpl');
const OrganizationUsersListView = require('./organization-users-list-view');
const loadingView = require('builder/components/loading/render-loading');
const noResultsTemplate = require('builder/components/no-results/no-results.tpl');
const errorTemplate = require('dashboard/views/data-library/content/error-template.tpl');
const ViewFactory = require('builder/components/view-factory');
const REQUIRED_OPTS = [
'userModel',
'configModel',
'organization',
'organizationUsers'
];
/**
* Organization users content, list, pagination,
* form, footer...
*
*/
module.exports = CoreView.extend({
events: {
'click .js-addUserOptions': '_openAddUserDropdown',
'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._modals = new ModalsServiceModel();
this.pagedSearchModel = new PagedSearchModel({
per_page: 50,
order: 'username'
});
this.paginationModel = new PaginationModel({
current_page: this.pagedSearchModel.get('page'),
total_count: this._organizationUsers.totalCount(),
per_page: this.pagedSearchModel.get('per_page')
});
// Include current user in fetch results
this._organizationUsers.excludeCurrentUser(false);
this._initBinds();
this.pagedSearchModel.fetch(this._organizationUsers);
},
render: function () {
this.clearSubViews();
this.$el.html(
template({
seats: this._organization.get('seats'),
assigned_seats: this._organization.get('assigned_seats'),
viewer_seats: this._organization.get('viewer_seats'),
assigned_viewer_seats: this._organization.get('assigned_viewer_seats'),
newUserUrl: this._organization.viewUrl().create()
})
);
this._initViews();
return this;
},
_initBinds: function () {
this.listenTo(this._organizationUsers, 'fetching', function () {
this._panes && this._panes.active('loading');
});
this.listenTo(this._organizationUsers, 'error', function (error) {
// Old requests can be stopped, so aborted requests are not
// considered as an error
if (!error || (error && error.statusText !== 'abort')) {
this._panes.active('error');
}
});
this.listenTo(this._organizationUsers, 'sync', function (collection) {
const total = collection.totalCount();
this.paginationModel.set({
total_count: total,
current_page: this.pagedSearchModel.get('page')
});
this._panes && this._panes.active(total > 0 ? 'users' : 'no_results');
});
this.listenTo(this._organizationUsers, 'sync error loading', function (collection) {
this[this.pagedSearchModel.get('q') ? '_showCleanSearchButton' : '_hideCleanSearchButton']();
});
this.listenTo(this.paginationModel, 'change:current_page', function (model) {
const newPage = model.get('current_page');
this.pagedSearchModel.set('page', newPage);
this.pagedSearchModel.fetch(this._organizationUsers);
});
$(document).on('click', '.js-inviteUser', this._onInviteClick.bind(this));
},
_initViews: function () {
this._panes = new TabPane({
el: this.$('.js-organizationUsersPanes')
});
this.addView(this._panes);
this._panes.addTab('users',
new OrganizationUsersListView({
organization: this._organization,
collection: this._organizationUsers,
paginationModel: this.paginationModel,
userModel: this._userModel
}).render()
);
this._panes.addTab('error', ViewFactory.createByHTML(errorTemplate({
msg: ''
})).render());
this._panes.addTab('no_results', ViewFactory.createByHTML(noResultsTemplate({
icon: 'CDB-IconFont-defaultUser',
title: 'Oh! No results',
desc: 'Unfortunately we haven\'t found any user with these parameters'
})).render());
this._panes.addTab('loading', ViewFactory.createByHTML(loadingView({
title: 'Getting users...'
})).render());
// Form footer
const footer = new OrganizationUsersFooterView({
model: this._organization,
organizationUsers: this._organizationUsers,
configModel: this._configModel
});
this.addView(footer);
this.$el.append(footer.render().el);
let activePane = 'loading';
if (this._organizationUsers.totalCount() === 0) {
activePane = 'no_results';
} else if (this._organizationUsers.totalCount() > 0) {
activePane = 'users';
}
this._panes.active(activePane);
},
_openAddUserDropdown: function (event) {
if (this._dropdownView) this._dropdownView.clean();
this._dropdownView = new DropdownAdminView({
className: 'Dropdown border',
target: $(event.target),
width: 120,
template: addUsersTemplate,
vertical_position: 'down',
horizontal_position: 'right',
createUrl: this._organization.viewUrl().create(),
tick: 'right'
});
$('body').append(this._dropdownView.render().el);
this._dropdownView.on('onDropdownHidden', function () {
this._dropdownView.clean();
}, this);
this._dropdownView.open(event);
},
_showCleanSearchButton: function () {
this.$('.js-clean-search').show();
},
_hideCleanSearchButton: function () {
this.$('.js-clean-search').hide();
},
_focusSearchInput: function () {
var $searchInput = this._$searchInput();
$searchInput.focus().val($searchInput.val());
},
_onSearchClick: function (e) {
if (e) this.killEvent(e);
this._$searchInput().focus();
},
_onCleanSearchClick: function (ev) {
this.killEvent(ev);
this._cleanSearch();
},
_onKeyDown: function (ev) {
var enterPressed = (ev.keyCode === $.ui.keyCode.ENTER);
var escapePressed = (ev.keyCode === $.ui.keyCode.ESCAPE);
if (enterPressed) {
this.killEvent(ev);
this._submitSearch();
} else if (escapePressed) {
this.killEvent(ev);
if (this.pagedSearchModel.get('q')) {
this._cleanSearch();
}
}
},
_submitSearch: function (e) {
var search = this._$searchInput().val().trim();
this.pagedSearchModel.set({
q: Utils.stripHTML(search),
page: 1
});
this.pagedSearchModel.fetch(this._organizationUsers);
},
_$searchInput: function () {
return this.$('.js-search-input');
},
_cleanSearch: function () {
this._$searchInput().val('');
this.pagedSearchModel.set({
q: '',
page: 1
});
this.pagedSearchModel.fetch(this._organizationUsers);
},
_onInviteClick: function (event) {
event.preventDefault();
this._dropdownView.clean();
this._modals.create(modalModel =>
new InviteUsersDialogView({
configModel: this._configModel,
organization: this._organization,
organizationUsers: this._organizationUsers,
modalModel
})
);
},
clean: function () {
this._organizationUsers.restoreExcludeCurrentUser();
$(document).off('click', '.js-inviteUser', this._onInviteClick.bind(this));
CoreView.prototype.clean.apply(this);
}
});
@@ -0,0 +1,26 @@
<div class="OrganizationSearch">
<div class="OrganizationSearch-Item">
<button class="OrganizationSearch-FormButton js-search-link u-alignCenter CDB-Text CDB-Size-medium u-actionTextColor">
<i class="CDB-IconFont CDB-IconFont-lens"></i>Search
</button>
</div>
<div class="OrganizationSearch-Item">
<form class="OrganizationSearch-Form js-search-form" action="#">
<input class="OrganizationSearch-FormInput CDB-Text CDB-Size-medium u-secondaryTextColor js-search-input" type="text" placeholder="Search by username or email" />
<button type="button" class="CDB-Text CDB-Size-large u-actionTextColor OrganizationSearch-FormButton--clean js-clean-search" style="display: none;">x</button>
</form>
</div>
<div class="u-flex u-alignCenter CDB-Text CDB-Size-small u-secondaryTextColor">
Builder (<%- assigned_seats %>/<%- seats %>) - Viewer (<%- assigned_viewer_seats %>/<%- viewer_seats %>)
</div>
<div class="OrganizationSearch-Item">
<button class="CDB-Button CDB-Button--secondary CDB-Button--small js-addUserOptions">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">
Add users
<i class="Button-arrowMenu CDB-IconFont CDB-IconFont-caretDown"></i>
</span>
</button>
</div>
</div>
<div class="CDB-Text js-organizationUsersPanes"></div>
@@ -0,0 +1,43 @@
const CoreView = require('backbone/core-view');
const template = require('./regenerate-keys.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'modalModel',
'type',
'scope',
'form_action',
'passwordNeeded',
'authenticity_token'
];
module.exports = CoreView.extend({
events: {
'click .js-cancel': '_closeDialog'
},
defaults: {
method: 'post'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._method = this.options.method;
},
render: function () {
return this.$el.html(template({
type: this._type,
scope: this._scope,
form_action: this._form_action,
passwordNeeded: this._passwordNeeded,
authenticity_token: this._authenticity_token,
method: this._method
}));
},
_closeDialog: function () {
this._modalModel.destroy();
}
});
@@ -0,0 +1,58 @@
<%
if (scope === 'organization') {
person_scope = 'You and all organization users';
} else if (scope === 'organization_user') {
person_scope = 'The user';
} else {
person_scope = 'You';
}
%>
<div class="CDB-Text Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--negative">
<i class="CDB-IconFont CDB-IconFont-keys"></i>
</div>
<p class="Dialog-headerTitle u-ellipsLongText">
<% if (scope === 'organization') { %>
You are about to regenerate the <%- type === "api" ? 'API keys' : 'OAuth credentials' %> for all the users of the organization
<% } else if (scope === 'organization_user') { %>
You are about to regenerate the <%- type === "api" ? 'API keys' : 'OAuth credentials' %> for this user of the organization
<% } else { %>
You are about to regenerate your <%- type === "api" ? 'API keys' : 'OAuth credentials' %>
<% } %>
</p>
<p class="Dialog-headerText">
<% if (type === "api") { %>
<%- person_scope %> will need to update all deployed apps with the new API keys. Are you sure you want to continue?
<% } else { %>
<%- person_scope %> will have to update all OAuth keys in apps where you are using CARTO. Are you sure?
<% } %>
</p>
</div>
<form action="<%- form_action %>" method="post">
<% 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="password_confirmation" class="CDB-InputText CDB-Text Form-input Form-input--long" value=""/>
</div>
</div>
</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">cancel</span>
</button>
<input name="utf8" type="hidden" value="&#x2713;" />
<input name="_method" type="hidden" value="<%- method %>" />
<input name="authenticity_token" type="hidden" value="<%- authenticity_token %>" />
<button type="submit" class="CDB-Button CDB-Button--error">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Regenerate <%- type === "api" ? 'API keys' : 'OAuth credentials' %></span>
</button>
</div>
</form>