Merge pull request #17239 from nextcloud/feature/17208/Move_usersmanagement_to_multiline
Move users management to multi line
This commit is contained in:
commit
42c8f9f679
|
@ -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,14 +22,17 @@
|
|||
|
||||
<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">
|
||||
|
||||
<div class="subtitle">
|
||||
{{ t('settings', 'Display name') }}
|
||||
</div>
|
||||
</div>
|
||||
<div id="headerPassword" class="password">
|
||||
{{ t('settings', 'Password') }}
|
||||
</div>
|
||||
|
@ -52,99 +55,103 @@
|
|||
class="languages">
|
||||
{{ t('settings', 'Language') }}
|
||||
</div>
|
||||
|
||||
<div v-if="showConfig.showUserBackend || showConfig.showStoragePath"
|
||||
class="headerUserBackend userBackend">
|
||||
<div v-if="showConfig.showUserBackend" class="userBackend">
|
||||
{{ t('settings', 'User backend') }}
|
||||
</div>
|
||||
<div v-if="showConfig.showStoragePath"
|
||||
class="headerStorageLocation storageLocation">
|
||||
class="subtitle storageLocation">
|
||||
{{ t('settings', 'Storage location') }}
|
||||
</div>
|
||||
<div v-if="showConfig.showUserBackend"
|
||||
class="headerUserBackend userBackend">
|
||||
{{ t('settings', 'User backend') }}
|
||||
</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"
|
||||
:disabled="loading.groups||loading.all"
|
||||
tag-placeholder="create"
|
||||
:placeholder="t('settings', 'Add user in group')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
class="multiselect-vue"
|
||||
:multiple="true"
|
||||
:taggable="true"
|
||||
:close-on-select="false"
|
||||
:disabled="loading.groups||loading.all"
|
||||
: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"
|
||||
@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)
|
||||
|
@ -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 */
|
||||
|
|
|
@ -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"
|
||||
alt=""
|
||||
width="32"
|
||||
height="32"
|
||||
:src="generateAvatar(user.id, 32)"
|
||||
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'">
|
||||
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
|
||||
alt=""
|
||||
height="32"
|
||||
width="32">
|
||||
</div>
|
||||
<div 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"
|
||||
alt=""
|
||||
width="32"
|
||||
height="32"
|
||||
:src="generateAvatar(user.id, 32)"
|
||||
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'">
|
||||
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
|
||||
alt=""
|
||||
height="32"
|
||||
width="32">
|
||||
</div>
|
||||
<!-- dirty hack to ellipsis on two lines -->
|
||||
<div class="name">
|
||||
{{ user.id }}
|
||||
</div>
|
||||
<form class="displayName" :class="{'icon-loading-small': loading.displayName}" @submit.prevent="updateDisplayName">
|
||||
<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"
|
||||
type="text"
|
||||
:disabled="loading.displayName||loading.all"
|
||||
:value="user.displayname"
|
||||
autocapitalize="off"
|
||||
autocomplete="new-password"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false">
|
||||
spellcheck="false"
|
||||
type="text">
|
||||
<input v-if="user.backendCapabilities.setDisplayName"
|
||||
type="submit"
|
||||
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">
|
||||
{{ user.displayname }}
|
||||
</div>
|
||||
<div v-else
|
||||
v-tooltip.auto="t('settings', 'The backend does not support changing the display name')"
|
||||
class="name" />
|
||||
</form>
|
||||
</div>
|
||||
<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"
|
||||
:limit="2"
|
||||
:multiple="true"
|
||||
:options="availableGroups"
|
||||
:placeholder="t('settings', 'Add user in group')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
class="multiselect-vue"
|
||||
:limit="2"
|
||||
:multiple="true"
|
||||
:tag-width="60"
|
||||
:taggable="settings.isAdmin"
|
||||
:close-on-select="false"
|
||||
:tag-width="60"
|
||||
@tag="createGroup"
|
||||
: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')"
|
||||
label="label"
|
||||
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" />
|
||||
:value="userQuota"
|
||||
class="multiselect-vue"
|
||||
label="label"
|
||||
tag-placeholder="create"
|
||||
track-by="id"
|
||||
@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')"
|
||||
:value="userLanguage"
|
||||
class="multiselect-vue"
|
||||
group-label="label"
|
||||
group-values="languages"
|
||||
label="name"
|
||||
track-by="code"
|
||||
class="multiselect-vue"
|
||||
:allow-empty="false"
|
||||
group-values="languages"
|
||||
group-label="label"
|
||||
@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
|
||||
|
@ -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) {
|
||||
|
@ -624,7 +557,10 @@ export default {
|
|||
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)
|
||||
|
@ -642,7 +578,10 @@ export default {
|
|||
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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
*
|
||||
* @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
|
||||
*
|
||||
|
@ -80,7 +81,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function rowForUser($user) {
|
||||
return Locator::forThe()->xpath("//div[@id='app-content']/div/div[normalize-space() = '$user']/..")->
|
||||
return Locator::forThe()->css("div.user-list-grid div.row[data-id=$user]")->
|
||||
describedAs("Row for user $user in Users Settings");
|
||||
}
|
||||
|
||||
|
@ -155,6 +156,23 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
|
|||
describedAs("The selected option of the $cell select for the user $user in Users Settings");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Locator
|
||||
*/
|
||||
public static function editModeToggle($user) {
|
||||
return Locator::forThe()->css(".toggleUserActions button.icon-rename")->
|
||||
descendantOf(self::rowForUser($user))->
|
||||
describedAs("The edit toggle button for the user $user in Users Settings");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Locator
|
||||
*/
|
||||
public static function editModeOn($user) {
|
||||
return Locator::forThe()->css("div.user-list-grid div.row.row--editable[data-id=$user]")->
|
||||
describedAs("I see the edit mode is on for the user $user in Users Settings");
|
||||
}
|
||||
|
||||
/**
|
||||
* @When I click the New user button
|
||||
*/
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -308,5 +334,10 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
|
|||
$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,6 +63,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
|
||||
# disabled because we need the TAB patch:
|
||||
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
|
||||
# When I assign the user user0 to the group admin
|
||||
|
@ -128,6 +130,8 @@ Feature: users
|
|||
And I am logged in as the admin
|
||||
And I 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,6 +153,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 user quota of user0 is Unlimited
|
||||
# disabled because we need the TAB patch:
|
||||
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
|
||||
|
|
Loading…
Reference in New Issue