diff options
author | Christopher Ng <chrng8@gmail.com> | 2023-06-16 18:03:35 -0700 |
---|---|---|
committer | Pytal <24800714+Pytal@users.noreply.github.com> | 2023-06-21 11:12:40 -0700 |
commit | 84ff000767aaf52a0a176cb28bde373cc7f24ca1 (patch) | |
tree | 2208411a5285269aaf6fc40e7fedf3bf17c4532a /apps/settings/src | |
parent | 3a557a88ce01a9c3694d1a72ca42bd89975aa66f (diff) | |
download | nextcloud-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.vue | 332 | ||||
-rw-r--r-- | apps/settings/src/components/Users/NewUserModal.vue | 461 | ||||
-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.vue | 104 |
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 |