1
0
Fork 0
mirror of https://github.com/TryGhost/Ghost-Admin.git synced 2023-12-14 02:33:04 +01:00

🎨 User profile settings page layout updates (#695)

closes TryGhost/Ghost#7134

Overhaul of the user settings page to make it more consistent with other settings panels.
The hardly readable validation for user "Full Name" is redundant as well, as the input field for it now has the same styles as the other input fields.
This commit is contained in:
Aileen Nowak 2017-05-17 21:24:31 +09:00 committed by Kevin Ansfield
parent 8066b2ef7c
commit c00af2554e
5 changed files with 183 additions and 281 deletions

View file

@ -16,9 +16,9 @@ export function countCharacters(params) {
el.className = 'word-count';
if (length > 180) {
el.style.color = '#E25440';
el.style.color = '#f05230';
} else {
el.style.color = '#9E9D95';
el.style.color = '#738a94';
}
el.innerHTML = 200 - length;

View file

@ -39,37 +39,16 @@
padding: 0;
}
@media (min-width: 901px) {
.content.settings-user {
padding: 0 40px;
}
}
.user-cover {
position: relative;
display: block;
overflow: hidden;
margin: 0;
width: auto;
width: 100%;
height: 300px;
margin-bottom: 30px;
background: #fafafa no-repeat center center;
background-size: cover;
}
@media (max-width: 900px) {
.user-cover {
margin: 0;
}
}
.user-cover:after {
/* Gradient overlay */
content: "";
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 110px;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.18));
border-radius: 6px;
}
.user-cover-edit {
@ -96,62 +75,11 @@
/* Edit user
/* ---------------------------------------------------------- */
@media (min-width: 651px) {
.first-form-group {
margin-right: 20px;
padding-left: 40px;
}
.first-form-group input {
max-width: 100%;
}
}
.user-details-top {
position: relative;
}
@media (max-width: 650px) {
.user-details-top {
margin-top: 40px;
margin-bottom: 0;
}
}
@media (min-width: 651px) {
.user-details-top {
margin-top: -91px;
margin-bottom: 0;
padding: 0;
}
.user-details-top p {
color: #fff;
}
.user-details-top label[for="user-name"] {
color: transparent;
}
.user-details-top .user-name {
border-color: #fff;
}
}
.user-profile {
position: relative;
z-index: 1;
}
@media (min-width: 651px) {
.user-profile {
padding-right: 20px;
padding-left: 143px;
}
}
@media (max-width: 650px) {
.user-profile fieldset {
padding: 0 40px;
}
}
@media (max-width: 550px) {
.user-profile fieldset {
padding: 0 15px;
@ -159,7 +87,7 @@
}
.user-profile textarea {
min-width: 240px;
min-width: 100%;
}
@ -168,43 +96,24 @@
.user-image {
position: absolute;
top: 200px;
left: 0;
z-index: 2;
display: block;
float: left;
overflow: hidden;
margin-right: 20px;
margin-left: -6px;
padding: 3px;
width: 126px;
height: 126px;
background: #fff;
border-radius: 100%;
margin: 0;
padding: 0;
width: 100px;
height: 100px;
border-radius: 0 6px;
text-align: center;
}
@media (min-width: 651px) {
.user-image {
top: -19px;
left: -98px;
}
}
@media (max-width: 650px) {
.user-image {
top: -135px;
left: 50%;
margin-right: 0;
margin-left: -63px;
}
}
.user-image .img {
display: block;
width: 120px;
height: 120px;
width: 100%;
height: 100%;
background-position: center center;
background-size: cover;
border-radius: 100%;
border-radius: 0 6px;
}
.user-image:hover .edit-user-image {
@ -213,17 +122,16 @@
.edit-user-image {
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
width: calc(100% - 6px);
top: 0;
left: 0;
width: 100%;
background: rgba(0, 0, 0, 0.5);
border-radius: 100%;
border-radius: 0 6px;
color: #fff;
text-decoration: none;
text-transform: uppercase;
line-height: 120px;
font-size: 12px;
line-height: 100px;
opacity: 0;
transition: opacity 0.3s ease;
}

View file

@ -84,171 +84,165 @@
</section>
</header>
<div class="view-container settings-user">
<div class="gm-main view-container settings-user">
<div class="gh-canvas">
<form class="user-profile" novalidate="novalidate" autocomplete="off" {{action (perform save) on="submit"}}>
<figure class="user-cover" style={{coverImageBackground}}>
<button class="gh-btn gh-btn-default user-cover-edit" {{action "toggleUploadCoverModal"}}><span>Change Cover</span></button>
{{#if showUploadCoverModal}}
{{gh-fullscreen-modal "upload-image"
model=(hash model=user imageProperty="coverImage")
close=(action "toggleUploadCoverModal")
modifier="action wide"}}
{{/if}}
</figure>
<figure class="user-cover" style={{coverImageBackground}}>
<button class="gh-btn gh-btn-default user-cover-edit" {{action "toggleUploadCoverModal"}}><span>Change Cover</span></button>
{{#if showUploadCoverModal}}
{{gh-fullscreen-modal "upload-image"
model=(hash model=user imageProperty="cover")
close=(action "toggleUploadCoverModal")
modifier="action wide"}}
{{/if}}
</figure>
<form class="user-profile" novalidate="novalidate" autocomplete="off" {{action (perform save) on="submit"}}>
<figure class="user-image">
<div id="user-image" class="img" style={{userImageBackground}}><span class="hidden">{{user.name}}"s Picture</span></div>
<button type="button" {{action "toggleUploadImageModal"}} class="edit-user-image">Edit Picture</button>
{{#if showUploadImageModal}}
{{gh-fullscreen-modal "upload-image"
model=(hash model=user imageProperty="image")
close=(action "toggleUploadImageModal")
modifier="action wide"}}
{{/if}}
</figure>
{{!-- Horrible hack to prevent Chrome from incorrectly auto-filling inputs --}}
<input style="display:none;" type="text" name="fakeusernameremembered"/>
<input style="display:none;" type="password" name="fakepasswordremembered"/>
{{!-- Horrible hack to prevent Chrome from incorrectly auto-filling inputs --}}
<input style="display:none;" type="text" name="fakeusernameremembered"/>
<input style="display:none;" type="password" name="fakepasswordremembered"/>
<fieldset class="user-details-top">
<fieldset class="user-details-bottom">
<figure class="user-image">
<div id="user-image" class="img" style={{userImageBackground}}><span class="hidden">{{user.name}}"s Picture</span></div>
<button type="button" {{action "toggleUploadImageModal"}} class="edit-user-image">Edit Picture</button>
{{#if showUploadImageModal}}
{{gh-fullscreen-modal "upload-image"
model=(hash model=user imageProperty="profileImage")
close=(action "toggleUploadImageModal")
modifier="action wide"}}
{{/if}}
</figure>
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="name" class="first-form-group"}}
<label for="user-name">Full Name</label>
{{gh-input user.name id="user-name" class="user-name" placeholder="Full Name" autocorrect="off" focusOut=(action "validate" "name" target=user) update=(action (mut user.name))}}
{{#if user.errors.name}}
{{gh-error-message errors=user.errors property="name"}}
{{else}}
<p>Use your real name so people can recognise you</p>
{{/if}}
{{/gh-form-group}}
</fieldset>
<fieldset class="user-details-bottom">
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="slug"}}
<label for="user-slug">Slug</label>
{{gh-input slugValue class="user-name" id="user-slug" name="user" focusOut=(action (perform updateSlug slugValue)) placeholder="Slug" selectOnClick="true" autocorrect="off" update=(action (mut slugValue))}}
<p>{{gh-blog-url}}/author/{{slugValue}}</p>
{{gh-error-message errors=user.errors property="slug"}}
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="email"}}
<label for="user-email">Email</label>
{{!-- Administrators only see text of Owner's email address but not input --}}
{{#if canChangeEmail}}
{{gh-input user.email type="email" id="user-email" name="email" placeholder="Email Address" autocapitalize="off" autocorrect="off" autocomplete="off" focusOut=(action "validate" "email" target=user) update=(action (mut user.email))}}
{{gh-error-message errors=user.errors property="email"}}
{{else}}
<span>{{user.email}}</span>
{{/if}}
<p>Used for notifications</p>
{{/gh-form-group}}
{{#if rolesDropdownIsVisible}}
<div class="form-group">
<label for="user-role">Role</label>
<span class="gh-select" tabindex="0">
{{one-way-select
id="new-user-role"
options=roles
optionValuePath="id"
optionLabelPath="name"
value=model.role
update=(action "changeRole")
}}
{{inline-svg "arrow-down-small"}}
</span>
<p>What permissions should this user have?</p>
</div>
{{/if}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="location"}}
<label for="user-location">Location</label>
{{gh-input user.location type="text" id="user-location" focusOut=(action "validate" "location" target=user) update=(action (mut user.location))}}
{{gh-error-message errors=user.errors property="location"}}
<p>Where in the world do you live?</p>
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="website"}}
<label for="user-website">Website</label>
{{gh-input user.website type="url" id="user-website" autocapitalize="off" autocorrect="off" autocomplete="off" focusOut=(action "validate" "website" target=user) update=(action (mut user.website))}}
{{gh-error-message errors=user.errors property="website"}}
<p>Have a website or blog other than this one? Link it!</p>
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="facebook"}}
<label for="user-facebook">Facebook Profile</label>
<input value={{user.facebook}} oninput={{action (mut _scratchFacebook) value="target.value"}} {{action "validateFacebookUrl" on="focusOut"}} type="url" class="gh-input" id="user-facebook" name="user[facebook]" placeholder="https://www.facebook.com/username" autocorrect="off" />
{{gh-error-message errors=user.errors property="facebook"}}
<p>URL of your personal Facebook Profile</p>
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="twitter"}}
<label for="user-twitter">Twitter Profile</label>
<input value={{user.twitter}} oninput={{action (mut _scratchTwitter) value="target.value"}} {{action "validateTwitterUrl" on="focusOut"}} type="url" class="gh-input" id="user-twitter" name="user[twitter]" placeholder="https://twitter.com/username" autocorrect="off" />
{{gh-error-message errors=user.errors property="twitter"}}
<p>URL of your personal Twitter profile</p>
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="bio" class="bio-container"}}
<label for="user-bio">Bio</label>
{{gh-textarea user.bio id="user-bio" focusOut=(action "validate" "bio" target=user) update=(action (mut user.bio))}}
{{gh-error-message errors=user.errors property="bio"}}
<p>
Write about you, in 200 characters or less.
{{gh-count-characters user.bio}}
</p>
{{/gh-form-group}}
<hr />
</fieldset>
</form> {{! user details form }}
{{!-- If an administrator is viewing Owner's profile or we're using Ghost.org OAuth then hide inputs for change password --}}
{{#if canChangePassword}}
<form id="password-reset" class="user-profile" novalidate="novalidate" autocomplete="off" {{action (perform user.saveNewPassword) on="submit"}}>
<fieldset>
{{#unless isNotOwnProfile}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="password"}}
<label for="user-password-old">Old Password</label>
{{gh-input value=user.password type="password" id="user-password-old" update=(action 'updatePassword') onenter=(action (perform user.saveNewPassword))}}
{{gh-error-message errors=user.errors property="password"}}
{{/gh-form-group}}
{{/unless}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="newPassword"}}
<label for="user-password-new">New Password</label>
{{gh-input user.newPassword type="password" id="user-password-new" update=(action 'updateNewPassword') onenter=(action (perform user.saveNewPassword))}}
{{gh-error-message errors=user.errors property="newPassword"}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="name" class="first-form-group"}}
<label for="user-name">Full Name</label>
{{gh-input user.name id="user-name" class="user-name" placeholder="Full Name" autocorrect="off" focusOut=(action "validate" "name" target=user) update=(action (mut user.name))}}
{{#if user.errors.name}}
{{gh-error-message errors=user.errors property="name"}}
{{else}}
<p>Use your real name so people can recognise you</p>
{{/if}}
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="ne2Password"}}
<label for="user-new-password-verification">Verify Password</label>
{{gh-input user.ne2Password type="password" id="user-new-password-verification" update=(action 'updateNe2Password') onenter=(action (perform user.saveNewPassword))}}
{{gh-error-message errors=user.errors property="ne2Password"}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="slug"}}
<label for="user-slug">Slug</label>
{{gh-input slugValue class="user-name" id="user-slug" name="user" focusOut=(action (perform updateSlug slugValue)) placeholder="Slug" selectOnClick="true" autocorrect="off" update=(action (mut slugValue))}}
<p>{{gh-blog-url}}/author/{{slugValue}}</p>
{{gh-error-message errors=user.errors property="slug"}}
{{/gh-form-group}}
<div class="form-group">
{{gh-task-button "Change Password"
class="gh-btn gh-btn-icon button-change-password"
idleClass="gh-btn-red"
task=user.saveNewPassword}}
</div>
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="email"}}
<label for="user-email">Email</label>
{{!-- Administrators only see text of Owner's email address but not input --}}
{{#if canChangeEmail}}
{{gh-input user.email type="email" id="user-email" name="email" placeholder="Email Address" autocapitalize="off" autocorrect="off" autocomplete="off" focusOut=(action "validate" "email" target=user) update=(action (mut user.email))}}
{{gh-error-message errors=user.errors property="email"}}
{{else}}
<span>{{user.email}}</span>
{{/if}}
<p>Used for notifications</p>
{{/gh-form-group}}
{{#if rolesDropdownIsVisible}}
<div class="form-group">
<label for="user-role">Role</label>
<span class="gh-select" tabindex="0">
{{one-way-select
id="new-user-role"
options=roles
optionValuePath="id"
optionLabelPath="name"
value=model.role
update=(action "changeRole")
}}
</span>
<p>What permissions should this user have?</p>
</div>
{{/if}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="location"}}
<label for="user-location">Location</label>
{{gh-input user.location type="text" id="user-location" focusOut=(action "validate" "location" target=user) update=(action (mut user.location))}}
{{gh-error-message errors=user.errors property="location"}}
<p>Where in the world do you live?</p>
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="website"}}
<label for="user-website">Website</label>
{{gh-input user.website type="url" id="user-website" autocapitalize="off" autocorrect="off" autocomplete="off" focusOut=(action "validate" "website" target=user) update=(action (mut user.website))}}
{{gh-error-message errors=user.errors property="website"}}
<p>Have a website or blog other than this one? Link it!</p>
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="facebook"}}
<label for="user-facebook">Facebook Profile</label>
<input value={{user.facebook}} oninput={{action (mut _scratchFacebook) value="target.value"}} {{action "validateFacebookUrl" on="focusOut"}} type="url" class="gh-input" id="user-facebook" name="user[facebook]" placeholder="https://www.facebook.com/username" autocorrect="off" />
{{gh-error-message errors=user.errors property="facebook"}}
<p>URL of your personal Facebook Profile</p>
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="twitter"}}
<label for="user-twitter">Twitter Profile</label>
<input value={{user.twitter}} oninput={{action (mut _scratchTwitter) value="target.value"}} {{action "validateTwitterUrl" on="focusOut"}} type="url" class="gh-input" id="user-twitter" name="user[twitter]" placeholder="https://twitter.com/username" autocorrect="off" />
{{gh-error-message errors=user.errors property="twitter"}}
<p>URL of your personal Twitter profile</p>
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="bio" class="bio-container"}}
<label for="user-bio">Bio</label>
{{gh-textarea user.bio id="user-bio" focusOut=(action "validate" "bio" target=user) update=(action (mut user.bio))}}
{{gh-error-message errors=user.errors property="bio"}}
<p>
Write about you, in 200 characters or less.
{{gh-count-characters user.bio}}
</p>
{{/gh-form-group}}
<hr />
</fieldset>
</form> {{! change password form }}
{{/if}}
{{!-- when using Ghost OAuth, users trying to change email/pass need to visit my.ghost.org --}}
{{#if showMyGhostLink}}
<div class="user-profile">
<p class="gh-box gh-box-info">{{inline-svg "lock"}} To change your login details please visit <a href="https://my.ghost.org/account" target="_blank">https://my.ghost.org/account</a></p>
</div>
{{/if}}
</form> {{! user details form }}
{{!-- If an administrator is viewing Owner's profile or we're using Ghost.org OAuth then hide inputs for change password --}}
{{#if canChangePassword}}
<form id="password-reset" class="user-profile" novalidate="novalidate" autocomplete="off" {{action (perform user.saveNewPassword) on="submit"}}>
<fieldset>
{{#unless isNotOwnProfile}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="password"}}
<label for="user-password-old">Old Password</label>
{{gh-input value=user.password type="password" id="user-password-old" update=(action 'updatePassword') onenter=(action (perform user.saveNewPassword))}}
{{gh-error-message errors=user.errors property="password"}}
{{/gh-form-group}}
{{/unless}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="newPassword"}}
<label for="user-password-new">New Password</label>
{{gh-input user.newPassword type="password" id="user-password-new" update=(action 'updateNewPassword') onenter=(action (perform user.saveNewPassword))}}
{{gh-error-message errors=user.errors property="newPassword"}}
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="ne2Password"}}
<label for="user-new-password-verification">Verify Password</label>
{{gh-input user.ne2Password type="password" id="user-new-password-verification" update=(action 'updateNe2Password') onenter=(action (perform user.saveNewPassword))}}
{{gh-error-message errors=user.errors property="ne2Password"}}
{{/gh-form-group}}
<div class="form-group">
{{gh-task-button "Change Password" class="gh-btn gh-btn-red gh-btn-icon button-change-password" task=user.saveNewPassword}}
</div>
</fieldset>
</form> {{! change password form }}
{{/if}}
{{!-- when using Ghost OAuth, users trying to change email/pass need to visit my.ghost.org --}}
{{#if showMyGhostLink}}
<div class="user-profile">
<p class="gh-box gh-box-info">{{inline-svg "lock"}} To change your login details please visit <a href="https://my.ghost.org/account" target="_blank">https://my.ghost.org/account</a></p>
</div>
{{/if}}
</div>
</div>
</section>

View file

@ -469,22 +469,22 @@ describe('Acceptance: Team', function () {
await visit('/team/test-1');
expect(currentURL(), 'currentURL').to.equal('/team/test-1');
expect(find('.user-details-top .first-form-group input.user-name').val(), 'current user name').to.equal('Test User');
expect(find('.user-details-bottom .first-form-group input.user-name').val(), 'current user name').to.equal('Test User');
// test empty user name
await fillIn('.user-details-top .first-form-group input.user-name', '');
await triggerEvent('.user-details-top .first-form-group input.user-name', 'blur');
await fillIn('.user-details-bottom .first-form-group input.user-name', '');
await triggerEvent('.user-details-bottom .first-form-group input.user-name', 'blur');
expect(find('.user-details-top .first-form-group').hasClass('error'), 'username input is in error state with blank input').to.be.true;
expect(find('.user-details-bottom .first-form-group').hasClass('error'), 'username input is in error state with blank input').to.be.true;
// test too long user name
await fillIn('.user-details-top .first-form-group input.user-name', new Array(160).join('a'));
await triggerEvent('.user-details-top .first-form-group input.user-name', 'blur');
await fillIn('.user-details-bottom .first-form-group input.user-name', new Array(160).join('a'));
await triggerEvent('.user-details-bottom .first-form-group input.user-name', 'blur');
expect(find('.user-details-top .first-form-group').hasClass('error'), 'username input is in error state with too long input').to.be.true;
expect(find('.user-details-bottom .first-form-group').hasClass('error'), 'username input is in error state with too long input').to.be.true;
// reset name field
await fillIn('.user-details-top .first-form-group input.user-name', 'Test User');
await fillIn('.user-details-bottom .first-form-group input.user-name', 'Test User');
expect(find('.user-details-bottom input[name="user"]').val(), 'slug value is default').to.equal('test-1');
@ -501,7 +501,7 @@ describe('Acceptance: Team', function () {
await fillIn('.user-details-bottom input[name="email"]', 'thisisnotanemail');
await triggerEvent('.user-details-bottom input[name="email"]', 'blur');
expect(find('.user-details-bottom .form-group:nth-of-type(2)').hasClass('error'), 'email input should be in error state with invalid email').to.be.true;
expect(find('.user-details-bottom .form-group:nth-of-type(3)').hasClass('error'), 'email input should be in error state with invalid email').to.be.true;
await fillIn('.user-details-bottom input[name="email"]', 'test@example.com');
await fillIn('#user-location', new Array(160).join('a'));

View file

@ -3,8 +3,8 @@ import {describe, it} from 'mocha';
import {countCharacters} from 'ghost-admin/helpers/gh-count-characters';
describe('Unit: Helper: gh-count-characters', function() {
let defaultStyle = 'color: rgb(158, 157, 149);';
let errorStyle = 'color: rgb(226, 84, 64);';
let defaultStyle = 'color: rgb(115, 138, 148);';
let errorStyle = 'color: rgb(240, 82, 48);';
it('counts remaining chars', function() {
let result = countCharacters(['test']);