Signed-off-by: Greta Doci <gretadoci@gmail.com>tags/v18.0.0beta3
@@ -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; | |||
} |
@@ -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" |