Browse Source

Move users management to multi line

Signed-off-by: Greta Doci <gretadoci@gmail.com>
tags/v18.0.0beta3
Greta Doci 4 years ago
parent
commit
c864bc8321
No account linked to committer's email address

+ 33
- 15
apps/settings/css/settings.scss View File

@@ -524,7 +524,6 @@ td, th {
visibility: hidden;
}
&.password,
&.displayName,
&.mailAddress {
min-width: 5em;
max-width: 12em;
@@ -705,6 +704,7 @@ span.version {
#searchresults {
display: none;
}

}
#apps-list.store {
.section {
@@ -1351,8 +1351,8 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {

/* USERS LIST -------------------------------------------------------------- */
#body-settings {
$grid-row-height: 46px;
$grid-col-min-width: 120px;
$grid-row-height: 60px;
$grid-col-min-width: 150px;
#app-content.user-list-grid {
display: grid;
grid-auto-columns: 1fr;
@@ -1376,7 +1376,6 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {

/* grid col width */
.name,
.displayName,
.password,
.mailAddress,
.languages,
@@ -1384,12 +1383,17 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
.userBackend,
.lastLogin {
min-width: $grid-col-min-width;
display: flex;
color: var(--color-text-dark);
vertical-align: baseline;
}
.groups,
.subadmins,
.quota {
.multiselect {
min-width: $grid-col-min-width;
color: var(--color-text-dark);
vertical-align: baseline;
}
}
.obfuscated {
@@ -1399,6 +1403,10 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
.userActions {
min-width: 44px;
}
.subtitle {
color: var(--color-text-maxcontrast);
vertical-align: baseline;
}

/* various */
&#grid-header,
@@ -1427,16 +1435,23 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
&#grid-header {
color: var(--color-text-maxcontrast);
z-index: 60; /* above new-user */
border-bottom-width: thin;

#headerDisplayName,
#headerPassword,
#headerAddress,
#headerGroups,
#headerSubAdmins,
#theHeaderUserBackend,
#theHeaderLastLogin,
#headerQuota,
#theHeaderStorageLocation,
#headerLanguages {
/* Line up header text with column content for when there’s inputs */
padding-left: 7px;
text-transform: none;
color: var(--color-text-maxcontrast);
vertical-align: baseline;
}
}
&:hover {
@@ -1451,8 +1466,7 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
> form {
grid-row: 1;
display: inline-flex;
align-items: center;
color: var(--color-text);
color: var(--color-text-lighter);
position: relative;
> input:not(:focus):not(:active) {
border-color: transparent;
@@ -1478,7 +1492,7 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
}
}
&.name,
&.storageLocation {
&.userBackend {
/* better multi-line visual */
line-height: 1.3em;
max-height: 100%;
@@ -1492,16 +1506,14 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
-webkit-box-orient: vertical;
}
&.quota {
.multiselect--active + progress {
display: none;
}
height: 44px;
display: flex;
align-items: center;
justify-content: center;
progress {
position: absolute;
width: calc(100% - 4px); /* minus left and right */
left: 2px;
bottom: 2px;
width: 100%;
margin: 0 10px;
height: 3px;
z-index: 5; /* above multiselect */
}
}
.icon-confirm {
@@ -1520,16 +1532,22 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
}
}
&.userActions {
.action-item {
position: absolute;
}
#newsubmit {
width: 100%;
}
.toggleUserActions {
position: relative;
display: block;
align-items: center;
.icon-more {
width: 44px;
height: 44px;
opacity: .5;
cursor: pointer;
margin-left: 40px;
&:hover {
opacity: .7;
}

+ 3282
- 0
apps/settings/js/vue-1.js
File diff suppressed because it is too large
View File


+ 1
- 0
apps/settings/js/vue-1.js.map
File diff suppressed because it is too large
View File


+ 4210
- 0
apps/settings/js/vue-2.js
File diff suppressed because it is too large
View File


+ 1
- 0
apps/settings/js/vue-2.js.map
File diff suppressed because it is too large
View File


+ 3418
- 0
apps/settings/js/vue-3.js
File diff suppressed because it is too large
View File


+ 1
- 0
apps/settings/js/vue-3.js.map
File diff suppressed because it is too large
View File


+ 2
- 2
apps/settings/js/vue-4.js
File diff suppressed because it is too large
View File


+ 1
- 1
apps/settings/js/vue-4.js.map
File diff suppressed because it is too large
View File


+ 23
- 2
apps/settings/js/vue-6.js
File diff suppressed because it is too large
View File


+ 1
- 1
apps/settings/js/vue-6.js.map
File diff suppressed because it is too large
View File


+ 5
- 5
apps/settings/js/vue-settings-admin-security.js
File diff suppressed because it is too large
View File


+ 1
- 1
apps/settings/js/vue-settings-admin-security.js.map
File diff suppressed because it is too large
View File


+ 5
- 5
apps/settings/js/vue-settings-apps-users-management.js
File diff suppressed because it is too large
View File


+ 1
- 1
apps/settings/js/vue-settings-apps-users-management.js.map
File diff suppressed because it is too large
View File


+ 5
- 5
apps/settings/js/vue-settings-personal-security.js
File diff suppressed because it is too large
View File


+ 1
- 1
apps/settings/js/vue-settings-personal-security.js.map
File diff suppressed because it is too large
View File


+ 3
- 1
apps/settings/src/components/AppList.vue View File

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

+ 83
- 70
apps/settings/src/components/UserList.vue View File

@@ -22,13 +22,16 @@

<template>
<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="headerName" class="name">
{{ t('settings', 'Username') }}
</div>
<div id="headerDisplayName" class="displayName">
{{ t('settings', 'Display name') }}

<div class="subtitle">
{{ t('settings', 'Display name') }}
</div>
</div>
<div id="headerPassword" class="password">
{{ t('settings', 'Password') }}
@@ -52,99 +55,103 @@
class="languages">
{{ t('settings', 'Language') }}
</div>
<div v-if="showConfig.showStoragePath"
class="headerStorageLocation storageLocation">
{{ t('settings', 'Storage location') }}
</div>
<div v-if="showConfig.showUserBackend"

<div v-if="showConfig.showUserBackend || showConfig.showStoragePath"
class="headerUserBackend userBackend">
{{ t('settings', 'User backend') }}
<div v-if="showConfig.showUserBackend" class="userBackend">
{{ t('settings', 'User backend') }}
</div>
<div v-if="showConfig.showStoragePath"
class="subtitle storageLocation">
{{ t('settings', 'Storage location') }}
</div>
</div>
<div v-if="showConfig.showLastLogin"
class="headerLastLogin lastLogin">
{{ t('settings', 'Last login') }}
</div>

<div class="userActions" />
</div>

<form v-show="showConfig.showNewUserForm"
id="new-user"
class="row"
:disabled="loading.all"
:class="{'sticky': scrolled && showConfig.showNewUserForm}"
:disabled="loading.all"
class="row"
@submit.prevent="createUser">
<div :class="loading.all?'icon-loading-small':'icon-add'" />
<div class="name">
<input id="newusername"
ref="newusername"
v-model="newUser.id"
type="text"
required
:disabled="settings.newUserGenerateUserID"
:placeholder="settings.newUserGenerateUserID
? t('settings', 'Will be autogenerated')
: t('settings', 'Username')"
name="username"
autocomplete="off"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
name="username"
pattern="[a-zA-Z0-9 _\.@\-']+"
:disabled="settings.newUserGenerateUserID">
required
type="text">
</div>
<div class="displayName">
<input id="newdisplayname"
v-model="newUser.displayName"
type="text"
:placeholder="t('settings', 'Display name')"
name="displayname"
autocomplete="off"
autocapitalize="none"
autocorrect="off">
autocomplete="off"
autocorrect="off"
name="displayname"
type="text">
</div>
<div class="password">
<input id="newuserpassword"
ref="newuserpassword"
v-model="newUser.password"
type="password"
:required="newUser.mailAddress===''"
:minlength="minPasswordLength"
:placeholder="t('settings', 'Password')"
name="password"
autocomplete="new-password"
:required="newUser.mailAddress===''"
autocapitalize="none"
autocomplete="new-password"
autocorrect="off"
:minlength="minPasswordLength">
name="password"
type="password">
</div>
<div class="mailAddress">
<input id="newemail"
v-model="newUser.mailAddress"
type="email"
:required="newUser.password==='' || settings.newUserRequireEmail"
:placeholder="t('settings', 'Email')"
name="email"
autocomplete="off"
:required="newUser.password==='' || settings.newUserRequireEmail"
autocapitalize="none"
autocorrect="off">
autocomplete="off"
autocorrect="off"
name="email"
type="email">
</div>
<div class="groups">
<!-- hidden input trick for vanilla html5 form validation -->
<input v-if="!settings.isAdmin"
id="newgroups"
type="text"
:class="{'icon-loading-small': loading.groups}"
:required="!settings.isAdmin"
:value="newUser.groups"
tabindex="-1"
:required="!settings.isAdmin"
:class="{'icon-loading-small': loading.groups}">
type="text">
<Multiselect v-model="newUser.groups"
:options="canAddGroups"
:close-on-select="false"
:disabled="loading.groups||loading.all"
tag-placeholder="create"
:multiple="true"
:options="canAddGroups"
:placeholder="t('settings', 'Add user in group')"
:tag-width="60"
:taggable="true"
class="multiselect-vue"
label="name"
tag-placeholder="create"
track-by="id"
class="multiselect-vue"
:multiple="true"
:taggable="true"
:close-on-select="false"
:tag-width="60"
@tag="createGroup">
<!-- If user is not admin, he is a subadmin.
Subadmins can't create users outside their groups
@@ -152,63 +159,64 @@
<span slot="noResult">{{ t('settings', 'No results') }}</span>
</Multiselect>
</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"
:close-on-select="false"
:multiple="true"
:options="subAdminsGroups"
:placeholder="t('settings', 'Set user as admin for')"
label="name"
track-by="id"
:tag-width="60"
class="multiselect-vue"
:multiple="true"
:close-on-select="false"
:tag-width="60">
label="name"
track-by="id">
<span slot="noResult">{{ t('settings', 'No results') }}</span>
</Multiselect>
</div>
<div class="quota">
<Multiselect v-model="newUser.quota"
:allow-empty="false"
:options="quotaOptions"
:placeholder="t('settings', 'Select user quota')"
:taggable="true"
class="multiselect-vue"
label="label"
track-by="id"
class="multiselect-vue"
:allow-empty="false"
:taggable="true"
@tag="validateQuota" />
</div>
<div v-if="showConfig.showLanguages" class="languages">
<Multiselect v-model="newUser.language"
:allow-empty="false"
:options="languages"
:placeholder="t('settings', 'Default language')"
label="name"
track-by="code"
class="multiselect-vue"
:allow-empty="false"
group-label="label"
group-values="languages"
group-label="label" />
label="name"
track-by="code" />
</div>
<div v-if="showConfig.showStoragePath" class="storageLocation" />
<div v-if="showConfig.showUserBackend" class="userBackend" />
<div v-if="showConfig.showLastLogin" class="lastLogin" />
<div class="userActions">
<input id="newsubmit"
type="submit"
:title="t('settings', 'Add a new user')"
class="button primary icon-checkmark-white has-tooltip"
value=""
:title="t('settings', 'Add a new user')">
type="submit"
value="">
</div>
</form>

<user-row v-for="(user, key) in filteredUsers"
:key="key"
:user="user"
:external-actions="externalActions"
:groups="groups"
:languages="languages"
:quota-options="quotaOptions"
:settings="settings"
:show-config="showConfig"
:groups="groups"
:sub-admins-groups="subAdminsGroups"
:quota-options="quotaOptions"
:languages="languages"
:external-actions="externalActions" />
:user="user" />
<InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
<div slot="spinner">
<div class="users-icon-loading icon-loading" />
@@ -328,7 +336,10 @@ export default {
},
quotaOptions() {
// 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
quotaPreset.unshift(this.unlimitedQuota)
quotaPreset.unshift(this.defaultQuota)
@@ -377,9 +388,9 @@ export default {
// deleting the last user, reset the list
if (val === 0 && old === 1) {
this.$refs.infiniteLoading.stateChanger.reset()
// adding the first user, warn the infiniteLoader that
// the list is not empty anymore (we don't fetch the newly
// added user as we already have all the info we need)
// adding the first user, warn the infiniteLoader that
// the list is not empty anymore (we don't fetch the newly
// added user as we already have all the info we need)
} else if (val === 1 && old === 0) {
this.$refs.infiniteLoading.stateChanger.loaded()
}
@@ -437,7 +448,9 @@ export default {
group: this.selectedGroup !== 'disabled' ? this.selectedGroup : '',
search: this.searchQuery
})
.then((response) => { response ? $state.loaded() : $state.complete() })
.then((response) => {
response ? $state.loaded() : $state.complete()
})
},

/* SEARCH */
@@ -492,10 +505,10 @@ export default {
if (error.response && error.response.data && error.response.data.ocs && error.response.data.ocs.meta) {
const statuscode = error.response.data.ocs.meta.statuscode
if (statuscode === 102) {
// wrong username
// wrong username
this.$refs.newusername.focus()
} else if (statuscode === 107) {
// wrong password
// wrong password
this.$refs.newuserpassword.focus()
}
}
@@ -542,7 +555,7 @@ export default {
redirectIfDisabled() {
const allGroups = this.$store.getters.getGroups
if (this.selectedGroup === 'disabled'
&& allGroups.findIndex(group => group.id === 'disabled' && group.usercount === 0) > -1) {
&& allGroups.findIndex(group => group.id === 'disabled' && group.usercount === 0) > -1) {
// disabled group is empty, redirection to all users
this.$router.push({ name: 'users' })
this.$refs.infiniteLoading.stateChanger.reset()

+ 212
- 273
apps/settings/src/components/UserList/UserRow.vue View File

@@ -24,14 +24,15 @@

<template>
<!-- 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 class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}">
<div v-if="Object.keys(user).length ===1" :data-id="user.id" class="row">
<div :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}"
class="avatar">
<img v-if="!loading.delete && !loading.disable && !loading.wipe"
:src="generateAvatar(user.id, 32)"
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
alt=""
width="32"
height="32"
:src="generateAvatar(user.id, 32)"
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'">
width="32">
</div>
<div class="name">
{{ user.id }}
@@ -42,163 +43,189 @@
</div>

<!-- 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
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}">
:data-id="user.id"
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"
:src="generateAvatar(user.id, 32)"
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
alt=""
width="32"
height="32"
:src="generateAvatar(user.id, 32)"
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'">
width="32">
</div>
<!-- dirty hack to ellipsis on two lines -->
<div class="name">
{{ user.id }}
<div class="displayName">
<form
:class="{'icon-loading-small': loading.displayName}"
class="displayName"
@submit.prevent="updateDisplayName">
<template v-if="user.backendCapabilities.setDisplayName">
<input v-if="user.backendCapabilities.setDisplayName"
:id="'displayName'+user.id+rand"
ref="displayName"
:disabled="loading.displayName||loading.all"
:value="user.displayname"
autocapitalize="off"
autocomplete="new-password"
autocorrect="off"
spellcheck="false"
type="text">
<input v-if="user.backendCapabilities.setDisplayName"
class="icon-confirm"
type="submit"
value="">
</template>
<div v-else
v-tooltip.auto="t('settings', 'The backend does not support changing the display name')"
class="name" />
</form>
</div>
<form class="displayName" :class="{'icon-loading-small': loading.displayName}" @submit.prevent="updateDisplayName">
<template v-if="user.backendCapabilities.setDisplayName">
<input v-if="user.backendCapabilities.setDisplayName"
:id="'displayName'+user.id+rand"
ref="displayName"
type="text"
:disabled="loading.displayName||loading.all"
:value="user.displayname"
autocomplete="new-password"
autocorrect="off"
autocapitalize="off"
spellcheck="false">
<input v-if="user.backendCapabilities.setDisplayName"
type="submit"
class="icon-confirm"
value="">
</template>
<div v-else v-tooltip.auto="t('settings', 'The backend does not support changing the display name')" class="name">
{{ user.displayname }}
</div>
</form>
<form v-if="settings.canChangePassword && user.backendCapabilities.setPassword"
class="password"
:class="{'icon-loading-small': loading.password}"
class="password"
@submit.prevent="updatePassword">
<input :id="'password'+user.id+rand"
ref="password"
type="password"
required
:disabled="loading.password||loading.all"
:disabled="loading.password || loading.all"
:minlength="minPasswordLength"
value=""
:placeholder="t('settings', 'New password')"
:placeholder="t('settings', 'Add new password')"
autocapitalize="off"
autocomplete="new-password"
autocorrect="off"
autocapitalize="off"
spellcheck="false">
<input type="submit" class="icon-confirm" value="">
required
spellcheck="false"
type="password"
value="">
<input class="icon-confirm" type="submit" value="">
</form>
<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"
ref="mailAddress"
type="email"
:disabled="loading.mailAddress||loading.all"
:placeholder="t('settings', 'Add new email address')"
:value="user.email"
autocapitalize="off"
autocomplete="new-password"
autocorrect="off"
autocapitalize="off"
spellcheck="false">
<input type="submit" class="icon-confirm" value="">
spellcheck="false"
type="email">
<input class="icon-confirm" type="submit" value="">
</form>
<div class="groups" :class="{'icon-loading-small': loading.groups}">
<Multiselect :value="userGroups"
:options="availableGroups"
<div :class="{'icon-loading-small': loading.groups}" class="groups">
<Multiselect :close-on-select="false"
:disabled="loading.groups||loading.all"
tag-placeholder="create"
:placeholder="t('settings', 'Add user in group')"
label="name"
track-by="id"
class="multiselect-vue"
:limit="2"
:multiple="true"
:taggable="settings.isAdmin"
:close-on-select="false"
:options="availableGroups"
:placeholder="t('settings', 'Add user in group')"
:tag-width="60"
@tag="createGroup"
:taggable="settings.isAdmin"
:value="userGroups"
class="multiselect-vue"
label="name"
tag-placeholder="create"
track-by="id"
@remove="removeUserGroup"
@select="addUserGroup"
@remove="removeUserGroup">
<span slot="limit" v-tooltip.auto="formatGroupsTitle(userGroups)" class="multiselect__limit">+{{ userGroups.length-2 }}</span>
@tag="createGroup">
<span slot="noResult">{{ t('settings', 'No results') }}</span>
</Multiselect>
</div>
<div v-if="subAdminsGroups.length>0 && settings.isAdmin" class="subadmins" :class="{'icon-loading-small': loading.subadmins}">
<Multiselect :value="userSubAdminsGroups"
:options="subAdminsGroups"
<div v-if="subAdminsGroups.length>0 && settings.isAdmin"
:class="{'icon-loading-small': loading.subadmins}"
class="subadmins">
<Multiselect :close-on-select="false"
:disabled="loading.subadmins||loading.all"
:placeholder="t('settings', 'Set user as admin for')"
label="name"
track-by="id"
class="multiselect-vue"
:limit="2"
:multiple="true"
:close-on-select="false"
:options="subAdminsGroups"
:placeholder="t('settings', 'Set user as admin for')"
:tag-width="60"
@select="addUserSubAdmin"
@remove="removeUserSubAdmin">
<span slot="limit" v-tooltip.auto="formatGroupsTitle(userSubAdminsGroups)" class="multiselect__limit">+{{ userSubAdminsGroups.length-2 }}</span>
:value="userSubAdminsGroups"
class="multiselect-vue"
label="name"
track-by="id"
@remove="removeUserSubAdmin"
@select="addUserSubAdmin">
<span slot="noResult">{{ t('settings', 'No results') }}</span>
</Multiselect>
</div>
<div v-tooltip.auto="usedSpace" class="quota" :class="{'icon-loading-small': loading.quota}">
<Multiselect :value="userQuota"
:options="quotaOptions"
<div v-tooltip.auto="usedSpace"
:class="{'icon-loading-small': loading.quota}"
class="quota">
<Multiselect :allow-empty="false"
:disabled="loading.quota||loading.all"
tag-placeholder="create"
:options="quotaOptions"
:placeholder="t('settings', 'Select user quota')"
:taggable="true"
:value="userQuota"
class="multiselect-vue"
label="label"
tag-placeholder="create"
track-by="id"
class="multiselect-vue"
:allow-empty="false"
:taggable="true"
@tag="validateQuota"
@input="setUserQuota" />
<progress class="quota-user-progress"
:class="{'warn':usedQuota>80}"
:value="usedQuota"
max="100" />
@input="setUserQuota"
@tag="validateQuota" />
</div>
<div v-if="showConfig.showLanguages"
class="languages"
:class="{'icon-loading-small': loading.languages}">
<Multiselect :value="userLanguage"
:options="languages"
:class="{'icon-loading-small': loading.languages}"
class="languages">
<Multiselect :allow-empty="false"
:disabled="loading.languages||loading.all"
:options="languages"
:placeholder="t('settings', 'No language set')"
label="name"
track-by="code"
:value="userLanguage"
class="multiselect-vue"
:allow-empty="false"
group-values="languages"
group-label="label"
group-values="languages"
label="name"
track-by="code"
@input="setUserLanguage" />
</div>
<div v-if="showConfig.showStoragePath" class="storageLocation">
{{ user.storageLocation }}
</div>
<div v-if="showConfig.showUserBackend" class="userBackend">
{{ user.backend }}
</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>

<!-- don't show this on edit mode -->
<div v-if="showConfig.showStoragePath || showConfig.showUserBackend"
class="storageLocation" />
<div v-if="showConfig.showLastLogin" />

<div class="userActions">
<div v-if="OC.currentUser !== user.id && user.id !== 'admin' && !loading.all" class="toggleUserActions">
<div v-click-outside="hideMenu" class="icon-more" @click="toggleMenu" />
<div class="popovermenu" :class="{ 'open': openedMenu }">
<div v-if="OC.currentUser !== user.id && user.id !== 'admin' && !loading.all"
class="toggleUserActions">
<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" />
</div>
</div>
<div class="feedback" :style="{opacity: feedbackMessage !== '' ? 1 : 0}">
<div :style="{opacity: feedbackMessage !== '' ? 1 : 0}"
class="feedback">
<div class="icon-checkmark" />
{{ feedbackMessage }}
</div>
@@ -210,19 +237,30 @@
import ClickOutside from 'vue-click-outside'
import Vue from 'vue'
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)

export default {
name: 'UserRow',
components: {
UserRowSimple,
PopoverMenu,
Actions,
ActionButton,
Multiselect
},
directives: {
ClickOutside
},
mixins: [UserRowMixin],
props: {
user: {
type: Object,
@@ -262,6 +300,7 @@ export default {
rand: parseInt(Math.random() * 1000),
openedMenu: false,
feedbackMessage: '',
editing: false,
loading: {
all: false,
displayName: false,
@@ -305,92 +344,9 @@ export default {
})
}
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: {
/* MENU HANDLING */
toggleMenu() {
@@ -400,35 +356,6 @@ export default {
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() {
let userid = this.user.id
OC.dialogs.confirmDestructive(
@@ -486,7 +413,10 @@ export default {
this.loading.all = true
let userid = this.user.id
let enabled = !this.user.enabled
return this.$store.dispatch('enableDisableUser', { userid, enabled })
return this.$store.dispatch('enableDisableUser', {
userid,
enabled
})
.then(() => {
this.loading.delete = false
this.loading.all = false
@@ -494,10 +424,10 @@ export default {
},

/**
* Set user displayName
*
* @param {string} displayName The display name
*/
* Set user displayName
*
* @param {string} displayName The display name
*/
updateDisplayName() {
let displayName = this.$refs.displayName.value
this.loading.displayName = true
@@ -512,10 +442,10 @@ export default {
},

/**
* Set user password
*
* @param {string} password The email adress
*/
* Set user password
*
* @param {string} password The email adress
*/
updatePassword() {
let password = this.$refs.password.value
this.loading.password = true
@@ -530,10 +460,10 @@ export default {
},

/**
* Set user mailAddress
*
* @param {string} mailAddress The email adress
*/
* Set user mailAddress
*
* @param {string} mailAddress The email adress
*/
updateEmail() {
let mailAddress = this.$refs.mailAddress.value
this.loading.mailAddress = true
@@ -548,10 +478,10 @@ export default {
},

/**
* Create a new group and add user to it
*
* @param {string} gid Group id
*/
* Create a new group and add user to it
*
* @param {string} gid Group id
*/
async createGroup(gid) {
this.loading = { groups: true, subadmins: true }
try {
@@ -567,10 +497,10 @@ export default {
},

/**
* Add user to group
*
* @param {object} group Group object
*/
* Add user to group
*
* @param {object} group Group object
*/
async addUserGroup(group) {
if (group.canAdd === false) {
return false
@@ -588,10 +518,10 @@ export default {
},

/**
* Remove user from group
*
* @param {object} group Group object
*/
* Remove user from group
*
* @param {object} group Group object
*/
async removeUserGroup(group) {
if (group.canRemove === false) {
return false
@@ -602,7 +532,10 @@ export default {
let gid = group.id

try {
await this.$store.dispatch('removeUserGroup', { userid, gid })
await this.$store.dispatch('removeUserGroup', {
userid,
gid
})
this.loading.groups = false
// remove user from current list if current list is the removed group
if (this.$route.params.selectedGroup === gid) {
@@ -614,17 +547,20 @@ export default {
},

/**
* Add user to group
*
* @param {object} group Group object
*/
* Add user to group
*
* @param {object} group Group object
*/
async addUserSubAdmin(group) {
this.loading.subadmins = true
let userid = this.user.id
let gid = group.id

try {
await this.$store.dispatch('addUserSubAdmin', { userid, gid })
await this.$store.dispatch('addUserSubAdmin', {
userid,
gid
})
this.loading.subadmins = false
} catch (error) {
console.error(error)
@@ -632,17 +568,20 @@ export default {
},

/**
* Remove user from group
*
* @param {object} group Group object
*/
* Remove user from group
*
* @param {object} group Group object
*/
async removeUserSubAdmin(group) {
this.loading.subadmins = true
let userid = this.user.id
let gid = group.id

try {
await this.$store.dispatch('removeUserSubAdmin', { userid, gid })
await this.$store.dispatch('removeUserSubAdmin', {
userid,
gid
})
} catch (error) {
console.error(error)
} finally {
@@ -651,11 +590,11 @@ export default {
},

/**
* Dispatch quota set request
*
* @param {string|Object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
* @returns {string}
*/
* Dispatch quota set request
*
* @param {string|Object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
* @returns {string}
*/
async setUserQuota(quota = 'none') {
this.loading.quota = true
// ensure we only send the preset id
@@ -676,11 +615,11 @@ export default {
},

/**
* Validate quota string to make sure it's a valid human file size
*
* @param {string} quota Quota in readable format '5 GB'
* @returns {Promise|boolean}
*/
* Validate quota string to make sure it's a valid human file size
*
* @param {string} quota Quota in readable format '5 GB'
* @returns {Promise|boolean}
*/
validateQuota(quota) {
// only used for new presets sent through @Tag
let validQuota = OC.Util.computerFileSize(quota)
@@ -693,11 +632,11 @@ export default {
},

/**
* Dispatch language set request
*
* @param {Object} lang language object {code:'en', name:'English'}
* @returns {Object}
*/
* Dispatch language set request
*
* @param {Object} lang language object {code:'en', name:'English'}
* @returns {Object}
*/
async setUserLanguage(lang) {
this.loading.languages = true
// ensure we only send the preset id
@@ -716,8 +655,8 @@ export default {
},

/**
* Dispatch new welcome mail request
*/
* Dispatch new welcome mail request
*/
sendWelcomeMail() {
this.loading.all = true
this.$store.dispatch('sendWelcomeMail', this.user.id)

+ 159
- 0
apps/settings/src/components/UserList/UserRowSimple.vue 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>

+ 171
- 0
apps/settings/src/mixins/UserRowMixin.js 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
}
)
}
}
}

+ 61
- 30
tests/acceptance/features/bootstrap/UsersSettingsContext.php View File

@@ -1,9 +1,10 @@
<?php

/**
*
*
* @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) 2019, Greta Doci <gretadoci@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@@ -33,7 +34,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public static function newUserForm() {
return Locator::forThe()->id("new-user")->
describedAs("New user form in Users Settings");
describedAs("New user form in Users Settings");
}

/**
@@ -41,7 +42,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public static function userNameFieldForNewUser() {
return Locator::forThe()->field("newusername")->
describedAs("User name field for new user in Users Settings");
describedAs("User name field for new user in Users Settings");
}

/**
@@ -49,7 +50,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public static function displayNameFieldForNewUser() {
return Locator::forThe()->field("newdisplayname")->
describedAs("Display name field for new user in Users Settings");
describedAs("Display name field for new user in Users Settings");
}

/**
@@ -57,7 +58,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public static function passwordFieldForNewUser() {
return Locator::forThe()->field("newuserpassword")->
describedAs("Password field for new user in Users Settings");
describedAs("Password field for new user in Users Settings");
}

/**
@@ -65,7 +66,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public static function newUserButton() {
return Locator::forThe()->id("new-user-button")->
describedAs("New user button in Users Settings");
describedAs("New user button in Users Settings");
}

/**
@@ -73,26 +74,26 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public static function createNewUserButton() {
return Locator::forThe()->xpath("//form[@id = 'new-user']//input[@type = 'submit']")->
describedAs("Create user button in Users Settings");
describedAs("Create user button in Users Settings");
}

/**
* @return Locator
*/
public static function rowForUser($user) {
return Locator::forThe()->xpath("//div[@id='app-content']/div/div[normalize-space() = '$user']/..")->
describedAs("Row for user $user in Users Settings");
return Locator::forThe()->css("div.user-list-grid div.row[data-id=$user]")->
describedAs("Row for user $user in Users Settings");
}

/**
* Warning: you need to watch out for the proper classes order
*
*
* @return Locator
*/
public static function classCellForUser($class, $user) {
return Locator::forThe()->xpath("//*[contains(concat(' ', normalize-space(@class), ' '), ' $class ')]")->
descendantOf(self::rowForUser($user))->
describedAs("$class cell for user $user in Users Settings");
descendantOf(self::rowForUser($user))->
describedAs("$class cell for user $user in Users Settings");
}

/**
@@ -100,8 +101,8 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public static function inputForUserInCell($cell, $user) {
return Locator::forThe()->css("input")->
descendantOf(self::classCellForUser($cell, $user))->
describedAs("$cell input for user $user in Users Settings");
descendantOf(self::classCellForUser($cell, $user))->
describedAs("$cell input for user $user in Users Settings");
}

/**
@@ -116,8 +117,8 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public static function optionInInputForUser($cell, $user) {
return Locator::forThe()->css(".multiselect__option--highlight")->
descendantOf(self::classCellForUser($cell, $user))->
describedAs("Selected $cell option in $cell input for user $user in Users Settings");
descendantOf(self::classCellForUser($cell, $user))->
describedAs("Selected $cell option in $cell input for user $user in Users Settings");
}

/**
@@ -125,8 +126,8 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public static function actionsMenuOf($user) {
return Locator::forThe()->css(".icon-more")->
descendantOf(self::rowForUser($user))->
describedAs("Actions menu for user $user in Users Settings");
descendantOf(self::rowForUser($user))->
describedAs("Actions menu for user $user in Users Settings");
}

/**
@@ -134,8 +135,8 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public static function theAction($action, $user) {
return Locator::forThe()->xpath("//button[normalize-space() = '$action']")->
descendantOf(self::rowForUser($user))->
describedAs("$action action for the user $user row in Users Settings");
descendantOf(self::rowForUser($user))->
describedAs("$action action for the user $user row in Users Settings");
}

/**
@@ -143,7 +144,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public static function theColumn($column) {
return Locator::forThe()->xpath("//div[@class='user-list-grid']//div[normalize-space() = '$column']")->
describedAs("The $column column in Users Settings");
describedAs("The $column column in Users Settings");
}

/**
@@ -151,8 +152,25 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public static function selectedSelectOption($cell, $user) {
return Locator::forThe()->css(".multiselect__single")->
descendantOf(self::classCellForUser($cell, $user))->
describedAs("The selected option of the $cell select for the user $user in Users Settings");
descendantOf(self::classCellForUser($cell, $user))->
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");
}

/**
@@ -204,6 +222,13 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
$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
*/
@@ -258,7 +283,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public function iSeeThatTheNewUserFormIsShown() {
PHPUnit_Framework_Assert::assertTrue(
$this->actor->find(self::newUserForm(), 10)->isVisible());
$this->actor->find(self::newUserForm(), 10)->isVisible());
}

/**
@@ -266,7 +291,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public function iSeeTheAction($action, $user) {
PHPUnit_Framework_Assert::assertTrue(
$this->actor->find(self::theAction($action, $user), 10)->isVisible());
$this->actor->find(self::theAction($action, $user), 10)->isVisible());
}

/**
@@ -274,7 +299,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
*/
public function iSeeThatTheColumnIsShown($column) {
PHPUnit_Framework_Assert::assertTrue(
$this->actor->find(self::theColumn($column), 10)->isVisible());
$this->actor->find(self::theColumn($column), 10)->isVisible());
}

/**
@@ -289,15 +314,16 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
* @Then I see that the display name for the user :user is :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());
}

/**
* @Then I see that the :cell cell for user :user is done loading
*/
public function iSeeThatTheCellForUserIsDoneLoading($cell, $user) {
WaitFor::elementToBeEventuallyShown($this->actor, self::classCellForUser($cell.' icon-loading-small', $user));
WaitFor::elementToBeEventuallyNotShown($this->actor, self::classCellForUser($cell.' icon-loading-small', $user));
WaitFor::elementToBeEventuallyShown($this->actor, self::classCellForUser($cell . ' icon-loading-small', $user));
WaitFor::elementToBeEventuallyNotShown($this->actor, self::classCellForUser($cell . ' icon-loading-small', $user));
}

/**
@@ -307,6 +333,11 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
PHPUnit_Framework_Assert::assertEquals(
$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));
}
}

+ 12
- 6
tests/acceptance/features/users.feature View File

@@ -63,18 +63,20 @@ Feature: users
And I am logged in as the admin
And I open the User settings
And I see that the list of users contains the user user0
# disabled because we need the TAB patch:
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:
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
# When I assign the user user0 to the group admin
# Then I see that the section Admins is shown
# And I see that the section Admins has a count of 2
Scenario: create and delete a group
Given I act as Jane
And I am logged in as the admin
And I open the User settings
And I see that the list of users contains the user user0
# disabled because we need the TAB patch:
# disabled because we need the TAB patch:
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
# And I assign the user user0 to the group Group1
# And I see that the section Group1 is shown
@@ -112,7 +114,7 @@ Feature: users
Then I see that the "Storage location" column is shown
When I toggle the showUserBackend checkbox in the settings
Then I see that the "User backend" column is shown
# Scenario: change display name
# Given I act as Jane
# And I am logged in as the admin
@@ -128,6 +130,8 @@ Feature: users
And I am logged in as the admin
And I open the User settings
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 ""
When I set the password for user0 to 123456
And I see that the password cell for user user0 is done loading
@@ -149,8 +153,10 @@ Feature: users
And I am logged in as the admin
And I open the User settings
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
# disabled because we need the TAB patch:
# disabled because we need the TAB patch:
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
# When I set the user user0 quota to 1GB
# And I see that the quota cell for user user0 is done loading
@@ -163,4 +169,4 @@ Feature: users
# Then I see that the user quota of user0 is "0 B"
# When I set the user user0 quota to Default
# And I see that the quota cell for user user0 is done loading
# Then I see that the user quota of user0 is "Default quota"
# Then I see that the user quota of user0 is "Default quota"

Loading…
Cancel
Save