nextcloud/settings/src/components/userList.vue

388 lines
14 KiB
Vue

<!--
- @copyright Copyright (c) 2018 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/>.
-
-->
<template>
<div id="app-content" class="user-list-grid" v-on:scroll.passive="onScroll">
<div class="row" id="grid-header" :class="{'sticky': scrolled && !showConfig.showNewUserForm}">
<div id="headerAvatar" class="avatar"></div>
<div id="headerName" class="name">{{ t('settings', 'Username') }}</div>
<div id="headerDisplayName" class="displayName">{{ t('settings', 'Display name') }}</div>
<div id="headerPassword" class="password">{{ t('settings', 'Password') }}</div>
<div id="headerAddress" class="mailAddress">{{ t('settings', 'Email') }}</div>
<div id="headerGroups" class="groups">{{ t('settings', 'Groups') }}</div>
<div id="headerSubAdmins" class="subadmins"
v-if="subAdminsGroups.length>0 && settings.isAdmin">{{ t('settings', 'Group admin for') }}</div>
<div id="headerQuota" class="quota">{{ t('settings', 'Quota') }}</div>
<div id="headerLanguages" class="languages"
v-if="showConfig.showLanguages">{{ t('settings', 'Language') }}</div>
<div class="headerStorageLocation storageLocation"
v-if="showConfig.showStoragePath">{{ t('settings', 'Storage location') }}</div>
<div class="headerUserBackend userBackend"
v-if="showConfig.showUserBackend">{{ t('settings', 'User backend') }}</div>
<div class="headerLastLogin lastLogin"
v-if="showConfig.showLastLogin">{{ t('settings', 'Last login') }}</div>
<div class="userActions"></div>
</div>
<form class="row" id="new-user" v-show="showConfig.showNewUserForm"
v-on:submit.prevent="createUser" :disabled="loading.all"
:class="{'sticky': scrolled && showConfig.showNewUserForm}">
<div :class="loading.all?'icon-loading-small':'icon-add'"></div>
<div class="name">
<input id="newusername" type="text" required v-model="newUser.id"
:placeholder="t('settings', 'Username')" name="username"
autocomplete="off" autocapitalize="none" autocorrect="off"
ref="newusername" pattern="[a-zA-Z0-9 _\.@\-']+">
</div>
<div class="displayName">
<input id="newdisplayname" type="text" v-model="newUser.displayName"
:placeholder="t('settings', 'Display name')" name="displayname"
autocomplete="off" autocapitalize="none" autocorrect="off">
</div>
<div class="password">
<input id="newuserpassword" type="password" v-model="newUser.password"
:required="newUser.mailAddress===''" ref="newuserpassword"
:placeholder="t('settings', 'Password')" name="password"
autocomplete="new-password" autocapitalize="none" autocorrect="off"
:minlength="minPasswordLength">
</div>
<div class="mailAddress">
<input id="newemail" type="email" v-model="newUser.mailAddress"
:required="newUser.password===''"
:placeholder="t('settings', 'Email')" name="email"
autocomplete="off" autocapitalize="none" autocorrect="off">
</div>
<div class="groups">
<!-- hidden input trick for vanilla html5 form validation -->
<input type="text" :value="newUser.groups" v-if="!settings.isAdmin"
tabindex="-1" id="newgroups" :required="!settings.isAdmin"
:class="{'icon-loading-small': loading.groups}"/>
<multiselect v-model="newUser.groups" :options="canAddGroups" :disabled="loading.groups||loading.all"
tag-placeholder="create" :placeholder="t('settings', 'Add user in group')"
label="name" track-by="id" class="multiselect-vue"
:multiple="true" :taggable="true" :close-on-select="false"
@tag="createGroup">
<!-- If user is not admin, he is a subadmin.
Subadmins can't create users outside their groups
Therefore, empty select is forbidden -->
<span slot="noResult">{{t('settings', 'No results')}}</span>
</multiselect>
</div>
<div class="subadmins" v-if="subAdminsGroups.length>0 && settings.isAdmin">
<multiselect :options="subAdminsGroups" v-model="newUser.subAdminsGroups"
:placeholder="t('settings', 'Set user as admin for')"
label="name" track-by="id" class="multiselect-vue"
:multiple="true" :close-on-select="false">
<span slot="noResult">{{t('settings', 'No results')}}</span>
</multiselect>
</div>
<div class="quota">
<multiselect :options="quotaOptions" v-model="newUser.quota"
:placeholder="t('settings', 'Select user quota')"
label="label" track-by="id" class="multiselect-vue"
:allowEmpty="false" :taggable="true"
@tag="validateQuota" >
</multiselect>
</div>
<div class="languages" v-if="showConfig.showLanguages">
<multiselect :options="languages" v-model="newUser.language"
:placeholder="t('settings', 'Default language')"
label="name" track-by="code" class="multiselect-vue"
:allowEmpty="false" group-values="languages" group-label="label">
</multiselect>
</div>
<div class="storageLocation" v-if="showConfig.showStoragePath"></div>
<div class="userBackend" v-if="showConfig.showUserBackend"></div>
<div class="lastLogin" v-if="showConfig.showLastLogin"></div>
<div class="userActions">
<input type="submit" id="newsubmit" class="button primary icon-checkmark-white has-tooltip"
value="" :title="t('settings', 'Add a new user')">
</div>
</form>
<user-row v-for="(user, key) in filteredUsers" :user="user" :key="key" :settings="settings" :showConfig="showConfig"
:groups="groups" :subAdminsGroups="subAdminsGroups" :quotaOptions="quotaOptions" :languages="languages"
:externalActions="externalActions" />
<infinite-loading @infinite="infiniteHandler" ref="infiniteLoading">
<div slot="spinner"><div class="users-icon-loading icon-loading"></div></div>
<div slot="no-more"><div class="users-list-end"></div></div>
<div slot="no-results">
<div id="emptycontent">
<div class="icon-contacts-dark"></div>
<h2>{{t('settings', 'No users in here')}}</h2>
</div>
</div>
</infinite-loading>
</div>
</template>
<script>
import userRow from './userList/userRow';
import Multiselect from 'vue-multiselect';
import InfiniteLoading from 'vue-infinite-loading';
import Vue from 'vue';
export default {
name: 'userList',
props: ['users', 'showConfig', 'selectedGroup', 'externalActions'],
components: {
userRow,
Multiselect,
InfiniteLoading
},
data() {
let unlimitedQuota = {id:'none', label:t('settings', 'Unlimited')},
defaultQuota = {id:'default', label:t('settings', 'Default quota')};
return {
unlimitedQuota: unlimitedQuota,
defaultQuota: defaultQuota,
loading: {
all: false,
groups: false
},
scrolled: false,
searchQuery: '',
newUser: {
id:'',
displayName:'',
password:'',
mailAddress:'',
groups: [],
subAdminsGroups: [],
quota: defaultQuota,
language: {code: 'en', name: t('settings', 'Default language')}
}
};
},
mounted() {
if (!this.settings.canChangePassword) {
OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'));
}
/**
* Init default language from server data. The use of this.settings
* requires a computed variable, which break the v-model binding of the form,
* this is a much easier solution than getter and setter on a computed var
*/
Vue.set(this.newUser.language, 'code', this.settings.defaultLanguage);
/**
* In case the user directly loaded the user list within a group
* the watch won't be triggered. We need to initialize it.
*/
this.setNewUserDefaultGroup(this.$route.params.selectedGroup);
/**
* Register search
*/
this.userSearch = new OCA.Search(this.search, this.resetSearch);
},
computed: {
settings() {
return this.$store.getters.getServerData;
},
filteredUsers() {
if (this.selectedGroup === 'disabled') {
let disabledUsers = this.users.filter(user => user.enabled === false);
if (disabledUsers.length===0 && this.$refs.infiniteLoading && this.$refs.infiniteLoading.isComplete) {
// disabled group is empty, redirection to all users
this.$router.push({name: 'users'});
this.$refs.infiniteLoading.$emit('$InfiniteLoading:reset');
}
return disabledUsers;
}
if (!this.settings.isAdmin) {
// we don't want subadmins to edit themselves
return this.users.filter(user => user.enabled !== false && user.id !== oc_current_user);
}
return this.users.filter(user => user.enabled !== false);
},
groups() {
// data provided php side + remove the disabled group
return this.$store.getters.getGroups
.filter(group => group.id !== 'disabled')
.sort((a, b) => a.name.localeCompare(b.name));
},
canAddGroups() {
// disabled if no permission to add new users to group
return this.groups.map(group => {
// clone object because we don't want
// to edit the original groups
group = Object.assign({}, group);
group.$isDisabled = group.canAdd === false;
return group;
});
},
subAdminsGroups() {
// data provided php side
return this.$store.getters.getSubadminGroups;
},
quotaOptions() {
// convert the preset array into objects
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);
return quotaPreset;
},
minPasswordLength() {
return this.$store.getters.getPasswordPolicyMinLength;
},
usersOffset() {
return this.$store.getters.getUsersOffset;
},
usersLimit() {
return this.$store.getters.getUsersLimit;
},
/* LANGUAGES */
languages() {
return Array(
{
label: t('settings', 'Common languages'),
languages: this.settings.languages.commonlanguages
},
{
label: t('settings', 'All languages'),
languages: this.settings.languages.languages
}
);
}
},
watch: {
// watch url change and group select
selectedGroup: function (val, old) {
this.$store.commit('resetUsers');
this.$refs.infiniteLoading.$emit('$InfiniteLoading:reset');
this.setNewUserDefaultGroup(val);
}
},
methods: {
onScroll(event) {
this.scrolled = event.target.scrollTo > 0;
},
/**
* Validate quota string to make sure it's a valid human file size
*
* @param {string} quota Quota in readable format '5 GB'
* @returns {Object}
*/
validateQuota(quota) {
// only used for new presets sent through @Tag
let validQuota = OC.Util.computerFileSize(quota);
if (validQuota !== null && validQuota >= 0) {
// unify format output
quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota));
return this.newUser.quota = {id: quota, label: quota};
}
// Default is unlimited
return this.newUser.quota = this.quotaOptions[0];
},
infiniteHandler($state) {
this.$store.dispatch('getUsers', {
offset: this.usersOffset,
limit: this.usersLimit,
group: this.selectedGroup !== 'disabled' ? this.selectedGroup : '',
search: this.searchQuery
})
.then((response) => { response ? $state.loaded() : $state.complete() });
},
/* SEARCH */
search(query) {
this.searchQuery = query;
this.$store.commit('resetUsers');
this.$refs.infiniteLoading.$emit('$InfiniteLoading:reset');
},
resetSearch() {
this.search('');
},
resetForm() {
// revert form to original state
Object.assign(this.newUser, this.$options.data.call(this).newUser);
this.loading.all = false;
},
createUser() {
this.loading.all = true;
this.$store.dispatch('addUser', {
userid: this.newUser.id,
password: this.newUser.password,
displayName: this.newUser.displayName,
email: this.newUser.mailAddress,
groups: this.newUser.groups.map(group => group.id),
subadmin: this.newUser.subAdminsGroups.map(group => group.id),
quota: this.newUser.quota.id,
language: this.newUser.language.code,
})
.then(() => this.resetForm())
.catch((error) => {
this.loading.all = false;
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
this.$refs.newusername.focus();
} else if (statuscode === 107) {
// wrong password
this.$refs.newuserpassword.focus();
}
}
});
},
setNewUserDefaultGroup(value) {
if (value && value.length > 0) {
// setting new user default group to the current selected one
let currentGroup = this.groups.find(group => group.id === value);
if (currentGroup) {
this.newUser.groups = [currentGroup];
return;
}
}
// fallback, empty selected group
this.newUser.groups = [];
},
/**
* Create a new group
*
* @param {string} groups Group id
* @returns {Promise}
*/
createGroup(gid) {
this.loading.groups = true;
this.$store.dispatch('addGroup', gid)
.then((group) => {
this.newUser.groups.push(this.groups.find(group => group.id === gid))
this.loading.groups = false;
})
.catch(() => {
this.loading.groups = false;
});
return this.$store.getters.getGroups[this.groups.length];
}
}
}
</script>