Skip to content

Commit b568d38

Browse files
Merge #921
921: Ability to add/update user email r=carols10cents This PR addresses issue #808, allow editing your own email address. This is the first step in a three part implementation, adding a field to edit an email address. Yet to be implemented before the issue is resolved is (1) the ability to verify emails with users and (2) figuring out how to get everyone to add their email to their account. This PR only implements the addition of a button to the email field such that a user's email can be added or updated, as well as hiding the email field from public view by separating `EncodableUser` into `EncodablePublicUser` and `EncodablePrivateUser`.
2 parents 71a6a1a + 45b6669 commit b568d38

File tree

12 files changed

+542
-27
lines changed

12 files changed

+542
-27
lines changed

app/components/email-input.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import Component from '@ember/component';
2+
import { empty } from '@ember/object/computed';
3+
4+
export default Component.extend({
5+
type: '',
6+
value: '',
7+
isEditing: false,
8+
user: null,
9+
disableSave: empty('user.email'),
10+
notValidEmail: false,
11+
prevEmail: '',
12+
emailIsNull: true,
13+
14+
actions: {
15+
editEmail() {
16+
let email = this.get('value');
17+
let isEmailNull = function(email) {
18+
return (email == null);
19+
};
20+
21+
this.set('emailIsNull', isEmailNull(email));
22+
this.set('isEditing', true);
23+
this.set('prevEmail', this.get('value'));
24+
},
25+
26+
saveEmail() {
27+
let userEmail = this.get('value');
28+
let user = this.get('user');
29+
30+
let emailIsProperFormat = function(userEmail) {
31+
let regExp = /^\S+@\S+\.\S+$/;
32+
return regExp.test(userEmail);
33+
};
34+
35+
if (!emailIsProperFormat(userEmail)) {
36+
this.set('notValidEmail', true);
37+
return;
38+
}
39+
40+
user.set('email', userEmail);
41+
user.save()
42+
.then(() => this.set('serverError', null))
43+
.catch(err => {
44+
let msg;
45+
if (err.errors && err.errors[0] && err.errors[0].detail) {
46+
msg = `An error occurred while saving this email, ${err.errors[0].detail}`;
47+
} else {
48+
msg = 'An unknown error occurred while saving this email.';
49+
}
50+
this.set('serverError', msg);
51+
});
52+
53+
this.set('isEditing', false);
54+
this.set('notValidEmail', false);
55+
},
56+
57+
cancelEdit() {
58+
this.set('isEditing', false);
59+
this.set('value', this.get('prevEmail'));
60+
}
61+
}
62+
});

app/components/validated-input.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Ember from 'ember';
2+
3+
const {
4+
computed,
5+
defineProperty
6+
} = Ember;
7+
8+
export default Ember.Component.extend({
9+
classNames: ['validated-input'],
10+
classNameBindings: ['showErrorClass:has-error', 'isValid:has-success'],
11+
model: null,
12+
value: null,
13+
type: 'text',
14+
valuePath: '',
15+
placeholder: '',
16+
validation: null,
17+
showValidations: false,
18+
didValidate: false,
19+
20+
notValidating: computed.not('validation.isValidating').readOnly(),
21+
hasContent: computed.notEmpty('value').readOnly(),
22+
hasWarnings: computed.notEmpty('validation.warnings').readOnly(),
23+
isValid: computed.and('hasContent', 'validation.isTruelyValid').readOnly(),
24+
shouldDisplayValidations: computed.or('showValidations', 'didValidate', 'hasContent').readOnly(),
25+
26+
showErrorClass: computed.and('notValidating', 'showErrorMessage', 'hasContent', 'validation').readOnly(),
27+
showErrorMessage: computed.and('shouldDisplayValidations', 'validation.isInvalid').readOnly(),
28+
29+
init() {
30+
this._super(...arguments);
31+
let valuePath = this.get('valuePath');
32+
33+
defineProperty(this, 'validation', computed.readOnly(`model.validations.attrs.${valuePath}`));
34+
defineProperty(this, 'value', computed.alias(`model.${valuePath}`));
35+
},
36+
37+
focusOut() {
38+
this._super(...arguments);
39+
this.set('showValidations', true);
40+
}
41+
});

app/styles/me.scss

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,48 @@
1616
clear: both;
1717
}
1818
dd { float: left; margin-left: 10px; }
19+
.align-center {
20+
@include align-items(center);
21+
}
22+
.row {
23+
width: 100%;
24+
border: 1px solid #d5d3cb;
25+
border-bottom-width: 0px;
26+
&:last-child { border-bottom-width: 1px; }
27+
padding: 10px 20px 10px 0;
28+
@include display-flex;
29+
background-color: $main-bg-dark;
30+
.small-text {
31+
font-size: 90%;
32+
}
33+
.error {
34+
color: rgb(216, 0, 41);
35+
}
36+
.label {
37+
@include flex(1);
38+
margin-right: 0.4em;
39+
font-weight: bold;
40+
}
41+
.actions {
42+
@include display-flex;
43+
@include align-items(center);
44+
}
45+
.space-left {
46+
margin-left: 10px;
47+
}
48+
.space-right {
49+
margin-right: 10px;
50+
}
51+
.no-space-left {
52+
margin-left: 0px;
53+
}
54+
p {
55+
width: 80%;
56+
}
57+
.space-bottom {
58+
margin-bottom: 10px;
59+
}
60+
}
1961
}
2062

2163
@media only screen and (max-width: 550px) {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{{#if isEditing }}
2+
<div class='row'>
3+
<div class='label'>
4+
<dt>Email</dt>
5+
</div>
6+
<form {{action 'saveEmail' on='submit'}}>
7+
{{input type=type value=value placeholder='Email' class='form-control space-bottom'}}
8+
{{#if notValidEmail }}
9+
<p class='small-text error'>Invalid email format. Please try again.</p>
10+
{{/if}}
11+
{{#if emailIsNull }}
12+
<p class='small-text'> Please add your email address. We will only use
13+
it to contact you about your account. We promise we'll never share it!
14+
</p>
15+
{{/if}}
16+
<div class='actions'>
17+
<button type='submit' class='small yellow-button space-right' disabled={{disableSave}}>Save</button>
18+
<button class='small yellow-button' {{action 'cancelEdit'}}>Cancel</button>
19+
</div>
20+
</form>
21+
</div>
22+
{{else}}
23+
<div class='row align-center'>
24+
<div class='label'>
25+
<dt>Email</dt>
26+
</div>
27+
<div class='email'>
28+
<dd class='no-space-left'>{{ user.email }}</dd>
29+
</div>
30+
<div class='actions'>
31+
<button class='small yellow-button space-left' {{action 'editEmail'}}>Edit</button>
32+
</div>
33+
</div>
34+
{{/if}}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<div class='form-group'>
2+
{{input type=type value=value placeholder=placeholder class='form-control' name=valuePath}}
3+
{{#if isValid}}
4+
<span class='valid-input fa fa-check'></span>
5+
{{/if}}
6+
7+
<div class='input-error'>
8+
{{#if showErrorMessage}}
9+
<div class='error'>
10+
{{v-get model valuePath 'message'}}
11+
</div>
12+
{{/if}}
13+
</div>
14+
</div>

app/templates/me/index.hbs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
<dd>{{ model.user.name }}</dd>
1717
<dt>GitHub Account</dt>
1818
<dd>{{ model.user.login }}</dd>
19-
<dt>Email</dt>
20-
<dd>{{ model.user.email }}</dd>
19+
{{email-input type='email' value=model.user.email user=model.user}}
2120
</dl>
2221
</div>
2322
</div>

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ pub fn middleware(app: Arc<App>) -> MiddlewareBuilder {
170170
api_router.get("/categories/:category_id", C(category::show));
171171
api_router.get("/category_slugs", C(category::slugs));
172172
api_router.get("/users/:user_id", C(user::show));
173+
api_router.put("/users/:user_id", C(user::update_user));
173174
api_router.get("/users/:user_id/stats", C(user::stats));
174175
api_router.get("/teams/:team_id", C(user::show_team));
175176
let api_router = Arc::new(R404(api_router));

src/owner.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ pub struct EncodableOwner {
6767
pub id: i32,
6868
pub login: String,
6969
pub kind: String,
70-
pub email: Option<String>,
7170
pub url: Option<String>,
7271
pub name: Option<String>,
7372
pub avatar: Option<String>,
@@ -361,7 +360,6 @@ impl Owner {
361360
match self {
362361
Owner::User(User {
363362
id,
364-
email,
365363
name,
366364
gh_login,
367365
gh_avatar,
@@ -371,7 +369,6 @@ impl Owner {
371369
EncodableOwner {
372370
id: id,
373371
login: gh_login,
374-
email: email,
375372
avatar: gh_avatar,
376373
url: Some(url),
377374
name: name,
@@ -389,7 +386,6 @@ impl Owner {
389386
EncodableOwner {
390387
id: id,
391388
login: login,
392-
email: None,
393389
url: Some(url),
394390
avatar: avatar,
395391
name: name,

src/tests/krate.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use cargo_registry::owner::EncodableOwner;
2222
use cargo_registry::schema::versions;
2323

2424
use cargo_registry::upload as u;
25-
use cargo_registry::user::EncodableUser;
25+
use cargo_registry::user::EncodablePublicUser;
2626
use cargo_registry::version::EncodableVersion;
2727
use cargo_registry::category::Category;
2828

@@ -1216,7 +1216,7 @@ fn following() {
12161216
fn owners() {
12171217
#[derive(Deserialize)]
12181218
struct R {
1219-
users: Vec<EncodableUser>,
1219+
users: Vec<EncodablePublicUser>,
12201220
}
12211221
#[derive(Deserialize)]
12221222
struct O {

0 commit comments

Comments
 (0)