Merge pull request #38826 from nextcloud/enh/a11y-new-user

This commit is contained in:
Pytal 2023-06-21 16:13:25 -07:00 committed by GitHub
commit 956eece9d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 552 additions and 391 deletions

View file

@ -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 userRow from './UserList/UserRow.vue'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
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
@ -591,25 +378,6 @@ export default {
this.newUser.groups = []
},
/**
* 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.
@ -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>

View file

@ -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>

View file

@ -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

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-login.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,3 @@
/*! For license information please see NcPasswordField.js.LICENSE.txt */
/**
* @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*

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

View file

@ -1,5 +1,3 @@
/*! For license information please see NcPasswordField.js.LICENSE.txt */
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*

File diff suppressed because one or more lines are too long

View file

@ -66,7 +66,7 @@ class AppSettingsContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function checkboxLabelInTheSettings($id) {
return Locator::forThe()->xpath("//label[@for = '$id']")->
return Locator::forThe()->css("[data-test=\"$id\"]")->
descendantOf(self::appSettingsContent())->
describedAs("The label for the $id checkbox in the settings");
}

View file

@ -34,7 +34,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function newUserForm() {
return Locator::forThe()->id("new-user")->
return Locator::forThe()->css('[data-test="form"]')->
describedAs("New user form in Users Settings");
}
@ -42,7 +42,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function userNameFieldForNewUser() {
return Locator::forThe()->field("newusername")->
return Locator::forThe()->css('[data-test="username"]')->
describedAs("User name field for new user in Users Settings");
}
@ -50,7 +50,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function displayNameFieldForNewUser() {
return Locator::forThe()->field("newdisplayname")->
return Locator::forThe()->css('[data-test="displayName"]')->
describedAs("Display name field for new user in Users Settings");
}
@ -58,7 +58,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function passwordFieldForNewUser() {
return Locator::forThe()->field("newuserpassword")->
return Locator::forThe()->css('[data-test="password"]')->
describedAs("Password field for new user in Users Settings");
}
@ -74,7 +74,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function createNewUserButton() {
return Locator::forThe()->xpath("//form[@id = 'new-user']//button[@type = 'submit']")->
return Locator::forThe()->css('[data-test="submit"]')->
describedAs("Create user button in Users Settings");
}