Move users management to multi line

Signed-off-by: Greta Doci <gretadoci@gmail.com>
This commit is contained in:
Greta Doci 2019-09-23 13:00:04 +02:00 committed by John Molakvoæ (skjnldsv)
parent c6e51924c8
commit c864bc8321
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF
24 changed files with 11698 additions and 425 deletions

View File

@ -524,7 +524,6 @@ td, th {
visibility: hidden; visibility: hidden;
} }
&.password, &.password,
&.displayName,
&.mailAddress { &.mailAddress {
min-width: 5em; min-width: 5em;
max-width: 12em; max-width: 12em;
@ -705,6 +704,7 @@ span.version {
#searchresults { #searchresults {
display: none; display: none;
} }
} }
#apps-list.store { #apps-list.store {
.section { .section {
@ -1351,8 +1351,8 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
/* USERS LIST -------------------------------------------------------------- */ /* USERS LIST -------------------------------------------------------------- */
#body-settings { #body-settings {
$grid-row-height: 46px; $grid-row-height: 60px;
$grid-col-min-width: 120px; $grid-col-min-width: 150px;
#app-content.user-list-grid { #app-content.user-list-grid {
display: grid; display: grid;
grid-auto-columns: 1fr; grid-auto-columns: 1fr;
@ -1376,7 +1376,6 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
/* grid col width */ /* grid col width */
.name, .name,
.displayName,
.password, .password,
.mailAddress, .mailAddress,
.languages, .languages,
@ -1384,12 +1383,17 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
.userBackend, .userBackend,
.lastLogin { .lastLogin {
min-width: $grid-col-min-width; min-width: $grid-col-min-width;
display: flex;
color: var(--color-text-dark);
vertical-align: baseline;
} }
.groups, .groups,
.subadmins, .subadmins,
.quota { .quota {
.multiselect { .multiselect {
min-width: $grid-col-min-width; min-width: $grid-col-min-width;
color: var(--color-text-dark);
vertical-align: baseline;
} }
} }
.obfuscated { .obfuscated {
@ -1399,6 +1403,10 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
.userActions { .userActions {
min-width: 44px; min-width: 44px;
} }
.subtitle {
color: var(--color-text-maxcontrast);
vertical-align: baseline;
}
/* various */ /* various */
&#grid-header, &#grid-header,
@ -1427,16 +1435,23 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
&#grid-header { &#grid-header {
color: var(--color-text-maxcontrast); color: var(--color-text-maxcontrast);
z-index: 60; /* above new-user */ z-index: 60; /* above new-user */
border-bottom-width: thin;
#headerDisplayName, #headerDisplayName,
#headerPassword, #headerPassword,
#headerAddress, #headerAddress,
#headerGroups, #headerGroups,
#headerSubAdmins, #headerSubAdmins,
#theHeaderUserBackend,
#theHeaderLastLogin,
#headerQuota, #headerQuota,
#theHeaderStorageLocation,
#headerLanguages { #headerLanguages {
/* Line up header text with column content for when theres inputs */ /* Line up header text with column content for when theres inputs */
padding-left: 7px; padding-left: 7px;
text-transform: none;
color: var(--color-text-maxcontrast);
vertical-align: baseline;
} }
} }
&:hover { &:hover {
@ -1451,8 +1466,7 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
> form { > form {
grid-row: 1; grid-row: 1;
display: inline-flex; display: inline-flex;
align-items: center; color: var(--color-text-lighter);
color: var(--color-text);
position: relative; position: relative;
> input:not(:focus):not(:active) { > input:not(:focus):not(:active) {
border-color: transparent; border-color: transparent;
@ -1478,7 +1492,7 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
} }
} }
&.name, &.name,
&.storageLocation { &.userBackend {
/* better multi-line visual */ /* better multi-line visual */
line-height: 1.3em; line-height: 1.3em;
max-height: 100%; max-height: 100%;
@ -1492,16 +1506,14 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
&.quota { &.quota {
.multiselect--active + progress { height: 44px;
display: none; display: flex;
} align-items: center;
justify-content: center;
progress { progress {
position: absolute; width: 100%;
width: calc(100% - 4px); /* minus left and right */ margin: 0 10px;
left: 2px;
bottom: 2px;
height: 3px; height: 3px;
z-index: 5; /* above multiselect */
} }
} }
.icon-confirm { .icon-confirm {
@ -1520,16 +1532,22 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
} }
} }
&.userActions { &.userActions {
.action-item {
position: absolute;
}
#newsubmit { #newsubmit {
width: 100%; width: 100%;
} }
.toggleUserActions { .toggleUserActions {
position: relative; position: relative;
display: block;
align-items: center;
.icon-more { .icon-more {
width: 44px; width: 44px;
height: 44px; height: 44px;
opacity: .5; opacity: .5;
cursor: pointer; cursor: pointer;
margin-left: 40px;
&:hover { &:hover {
opacity: .7; opacity: .7;
} }

3282
apps/settings/js/vue-1.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

4210
apps/settings/js/vue-2.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

3418
apps/settings/js/vue-3.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -29,7 +29,9 @@
<button v-if="showUpdateAll" <button v-if="showUpdateAll"
id="app-list-update-all" id="app-list-update-all"
class="primary" class="primary"
@click="updateAll">{{t('settings', 'Update all')}}</button> @click="updateAll">
{{ t('settings', 'Update all') }}
</button>
</div> </div>
<transition-group name="app-list" tag="div" class="apps-list-container"> <transition-group name="app-list" tag="div" class="apps-list-container">
<AppItem v-for="app in apps" <AppItem v-for="app in apps"

View File

@ -22,14 +22,17 @@
<template> <template>
<div id="app-content" class="user-list-grid" @scroll.passive="onScroll"> <div id="app-content" class="user-list-grid" @scroll.passive="onScroll">
<div id="grid-header" class="row" :class="{'sticky': scrolled && !showConfig.showNewUserForm}"> <div id="grid-header"
:class="{'sticky': scrolled && !showConfig.showNewUserForm}"
class="row">
<div id="headerAvatar" class="avatar" /> <div id="headerAvatar" class="avatar" />
<div id="headerName" class="name"> <div id="headerName" class="name">
{{ t('settings', 'Username') }} {{ t('settings', 'Username') }}
</div>
<div id="headerDisplayName" class="displayName"> <div class="subtitle">
{{ t('settings', 'Display name') }} {{ t('settings', 'Display name') }}
</div> </div>
</div>
<div id="headerPassword" class="password"> <div id="headerPassword" class="password">
{{ t('settings', 'Password') }} {{ t('settings', 'Password') }}
</div> </div>
@ -52,99 +55,103 @@
class="languages"> class="languages">
{{ t('settings', 'Language') }} {{ t('settings', 'Language') }}
</div> </div>
<div v-if="showConfig.showUserBackend || showConfig.showStoragePath"
class="headerUserBackend userBackend">
<div v-if="showConfig.showUserBackend" class="userBackend">
{{ t('settings', 'User backend') }}
</div>
<div v-if="showConfig.showStoragePath" <div v-if="showConfig.showStoragePath"
class="headerStorageLocation storageLocation"> class="subtitle storageLocation">
{{ t('settings', 'Storage location') }} {{ t('settings', 'Storage location') }}
</div> </div>
<div v-if="showConfig.showUserBackend"
class="headerUserBackend userBackend">
{{ t('settings', 'User backend') }}
</div> </div>
<div v-if="showConfig.showLastLogin" <div v-if="showConfig.showLastLogin"
class="headerLastLogin lastLogin"> class="headerLastLogin lastLogin">
{{ t('settings', 'Last login') }} {{ t('settings', 'Last login') }}
</div> </div>
<div class="userActions" /> <div class="userActions" />
</div> </div>
<form v-show="showConfig.showNewUserForm" <form v-show="showConfig.showNewUserForm"
id="new-user" id="new-user"
class="row"
:disabled="loading.all"
:class="{'sticky': scrolled && showConfig.showNewUserForm}" :class="{'sticky': scrolled && showConfig.showNewUserForm}"
:disabled="loading.all"
class="row"
@submit.prevent="createUser"> @submit.prevent="createUser">
<div :class="loading.all?'icon-loading-small':'icon-add'" /> <div :class="loading.all?'icon-loading-small':'icon-add'" />
<div class="name"> <div class="name">
<input id="newusername" <input id="newusername"
ref="newusername" ref="newusername"
v-model="newUser.id" v-model="newUser.id"
type="text" :disabled="settings.newUserGenerateUserID"
required
:placeholder="settings.newUserGenerateUserID :placeholder="settings.newUserGenerateUserID
? t('settings', 'Will be autogenerated') ? t('settings', 'Will be autogenerated')
: t('settings', 'Username')" : t('settings', 'Username')"
name="username"
autocomplete="off"
autocapitalize="none" autocapitalize="none"
autocomplete="off"
autocorrect="off" autocorrect="off"
name="username"
pattern="[a-zA-Z0-9 _\.@\-']+" pattern="[a-zA-Z0-9 _\.@\-']+"
:disabled="settings.newUserGenerateUserID"> required
type="text">
</div> </div>
<div class="displayName"> <div class="displayName">
<input id="newdisplayname" <input id="newdisplayname"
v-model="newUser.displayName" v-model="newUser.displayName"
type="text"
:placeholder="t('settings', 'Display name')" :placeholder="t('settings', 'Display name')"
name="displayname"
autocomplete="off"
autocapitalize="none" autocapitalize="none"
autocorrect="off"> autocomplete="off"
autocorrect="off"
name="displayname"
type="text">
</div> </div>
<div class="password"> <div class="password">
<input id="newuserpassword" <input id="newuserpassword"
ref="newuserpassword" ref="newuserpassword"
v-model="newUser.password" v-model="newUser.password"
type="password" :minlength="minPasswordLength"
:required="newUser.mailAddress===''"
:placeholder="t('settings', 'Password')" :placeholder="t('settings', 'Password')"
name="password" :required="newUser.mailAddress===''"
autocomplete="new-password"
autocapitalize="none" autocapitalize="none"
autocomplete="new-password"
autocorrect="off" autocorrect="off"
:minlength="minPasswordLength"> name="password"
type="password">
</div> </div>
<div class="mailAddress"> <div class="mailAddress">
<input id="newemail" <input id="newemail"
v-model="newUser.mailAddress" v-model="newUser.mailAddress"
type="email"
:required="newUser.password==='' || settings.newUserRequireEmail"
:placeholder="t('settings', 'Email')" :placeholder="t('settings', 'Email')"
name="email" :required="newUser.password==='' || settings.newUserRequireEmail"
autocomplete="off"
autocapitalize="none" autocapitalize="none"
autocorrect="off"> autocomplete="off"
autocorrect="off"
name="email"
type="email">
</div> </div>
<div class="groups"> <div class="groups">
<!-- hidden input trick for vanilla html5 form validation --> <!-- hidden input trick for vanilla html5 form validation -->
<input v-if="!settings.isAdmin" <input v-if="!settings.isAdmin"
id="newgroups" id="newgroups"
type="text" :class="{'icon-loading-small': loading.groups}"
:required="!settings.isAdmin"
:value="newUser.groups" :value="newUser.groups"
tabindex="-1" tabindex="-1"
:required="!settings.isAdmin" type="text">
:class="{'icon-loading-small': loading.groups}">
<Multiselect v-model="newUser.groups" <Multiselect v-model="newUser.groups"
:options="canAddGroups"
:disabled="loading.groups||loading.all"
tag-placeholder="create"
:placeholder="t('settings', 'Add user in group')"
label="name"
track-by="id"
class="multiselect-vue"
:multiple="true"
:taggable="true"
:close-on-select="false" :close-on-select="false"
:disabled="loading.groups||loading.all"
:multiple="true"
:options="canAddGroups"
:placeholder="t('settings', 'Add user in group')"
:tag-width="60" :tag-width="60"
:taggable="true"
class="multiselect-vue"
label="name"
tag-placeholder="create"
track-by="id"
@tag="createGroup"> @tag="createGroup">
<!-- If user is not admin, he is a subadmin. <!-- If user is not admin, he is a subadmin.
Subadmins can't create users outside their groups Subadmins can't create users outside their groups
@ -152,63 +159,64 @@
<span slot="noResult">{{ t('settings', 'No results') }}</span> <span slot="noResult">{{ t('settings', 'No results') }}</span>
</Multiselect> </Multiselect>
</div> </div>
<div v-if="subAdminsGroups.length>0 && settings.isAdmin" class="subadmins"> <div v-if="subAdminsGroups.length>0 && settings.isAdmin"
class="subadmins">
<Multiselect v-model="newUser.subAdminsGroups" <Multiselect v-model="newUser.subAdminsGroups"
:close-on-select="false"
:multiple="true"
:options="subAdminsGroups" :options="subAdminsGroups"
:placeholder="t('settings', 'Set user as admin for')" :placeholder="t('settings', 'Set user as admin for')"
label="name" :tag-width="60"
track-by="id"
class="multiselect-vue" class="multiselect-vue"
:multiple="true" label="name"
:close-on-select="false" track-by="id">
:tag-width="60">
<span slot="noResult">{{ t('settings', 'No results') }}</span> <span slot="noResult">{{ t('settings', 'No results') }}</span>
</Multiselect> </Multiselect>
</div> </div>
<div class="quota"> <div class="quota">
<Multiselect v-model="newUser.quota" <Multiselect v-model="newUser.quota"
:allow-empty="false"
:options="quotaOptions" :options="quotaOptions"
:placeholder="t('settings', 'Select user quota')" :placeholder="t('settings', 'Select user quota')"
:taggable="true"
class="multiselect-vue"
label="label" label="label"
track-by="id" track-by="id"
class="multiselect-vue"
:allow-empty="false"
:taggable="true"
@tag="validateQuota" /> @tag="validateQuota" />
</div> </div>
<div v-if="showConfig.showLanguages" class="languages"> <div v-if="showConfig.showLanguages" class="languages">
<Multiselect v-model="newUser.language" <Multiselect v-model="newUser.language"
:allow-empty="false"
:options="languages" :options="languages"
:placeholder="t('settings', 'Default language')" :placeholder="t('settings', 'Default language')"
label="name"
track-by="code"
class="multiselect-vue" class="multiselect-vue"
:allow-empty="false" group-label="label"
group-values="languages" group-values="languages"
group-label="label" /> label="name"
track-by="code" />
</div> </div>
<div v-if="showConfig.showStoragePath" class="storageLocation" /> <div v-if="showConfig.showStoragePath" class="storageLocation" />
<div v-if="showConfig.showUserBackend" class="userBackend" /> <div v-if="showConfig.showUserBackend" class="userBackend" />
<div v-if="showConfig.showLastLogin" class="lastLogin" /> <div v-if="showConfig.showLastLogin" class="lastLogin" />
<div class="userActions"> <div class="userActions">
<input id="newsubmit" <input id="newsubmit"
type="submit" :title="t('settings', 'Add a new user')"
class="button primary icon-checkmark-white has-tooltip" class="button primary icon-checkmark-white has-tooltip"
value="" type="submit"
:title="t('settings', 'Add a new user')"> value="">
</div> </div>
</form> </form>
<user-row v-for="(user, key) in filteredUsers" <user-row v-for="(user, key) in filteredUsers"
:key="key" :key="key"
:user="user" :external-actions="externalActions"
:groups="groups"
:languages="languages"
:quota-options="quotaOptions"
:settings="settings" :settings="settings"
:show-config="showConfig" :show-config="showConfig"
:groups="groups"
:sub-admins-groups="subAdminsGroups" :sub-admins-groups="subAdminsGroups"
:quota-options="quotaOptions" :user="user" />
:languages="languages"
:external-actions="externalActions" />
<InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler"> <InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
<div slot="spinner"> <div slot="spinner">
<div class="users-icon-loading icon-loading" /> <div class="users-icon-loading icon-loading" />
@ -328,7 +336,10 @@ export default {
}, },
quotaOptions() { quotaOptions() {
// convert the preset array into objects // convert the preset array into objects
let quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({ id: cur, label: cur }), []) let quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({
id: cur,
label: cur
}), [])
// add default presets // add default presets
quotaPreset.unshift(this.unlimitedQuota) quotaPreset.unshift(this.unlimitedQuota)
quotaPreset.unshift(this.defaultQuota) quotaPreset.unshift(this.defaultQuota)
@ -437,7 +448,9 @@ export default {
group: this.selectedGroup !== 'disabled' ? this.selectedGroup : '', group: this.selectedGroup !== 'disabled' ? this.selectedGroup : '',
search: this.searchQuery search: this.searchQuery
}) })
.then((response) => { response ? $state.loaded() : $state.complete() }) .then((response) => {
response ? $state.loaded() : $state.complete()
})
}, },
/* SEARCH */ /* SEARCH */

View File

@ -24,14 +24,15 @@
<template> <template>
<!-- Obfuscated user: Logged in user does not have permissions to see all of the data --> <!-- Obfuscated user: Logged in user does not have permissions to see all of the data -->
<div v-if="Object.keys(user).length ===1" class="row" :data-id="user.id"> <div v-if="Object.keys(user).length ===1" :data-id="user.id" class="row">
<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}"> <div :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}"
class="avatar">
<img v-if="!loading.delete && !loading.disable && !loading.wipe" <img v-if="!loading.delete && !loading.disable && !loading.wipe"
alt=""
width="32"
height="32"
:src="generateAvatar(user.id, 32)" :src="generateAvatar(user.id, 32)"
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"> :srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
alt=""
height="32"
width="32">
</div> </div>
<div class="name"> <div class="name">
{{ user.id }} {{ user.id }}
@ -42,163 +43,189 @@
</div> </div>
<!-- User full data --> <!-- User full data -->
<UserRowSimple
v-else-if="!editing"
:editing.sync="editing"
:feedback-message="feedbackMessage"
:groups="groups"
:languages="languages"
:loading="loading"
:opened-menu="openedMenu"
:settings="settings"
:show-config="showConfig"
:sub-admins-groups="subAdminsGroups"
:user-actions="userActions"
:user="user"
@hideMenu="hideMenu"
@toggleMenu="toggleMenu" />
<div v-else <div v-else
class="row"
:class="{'disabled': loading.delete || loading.disable}" :class="{'disabled': loading.delete || loading.disable}"
:data-id="user.id"> :data-id="user.id"
<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}"> class="row row--editable">
<div :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}"
class="avatar">
<img v-if="!loading.delete && !loading.disable && !loading.wipe" <img v-if="!loading.delete && !loading.disable && !loading.wipe"
alt=""
width="32"
height="32"
:src="generateAvatar(user.id, 32)" :src="generateAvatar(user.id, 32)"
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"> :srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
alt=""
height="32"
width="32">
</div> </div>
<!-- dirty hack to ellipsis on two lines --> <!-- dirty hack to ellipsis on two lines -->
<div class="name"> <div class="displayName">
{{ user.id }} <form
</div> :class="{'icon-loading-small': loading.displayName}"
<form class="displayName" :class="{'icon-loading-small': loading.displayName}" @submit.prevent="updateDisplayName"> class="displayName"
@submit.prevent="updateDisplayName">
<template v-if="user.backendCapabilities.setDisplayName"> <template v-if="user.backendCapabilities.setDisplayName">
<input v-if="user.backendCapabilities.setDisplayName" <input v-if="user.backendCapabilities.setDisplayName"
:id="'displayName'+user.id+rand" :id="'displayName'+user.id+rand"
ref="displayName" ref="displayName"
type="text"
:disabled="loading.displayName||loading.all" :disabled="loading.displayName||loading.all"
:value="user.displayname" :value="user.displayname"
autocapitalize="off"
autocomplete="new-password" autocomplete="new-password"
autocorrect="off" autocorrect="off"
autocapitalize="off" spellcheck="false"
spellcheck="false"> type="text">
<input v-if="user.backendCapabilities.setDisplayName" <input v-if="user.backendCapabilities.setDisplayName"
type="submit"
class="icon-confirm" class="icon-confirm"
type="submit"
value=""> value="">
</template> </template>
<div v-else v-tooltip.auto="t('settings', 'The backend does not support changing the display name')" class="name"> <div v-else
{{ user.displayname }} v-tooltip.auto="t('settings', 'The backend does not support changing the display name')"
</div> class="name" />
</form> </form>
</div>
<form v-if="settings.canChangePassword && user.backendCapabilities.setPassword" <form v-if="settings.canChangePassword && user.backendCapabilities.setPassword"
class="password"
:class="{'icon-loading-small': loading.password}" :class="{'icon-loading-small': loading.password}"
class="password"
@submit.prevent="updatePassword"> @submit.prevent="updatePassword">
<input :id="'password'+user.id+rand" <input :id="'password'+user.id+rand"
ref="password" ref="password"
type="password"
required
:disabled="loading.password || loading.all" :disabled="loading.password || loading.all"
:minlength="minPasswordLength" :minlength="minPasswordLength"
value="" :placeholder="t('settings', 'Add new password')"
:placeholder="t('settings', 'New password')" autocapitalize="off"
autocomplete="new-password" autocomplete="new-password"
autocorrect="off" autocorrect="off"
autocapitalize="off" required
spellcheck="false"> spellcheck="false"
<input type="submit" class="icon-confirm" value=""> type="password"
value="">
<input class="icon-confirm" type="submit" value="">
</form> </form>
<div v-else /> <div v-else />
<form class="mailAddress" :class="{'icon-loading-small': loading.mailAddress}" @submit.prevent="updateEmail"> <form :class="{'icon-loading-small': loading.mailAddress}"
class="mailAddress"
@submit.prevent="updateEmail">
<input :id="'mailAddress'+user.id+rand" <input :id="'mailAddress'+user.id+rand"
ref="mailAddress" ref="mailAddress"
type="email"
:disabled="loading.mailAddress||loading.all" :disabled="loading.mailAddress||loading.all"
:placeholder="t('settings', 'Add new email address')"
:value="user.email" :value="user.email"
autocapitalize="off"
autocomplete="new-password" autocomplete="new-password"
autocorrect="off" autocorrect="off"
autocapitalize="off" spellcheck="false"
spellcheck="false"> type="email">
<input type="submit" class="icon-confirm" value=""> <input class="icon-confirm" type="submit" value="">
</form> </form>
<div class="groups" :class="{'icon-loading-small': loading.groups}"> <div :class="{'icon-loading-small': loading.groups}" class="groups">
<Multiselect :value="userGroups" <Multiselect :close-on-select="false"
:options="availableGroups"
:disabled="loading.groups||loading.all" :disabled="loading.groups||loading.all"
tag-placeholder="create" :limit="2"
:multiple="true"
:options="availableGroups"
:placeholder="t('settings', 'Add user in group')" :placeholder="t('settings', 'Add user in group')"
label="name" :tag-width="60"
track-by="id"
class="multiselect-vue"
:limit="2"
:multiple="true"
:taggable="settings.isAdmin" :taggable="settings.isAdmin"
:close-on-select="false" :value="userGroups"
:tag-width="60" class="multiselect-vue"
@tag="createGroup" label="name"
tag-placeholder="create"
track-by="id"
@remove="removeUserGroup"
@select="addUserGroup" @select="addUserGroup"
@remove="removeUserGroup"> @tag="createGroup">
<span slot="limit" v-tooltip.auto="formatGroupsTitle(userGroups)" class="multiselect__limit">+{{ userGroups.length-2 }}</span>
<span slot="noResult">{{ t('settings', 'No results') }}</span> <span slot="noResult">{{ t('settings', 'No results') }}</span>
</Multiselect> </Multiselect>
</div> </div>
<div v-if="subAdminsGroups.length>0 && settings.isAdmin" class="subadmins" :class="{'icon-loading-small': loading.subadmins}"> <div v-if="subAdminsGroups.length>0 && settings.isAdmin"
<Multiselect :value="userSubAdminsGroups" :class="{'icon-loading-small': loading.subadmins}"
:options="subAdminsGroups" class="subadmins">
<Multiselect :close-on-select="false"
:disabled="loading.subadmins||loading.all" :disabled="loading.subadmins||loading.all"
:placeholder="t('settings', 'Set user as admin for')"
label="name"
track-by="id"
class="multiselect-vue"
:limit="2" :limit="2"
:multiple="true" :multiple="true"
:close-on-select="false" :options="subAdminsGroups"
:placeholder="t('settings', 'Set user as admin for')"
:tag-width="60" :tag-width="60"
@select="addUserSubAdmin" :value="userSubAdminsGroups"
@remove="removeUserSubAdmin"> class="multiselect-vue"
<span slot="limit" v-tooltip.auto="formatGroupsTitle(userSubAdminsGroups)" class="multiselect__limit">+{{ userSubAdminsGroups.length-2 }}</span> label="name"
track-by="id"
@remove="removeUserSubAdmin"
@select="addUserSubAdmin">
<span slot="noResult">{{ t('settings', 'No results') }}</span> <span slot="noResult">{{ t('settings', 'No results') }}</span>
</Multiselect> </Multiselect>
</div> </div>
<div v-tooltip.auto="usedSpace" class="quota" :class="{'icon-loading-small': loading.quota}"> <div v-tooltip.auto="usedSpace"
<Multiselect :value="userQuota" :class="{'icon-loading-small': loading.quota}"
:options="quotaOptions" class="quota">
<Multiselect :allow-empty="false"
:disabled="loading.quota||loading.all" :disabled="loading.quota||loading.all"
tag-placeholder="create" :options="quotaOptions"
:placeholder="t('settings', 'Select user quota')" :placeholder="t('settings', 'Select user quota')"
label="label"
track-by="id"
class="multiselect-vue"
:allow-empty="false"
:taggable="true" :taggable="true"
@tag="validateQuota" :value="userQuota"
@input="setUserQuota" /> class="multiselect-vue"
<progress class="quota-user-progress" label="label"
:class="{'warn':usedQuota>80}" tag-placeholder="create"
:value="usedQuota" track-by="id"
max="100" /> @input="setUserQuota"
@tag="validateQuota" />
</div> </div>
<div v-if="showConfig.showLanguages" <div v-if="showConfig.showLanguages"
class="languages" :class="{'icon-loading-small': loading.languages}"
:class="{'icon-loading-small': loading.languages}"> class="languages">
<Multiselect :value="userLanguage" <Multiselect :allow-empty="false"
:options="languages"
:disabled="loading.languages||loading.all" :disabled="loading.languages||loading.all"
:options="languages"
:placeholder="t('settings', 'No language set')" :placeholder="t('settings', 'No language set')"
:value="userLanguage"
class="multiselect-vue"
group-label="label"
group-values="languages"
label="name" label="name"
track-by="code" track-by="code"
class="multiselect-vue"
:allow-empty="false"
group-values="languages"
group-label="label"
@input="setUserLanguage" /> @input="setUserLanguage" />
</div> </div>
<div v-if="showConfig.showStoragePath" class="storageLocation">
{{ user.storageLocation }} <!-- don't show this on edit mode -->
</div> <div v-if="showConfig.showStoragePath || showConfig.showUserBackend"
<div v-if="showConfig.showUserBackend" class="userBackend"> class="storageLocation" />
{{ user.backend }} <div v-if="showConfig.showLastLogin" />
</div>
<div v-if="showConfig.showLastLogin" v-tooltip.auto="user.lastLogin>0 ? OC.Util.formatDate(user.lastLogin) : ''" class="lastLogin">
{{ user.lastLogin>0 ? OC.Util.relativeModifiedDate(user.lastLogin) : t('settings','Never') }}
</div>
<div class="userActions"> <div class="userActions">
<div v-if="OC.currentUser !== user.id && user.id !== 'admin' && !loading.all" class="toggleUserActions"> <div v-if="OC.currentUser !== user.id && user.id !== 'admin' && !loading.all"
<div v-click-outside="hideMenu" class="icon-more" @click="toggleMenu" /> class="toggleUserActions">
<div class="popovermenu" :class="{ 'open': openedMenu }"> <Actions>
<ActionButton icon="icon-checkmark"
@click="editing = false">
{{ t('settings', 'Done') }}
</ActionButton>
</Actions>
<div v-click-outside="hideMenu"
class="icon-more"
@click="toggleMenu" />
<div :class="{ 'open': openedMenu }" class="popovermenu">
<PopoverMenu :menu="userActions" /> <PopoverMenu :menu="userActions" />
</div> </div>
</div> </div>
<div class="feedback" :style="{opacity: feedbackMessage !== '' ? 1 : 0}"> <div :style="{opacity: feedbackMessage !== '' ? 1 : 0}"
class="feedback">
<div class="icon-checkmark" /> <div class="icon-checkmark" />
{{ feedbackMessage }} {{ feedbackMessage }}
</div> </div>
@ -210,19 +237,30 @@
import ClickOutside from 'vue-click-outside' import ClickOutside from 'vue-click-outside'
import Vue from 'vue' import Vue from 'vue'
import VTooltip from 'v-tooltip' import VTooltip from 'v-tooltip'
import { PopoverMenu, Multiselect } from 'nextcloud-vue' import {
PopoverMenu,
Multiselect,
Actions,
ActionButton
} from 'nextcloud-vue'
import UserRowSimple from './UserRowSimple'
import UserRowMixin from '../../mixins/UserRowMixin'
Vue.use(VTooltip) Vue.use(VTooltip)
export default { export default {
name: 'UserRow', name: 'UserRow',
components: { components: {
UserRowSimple,
PopoverMenu, PopoverMenu,
Actions,
ActionButton,
Multiselect Multiselect
}, },
directives: { directives: {
ClickOutside ClickOutside
}, },
mixins: [UserRowMixin],
props: { props: {
user: { user: {
type: Object, type: Object,
@ -262,6 +300,7 @@ export default {
rand: parseInt(Math.random() * 1000), rand: parseInt(Math.random() * 1000),
openedMenu: false, openedMenu: false,
feedbackMessage: '', feedbackMessage: '',
editing: false,
loading: { loading: {
all: false, all: false,
displayName: false, displayName: false,
@ -305,92 +344,9 @@ export default {
}) })
} }
return actions.concat(this.externalActions) return actions.concat(this.externalActions)
}
}, },
/* GROUPS MANAGEMENT */
userGroups() {
let userGroups = this.groups.filter(group => this.user.groups.includes(group.id))
return userGroups
},
userSubAdminsGroups() {
let userSubAdminsGroups = this.subAdminsGroups.filter(group => this.user.subadmin.includes(group.id))
return userSubAdminsGroups
},
availableGroups() {
return this.groups.map((group) => {
// clone object because we don't want
// to edit the original groups
let groupClone = Object.assign({}, group)
// two settings here:
// 1. user NOT in group but no permission to add
// 2. user is in group but no permission to remove
groupClone.$isDisabled
= (group.canAdd === false
&& !this.user.groups.includes(group.id))
|| (group.canRemove === false
&& this.user.groups.includes(group.id))
return groupClone
})
},
/* QUOTA MANAGEMENT */
usedSpace() {
if (this.user.quota.used) {
return t('settings', '{size} used', { size: OC.Util.humanFileSize(this.user.quota.used) })
}
return t('settings', '{size} used', { size: OC.Util.humanFileSize(0) })
},
usedQuota() {
let quota = this.user.quota.quota
if (quota > 0) {
quota = Math.min(100, Math.round(this.user.quota.used / quota * 100))
} else {
var usedInGB = this.user.quota.used / (10 * Math.pow(2, 30))
// asymptotic curve approaching 50% at 10GB to visualize used stace with infinite quota
quota = 95 * (1 - (1 / (usedInGB + 1)))
}
return isNaN(quota) ? 0 : quota
},
// Mapping saved values to objects
userQuota() {
if (this.user.quota.quota >= 0) {
// if value is valid, let's map the quotaOptions or return custom quota
let humanQuota = OC.Util.humanFileSize(this.user.quota.quota)
let userQuota = this.quotaOptions.find(quota => quota.id === humanQuota)
return userQuota || { id: humanQuota, label: humanQuota }
} else if (this.user.quota.quota === 'default') {
// default quota is replaced by the proper value on load
return this.quotaOptions[0]
}
return this.quotaOptions[1] // unlimited
},
/* PASSWORD POLICY? */
minPasswordLength() {
return this.$store.getters.getPasswordPolicyMinLength
},
/* LANGUAGE */
userLanguage() {
let availableLanguages = this.languages[0].languages.concat(this.languages[1].languages)
let userLang = availableLanguages.find(lang => lang.code === this.user.language)
if (typeof userLang !== 'object' && this.user.language !== '') {
return {
code: this.user.language,
name: this.user.language
}
} else if (this.user.language === '') {
return false
}
return userLang
}
},
mounted() {
// required if popup needs to stay opened after menu click
// since we only have disable/delete actions, let's close it directly
// this.popupItem = this.$el;
},
methods: { methods: {
/* MENU HANDLING */ /* MENU HANDLING */
toggleMenu() { toggleMenu() {
@ -400,35 +356,6 @@ export default {
this.openedMenu = false this.openedMenu = false
}, },
/**
* Generate avatar url
*
* @param {string} user The user name
* @param {int} size Size integer, default 32
* @returns {string}
*/
generateAvatar(user, size = 32) {
return OC.generateUrl(
'/avatar/{user}/{size}?v={version}',
{
user: user,
size: size,
version: oc_userconfig.avatar.version
}
)
},
/**
* Format array of groups objects to a string for the popup
*
* @param {array} groups The groups
* @returns {string}
*/
formatGroupsTitle(groups) {
let names = groups.map(group => group.name)
return names.slice(2).join(', ')
},
wipeUserDevices() { wipeUserDevices() {
let userid = this.user.id let userid = this.user.id
OC.dialogs.confirmDestructive( OC.dialogs.confirmDestructive(
@ -486,7 +413,10 @@ export default {
this.loading.all = true this.loading.all = true
let userid = this.user.id let userid = this.user.id
let enabled = !this.user.enabled let enabled = !this.user.enabled
return this.$store.dispatch('enableDisableUser', { userid, enabled }) return this.$store.dispatch('enableDisableUser', {
userid,
enabled
})
.then(() => { .then(() => {
this.loading.delete = false this.loading.delete = false
this.loading.all = false this.loading.all = false
@ -602,7 +532,10 @@ export default {
let gid = group.id let gid = group.id
try { try {
await this.$store.dispatch('removeUserGroup', { userid, gid }) await this.$store.dispatch('removeUserGroup', {
userid,
gid
})
this.loading.groups = false this.loading.groups = false
// remove user from current list if current list is the removed group // remove user from current list if current list is the removed group
if (this.$route.params.selectedGroup === gid) { if (this.$route.params.selectedGroup === gid) {
@ -624,7 +557,10 @@ export default {
let gid = group.id let gid = group.id
try { try {
await this.$store.dispatch('addUserSubAdmin', { userid, gid }) await this.$store.dispatch('addUserSubAdmin', {
userid,
gid
})
this.loading.subadmins = false this.loading.subadmins = false
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -642,7 +578,10 @@ export default {
let gid = group.id let gid = group.id
try { try {
await this.$store.dispatch('removeUserSubAdmin', { userid, gid }) await this.$store.dispatch('removeUserSubAdmin', {
userid,
gid
})
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally { } finally {

View File

@ -0,0 +1,159 @@
<template>
<div
class="row"
:class="{'disabled': loading.delete || loading.disable}"
:data-id="user.id">
<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}">
<img v-if="!loading.delete && !loading.disable && !loading.wipe"
alt=""
width="32"
height="32"
:src="generateAvatar(user.id, 32)"
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'">
</div>
<!-- dirty hack to ellipsis on two lines -->
<div class="name">
{{ user.id }}
<div class="displayName subtitle">
{{ user.displayname }}
</div>
</div>
<div />
<div class="mailAddress">
{{ user.email }}
</div>
<div class="groups">
{{ userGroupsLabels }}
</div>
<div v-if="subAdminsGroups.length > 0 && settings.isAdmin" class="subAdminsGroups">
{{ userSubAdminsGroupsLabels }}
</div>
<div v-tooltip.auto="usedSpace" class="quota">
<progress
class="quota-user-progress"
:class="{'warn': usedQuota > 80}"
:value="usedQuota"
max="100" />
</div>
<div v-if="showConfig.showLanguages" class="languages">
{{ userLanguage.name }}
</div>
<div v-if="showConfig.showUserBackend || showConfig.showStoragePath" class="userBackend">
<div v-if="showConfig.showUserBackend" class="userBackend">
{{ user.backend }}
</div>
<div v-if="showConfig.showStoragePath" class="storageLocation subtitle">
{{ user.storageLocation }}
</div>
</div>
<div v-if="showConfig.showLastLogin" v-tooltip.auto="userLastLoginTooltip" class="lastLogin">
{{ userLastLogin }}
</div>
<div class="userActions">
<div v-if="canEdit && !loading.all" class="toggleUserActions">
<Actions>
<ActionButton icon="icon-rename" @click="toggleEdit">
{{ t('settings', 'Edit User') }}
</ActionButton>
</Actions>
<div v-click-outside="hideMenu" class="icon-more" @click="$emit('toggleMenu')" />
<div class="popovermenu" :class="{ 'open': openedMenu }">
<PopoverMenu :menu="userActions" />
</div>
</div>
<div class="feedback" :style="{opacity: feedbackMessage !== '' ? 1 : 0}">
<div class="icon-checkmark" />
{{ feedbackMessage }}
</div>
</div>
</div>
</template>
<script>
import { PopoverMenu, Actions, ActionButton } from 'nextcloud-vue'
import ClickOutside from 'vue-click-outside'
import { getCurrentUser } from '@nextcloud/auth'
import UserRowMixin from '../../mixins/UserRowMixin'
export default {
name: 'UserRowSimple',
components: {
PopoverMenu,
ActionButton,
Actions
},
directives: {
ClickOutside
},
mixins: [UserRowMixin],
props: {
user: {
type: Object,
required: true
},
loading: {
type: Object,
required: true
},
showConfig: {
type: Object,
required: true
},
userActions: {
type: Array,
required: true
},
openedMenu: {
type: Boolean,
required: true
},
feedbackMessage: {
type: String,
required: true
},
subAdminsGroups: {
type: Array,
required: true
},
settings: {
type: Object,
required: true
}
},
computed: {
userGroupsLabels() {
return this.userGroups
.map(group => group.name)
.join(', ')
},
userSubAdminsGroupsLabels() {
return this.userSubAdminsGroups
.map(group => group.name)
.join(', ')
},
usedSpace() {
if (this.user.quota.used) {
return t('settings', '{size} used', { size: OC.Util.humanFileSize(this.user.quota.used) })
}
return t('settings', '{size} used', { size: OC.Util.humanFileSize(0) })
},
canEdit() {
return getCurrentUser().uid !== this.user.id && this.user.id !== 'admin'
}
},
methods: {
hideMenu() {
this.$emit('hideMenu')
},
toggleEdit() {
this.$emit('update:editing', true)
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,171 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
export default {
props: {
user: {
type: Object,
required: true
},
settings: {
type: Object,
default: () => ({})
},
groups: {
type: Array,
default: () => []
},
subAdminsGroups: {
type: Array,
default: () => []
},
quotaOptions: {
type: Array,
default: () => []
},
showConfig: {
type: Object,
default: () => ({})
},
languages: {
type: Array,
required: true
},
externalActions: {
type: Array,
default: () => []
}
},
computed: {
/* GROUPS MANAGEMENT */
userGroups() {
const userGroups = this.groups.filter(group => this.user.groups.includes(group.id))
return userGroups
},
userSubAdminsGroups() {
const userSubAdminsGroups = this.subAdminsGroups.filter(group => this.user.subadmin.includes(group.id))
return userSubAdminsGroups
},
availableGroups() {
return this.groups.map((group) => {
// clone object because we don't want
// to edit the original groups
let groupClone = Object.assign({}, group)
// two settings here:
// 1. user NOT in group but no permission to add
// 2. user is in group but no permission to remove
groupClone.$isDisabled
= (group.canAdd === false
&& !this.user.groups.includes(group.id))
|| (group.canRemove === false
&& this.user.groups.includes(group.id))
return groupClone
})
},
/* QUOTA MANAGEMENT */
usedSpace() {
if (this.user.quota.used) {
return t('settings', '{size} used', { size: OC.Util.humanFileSize(this.user.quota.used) })
}
return t('settings', '{size} used', { size: OC.Util.humanFileSize(0) })
},
usedQuota() {
let quota = this.user.quota.quota
if (quota > 0) {
quota = Math.min(100, Math.round(this.user.quota.used / quota * 100))
} else {
var usedInGB = this.user.quota.used / (10 * Math.pow(2, 30))
// asymptotic curve approaching 50% at 10GB to visualize used stace with infinite quota
quota = 95 * (1 - (1 / (usedInGB + 1)))
}
return isNaN(quota) ? 0 : quota
},
// Mapping saved values to objects
userQuota() {
if (this.user.quota.quota >= 0) {
// if value is valid, let's map the quotaOptions or return custom quota
let humanQuota = OC.Util.humanFileSize(this.user.quota.quota)
let userQuota = this.quotaOptions.find(quota => quota.id === humanQuota)
return userQuota || { id: humanQuota, label: humanQuota }
} else if (this.user.quota.quota === 'default') {
// default quota is replaced by the proper value on load
return this.quotaOptions[0]
}
return this.quotaOptions[1] // unlimited
},
/* PASSWORD POLICY? */
minPasswordLength() {
return this.$store.getters.getPasswordPolicyMinLength
},
/* LANGUAGE */
userLanguage() {
let availableLanguages = this.languages[0].languages.concat(this.languages[1].languages)
let userLang = availableLanguages.find(lang => lang.code === this.user.language)
if (typeof userLang !== 'object' && this.user.language !== '') {
return {
code: this.user.language,
name: this.user.language
}
} else if (this.user.language === '') {
return false
}
return userLang
},
/* LAST LOGIN */
userLastLoginTooltip() {
if (this.user.lastLogin > 0) {
return OC.Util.formatDate(this.user.lastLogin)
}
return ''
},
userLastLogin() {
if (this.user.lastLogin > 0) {
return OC.Util.relativeModifiedDate(this.user.lastLogin)
}
return t('settings', 'Never')
}
},
methods: {
/**
* Generate avatar url
*
* @param {string} user The user name
* @param {int} size Size integer, default 32
* @returns {string}
*/
generateAvatar(user, size = 32) {
return OC.generateUrl(
'/avatar/{user}/{size}?v={version}',
{
user: user,
size: size,
version: oc_userconfig.avatar.version
}
)
}
}
}

View File

@ -4,6 +4,7 @@
* *
* @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com) * @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
* @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> * @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
* @copyright Copyright (c) 2019, Greta Doci <gretadoci@gmail.com>
* *
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -80,7 +81,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
* @return Locator * @return Locator
*/ */
public static function rowForUser($user) { public static function rowForUser($user) {
return Locator::forThe()->xpath("//div[@id='app-content']/div/div[normalize-space() = '$user']/..")-> return Locator::forThe()->css("div.user-list-grid div.row[data-id=$user]")->
describedAs("Row for user $user in Users Settings"); describedAs("Row for user $user in Users Settings");
} }
@ -155,6 +156,23 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
describedAs("The selected option of the $cell select for the user $user in Users Settings"); describedAs("The selected option of the $cell select for the user $user in Users Settings");
} }
/**
* @return Locator
*/
public static function editModeToggle($user) {
return Locator::forThe()->css(".toggleUserActions button.icon-rename")->
descendantOf(self::rowForUser($user))->
describedAs("The edit toggle button for the user $user in Users Settings");
}
/**
* @return Locator
*/
public static function editModeOn($user) {
return Locator::forThe()->css("div.user-list-grid div.row.row--editable[data-id=$user]")->
describedAs("I see the edit mode is on for the user $user in Users Settings");
}
/** /**
* @When I click the New user button * @When I click the New user button
*/ */
@ -204,6 +222,13 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
$this->actor->find(self::createNewUserButton(), 10)->click(); $this->actor->find(self::createNewUserButton(), 10)->click();
} }
/**
* @When I toggle the edit mode for the user :user
*/
public function iToggleTheEditModeForUser($user) {
$this->actor->find(self::editModeToggle($user), 10)->click();
}
/** /**
* @When I create user :user with password :password * @When I create user :user with password :password
*/ */
@ -289,7 +314,8 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
* @Then I see that the display name for the user :user is :displayName * @Then I see that the display name for the user :user is :displayName
*/ */
public function iSeeThatTheDisplayNameForTheUserIs($user, $displayName) { public function iSeeThatTheDisplayNameForTheUserIs($user, $displayName) {
PHPUnit_Framework_Assert::assertEquals($displayName, $this->actor->find(self::displayNameCellForUser($user), 10)->getValue()); PHPUnit_Framework_Assert::assertEquals(
$displayName, $this->actor->find(self::displayNameCellForUser($user), 10)->getValue());
} }
/** /**
@ -308,5 +334,10 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
$this->actor->find(self::selectedSelectOption('quota', $user), 2)->getText(), $quota); $this->actor->find(self::selectedSelectOption('quota', $user), 2)->getText(), $quota);
} }
/**
* @Then I see that the edit mode is on for user :user
*/
public function iSeeThatTheEditModeIsOn($user) {
WaitFor::elementToBeEventuallyShown($this->actor, self::editModeOn($user));
}
} }

View File

@ -63,6 +63,8 @@ Feature: users
And I am logged in as the admin And I am logged in as the admin
And I open the User settings And I open the User settings
And I see that the list of users contains the user user0 And I see that the list of users contains the user user0
When I toggle the edit mode for the user user0
Then I see that the edit mode is on for user user0
# disabled because we need the TAB patch: # disabled because we need the TAB patch:
# https://github.com/minkphp/MinkSelenium2Driver/pull/244 # https://github.com/minkphp/MinkSelenium2Driver/pull/244
# When I assign the user user0 to the group admin # When I assign the user user0 to the group admin
@ -128,6 +130,8 @@ Feature: users
And I am logged in as the admin And I am logged in as the admin
And I open the User settings And I open the User settings
And I see that the list of users contains the user user0 And I see that the list of users contains the user user0
When I toggle the edit mode for the user user0
Then I see that the edit mode is on for user user0
And I see that the password of user0 is "" And I see that the password of user0 is ""
When I set the password for user0 to 123456 When I set the password for user0 to 123456
And I see that the password cell for user user0 is done loading And I see that the password cell for user user0 is done loading
@ -149,6 +153,8 @@ Feature: users
And I am logged in as the admin And I am logged in as the admin
And I open the User settings And I open the User settings
And I see that the list of users contains the user user0 And I see that the list of users contains the user user0
When I toggle the edit mode for the user user0
Then I see that the edit mode is on for user user0
And I see that the user quota of user0 is Unlimited And I see that the user quota of user0 is Unlimited
# disabled because we need the TAB patch: # disabled because we need the TAB patch:
# https://github.com/minkphp/MinkSelenium2Driver/pull/244 # https://github.com/minkphp/MinkSelenium2Driver/pull/244