Skip to content

Commit 06fe379

Browse files
Merge pull request #15933 from CartoDB/email-settings
Email settings section in Account page to manage notifications
2 parents ffa1d77 + 8244d4f commit 06fe379

File tree

10 files changed

+173
-11
lines changed

10 files changed

+173
-11
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Development
66

77
### Features
88
* Email notifications toggle API endpoint [#15930](https://github.com/CartoDB/cartodb/pull/15930)
9+
* New Email settings section in Account page to manage notifications [#15933](https://github.com/CartoDB/cartodb/pull/15933)
910

1011
### Bug fixes / enhancements
1112
* Fix BigQuery connector not importing 0-bytes-processed datasets [#15916](https://github.com/CartoDB/cartodb/pull/15916)

app/controllers/carto/api/email_notifications_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ def load_notifications
2727

2828
def decorate_notifications
2929
payload = {}
30+
Carto::UserEmailNotification::VALID_NOTIFICATIONS.map { |n| payload[n] = true }
31+
3032
@notifications.each do |notification|
3133
payload[notification.notification] = notification.enabled
3234
end

assets/stylesheets/common/account_forms.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ $sLabel-width: 140px;
7979
margin-bottom: 16px;
8080
}
8181

82+
.FormAccount-row--mediumMarginBottom {
83+
margin-bottom: 24px;
84+
}
85+
8286
.FormAccount-row--wideMarginBottom {
8387
margin-bottom: 100px;
8488
}
@@ -215,6 +219,12 @@ $sLabel-width: 140px;
215219
align-items: flex-start;
216220
}
217221

222+
.FormAccount-rowData--listItemWithAction {
223+
justify-content: space-between;
224+
background: $cStructure-grayBkg;
225+
padding: 11px 11px 10px 12px;
226+
}
227+
218228
.FormAccount-planTag {
219229
padding: 5px 10px;
220230
border-radius: 4px;

lib/assets/javascripts/carto-node/lib/clients/authenticated.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,26 @@ class AuthenticatedClient extends PublicClient {
267267
}
268268
};
269269
}
270+
271+
/**
272+
* API to enable/disable email notifications, such as DO notifications
273+
*/
274+
emailNotifications () {
275+
const notificationsURLParts = ['api/v3/email_notifications'];
276+
return {
277+
get: (callback) => {
278+
return this.get(notificationsURLParts, callback);
279+
},
280+
281+
set: (notifications, callback) => {
282+
const opts = {
283+
data: JSON.stringify({ notifications }),
284+
dataType: 'json'
285+
};
286+
return this.put(notificationsURLParts, opts, callback);
287+
}
288+
};
289+
}
270290
}
271291

272292
module.exports = AuthenticatedClient;

lib/assets/javascripts/dashboard/views/account/account-form-view.js

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ module.exports = CoreView.extend({
2727
events: {
2828
'click .js-save': '_onClickSave',
2929
'submit form': '_onClickSave',
30-
'change .js-toggle-mfa': '_onToggleMfa'
30+
'change .js-toggle-mfa': '_onToggleMfa',
31+
'change .js-toggle-notification': '_onToggleNotification'
3132
},
3233

3334
initialize: function (options) {
@@ -40,6 +41,7 @@ module.exports = CoreView.extend({
4041
_initModels: function () {
4142
this._errors = this.options.errors || {};
4243
this.add_related_model(this._renderModel);
44+
this._getNotifications();
4345
},
4446

4547
_initBinds: function () {
@@ -75,6 +77,7 @@ module.exports = CoreView.extend({
7577
services: this._getField('services') || [],
7678
mfaEnabled: this._getField('mfa_configured'),
7779
licenseExpiration: this._formatLicenseExpiration(),
80+
notifications: this._notifications || {},
7881
view: this
7982
};
8083

@@ -133,22 +136,30 @@ module.exports = CoreView.extend({
133136
_onClickSave: function (event) {
134137
this.killEvent(event);
135138

139+
// updated user info
136140
const origin = this._getUserFields();
137141
const destination = this._getDestinationValues();
138142
const destinationKeys = _.keys(destination);
139143
const differenceKeys = _.filter(destinationKeys, key =>
140144
origin[key] !== destination[key]
141145
);
142-
143146
const user = _.pick(destination, differenceKeys);
144147

148+
// updated notifications info
149+
const notifications = this._getNotificationValuesFromUI();
150+
145151
if (!this._userModel.get('needs_password_confirmation')) {
146-
return this._updateUser(user);
152+
this._updateUser(user);
153+
this._updateNotifications(notifications);
154+
return;
147155
}
148156

149157
PasswordValidatedForm.showPasswordModal({
150158
modalService: this._modals,
151-
onPasswordTyped: (password) => this._updateUser(user, password),
159+
onPasswordTyped: (password) => {
160+
this._updateUser(user, password);
161+
this._updateNotifications(notifications);
162+
},
152163
updatePassword: destination.new_password !== '' && destination.confirm_password !== ''
153164
});
154165
},
@@ -161,6 +172,18 @@ module.exports = CoreView.extend({
161172
this._mfaLabel().html(newLabel);
162173
},
163174

175+
_onToggleNotification: function (event) {
176+
this.killEvent(event);
177+
178+
const id = event.target.id;
179+
const newLabel = (this._notificationStatus(id)
180+
? _t('account.views.form.email_section.notifications.enabled')
181+
: _t('account.views.form.email_section.notifications.disabled')
182+
);
183+
184+
this._notificationLabel(id).html(newLabel);
185+
},
186+
164187
_updateUser: function (user, password) {
165188
this._setLoading('Saving changes');
166189

@@ -193,6 +216,31 @@ module.exports = CoreView.extend({
193216
};
194217
},
195218

219+
_getNotifications: function () {
220+
this._client.emailNotifications().get((errors, response, data) => {
221+
if (errors) {
222+
this.options.onError(data, response, errors);
223+
} else {
224+
this._notifications = data.notifications;
225+
}
226+
227+
this.render();
228+
});
229+
},
230+
231+
_updateNotifications: function (notifications) {
232+
this._setLoading('Saving email notifications');
233+
234+
this._client.emailNotifications().set(notifications, (errors, response, data) => {
235+
if (errors) {
236+
this.options.onError(data, errors);
237+
this.render();
238+
} else {
239+
this._getNotifications();
240+
}
241+
});
242+
},
243+
196244
_getDestinationValues: function () {
197245
return {
198246
username: this._username(),
@@ -202,6 +250,13 @@ module.exports = CoreView.extend({
202250
};
203251
},
204252

253+
_getNotificationValuesFromUI: function () {
254+
return _.reduce(this._notifications, function (params, value, key) {
255+
params[key] = this._notificationStatus(key);
256+
return params;
257+
}, {}, this);
258+
},
259+
205260
_username: function () {
206261
return this.$('#user_username').val();
207262
},
@@ -225,6 +280,18 @@ module.exports = CoreView.extend({
225280
return this.$('.js-mfa-label');
226281
},
227282

283+
_notificationStatus: function (id) {
284+
const selector = `.js-toggle-notification-${id}`;
285+
if (this.$(selector).length === 0) {
286+
return false;
287+
}
288+
return this.$(selector)[0].checked;
289+
},
290+
291+
_notificationLabel: function (id) {
292+
return this.$(`.js-notification-label-${id}`);
293+
},
294+
228295
_formatLicenseExpiration: function () {
229296
if (!this._getField('license_expiration')) { return null; }
230297

lib/assets/javascripts/dashboard/views/account/account-form.tpl

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<form accept-charset="UTF-8">
2+
3+
<!-- Username -->
24
<div class="FormAccount-row">
35
<div class="FormAccount-rowLabel">
46
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor"><%= _t('account.views.form.username') %></label>
@@ -12,6 +14,7 @@
1214
</div>
1315
</div>
1416

17+
<!-- Change password -->
1518
<% if (!hidePasswordFields) { %>
1619
<div class="VerticalAligned--FormRow">
1720
<div class="FormAccount-row">
@@ -41,6 +44,7 @@
4144

4245
<%= accountFormExtension %>
4346

47+
<!-- Multifactor authentication -->
4448
<div class="FormAccount-row">
4549
<div class="FormAccount-rowLabel">
4650
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor">
@@ -59,11 +63,12 @@
5963
</p>
6064
</div>
6165
</div>
62-
<div class="FormAccount-rowData u-tspace-xs">
66+
<div class="FormAccount-rowData u-tspace-xs u-vspace-s">
6367
<p class="CDB-Text CDB-Size-small u-altTextColor"><%= _t('account.views.form.mfa_description') %></p>
6468
</div>
6569
</div>
6670

71+
<!-- Account type -->
6772
<% if (isCartoDBHosted) { %>
6873
<% if ((isOrgAdmin || isOrgOwner) && licenseExpiration) { %>
6974
<div class="FormAccount-title">
@@ -111,6 +116,46 @@
111116
<% } %>
112117
<% } %>
113118

119+
<!-- Email settings -->
120+
<% if (Object.keys(notifications).length > 0) { %>
121+
<div class="FormAccount-title">
122+
<p class="FormAccount-titleText"><%= _t('account.views.form.email_section.title') %></p>
123+
</div>
124+
125+
<span class="FormAccount-separator"></span>
126+
127+
<div class="FormAccount-row FormAccount-row--mediumMarginBottom">
128+
<p class="CDB-Text CDB-Size-medium"><%= _t('account.views.form.email_section.description') %></p>
129+
</div>
130+
131+
<div class="FormAccount-row">
132+
<!-- One row per notification (eg: do_subscriptions) -->
133+
<% Object.keys(notifications).forEach(function (notificationKey) { %>
134+
<div class="FormAccount-rowData FormAccount-rowData--listItemWithAction">
135+
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor">
136+
<!-- extract to a 'description' property -->
137+
<%= _t('account.views.form.email_section.notifications.' + notificationKey) %>
138+
</label>
139+
140+
<div class="FormAccount-rowData">
141+
<div class="Toggler">
142+
<input name="<%='notifications[' + notificationKey + ']'%>" type="hidden" value="0">
143+
<input class="js-toggle-notification <%='js-toggle-notification-' + notificationKey%>" id="<%=notificationKey%>" name="<%='notifications[' + notificationKey + ']'%>" type="checkbox" value="1" <% if (notifications[notificationKey]) { %>checked="checked"<% } %>>
144+
<label for="<%=notificationKey%>"></label>
145+
</div>
146+
147+
<div class="FormAccount-rowInfo u-lSpace--xl">
148+
<p class="CDB-Text CDB-Size-medium <%='js-notification-label-' + notificationKey%>">
149+
<%= notifications[notificationKey] ? _t('account.views.form.email_section.notifications.enabled') : _t('account.views.form.email_section.notifications.disabled') %>
150+
</p>
151+
</div>
152+
</div>
153+
</div>
154+
<% }); %>
155+
</div>
156+
<% } %>
157+
158+
<!-- External datasources -->
114159
<% if (services.length > 0) { %>
115160
<div class="FormAccount-title">
116161
<p class="FormAccount-titleText"><%= _t('account.views.form.connect_external_datasources') %></p>
@@ -121,6 +166,7 @@
121166
<div class="js-datasourcesContent"></div>
122167
<% } %>
123168

169+
<!-- Delete account -->
124170
<div class="FormAccount-footer <% if (cantBeDeletedReason) { %>FormAccount-footer--noMarginBottom<% } %>">
125171
<% if (cantBeDeletedReason) { %>
126172
<p class="FormAccount-footerText">

lib/assets/javascripts/locale/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@
8585
"email_google": "Your account is linked to your Google account.",
8686
"errors": {
8787
"change_email": "You can change the email if you set a password."
88+
},
89+
"email_section": {
90+
"title": "Email settings",
91+
"description": "Choose what type of emails you'd like to receive.",
92+
"notifications": {
93+
"enabled": "On",
94+
"disabled": "Off",
95+
"do_subscriptions":"Emails from Data Observatory"
96+
}
8897
}
8998
}
9099
}

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cartodb-ui",
3-
"version": "1.0.0-assets.207",
3+
"version": "1.0.0-assets.208",
44
"description": "CARTO UI frontend",
55
"repository": {
66
"type": "git",

spec/requests/carto/api/email_notifications_controller_spec.rb

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,30 @@
66

77
before(:all) do
88
@carto_user = FactoryGirl.create(:carto_user)
9-
@carto_user.email_notifications = {
10-
do_subscriptions: true
11-
}
129
end
1310

1411
let(:auth_params) do
1512
{ user_domain: @carto_user.username, api_key: @carto_user.api_key }
1613
end
1714

1815
describe '#show' do
19-
it 'list the current notifications' do
16+
it 'always list available notifications with default value if missing in database' do
2017
get_json(api_v3_email_notifications_show_url(auth_params)) do |response|
2118
response.status.should eq 200
2219
response.body.should eq({ notifications: { do_subscriptions: true } })
2320
end
2421
end
2522

23+
it 'list the current notifications' do
24+
@carto_user.email_notifications = {
25+
do_subscriptions: false
26+
}
27+
get_json(api_v3_email_notifications_show_url(auth_params)) do |response|
28+
response.status.should eq 200
29+
response.body.should eq({ notifications: { do_subscriptions: false } })
30+
end
31+
end
32+
2633
it 'return error if unauthenticated' do
2734
get_json(api_v3_email_notifications_show_url({})) do |response|
2835
response.status.should eq 401

0 commit comments

Comments
 (0)