Skip to content

Commit ca965ce

Browse files
committed
Auto merge of #1913 - DSpeckhals:email-preference-front-end, r=carols10cents
front-end: Add email notification preferences This commit adds the front-end capabilities to coincide with the API endpoint that was built on the server. There's also a small bug fixed in the api-token-row; it was throwing an error every time the "/me" route was loaded. This PR addresses the second item in #1895. I like how the UI ended up, but it may very well be creators bias 😄. If this doesn't work, then I'll be happy to iterate! ### Screenshot of a crate with notifications on and one that's off ![image](https://user-images.githubusercontent.com/3310769/69435559-c6d7f880-0d0d-11ea-9d30-0b38a4fc7cfb.png)
2 parents 6138687 + 12ce748 commit ca965ce

File tree

8 files changed

+257
-1
lines changed

8 files changed

+257
-1
lines changed

app/components/owned-crate-row.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Component from '@ember/component';
2+
import { computed } from '@ember/object';
3+
import { alias } from '@ember/object/computed';
4+
5+
export default Component.extend({
6+
tagName: 'li',
7+
8+
name: alias('ownedCrate.name'),
9+
controlId: computed('ownedCrate.id', function() {
10+
return `${this.ownedCrate.id}-email-notifications`;
11+
}),
12+
emailNotifications: alias('ownedCrate.email_notifications'),
13+
14+
actions: {
15+
toggleEmailNotifications() {
16+
this.set('emailNotifications', !this.get('emailNotifications'));
17+
},
18+
},
19+
});

app/controllers/me/index.js

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Controller from '@ember/controller';
2-
import { sort, filterBy, notEmpty } from '@ember/object/computed';
2+
import { alias, sort, filterBy, notEmpty } from '@ember/object/computed';
33
import { inject as service } from '@ember/service';
4+
import ajax from 'ember-fetch/ajax';
45

56
export default Controller.extend({
67
// eslint-disable-next-line ember/avoid-leaking-state-in-ember-objects
@@ -12,10 +13,50 @@ export default Controller.extend({
1213

1314
isResetting: false,
1415

16+
ownedCrates: alias('model.ownedCrates'),
17+
1518
newTokens: filterBy('model.api_tokens', 'isNew', true),
1619
disableCreate: notEmpty('newTokens'),
1720

21+
emailNotificationsError: false,
22+
emailNotificationsSuccess: false,
23+
24+
setAllEmailNotifications(value) {
25+
this.get('ownedCrates').forEach(c => {
26+
c.set('email_notifications', value);
27+
});
28+
},
29+
1830
actions: {
31+
async saveEmailNotifications() {
32+
try {
33+
await ajax(`/api/v1/me/email_notifications`, {
34+
method: 'PUT',
35+
body: JSON.stringify(
36+
this.get('ownedCrates').map(c => ({
37+
id: parseInt(c.id, 10),
38+
email_notifications: c.email_notifications,
39+
})),
40+
),
41+
});
42+
this.setProperties({
43+
emailNotificationsError: false,
44+
emailNotificationsSuccess: true,
45+
});
46+
} catch (err) {
47+
console.error(err);
48+
this.setProperties({
49+
emailNotificationsError: true,
50+
emailNotificationsSuccess: false,
51+
});
52+
}
53+
},
54+
emailNotificationsSelectAll() {
55+
this.setAllEmailNotifications(true);
56+
},
57+
emailNotificationsSelectNone() {
58+
this.setAllEmailNotifications(false);
59+
},
1960
startNewToken() {
2061
this.store.createRecord('api-token', {
2162
created_at: new Date(Date.now() + 2000),

app/models/owned-crate.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import DS from 'ember-data';
2+
3+
export default DS.Model.extend({
4+
name: DS.attr('string'),
5+
email_notifications: DS.attr('boolean'),
6+
});

app/routes/me/index.js

+11
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,20 @@ import Route from '@ember/routing/route';
33
import AuthenticatedRoute from '../../mixins/authenticated-route';
44

55
export default Route.extend(AuthenticatedRoute, {
6+
actions: {
7+
willTransition: function() {
8+
this.controller
9+
.setProperties({
10+
emailNotificationsSuccess: false,
11+
emailNotificationsError: false,
12+
})
13+
.clear();
14+
},
15+
},
616
model() {
717
return {
818
user: this.get('session.currentUser'),
19+
ownedCrates: this.get('session.ownedCrates'),
920
api_tokens: this.store.findAll('api-token'),
1021
};
1122
},

app/services/session.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { A } from '@ember/array';
12
import Service, { inject as service } from '@ember/service';
23
import ajax from 'ember-fetch/ajax';
34

@@ -7,6 +8,7 @@ export default Service.extend({
78
isLoggedIn: false,
89
currentUser: null,
910
currentUserDetected: false,
11+
ownedCrates: A(),
1012

1113
store: service(),
1214
router: service(),
@@ -66,6 +68,9 @@ export default Service.extend({
6668
fetchUser() {
6769
return ajax('/api/v1/me').then(response => {
6870
this.set('currentUser', this.store.push(this.store.normalize('user', response.user)));
71+
this.ownedCrates.pushObjects(
72+
response.owned_crates.map(c => this.store.push(this.store.normalize('owned-crate', c))),
73+
);
6974
});
7075
},
7176

app/styles/me.scss

+120
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,126 @@
116116
}
117117
}
118118

119+
#me-email-notifications {
120+
border-bottom: 5px solid $gray-border;
121+
padding-bottom: 20px;
122+
margin-bottom: 20px;
123+
@include display-flex;
124+
@include flex-direction(column);
125+
126+
.row {
127+
@include display-flex;
128+
@include flex-direction(row);
129+
130+
.error, .success {
131+
border-top-width: 0px;
132+
font-weight: bold;
133+
134+
padding: 0px 10px 10px 20px;
135+
}
136+
137+
.error {
138+
color: rgb(216, 0, 41);
139+
}
140+
141+
.success {
142+
color: green;
143+
}
144+
}
145+
146+
.button-container {
147+
margin-right: 1rem;
148+
}
149+
150+
ul {
151+
padding: 0;
152+
@include flex-grow(1);
153+
154+
li {
155+
background-color: #fff;
156+
display: block;
157+
position: relative;
158+
border: 1px solid #d5d3cb;
159+
}
160+
161+
label {
162+
padding: 20px 30px;
163+
width: 100%;
164+
display: block;
165+
text-align: left;
166+
font-weight: bold;
167+
cursor: pointer;
168+
position: relative;
169+
z-index: 2;
170+
transition: color 200ms ease-in;
171+
overflow: hidden;
172+
173+
&:before {
174+
width: 100%;
175+
height: 10px;
176+
border-radius: 50%;
177+
content: '';
178+
background-color: $main-bg-dark;
179+
position: absolute;
180+
left: 50%;
181+
top: 50%;
182+
transform: translate(-50%, -50%) scale3d(1, 1, 1);
183+
opacity: 0;
184+
z-index: -1;
185+
}
186+
187+
&:after {
188+
width: 32px;
189+
height: 32px;
190+
content: '';
191+
border: 2px solid #d5d3cb;
192+
border-radius: 50%;
193+
z-index: 2;
194+
position: absolute;
195+
right: 30px;
196+
top: 50%;
197+
transform: translateY(-50%);
198+
cursor: pointer;
199+
}
200+
}
201+
202+
input:checked ~ label {
203+
&:before {
204+
transform: translate(-50%, -50%) scale3d(56, 56, 1);
205+
opacity: 1;
206+
}
207+
208+
&:after {
209+
background-color: #cfc487;
210+
border-color: #cfc487;
211+
background-image: url("data:image/svg+xml,%3Csvg width='32' height='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.414 11L4 12.414l5.414 5.414L20.828 6.414 19.414 5l-10 10z' fill='%23383838' fill-rule='nonzero'/%3E%3C/svg%3E ");
212+
background-repeat: no-repeat;
213+
background-position: 3px 4px;
214+
}
215+
}
216+
217+
input {
218+
width: 32px;
219+
height: 32px;
220+
order: 1;
221+
z-index: 2;
222+
position: absolute;
223+
right: 30px;
224+
top: 50%;
225+
transform: translateY(-50%);
226+
cursor: pointer;
227+
visibility: hidden;
228+
}
229+
}
230+
231+
.right {
232+
@include flex(2);
233+
@include display-flex;
234+
@include justify-content(flex-end);
235+
@include align-self(center);
236+
}
237+
}
238+
119239
#me-api {
120240
@media only screen and (max-width: 350px) {
121241
.api { display: none; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{{input
2+
type="checkbox"
3+
name=controlId
4+
id=controlId
5+
checked=emailNotifications
6+
change=(action "toggleEmailNotifications")
7+
class="form-control"
8+
}}
9+
<label for="{{ controlId }}">
10+
{{ name }}
11+
</label>

app/templates/me/index.hbs

+43
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,49 @@
2525
{{email-input type='email' value=model.user.email user=model.user}}
2626
</div>
2727

28+
<form id='me-email-notifications' {{ action 'saveEmailNotifications' on='submit' }} >
29+
<h2>Email Notification Preferences</h2>
30+
31+
<p>
32+
To aid detection of unauthorized crate changes, we email you each time a new version of a crate you own is pushed.
33+
By receiving and reading these emails, you help protect the Rust ecosystem. You may also choose to turn these
34+
emails off for any of your crates listed below.
35+
</p>
36+
37+
<div class='row'>
38+
<div class='button-container'>
39+
<button type='button' class='yellow-button small' {{action 'emailNotificationsSelectAll'}}>Select All</button>
40+
</div>
41+
<div class='button-container'>
42+
<button type='button' class='yellow-button small' {{action 'emailNotificationsSelectNone'}}>Deselect All</button>
43+
</div>
44+
</div>
45+
46+
<div class='row'>
47+
<ul>
48+
{{#each ownedCrates as |ownedCrate|}}
49+
{{owned-crate-row ownedCrate=ownedCrate}}
50+
{{/each}}
51+
</ul>
52+
</div>
53+
54+
<div class='row'>
55+
{{#if emailNotificationsError}}
56+
<div class='error'>
57+
An error occurred while saving your email preferences.
58+
</div>
59+
{{/if}}
60+
{{#if emailNotificationsSuccess}}
61+
<div class='success'>
62+
Your email notification preferences have been updated!
63+
</div>
64+
{{/if}}
65+
<div class='right'>
66+
<button type='submit' class='yellow-button'>Update</button>
67+
</div>
68+
</div>
69+
</form>
70+
2871
<div id='me-api'>
2972
<div class='me-subheading'>
3073
<h2>API Access</h2>

0 commit comments

Comments
 (0)