Merge pull request #7600 from nextcloud/new-user-button-to-sidebar

New user button to sidebar
This commit is contained in:
Roeland Jago Douma 2018-02-28 12:33:30 +01:00 committed by GitHub
commit 926419e15c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 274 additions and 185 deletions

View file

@ -92,6 +92,22 @@ kbd {
border-right: 1px solid $color-border;
display: flex;
flex-direction: column;
/* 'New' button */
.app-navigation-new {
display: block;
padding: 10px;
button {
display: inline-block;
width: 100%;
padding: 10px;
padding-left: 34px;
background-position: 10px center;
text-align: left;
margin: 0;
}
}
li {
position: relative;
}
@ -794,14 +810,14 @@ kbd {
flex: 0 0 auto;
> button,
> a,
> .menuitem {
> .menuitem,
> label {
cursor: pointer;
line-height: 36px;
border: 0;
background-color: transparent;
display: flex;
align-items: center;
width: auto;
height: auto;
margin: 0;
font-weight: 300;
@ -869,6 +885,9 @@ kbd {
> input.radio + label::before {
margin: -2px 11px 0;
}
> input:not([type=radio]):not([type=checkbox]):not([type=image]) {
width: 150px;
}
}
> button {
padding: 0;
@ -880,7 +899,7 @@ kbd {
}
}
/* CONTENT WRAPPER --------------------------------------------------------- */
#app-content-wrapper {
display: flex;
position: relative;
@ -895,6 +914,8 @@ kbd {
overflow-y: auto;
}
}
/* CONTENT LIST ------------------------------------------------------------- */
.app-content-list {
width: 300px;
border-right: 1px solid nc-darken($color-main-background, 8%);
@ -1040,7 +1061,8 @@ kbd {
}
}
}
/* App content */
/* CONTENT ------------------------------------------------------------------ */
.app-content-detail {
/* grow full width */
flex-grow: 1;
@ -1050,6 +1072,7 @@ kbd {
}
}
/* MOBILE ------------------------------------------------------------------- */
/* Mobile width < 768px */
@media only screen and (max-width: 768px) {

View file

@ -149,6 +149,11 @@ input[type='reset'] {
cursor: pointer;
box-sizing: border-box;
background-color: nc-darken($color-main-background, 3%);
&.icon-confirm:not(:empty),
&.icon-confirm[value]:not([value=""]) {
background-position: calc(100% - 6px) center;
padding-right: 30px;
}
}
/* Buttons */

View file

@ -25,7 +25,7 @@ ul.multiselectoptions {
position: absolute;
max-height: 20em;
overflow-y: auto;
z-index: 49;
z-index: 149;
&.down {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
@ -96,7 +96,7 @@ div.multiselect {
&.active {
background-color: $color-main-background;
position: relative;
z-index: 50;
z-index: 150;
}
&.up {
border-top: 0 none;
@ -116,11 +116,16 @@ div.multiselect {
text-overflow: ellipsis;
width: 90%;
white-space: nowrap;
line-height: 20px;
}
&:last-child {
position: absolute;
right: 8px;
top: 8px;
padding: 0 !important;
min-width: 16px;
min-height: 16px;
background-position: center;
}
}
}

View file

@ -137,7 +137,7 @@ input {
}
.personal-settings-setting-box input {
&[type="text"], &[type="email"], &[type="tel"], &[type="url"] {
&[type='text'], &[type='email'], &[type='tel'], &[type='url'] {
width: 100%;
}
}
@ -183,7 +183,7 @@ input {
}
}
> form span {
&[class^="icon-checkmark"], &[class^="icon-error"] {
&[class^='icon-checkmark'], &[class^='icon-error'] {
position: relative;
right: 8px;
top: -28px;
@ -491,9 +491,12 @@ table.grid {
}
}
td {
td, th {
&.name {
padding-left: .8em;
width: 10em;
min-width: 10em;
max-width: 10em;
}
&.password {
padding-left: .8em;
@ -504,8 +507,12 @@ td {
&.displayName > img {
visibility: hidden;
}
&.password, &.displayName {
&.password,
&.displayName,
&.mailAddress {
width: 12em;
min-width: 12em;
max-width: 12em;
cursor: pointer;
}
&.mailAddress {
@ -524,14 +531,14 @@ span.usersLastLoginTooltip {
/* dropdowns will be relative to this element */
#userlist {
position: relative;
.mailAddress, .storageLocation, .userBackend, .lastLogin {
.storageLocation, .userBackend, .lastLogin {
display: none;
}
th.name {
color: #000;
}
tr {
height: 51px;
height: 50px;
}
.mailAddress .loading-small {
width: 16px;
@ -543,16 +550,17 @@ span.usersLastLoginTooltip {
.groupsListContainer.hidden {
display: none;
}
thead th,
thead tr {
z-index: 100;
background-color: $color-main-background;
position: sticky;
// positional attribute is required for position to take affect.
top: 0;
}
}
/* because of accessibility the name cell is <th> - therefore we enforce the black color */
/* use same height as in files app */
#newuser {
/* positioning fixes */
padding-left: 3px;
.groups {
display: inline;
}
.groupsListContainer.hidden {
display: none;
}
@ -561,6 +569,24 @@ span.usersLastLoginTooltip {
position: relative;
top: -1px;
}
input {
&:not([type='submit']),
&:not([type='reset']) {
width: 100%;
}
}
.userActions input {
width: 44px;
height: 44px;
&.icon-close {
border: none;
background-color: initial;
opacity: .5;
}
&:hover {
opacity: 1;
}
}
}
tr:hover > td {
@ -575,7 +601,6 @@ tr:hover > td {
}
td.userActions {
width: 44px;
.toggleUserActions {
width: 44px;
height: 44px;
@ -834,7 +859,7 @@ span.version {
margin-bottom: 18px;
}
/* capitalize "Other" category */
/* capitalize 'Other' category */
#app-category-925 {
text-transform: capitalize;

View file

@ -2,6 +2,7 @@
* Copyright (c) 2014, Arthur Schiwon <blizzz@owncloud.com>
* Copyright (c) 2014, Raghu Nayyar <beingminimal@gmail.com>
* Copyright (c) 2011, Robin Appelman <icewind1991@gmail.com>
* Copyright (c) 2017, John Molakvoæ <skjnldsv@protonmail.com>
* This file is licensed under the Affero General Public License version 3 or later.
* See the COPYING-README file.
*/
@ -31,6 +32,17 @@ var UserList = {
// initially the list might already contain user entries (not fully ajaxified yet)
// initialize these entries
this.$el.find('.quota-user').singleSelect().on('change', this.onQuotaSelect);
$('#new-user-button').on('click', function(event) {
event.preventDefault();
$('#newuserHeader').slideToggle(OC.menuSpeed);
$('#newusername').focus();
});
$('#newreset').on('click', function(event) {
$('#newuserHeader').slideToggle(OC.menuSpeed);
});
$('.has-tooltip').tooltip({
placement: 'bottom'
});
},
/**
@ -508,7 +520,7 @@ var UserList = {
checked: checked,
oncheck: addUserToGroup,
onuncheck: removeUserFromGroup,
minWidth: 100
minWidth: 150
});
},
@ -542,7 +554,7 @@ var UserList = {
checked: checked,
oncheck: checkHandler,
onuncheck: checkHandler,
minWidth: 100
minWidth: 150
});
},
@ -1134,24 +1146,6 @@ $(document).ready(function () {
}
});
if ($('#CheckboxEmailAddress').is(':checked')) {
$("#userlist .mailAddress").show();
}
// Option to display/hide the "Mail Address" column
$('#CheckboxEmailAddress').click(function () {
if ($('#CheckboxEmailAddress').is(':checked')) {
$("#userlist .mailAddress").show();
if (OC.isUserAdmin()) {
OCP.AppConfig.setValue('core', 'umgmt_show_email', 'true');
}
} else {
$("#userlist .mailAddress").hide();
if (OC.isUserAdmin()) {
OCP.AppConfig.setValue('core', 'umgmt_show_email', 'false');
}
}
});
if ($('#CheckboxUserBackend').is(':checked')) {
$("#userlist .userBackend").show();
}
@ -1170,24 +1164,6 @@ $(document).ready(function () {
}
});
if ($('#CheckboxMailOnUserCreate').is(':checked')) {
$("#newemail").show();
}
// Option to display/hide the "E-Mail" input field
$('#CheckboxMailOnUserCreate').click(function () {
if ($('#CheckboxMailOnUserCreate').is(':checked')) {
$("#newemail").show();
if (OC.isUserAdmin()) {
OCP.AppConfig.setValue('core', 'umgmt_send_email', 'true');
}
} else {
$("#newemail").hide();
if (OC.isUserAdmin()) {
OCP.AppConfig.setValue('core', 'umgmt_send_email', 'false');
}
}
});
// calculate initial limit of users to load
var initialUserCountLimit = UserList.initialUsersToLoad,
containerHeight = $('#app-content').height();

View file

@ -1,6 +1,7 @@
<?php
/**
* Copyright (c) 2011, Robin Appelman <icewind1991@gmail.com>
* Copyright (c) 2017, John Molakvoæ <skjnldsv@protonmail.com>
* This file is licensed under the Affero General Public License version 3 or later.
* See the COPYING-README file.
*/
@ -35,6 +36,7 @@ translation('settings');
?>
<div id="app-navigation">
<?php print_unescaped($this->inc('users/part.createuser')); ?>
<?php print_unescaped($this->inc('users/part.grouplist')); ?>
<div id="app-settings">
<div id="app-settings-header">
@ -65,20 +67,6 @@ translation('settings');
<?php p($l->t('Show last login')) ?>
</label>
</p>
<p>
<input type="checkbox" name="EmailAddress" value="EmailAddress" id="CheckboxEmailAddress"
class="checkbox" <?php if ($_['show_email'] === 'true') print_unescaped('checked="checked"'); ?> />
<label for="CheckboxEmailAddress">
<?php p($l->t('Show email address')) ?>
</label>
</p>
<p>
<input type="checkbox" name="MailOnUserCreate" value="MailOnUserCreate" id="CheckboxMailOnUserCreate"
class="checkbox" <?php if ($_['send_email'] === 'true') print_unescaped('checked="checked"'); ?> />
<label for="CheckboxMailOnUserCreate">
<?php p($l->t('Send email to new user')) ?>
</label>
</p>
<p class="info-text">
<?php p($l->t('When the password of a new user is left empty, an activation email with a link to set the password is sent.')) ?>
</p>
@ -88,6 +76,5 @@ translation('settings');
</div>
<div id="app-content">
<?php print_unescaped($this->inc('users/part.createuser')); ?>
<?php print_unescaped($this->inc('users/part.userlist', $userlistParams)); ?>
</div>

View file

@ -1,25 +1,3 @@
<div id="controls">
<form id="newuser" autocomplete="off">
<input id="newusername" type="text"
placeholder="<?php p($l->t('Username'))?>"
autocomplete="off" autocapitalize="none" autocorrect="off" />
<input
type="password" id="newuserpassword"
placeholder="<?php p($l->t('Password'))?>"
autocomplete="off" autocapitalize="none" autocorrect="off" />
<input id="newemail" type="email" style="display:none"
placeholder="<?php p($l->t('E-Mail'))?>"
autocomplete="off" autocapitalize="none" autocorrect="off" />
<div class="groups"><div class="groupsListContainer multiselect button" data-placeholder="<?php p($l->t('Groups'))?>"><span class="title groupsList"></span><span class="icon-triangle-s"></span></div></div>
<input type="submit" class="button" value="<?php p($l->t('Create'))?>" />
</form>
<?php if((bool)$_['recoveryAdminEnabled']): ?>
<div class="recoveryPassword">
<input id="recoveryPassword"
type="password"
placeholder="<?php p($l->t('Admin Recovery Password'))?>"
title="<?php p($l->t('Enter the recovery password in order to recover the users files during password change'))?>"
alt="<?php p($l->t('Enter the recovery password in order to recover the users files during password change'))?>"/>
</div>
<?php endif; ?>
<div class="app-navigation-new">
<button type="button" id="new-user-button" class="icon-add"><?php p($l->t('Add user'))?></button>
</div>

View file

@ -1,90 +1,147 @@
<table id="userlist" class="grid" data-groups="<?php p($_['allGroups']);?>">
<thead>
<tr>
<th id="headerAvatar" scope="col"></th>
<th id="headerName" scope="col"><?php p($l->t('Username'))?></th>
<th id="headerDisplayName" scope="col"><?php p($l->t( 'Full name' )); ?></th>
<th id="headerPassword" scope="col"><?php p($l->t( 'Password' )); ?></th>
<th class="mailAddress" scope="col"><?php p($l->t( 'Email' )); ?></th>
<th id="headerGroups" scope="col"><?php p($l->t( 'Groups' )); ?></th>
<?php if(is_array($_['subadmins']) || $_['subadmins']): ?>
<th id="headerSubAdmins" scope="col"><?php p($l->t('Group admin for')); ?></th>
<?php endif;?>
<th id="headerQuota" scope="col"><?php p($l->t('Quota')); ?></th>
<th class="storageLocation" scope="col"><?php p($l->t('Storage location')); ?></th>
<th class="userBackend" scope="col"><?php p($l->t('User backend')); ?></th>
<th class="lastLogin" scope="col"><?php p($l->t('Last login')); ?></th>
<th class="userActions"></th>
</tr>
</thead>
<tbody>
<!-- the following <tr> is used as a template for the JS part -->
<tr style="display:none">
<td class="avatar"><div class="avatardiv"></div></td>
<th class="name" scope="row"></th>
<td class="displayName"><span></span> <img class="action"
src="<?php p(image_path('core', 'actions/rename.svg'))?>"
alt="<?php p($l->t("change full name"))?>" title="<?php p($l->t("change full name"))?>"/>
</td>
<td class="password"><span>●●●●●●●</span> <img class="action"
src="<?php print_unescaped(image_path('core', 'actions/rename.svg'))?>"
alt="<?php p($l->t("set new password"))?>" title="<?php p($l->t("set new password"))?>"/>
</td>
<td class="mailAddress"><span></span><div class="loading-small hidden"></div> <img class="action"
src="<?php p(image_path('core', 'actions/rename.svg'))?>"
alt="<?php p($l->t('change email address'))?>" title="<?php p($l->t('change email address'))?>"/>
</td>
<td class="groups"><div class="groupsListContainer multiselect button"
><span class="title groupsList"></span><span class="icon-triangle-s"></span></div>
</td>
<?php if(is_array($_['subadmins']) || $_['subadmins']): ?>
<td class="subadmins"><div class="groupsListContainer multiselect button"
><span class="title groupsList"></span><span class="icon-triangle-s"></span></div>
</td>
<?php endif;?>
<td class="quota">
<select class="quota-user" data-inputtitle="<?php p($l->t('Please enter storage quota (ex: "512 MB" or "12 GB")')) ?>">
<option value='default'>
<?php p($l->t('Default'));?>
</option>
<option value='none'>
<?php p($l->t('Unlimited'));?>
</option>
<?php foreach($_['quota_preset'] as $preset):?>
<option value='<?php p($preset);?>'>
<?php p($preset);?>
</option>
<?php endforeach;?>
<option value='other' data-new>
<?php p($l->t('Other'));?> ...
</option>
</select>
<progress class="quota-user-progress" value="" max="100"></progress>
</td>
<td class="storageLocation"></td>
<td class="userBackend"></td>
<td class="lastLogin"></td>
<td class="userActions">
<div class="toggleUserActions">
<a class="action"><span class="icon-more"></span></a>
<div class="popovermenu">
<ul class="userActionsMenu">
<li>
<a href="#" class="menuitem action-togglestate permanent" data-action="togglestate"></a>
</li>
<li>
<a href="#" class="menuitem action-remove permanent" data-action="remove">
<span class="icon icon-delete"></span>
<span><?php p($l->t('Delete')); ?></span>
</a>
</li>
</ul>
<form class="newUserMenu" id="newuser" autocomplete="off">
<table id="userlist" class="grid" data-groups="<?php p($_['allGroups']);?>">
<thead>
<tr>
<th id="headerAvatar" scope="col"></th>
<th id="headerName" scope="col"><?php p($l->t('Username'))?></th>
<th id="headerDisplayName" scope="col"><?php p($l->t( 'Full name' )); ?></th>
<th id="headerPassword" scope="col"><?php p($l->t( 'Password' )); ?></th>
<th class="mailAddress" scope="col"><?php p($l->t( 'Email' )); ?></th>
<th id="headerGroups" scope="col"><?php p($l->t( 'Groups' )); ?></th>
<?php if(is_array($_['subadmins']) || $_['subadmins']): ?>
<th id="headerSubAdmins" scope="col"><?php p($l->t('Group admin for')); ?></th>
<?php endif;?>
<?php if((bool)$_['recoveryAdminEnabled']): ?>
<th id="recoveryPassword" scope="col"><?php p($l->t('Recovery password')); ?></th>
<?php endif; ?>
<th id="headerQuota" scope="col"><?php p($l->t('Quota')); ?></th>
<th class="storageLocation" scope="col"><?php p($l->t('Storage location')); ?></th>
<th class="userBackend" scope="col"><?php p($l->t('User backend')); ?></th>
<th class="lastLogin" scope="col"><?php p($l->t('Last login')); ?></th>
<th class="userActions"></th>
</tr>
<tr id="newuserHeader" style="display:none">
<td class="icon-add"></td>
<td class="name">
<input id="newusername" type="text" required
placeholder="<?php p($l->t('Username'))?>" name="username"
autocomplete="off" autocapitalize="none" autocorrect="off" />
</td>
<td class="displayName">
<input id="newdisplayname" type="text"
placeholder="<?php p($l->t('Full name'))?>" name="displayname"
autocomplete="off" autocapitalize="none" autocorrect="off" />
</td>
<td class="password">
<input id="newuserpassword" type="password" required
placeholder="<?php p($l->t('Password'))?>" name="password"
autocomplete="new-password" autocapitalize="none" autocorrect="off" />
</td>
<td class="mailAddress">
<input id="newemail" type="email"
placeholder="<?php p($l->t('E-Mail'))?>" name="email"
autocomplete="off" autocapitalize="none" autocorrect="off" />
</td>
<td class="groups">
<div class="groupsListContainer multiselect button" data-placeholder="<?php p($l->t('Groups'))?>">
<span class="title groupsList"></span>
<span class="icon-triangle-s"></span>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</td>
<?php if(is_array($_['subadmins']) || $_['subadmins']): ?>
<td></td>
<?php endif;?>
<?php if((bool)$_['recoveryAdminEnabled']): ?>
<td class="recoveryPassword">
<input id="recoveryPassword"
type="password"
placeholder="<?php p($l->t('Admin Recovery Password'))?>"
title="<?php p($l->t('Enter the recovery password in order to recover the users files during password change'))?>"
alt="<?php p($l->t('Enter the recovery password in order to recover the users files during password change'))?>"/>
</td>
<?php endif; ?>
<td class="quota"></td>
<td class="storageLocation" scope="col"></td>
<td class="userBackend" scope="col"></td>
<td class="lastLogin" scope="col"></td>
<td class="userActions">
<input type="submit" id="newsubmit" class="button primary icon-checkmark-white has-tooltip" value="" title="<?php p($l->t('Add user'))?>" />
<input type="reset" id="newreset" class="button icon-close has-tooltip" value="" title="<?php p($l->t('Cancel'))?>" />
</td>
</tr>
</thead>
<tbody>
<!-- the following <tr> is used as a template for the JS part -->
<tr style="display:none">
<td class="avatar"><div class="avatardiv"></div></td>
<td class="name" scope="row"></td>
<td class="displayName"><span></span> <img class="action"
src="<?php p(image_path('core', 'actions/rename.svg'))?>"
alt="<?php p($l->t('change full name'))?>" title="<?php p($l->t('change full name'))?>"/>
</td>
<td class="password"><span>●●●●●●●</span> <img class="action"
src="<?php print_unescaped(image_path('core', 'actions/rename.svg'))?>"
alt="<?php p($l->t('set new password'))?>" title="<?php p($l->t('set new password'))?>"/>
</td>
<td class="mailAddress"><span></span><div class="loading-small hidden"></div> <img class="action"
src="<?php p(image_path('core', 'actions/rename.svg'))?>"
alt="<?php p($l->t('change email address'))?>" title="<?php p($l->t('change email address'))?>"/>
</td>
<td class="groups"><div class="groupsListContainer multiselect button"
><span class="title groupsList"></span><span class="icon-triangle-s"></span></div>
</td>
<?php if(is_array($_['subadmins']) || $_['subadmins']): ?>
<td class="subadmins"><div class="groupsListContainer multiselect button"
><span class="title groupsList"></span><span class="icon-triangle-s"></span></div>
</td>
<?php endif;?>
<?php if((bool)$_['recoveryAdminEnabled']): ?>
<td></td>
<?php endif; ?>
<td class="quota">
<select class="quota-user" data-inputtitle="<?php p($l->t('Please enter storage quota (ex: "512 MB" or "12 GB")')) ?>">
<option value='default'>
<?php p($l->t('Default'));?>
</option>
<option value='none'>
<?php p($l->t('Unlimited'));?>
</option>
<?php foreach($_['quota_preset'] as $preset):?>
<option value='<?php p($preset);?>'>
<?php p($preset);?>
</option>
<?php endforeach;?>
<option value='other' data-new>
<?php p($l->t('Other'));?> ...
</option>
</select>
<progress class="quota-user-progress" value="" max="100"></progress>
</td>
<td class="storageLocation"></td>
<td class="userBackend"></td>
<td class="lastLogin"></td>
<td class="userActions">
<div class="toggleUserActions">
<a class="action"><span class="icon-more"></span></a>
<div class="popovermenu">
<ul class="userActionsMenu">
<li>
<a href="#" class="menuitem action-togglestate permanent" data-action="togglestate"></a>
</li>
<li>
<a href="#" class="menuitem action-remove permanent" data-action="remove">
<span class="icon icon-delete"></span>
<span><?php p($l->t('Delete')); ?></span>
</a>
</li>
</ul>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</form>
<div class="emptycontent" style="display:none">
<div class="icon-search"></div>

View file

@ -27,6 +27,14 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
use ActorAware;
/**
* @return Locator
*/
public static function newUserForm() {
return Locator::forThe()->id("newuserHeader")->
describedAs("New user form in Users Settings");
}
/**
* @return Locator
*/
@ -43,6 +51,14 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
describedAs("Password field for new user in Users Settings");
}
/**
* @return Locator
*/
public static function newUserButton() {
return Locator::forThe()->id("new-user-button")->
describedAs("New user button in Users Settings");
}
/**
* @return Locator
*/
@ -55,7 +71,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function rowForUser($user) {
return Locator::forThe()->xpath("//table[@id = 'userlist']//th[normalize-space() = '$user']/..")->
return Locator::forThe()->xpath("//table[@id = 'userlist']//td[normalize-space() = '$user']/..")->
describedAs("Row for user $user in Users Settings");
}
@ -75,6 +91,13 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
describedAs("Password input for user $user in Users Settings");
}
/**
* @When I click the New user button
*/
public function iClickTheNewUserButton() {
$this->actor->find(self::newUserButton())->click();
}
/**
* @When I create user :user with password :password
*/
@ -99,4 +122,12 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
PHPUnit_Framework_Assert::assertNotNull($this->actor->find(self::rowForUser($user), 10));
}
/**
* @Then I see that the new user form is shown
*/
public function iSeeThatTheNewUserFormIsShown() {
PHPUnit_Framework_Assert::assertTrue(
$this->actor->find(self::newUserForm(), 10)->isVisible());
}
}

View file

@ -35,6 +35,8 @@ Feature: login
When I act as Jane
And I am logged in as the admin
And I open the User settings
And I click the New user button
And I see that the new user form is shown
And I create user unknownUser with password 123456acb
And I see that the list of users contains the user unknownUser
And I act as John