aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src
diff options
context:
space:
mode:
authorChristopher Ng <chrng8@gmail.com>2023-06-16 18:03:35 -0700
committerPytal <24800714+Pytal@users.noreply.github.com>2023-06-21 11:12:40 -0700
commit84ff000767aaf52a0a176cb28bde373cc7f24ca1 (patch)
tree2208411a5285269aaf6fc40e7fedf3bf17c4532a /apps/settings/src
parent3a557a88ce01a9c3694d1a72ca42bd89975aa66f (diff)
downloadnextcloud-server-84ff000767aaf52a0a176cb28bde373cc7f24ca1.tar.gz
nextcloud-server-84ff000767aaf52a0a176cb28bde373cc7f24ca1.zip
enh(a11y): New user modal
Signed-off-by: Christopher Ng <chrng8@gmail.com>
Diffstat (limited to 'apps/settings/src')
-rw-r--r--apps/settings/src/components/UserList.vue332
-rw-r--r--apps/settings/src/components/Users/NewUserModal.vue461
-rw-r--r--apps/settings/src/components/Users/UserRow.vue (renamed from apps/settings/src/components/UserList/UserRow.vue)0
-rw-r--r--apps/settings/src/components/Users/UserRowSimple.vue (renamed from apps/settings/src/components/UserList/UserRowSimple.vue)0
-rw-r--r--apps/settings/src/views/Users.vue104
5 files changed, 531 insertions, 366 deletions
diff --git a/apps/settings/src/components/UserList.vue b/apps/settings/src/components/UserList.vue
index 9a97aff085f..2f3da92ca02 100644
--- a/apps/settings/src/components/UserList.vue
+++ b/apps/settings/src/components/UserList.vue
@@ -26,150 +26,12 @@
:aria-label="t('settings', 'User\'s table')"
class="user-list-grid"
@scroll.passive="onScroll">
- <NcModal v-if="showConfig.showNewUserForm" size="small" @close="closeModal">
- <form id="new-user"
- :disabled="loading.all"
- class="modal__content"
- @submit.prevent="createUser">
- <h2>{{ t('settings','New user') }}</h2>
- <input id="newusername"
- ref="newusername"
- v-model="newUser.id"
- :disabled="settings.newUserGenerateUserID"
- :placeholder="settings.newUserGenerateUserID
- ? t('settings', 'Will be autogenerated')
- : t('settings', 'Username')"
- autocapitalize="none"
- autocomplete="off"
- autocorrect="off"
- class="modal__item"
- name="username"
- pattern="[a-zA-Z0-9 _\.@\-']+"
- required
- type="text">
- <input id="newdisplayname"
- v-model="newUser.displayName"
- :placeholder="t('settings', 'Display name')"
- autocapitalize="none"
- autocomplete="off"
- autocorrect="off"
- class="modal__item"
- name="displayname"
- type="text">
- <input id="newuserpassword"
- ref="newuserpassword"
- v-model="newUser.password"
- :minlength="minPasswordLength"
- :maxlength="469"
- :placeholder="t('settings', 'Password')"
- :required="newUser.mailAddress===''"
- autocapitalize="none"
- autocomplete="new-password"
- autocorrect="off"
- class="modal__item"
- name="password"
- type="password">
- <input id="newemail"
- v-model="newUser.mailAddress"
- :placeholder="t('settings', 'Email')"
- :required="newUser.password==='' || settings.newUserRequireEmail"
- autocapitalize="none"
- autocomplete="off"
- autocorrect="off"
- class="modal__item"
- name="email"
- type="email">
- <div class="groups modal__item">
- <!-- hidden input trick for vanilla html5 form validation -->
- <input v-if="!settings.isAdmin"
- id="newgroups"
- :class="{'icon-loading-small': loading.groups}"
- :required="!settings.isAdmin"
- :value="newUser.groups"
- tabindex="-1"
- type="text">
- <NcMultiselect v-model="newUser.groups"
- :close-on-select="false"
- :disabled="loading.groups||loading.all"
- :multiple="true"
- :options="canAddGroups"
- :placeholder="t('settings', 'Add user to 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
- Therefore, empty select is forbidden -->
- <span slot="noResult">{{ t('settings', 'No results') }}</span>
- </NcMultiselect>
- </div>
- <div v-if="subAdminsGroups.length>0 && settings.isAdmin"
- class="subadmins modal__item">
- <NcMultiselect v-model="newUser.subAdminsGroups"
- :close-on-select="false"
- :multiple="true"
- :options="subAdminsGroups"
- :placeholder="t('settings', 'Set user as admin for')"
- :tag-width="60"
- class="multiselect-vue"
- label="name"
- track-by="id">
- <span slot="noResult">{{ t('settings', 'No results') }}</span>
- </NcMultiselect>
- </div>
- <div class="quota modal__item">
- <NcMultiselect 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"
- @tag="validateQuota" />
- </div>
- <div v-if="showConfig.showLanguages" class="languages modal__item">
- <NcMultiselect v-model="newUser.language"
- :allow-empty="false"
- :options="languages"
- :placeholder="t('settings', 'Default language')"
- class="multiselect-vue"
- group-label="label"
- group-values="languages"
- 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="{'icon-loading-small': loading.manager}" class="modal__item managers">
- <NcMultiselect ref="manager"
- v-model="newUser.manager"
- :close-on-select="true"
- :user-select="true"
- :options="possibleManagers"
- :placeholder="t('settings', 'Select user manager')"
- class="multiselect-vue"
- label="displayname"
- track-by="id"
- @search-change="searchUserManager">
- <span slot="noResult">{{ t('settings', 'No results') }}</span>
- </NcMultiselect>
- </div>
- <div class="user-actions">
- <NcButton id="newsubmit"
- type="primary"
- native-type="submit"
- value="">
- {{ t('settings', 'Add a new user') }}
- </NcButton>
- </div>
- </form>
- </NcModal>
+ <NewUserModal v-if="showConfig.showNewUserForm"
+ :loading="loading"
+ :new-user="newUser"
+ :show-config="showConfig"
+ @reset="resetForm"
+ @close="showConfig.showNewUserForm = false" />
<div id="grid-header"
:class="{'sticky': scrolled && !showConfig.showNewUserForm}"
class="row">
@@ -225,7 +87,7 @@
<div class="userActions" />
</div>
- <user-row v-for="user in filteredUsers"
+ <UserRow v-for="user in filteredUsers"
:key="user.id"
:external-actions="externalActions"
:groups="groups"
@@ -256,23 +118,24 @@
</template>
<script>
-import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-import InfiniteLoading from 'vue-infinite-loading'
import Vue from 'vue'
-import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcMultiselect from '@nextcloud/vue/dist/Components/NcMultiselect.js'
+import InfiniteLoading from 'vue-infinite-loading'
+
+import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-import userRow from './UserList/UserRow.vue'
+import UserRow from './Users/UserRow.vue'
+import NewUserModal from './Users/NewUserModal.vue'
const unlimitedQuota = {
id: 'none',
label: t('settings', 'Unlimited'),
}
+
const defaultQuota = {
id: 'default',
label: t('settings', 'Default quota'),
}
+
const newUser = {
id: '',
displayName: '',
@@ -290,13 +153,13 @@ const newUser = {
export default {
name: 'UserList',
+
components: {
- NcModal,
- userRow,
- NcMultiselect,
InfiniteLoading,
- NcButton,
+ NewUserModal,
+ UserRow,
},
+
props: {
users: {
type: Array,
@@ -315,20 +178,19 @@ export default {
default: () => [],
},
},
+
data() {
return {
- unlimitedQuota,
- defaultQuota,
loading: {
all: false,
groups: false,
},
scrolled: false,
- possibleManagers: [],
searchQuery: '',
newUser: Object.assign({}, newUser),
}
},
+
computed: {
settings() {
return this.$store.getters.getServerData
@@ -352,16 +214,6 @@ export default {
.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
@@ -374,14 +226,11 @@ export default {
}), [])
// add default presets
if (this.settings.allowUnlimitedQuota) {
- quotaPreset.unshift(this.unlimitedQuota)
+ quotaPreset.unshift(unlimitedQuota)
}
- quotaPreset.unshift(this.defaultQuota)
+ quotaPreset.unshift(defaultQuota)
return quotaPreset
},
- minPasswordLength() {
- return this.$store.getters.getPasswordPolicyMinLength
- },
usersOffset() {
return this.$store.getters.getUsersOffset
},
@@ -435,10 +284,6 @@ export default {
},
},
- async beforeMount() {
- await this.searchUserManager()
- },
-
mounted() {
if (!this.settings.canChangePassword) {
OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'))
@@ -466,38 +311,10 @@ export default {
},
methods: {
- async searchUserManager(query) {
- await this.$store.dispatch('searchUsers', { offset: 0, limit: 10, search: query }).then(response => {
- const users = response?.data ? Object.values(response?.data.ocs.data.users) : []
- if (users.length > 0) {
- this.possibleManagers = users
- }
- })
- },
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'
- * @return {object}
- */
- validateQuota(quota) {
- // only used for new presets sent through @Tag
- const validQuota = OC.Util.computerFileSize(quota)
- if (validQuota !== null && validQuota >= 0) {
- // unify format output
- quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota))
- this.newUser.quota = { id: quota, label: quota }
- return this.newUser.quota
- }
- // Default is unlimited
- this.newUser.quota = this.quotaOptions[0]
- return this.quotaOptions[0]
- },
-
infiniteHandler($state) {
this.$store.dispatch('getUsers', {
offset: this.usersOffset,
@@ -521,6 +338,7 @@ export default {
this.$store.commit('resetUsers')
this.$refs.infiniteLoading.stateChanger.reset()
},
+
resetSearch() {
this.search({ query: '' })
},
@@ -546,38 +364,7 @@ export default {
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,
- manager: this.newUser.manager.id,
- })
- .then(() => {
- this.resetForm()
- this.$refs.newusername.focus()
- this.closeModal()
- })
- .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
@@ -592,25 +379,6 @@ export default {
},
/**
- * Create a new group
- *
- * @param {string} gid Group id
- * @return {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]
- },
-
- /**
* 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
@@ -625,58 +393,6 @@ export default {
this.$refs.infiniteLoading.stateChanger.reset()
}
},
- closeModal() {
- // eslint-disable-next-line vue/no-mutating-props
- this.showConfig.showNewUserForm = false
- },
},
}
</script>
-<style lang="scss" scoped>
- .modal-wrapper {
- margin: 2vh 0;
- align-items: flex-start;
- }
- .modal__content {
- display: flex;
- padding: 20px;
- flex-direction: column;
- align-items: center;
- text-align: center;
- }
- .modal__item {
- margin-bottom: 16px;
- width: 100%;
- }
- .modal__item:not(:focus):not(:active) {
- border-color: var(--color-border-dark);
- }
- .modal__item::v-deep .multiselect {
- width: 100%;
- }
- .user-actions {
- margin-top: 20px;
- }
- .modal__content::v-deep .multiselect__single {
- text-align: left;
- box-sizing: border-box;
- }
- .modal__content::v-deep .multiselect__content-wrapper {
- box-sizing: border-box;
- }
- .row::v-deep .multiselect__single {
- z-index: auto !important;
- }
-
- /* fake input for groups validation */
- input#newgroups {
- position: absolute;
- opacity: 0;
- /* The "hidden" input is behind the Multiselect, so in general it does
- * not receives clicks. However, with Firefox, after the validation
- * fails, it will receive the first click done on it, so its width needs
- * to be set to 0 to prevent that ("pointer-events: none" does not
- * prevent it). */
- width: 0;
- }
-</style>
diff --git a/apps/settings/src/components/Users/NewUserModal.vue b/apps/settings/src/components/Users/NewUserModal.vue
new file mode 100644
index 00000000000..be7714f9373
--- /dev/null
+++ b/apps/settings/src/components/Users/NewUserModal.vue
@@ -0,0 +1,461 @@
+<!--
+ - @copyright 2023 Christopher Ng <chrng8@gmail.com>
+ -
+ - @author Christopher Ng <chrng8@gmail.com>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - 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>
+ <NcModal class="modal"
+ size="small"
+ v-on="$listeners">
+ <form class="modal__form"
+ data-test="form"
+ :disabled="loading.all"
+ @submit.prevent="createUser">
+ <h2>{{ t('settings', 'New user') }}</h2>
+ <NcTextField class="modal__item"
+ ref="username"
+ data-test="username"
+ :value.sync="newUser.id"
+ :disabled="settings.newUserGenerateUserID"
+ :label="usernameLabel"
+ :label-visible="true"
+ autocapitalize="none"
+ autocomplete="off"
+ autocorrect="off"
+ pattern="[a-zA-Z0-9 _\.@\-']+"
+ required />
+ <NcTextField class="modal__item"
+ data-test="displayName"
+ :value.sync="newUser.displayName"
+ :label="t('settings', 'Display name')"
+ :label-visible="true"
+ autocapitalize="none"
+ autocomplete="off"
+ autocorrect="off" />
+ <span v-if="!settings.newUserRequireEmail"
+ class="modal__hint"
+ id="password-email-hint">
+ {{ t('settings', 'Either password or email is required') }}
+ </span>
+ <NcPasswordField class="modal__item"
+ ref="password"
+ data-test="password"
+ :value.sync="newUser.password"
+ :minlength="minPasswordLength"
+ :maxlength="469"
+ aria-describedby="password-email-hint"
+ :label="newUser.mailAddress === '' ? t('settings', 'Password (required)') : t('settings', 'Password')"
+ :label-visible="true"
+ autocapitalize="none"
+ autocomplete="new-password"
+ autocorrect="off"
+ :required="newUser.mailAddress === ''" />
+ <NcTextField class="modal__item"
+ data-test="email"
+ type="email"
+ :value.sync="newUser.mailAddress"
+ aria-describedby="password-email-hint"
+ :label="newUser.password === '' || settings.newUserRequireEmail ? t('settings', 'Email (required)') : t('settings', 'Email')"
+ :label-visible="true"
+ autocapitalize="none"
+ autocomplete="off"
+ autocorrect="off"
+ :required="newUser.password === '' || settings.newUserRequireEmail" />
+ <div class="modal__item">
+ <!-- hidden input trick for vanilla html5 form validation -->
+ <NcTextField v-if="!settings.isAdmin"
+ tabindex="-1"
+ id="new-user-groups-input"
+ :class="{ 'icon-loading-small': loading.groups }"
+ :value="newUser.groups"
+ :required="!settings.isAdmin" />
+ <label class="modal__label"
+ for="new-user-groups">
+ {{ !settings.isAdmin ? t('settings', 'Groups (required)') : t('settings', 'Groups') }}
+ </label>
+ <NcSelect class="modal__select"
+ input-id="new-user-groups"
+ :placeholder="t('settings', 'Set user groups')"
+ :disabled="loading.groups || loading.all"
+ :options="canAddGroups"
+ :value="newUser.groups"
+ label="name"
+ :close-on-select="false"
+ :multiple="true"
+ :taggable="true"
+ @input="handleGroupInput"
+ @option:created="createGroup" />
+ <!-- If user is not admin, he is a subadmin.
+ Subadmins can't create users outside their groups
+ Therefore, empty select is forbidden -->
+ </div>
+ <div v-if="subAdminsGroups.length > 0 && settings.isAdmin"
+ class="modal__item">
+ <label class="modal__label"
+ for="new-user-sub-admin">
+ {{ t('settings', 'Administered groups') }}
+ </label>
+ <NcSelect class="modal__select"
+ input-id="new-user-sub-admin"
+ :placeholder="t('settings', 'Set user as admin for …')"
+ :options="subAdminsGroups"
+ v-model="newUser.subAdminsGroups"
+ :close-on-select="false"
+ :multiple="true"
+ label="name" />
+ </div>
+ <div class="modal__item">
+ <label class="modal__label"
+ for="new-user-quota">
+ {{ t('settings', 'Quota') }}
+ </label>
+ <NcSelect class="modal__select"
+ input-id="new-user-quota"
+ :placeholder="t('settings', 'Set user quota')"
+ :options="quotaOptions"
+ v-model="newUser.quota"
+ :clearable="false"
+ :taggable="true"
+ :create-option="validateQuota" />
+ </div>
+ <div v-if="showConfig.showLanguages"
+ class="modal__item">
+ <label class="modal__label"
+ for="new-user-language">
+ {{ t('settings', 'Language') }}
+ </label>
+ <NcSelect class="modal__select"
+ input-id="new-user-language"
+ :placeholder="t('settings', 'Set default language')"
+ :clearable="false"
+ :selectable="option => !option.languages"
+ :filter-by="languageFilterBy"
+ :options="languages"
+ v-model="newUser.language"
+ label="name" />
+ </div>
+ <div :class="['modal__item managers', { 'icon-loading-small': loading.manager }]">
+ <label class="modal__label"
+ for="new-user-manager">
+ {{ t('settings', 'Manager') }}
+ </label>
+ <NcSelect class="modal__select"
+ input-id="new-user-manager"
+ :placeholder="t('settings', 'Set user manager')"
+ :options="possibleManagers"
+ v-model="newUser.manager"
+ :user-select="true"
+ label="displayname"
+ @search="searchUserManager" />
+ </div>
+ <NcButton class="modal__submit"
+ data-test="submit"
+ type="primary"
+ native-type="submit">
+ {{ t('settings', 'Add new user') }}
+ </NcButton>
+ </form>
+ </NcModal>
+</template>
+
+<script>
+import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
+import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js'
+import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
+import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+
+const unlimitedQuota = {
+ id: 'none',
+ label: t('settings', 'Unlimited'),
+}
+
+const defaultQuota = {
+ id: 'default',
+ label: t('settings', 'Default quota'),
+}
+
+export default {
+ name: 'NewUserModal',
+
+ components: {
+ NcButton,
+ NcModal,
+ NcPasswordField,
+ NcSelect,
+ NcTextField,
+ },
+
+ props: {
+ loading: {
+ type: Object,
+ required: true,
+ },
+
+ newUser: {
+ type: Object,
+ required: true,
+ },
+
+ showConfig: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ possibleManagers: [],
+ }
+ },
+
+ computed: {
+ settings() {
+ return this.$store.getters.getServerData
+ },
+
+ usernameLabel() {
+ if (this.settings.newUserGenerateUserID) {
+ return t('settings', 'Username will be autogenerated')
+ }
+ return t('settings', 'Username (required)')
+ },
+
+ minPasswordLength() {
+ return this.$store.getters.getPasswordPolicyMinLength
+ },
+
+ 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))
+ },
+
+ subAdminsGroups() {
+ // data provided php side
+ return this.$store.getters.getSubadminGroups
+ },
+
+ 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
+ })
+ },
+
+ 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
+ },
+
+ languages() {
+ return [
+ {
+ name: t('settings', 'Common languages'),
+ languages: this.settings.languages.commonLanguages,
+ },
+ ...this.settings.languages.commonLanguages,
+ {
+ name: t('settings', 'Other languages'),
+ languages: this.settings.languages.otherLanguages,
+ },
+ ...this.settings.languages.otherLanguages,
+ ]
+ },
+ },
+
+ async beforeMount() {
+ await this.searchUserManager()
+ },
+
+ methods: {
+ async createUser() {
+ this.loading.all = true
+ try {
+ await 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,
+ manager: this.newUser.manager.id,
+ })
+
+ this.$emit('reset')
+ this.$refs.username?.$refs?.inputField?.$refs?.input?.focus?.()
+ this.$emit('close')
+ } 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.username?.$refs?.inputField?.$refs?.input?.focus?.()
+ } else if (statuscode === 107) {
+ // wrong password
+ this.$refs.password?.$refs?.inputField?.$refs?.input?.focus?.()
+ }
+ }
+ }
+ },
+
+ handleGroupInput(groups) {
+ /**
+ * Filter out groups with no id to prevent duplicate selected options
+ *
+ * Created groups are added programmatically by `createGroup()`
+ */
+ this.newUser.groups = groups.filter(group => Boolean(group.id))
+ },
+
+ /**
+ * Create a new group
+ *
+ * @param {any} group Group
+ * @param {string} group.name Group id
+ */
+ async createGroup({ name: gid }) {
+ this.loading.groups = true
+ try {
+ await this.$store.dispatch('addGroup', gid)
+ this.newUser.groups.push(this.groups.find(group => group.id === gid))
+ this.loading.groups = false
+ } catch (error) {
+ this.loading.groups = false
+ }
+ },
+
+ /**
+ * Validate quota string to make sure it's a valid human file size
+ *
+ * @param {string} quota Quota in readable format '5 GB'
+ * @return {object}
+ */
+ validateQuota(quota) {
+ // only used for new presets sent through @Tag
+ const validQuota = OC.Util.computerFileSize(quota)
+ if (validQuota !== null && validQuota >= 0) {
+ // unify format output
+ quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota))
+ this.newUser.quota = { id: quota, label: quota }
+ return this.newUser.quota
+ }
+ // Default is unlimited
+ this.newUser.quota = this.quotaOptions[0]
+ return this.quotaOptions[0]
+ },
+
+ languageFilterBy(option, label, search) {
+ // Show group header of the language
+ if (option.languages) {
+ return option.languages.some(
+ ({ name }) => name.toLocaleLowerCase().includes(search.toLocaleLowerCase())
+ )
+ }
+
+ return (label || '').toLocaleLowerCase().includes(search.toLocaleLowerCase())
+ },
+
+ async searchUserManager(query) {
+ await this.$store.dispatch(
+ 'searchUsers',
+ {
+ offset: 0,
+ limit: 10,
+ search: query,
+ },
+ ).then(response => {
+ const users = response?.data ? Object.values(response?.data.ocs.data.users) : []
+ if (users.length > 0) {
+ this.possibleManagers = users
+ }
+ })
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.modal {
+ &__form {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 20px;
+ gap: 4px 0;
+
+ /* fake input for groups validation */
+ #new-user-groups-input {
+ position: absolute;
+ opacity: 0;
+ /* The "hidden" input is behind the NcSelect, so in general it does
+ * not receives clicks. However, with Firefox, after the validation
+ * fails, it will receive the first click done on it, so its width needs
+ * to be set to 0 to prevent that ("pointer-events: none" does not
+ * prevent it). */
+ width: 0;
+ }
+ }
+
+ &__item {
+ width: 100%;
+
+ &:not(:focus):not(:active) {
+ border-color: var(--color-border-dark);
+ }
+ }
+
+ &__hint {
+ color: var(--color-text-maxcontrast);
+ margin-top: 8px;
+ align-self: flex-start;
+ }
+
+ &__label {
+ display: block;
+ padding: 4px 0;
+ }
+
+ &__select {
+ width: 100%;
+ }
+
+ &__submit {
+ margin-top: 20px;
+ }
+}
+</style>
diff --git a/apps/settings/src/components/UserList/UserRow.vue b/apps/settings/src/components/Users/UserRow.vue
index 60dfbd03934..60dfbd03934 100644
--- a/apps/settings/src/components/UserList/UserRow.vue
+++ b/apps/settings/src/components/Users/UserRow.vue
diff --git a/apps/settings/src/components/UserList/UserRowSimple.vue b/apps/settings/src/components/Users/UserRowSimple.vue
index 20a3e96d54f..20a3e96d54f 100644
--- a/apps/settings/src/components/UserList/UserRowSimple.vue
+++ b/apps/settings/src/components/Users/UserRowSimple.vue
diff --git a/apps/settings/src/views/Users.vue b/apps/settings/src/views/Users.vue
index d2c8c6a0a4e..de9648c5d0f 100644
--- a/apps/settings/src/views/Users.vue
+++ b/apps/settings/src/views/Users.vue
@@ -83,53 +83,41 @@
</template>
<template #footer>
<NcAppNavigationSettings exclude-click-outside-selectors=".vs__dropdown-menu">
- <div>
- <label for="default-quota-multiselect">{{ t('settings', 'Default quota:') }}</label>
- <NcSelect v-model="defaultQuota"
- input-id="default-quota-multiselect"
- :taggable="true"
- :options="quotaOptions"
- :create-option="validateQuota"
- :placeholder="t('settings', 'Select default quota')"
- :close-on-select="true"
- @option:selected="setDefaultQuota" />
- </div>
- <div>
- <input id="showLanguages"
- v-model="showLanguages"
- type="checkbox"
- class="checkbox">
- <label for="showLanguages">{{ t('settings', 'Show Languages') }}</label>
- </div>
- <div>
- <input id="showLastLogin"
- v-model="showLastLogin"
- type="checkbox"
- class="checkbox">
- <label for="showLastLogin">{{ t('settings', 'Show last login') }}</label>
- </div>
- <div>
- <input id="showUserBackend"
- v-model="showUserBackend"
- type="checkbox"
- class="checkbox">
- <label for="showUserBackend">{{ t('settings', 'Show user backend') }}</label>
- </div>
- <div>
- <input id="showStoragePath"
- v-model="showStoragePath"
- type="checkbox"
- class="checkbox">
- <label for="showStoragePath">{{ t('settings', 'Show storage path') }}</label>
- </div>
- <div>
- <input id="sendWelcomeMail"
- v-model="sendWelcomeMail"
- :disabled="loadingSendMail"
- type="checkbox"
- class="checkbox">
- <label for="sendWelcomeMail">{{ t('settings', 'Send email to new user') }}</label>
- </div>
+ <label for="default-quota-select">{{ t('settings', 'Default quota:') }}</label>
+ <NcSelect v-model="defaultQuota"
+ input-id="default-quota-select"
+ :taggable="true"
+ :options="quotaOptions"
+ :create-option="validateQuota"
+ :placeholder="t('settings', 'Select default quota')"
+ :clearable="false"
+ @option:selected="setDefaultQuota" />
+ <NcCheckboxRadioSwitch type="switch"
+ data-test="showLanguages"
+ :checked.sync="showLanguages">
+ {{ t('settings', 'Show languages') }}
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch type="switch"
+ data-test="showLastLogin"
+ :checked.sync="showLastLogin">
+ {{ t('settings', 'Show last login') }}
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch type="switch"
+ data-test="showUserBackend"
+ :checked.sync="showUserBackend">
+ {{ t('settings', 'Show user backend') }}
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch type="switch"
+ data-test="showStoragePath"
+ :checked.sync="showStoragePath">
+ {{ t('settings', 'Show storage path') }}
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch type="switch"
+ data-test="sendWelcomeMail"
+ :checked.sync="sendWelcomeMail"
+ :disabled="loadingSendMail">
+ {{ t('settings', 'Send email to new user') }}
+ </NcCheckboxRadioSwitch>
</NcAppNavigationSettings>
</template>
</NcAppNavigation>
@@ -143,6 +131,9 @@
</template>
<script>
+import Vue from 'vue'
+import VueLocalStorage from 'vue-localstorage'
+
import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
import NcAppNavigationCaption from '@nextcloud/vue/dist/Components/NcAppNavigationCaption.js'
@@ -151,22 +142,24 @@ import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationI
import NcAppNavigationNew from '@nextcloud/vue/dist/Components/NcAppNavigationNew.js'
import NcAppNavigationNewItem from '@nextcloud/vue/dist/Components/NcAppNavigationNewItem.js'
import NcAppNavigationSettings from '@nextcloud/vue/dist/Components/NcAppNavigationSettings.js'
-import axios from '@nextcloud/axios'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcContent from '@nextcloud/vue/dist/Components/NcContent.js'
-import { generateUrl } from '@nextcloud/router'
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
-import Vue from 'vue'
-import VueLocalStorage from 'vue-localstorage'
+
+import Plus from 'vue-material-design-icons/Plus.vue'
+
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
import GroupListItem from '../components/GroupListItem.vue'
import UserList from '../components/UserList.vue'
-import Plus from 'vue-material-design-icons/Plus.vue'
Vue.use(VueLocalStorage)
export default {
name: 'Users',
components: {
+ GroupListItem,
NcAppContent,
NcAppNavigation,
NcAppNavigationCaption,
@@ -175,8 +168,8 @@ export default {
NcAppNavigationNew,
NcAppNavigationNewItem,
NcAppNavigationSettings,
+ NcCheckboxRadioSwitch,
NcContent,
- GroupListItem,
NcSelect,
Plus,
UserList,
@@ -341,11 +334,6 @@ export default {
methods: {
showNewUserMenu() {
this.showConfig.showNewUserForm = true
- if (this.showConfig.showNewUserForm) {
- Vue.nextTick(() => {
- window.newusername.focus()
- })
- }
},
getLocalstorage(key) {
// force initialization