Move users management to multi line

Signed-off-by: Greta Doci <gretadoci@gmail.com>
pull/17239/head
Greta Doci 5 years ago committed by John Molakvoæ (skjnldsv)
parent c6e51924c8
commit c864bc8321
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF
  1. 48
      apps/settings/css/settings.scss
  2. 3282
      apps/settings/js/vue-1.js
  3. 1
      apps/settings/js/vue-1.js.map
  4. 4210
      apps/settings/js/vue-2.js
  5. 1
      apps/settings/js/vue-2.js.map
  6. 3418
      apps/settings/js/vue-3.js
  7. 1
      apps/settings/js/vue-3.js.map
  8. 4
      apps/settings/js/vue-4.js
  9. 2
      apps/settings/js/vue-4.js.map
  10. 25
      apps/settings/js/vue-6.js
  11. 2
      apps/settings/js/vue-6.js.map
  12. 10
      apps/settings/js/vue-settings-admin-security.js
  13. 2
      apps/settings/js/vue-settings-admin-security.js.map
  14. 10
      apps/settings/js/vue-settings-apps-users-management.js
  15. 2
      apps/settings/js/vue-settings-apps-users-management.js.map
  16. 10
      apps/settings/js/vue-settings-personal-security.js
  17. 2
      apps/settings/js/vue-settings-personal-security.js.map
  18. 4
      apps/settings/src/components/AppList.vue
  19. 153
      apps/settings/src/components/UserList.vue
  20. 485
      apps/settings/src/components/UserList/UserRow.vue
  21. 159
      apps/settings/src/components/UserList/UserRowSimple.vue
  22. 171
      apps/settings/src/mixins/UserRowMixin.js
  23. 91
      tests/acceptance/features/bootstrap/UsersSettingsContext.php
  24. 18
      tests/acceptance/features/users.feature

@ -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;
}

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 it is too large Load Diff

File diff suppressed because one or more lines are too long

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

@ -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"

@ -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()

@ -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)

@ -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>

@ -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
}
)
}
}
}

@ -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));
}
}

@ -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