aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src/components/UserList.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/settings/src/components/UserList.vue')
-rw-r--r--apps/settings/src/components/UserList.vue409
1 files changed, 409 insertions, 0 deletions
diff --git a/apps/settings/src/components/UserList.vue b/apps/settings/src/components/UserList.vue
new file mode 100644
index 00000000000..459548fad26
--- /dev/null
+++ b/apps/settings/src/components/UserList.vue
@@ -0,0 +1,409 @@
+<!--
+ - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <Fragment>
+ <NewUserDialog v-if="showConfig.showNewUserForm"
+ :loading="loading"
+ :new-user="newUser"
+ :quota-options="quotaOptions"
+ @reset="resetForm"
+ @closing="closeDialog" />
+
+ <NcEmptyContent v-if="filteredUsers.length === 0"
+ class="empty"
+ :name="isInitialLoad && loading.users ? null : t('settings', 'No accounts')">
+ <template #icon>
+ <NcLoadingIcon v-if="isInitialLoad && loading.users"
+ :name="t('settings', 'Loading accounts …')"
+ :size="64" />
+ <NcIconSvgWrapper v-else :path="mdiAccountGroupOutline" :size="64" />
+ </template>
+ </NcEmptyContent>
+
+ <VirtualList v-else
+ :data-component="UserRow"
+ :data-sources="filteredUsers"
+ data-key="id"
+ data-cy-user-list
+ :item-height="rowHeight"
+ :style="style"
+ :extra-props="{
+ users,
+ settings,
+ hasObfuscated,
+ quotaOptions,
+ languages,
+ externalActions,
+ }"
+ @scroll-end="handleScrollEnd">
+ <template #before>
+ <caption class="hidden-visually">
+ {{ t('settings', 'List of accounts. This list is not fully rendered for performance reasons. The accounts will be rendered as you navigate through the list.') }}
+ </caption>
+ </template>
+
+ <template #header>
+ <UserListHeader :has-obfuscated="hasObfuscated" />
+ </template>
+
+ <template #footer>
+ <UserListFooter :loading="loading.users"
+ :filtered-users="filteredUsers" />
+ </template>
+ </VirtualList>
+ </Fragment>
+</template>
+
+<script>
+import { mdiAccountGroupOutline } from '@mdi/js'
+import { showError } from '@nextcloud/dialogs'
+import { subscribe, unsubscribe } from '@nextcloud/event-bus'
+import { Fragment } from 'vue-frag'
+
+import Vue from 'vue'
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+
+import VirtualList from './Users/VirtualList.vue'
+import NewUserDialog from './Users/NewUserDialog.vue'
+import UserListFooter from './Users/UserListFooter.vue'
+import UserListHeader from './Users/UserListHeader.vue'
+import UserRow from './Users/UserRow.vue'
+
+import { defaultQuota, isObfuscated, unlimitedQuota } from '../utils/userUtils.ts'
+import logger from '../logger.ts'
+
+const newUser = Object.freeze({
+ id: '',
+ displayName: '',
+ password: '',
+ mailAddress: '',
+ groups: [],
+ manager: '',
+ subAdminsGroups: [],
+ quota: defaultQuota,
+ language: {
+ code: 'en',
+ name: t('settings', 'Default language'),
+ },
+})
+
+export default {
+ name: 'UserList',
+
+ components: {
+ Fragment,
+ NcEmptyContent,
+ NcIconSvgWrapper,
+ NcLoadingIcon,
+ NewUserDialog,
+ UserListFooter,
+ UserListHeader,
+ VirtualList,
+ },
+
+ props: {
+ selectedGroup: {
+ type: String,
+ default: null,
+ },
+ externalActions: {
+ type: Array,
+ default: () => [],
+ },
+ },
+
+ setup() {
+ // non reactive properties
+ return {
+ mdiAccountGroupOutline,
+ rowHeight: 55,
+
+ UserRow,
+ }
+ },
+
+ data() {
+ return {
+ loading: {
+ all: false,
+ groups: false,
+ users: false,
+ },
+ newUser: { ...newUser },
+ isInitialLoad: true,
+ searchQuery: '',
+ }
+ },
+
+ computed: {
+ showConfig() {
+ return this.$store.getters.getShowConfig
+ },
+
+ settings() {
+ return this.$store.getters.getServerData
+ },
+
+ style() {
+ return {
+ '--row-height': `${this.rowHeight}px`,
+ }
+ },
+
+ hasObfuscated() {
+ return this.filteredUsers.some(user => isObfuscated(user))
+ },
+
+ users() {
+ return this.$store.getters.getUsers
+ },
+
+ filteredUsers() {
+ if (this.selectedGroup === 'disabled') {
+ return this.users.filter(user => user.enabled === false)
+ }
+ return this.users.filter(user => user.enabled !== false)
+ },
+
+ groups() {
+ return this.$store.getters.getSortedGroups
+ .filter(group => group.id !== '__nc_internal_recent' && group.id !== 'disabled')
+ },
+
+ quotaOptions() {
+ // convert the preset array into objects
+ const quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({
+ id: cur,
+ label: cur,
+ }), [])
+ // add default presets
+ if (this.settings.allowUnlimitedQuota) {
+ quotaPreset.unshift(unlimitedQuota)
+ }
+ quotaPreset.unshift(defaultQuota)
+ return quotaPreset
+ },
+
+ usersOffset() {
+ return this.$store.getters.getUsersOffset
+ },
+
+ usersLimit() {
+ return this.$store.getters.getUsersLimit
+ },
+
+ disabledUsersOffset() {
+ return this.$store.getters.getDisabledUsersOffset
+ },
+
+ disabledUsersLimit() {
+ return this.$store.getters.getDisabledUsersLimit
+ },
+
+ usersCount() {
+ return this.users.length
+ },
+
+ /* LANGUAGES */
+ languages() {
+ return [
+ {
+ label: t('settings', 'Common languages'),
+ languages: this.settings.languages.commonLanguages,
+ },
+ {
+ label: t('settings', 'Other languages'),
+ languages: this.settings.languages.otherLanguages,
+ },
+ ]
+ },
+ },
+
+ watch: {
+ // watch url change and group select
+ async selectedGroup(val) {
+ this.isInitialLoad = true
+ // if selected is the disabled group but it's empty
+ await this.redirectIfDisabled()
+ this.$store.commit('resetUsers')
+ await this.loadUsers()
+ this.setNewUserDefaultGroup(val)
+ },
+
+ filteredUsers(filteredUsers) {
+ logger.debug(`${filteredUsers.length} filtered user(s)`)
+ },
+ },
+
+ async created() {
+ await this.loadUsers()
+ },
+
+ async mounted() {
+ if (!this.settings.canChangePassword) {
+ OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'))
+ }
+
+ /**
+ * Reset and init new user form
+ */
+ this.resetForm()
+
+ /**
+ * Register search
+ */
+ subscribe('nextcloud:unified-search.search', this.search)
+ subscribe('nextcloud:unified-search.reset', this.resetSearch)
+
+ /**
+ * If disabled group but empty, redirect
+ */
+ await this.redirectIfDisabled()
+ },
+
+ beforeDestroy() {
+ unsubscribe('nextcloud:unified-search.search', this.search)
+ unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
+ },
+
+ methods: {
+ async handleScrollEnd() {
+ await this.loadUsers()
+ },
+
+ async loadUsers() {
+ this.loading.users = true
+ try {
+ if (this.selectedGroup === 'disabled') {
+ await this.$store.dispatch('getDisabledUsers', {
+ offset: this.disabledUsersOffset,
+ limit: this.disabledUsersLimit,
+ search: this.searchQuery,
+ })
+ } else if (this.selectedGroup === '__nc_internal_recent') {
+ await this.$store.dispatch('getRecentUsers', {
+ offset: this.usersOffset,
+ limit: this.usersLimit,
+ search: this.searchQuery,
+ })
+ } else {
+ await this.$store.dispatch('getUsers', {
+ offset: this.usersOffset,
+ limit: this.usersLimit,
+ group: this.selectedGroup,
+ search: this.searchQuery,
+ })
+ }
+ logger.debug(`${this.users.length} total user(s) loaded`)
+ } catch (error) {
+ logger.error('Failed to load accounts', { error })
+ showError('Failed to load accounts')
+ }
+ this.loading.users = false
+ this.isInitialLoad = false
+ },
+
+ closeDialog() {
+ this.$store.commit('setShowConfig', {
+ key: 'showNewUserForm',
+ value: false,
+ })
+ },
+
+ async search({ query }) {
+ this.searchQuery = query
+ this.$store.commit('resetUsers')
+ await this.loadUsers()
+ },
+
+ resetSearch() {
+ this.search({ query: '' })
+ },
+
+ resetForm() {
+ // revert form to original state
+ this.newUser = Object.assign({}, newUser)
+
+ /**
+ * 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
+ */
+ if (this.settings.defaultLanguage) {
+ 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.selectedGroup)
+
+ this.loading.all = false
+ },
+
+ setNewUserDefaultGroup(value) {
+ // Is no value set, but user is a line manager we set their group as this is a requirement for line manager
+ if (!value && !this.settings.isAdmin && !this.settings.isDelegatedAdmin) {
+ const groups = this.$store.getters.getSubAdminGroups
+ // if there are multiple groups we do not know which to add,
+ // so we cannot make the managers life easier by preselecting it.
+ if (groups.length === 1) {
+ this.newUser.groups = [...groups]
+ }
+ return
+ }
+
+ if (value) {
+ // setting new account default group to the current selected one
+ const currentGroup = this.groups.find(group => group.id === value)
+ if (currentGroup) {
+ this.newUser.groups = [currentGroup]
+ return
+ }
+ }
+ // fallback, empty selected group
+ this.newUser.groups = []
+ },
+
+ /**
+ * If the selected group is the disabled group but the count is 0
+ * redirect to the all users page.
+ * we only check for 0 because we don't have the count on ldap
+ * and we therefore set the usercount to -1 in this specific case
+ */
+ async redirectIfDisabled() {
+ const allGroups = this.$store.getters.getGroups
+ if (this.selectedGroup === 'disabled'
+ && allGroups.findIndex(group => group.id === 'disabled' && group.usercount === 0) > -1) {
+ // disabled group is empty, redirection to all users
+ this.$router.push({ name: 'users' })
+ await this.loadUsers()
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+@use './Users/shared/styles' as *;
+
+.empty {
+ :deep {
+ .icon-vue {
+ width: 64px;
+ height: 64px;
+
+ svg {
+ max-width: 64px;
+ max-height: 64px;
+ }
+ }
+ }
+}
+</style>