mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
2c344a0bc0
This patch should fix most of the problems for users trying to access the user settings via screen reader. It makes sure user interface elements can be reached via keyboard and provides proper labels, roles and values so you not only can interact with elements but also know what you are actually changing. While not focused on other views, this should also already fix a number of accessibility issues with other settings pages.
349 lines
12 KiB
Vue
349 lines
12 KiB
Vue
<template>
|
|
<modals-modal ref="modal" v-model="show" name="account" :width="800" :height="'unset'" :processing="processing">
|
|
<template #outer>
|
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
|
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
|
</div>
|
|
</template>
|
|
<form @submit.prevent="submitForm">
|
|
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
|
<div class="w-full p-8">
|
|
<div class="flex py-2">
|
|
<div class="w-1/2 px-2">
|
|
<ui-text-input-with-label v-model="newUser.username" :label="$strings.LabelUsername" />
|
|
</div>
|
|
<div class="w-1/2 px-2">
|
|
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? $strings.LabelPassword : $strings.LabelChangePassword" type="password" />
|
|
</div>
|
|
</div>
|
|
<div v-show="!isEditingRoot" class="flex py-2">
|
|
<div class="px-2 w-52">
|
|
<ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" @input="userTypeUpdated" />
|
|
</div>
|
|
<div class="flex-grow" />
|
|
<div class="flex items-center pt-4 px-2">
|
|
<p class="px-3 font-semibold" id="user-enabled-toggle" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p>
|
|
<ui-toggle-switch labeledBy="user-enabled-toggle" v-model="newUser.isActive" :disabled="isEditingRoot" />
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!isEditingRoot && newUser.permissions" class="w-full border-t border-b border-black-200 py-2 px-3 mt-4">
|
|
<p class="text-lg mb-2 font-semibold">{{ $strings.HeaderPermissions }}</p>
|
|
<div class="flex items-center my-2 max-w-md">
|
|
<div class="w-1/2">
|
|
<p id="download-permissions-toggle">{{ $strings.LabelPermissionsDownload }}</p>
|
|
</div>
|
|
<div class="w-1/2">
|
|
<ui-toggle-switch labeledBy="download-permissions-toggle" v-model="newUser.permissions.download" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center my-2 max-w-md">
|
|
<div class="w-1/2">
|
|
<p id="update-permissions-toggle">{{ $strings.LabelPermissionsUpdate }}</p>
|
|
</div>
|
|
<div class="w-1/2">
|
|
<ui-toggle-switch labeledBy="update-permissions-toggle" v-model="newUser.permissions.update" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center my-2 max-w-md">
|
|
<div class="w-1/2">
|
|
<p id="delete-permissions-toggle">{{ $strings.LabelPermissionsDelete }}</p>
|
|
</div>
|
|
<div class="w-1/2">
|
|
<ui-toggle-switch labeledBy="delete-permissions-toggle" v-model="newUser.permissions.delete" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center my-2 max-w-md">
|
|
<div class="w-1/2">
|
|
<p id="upload-permissions-toggle">{{ $strings.LabelPermissionsUpload }}</p>
|
|
</div>
|
|
<div class="w-1/2">
|
|
<ui-toggle-switch labeledBy="upload-permissions-toggle" v-model="newUser.permissions.upload" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center my-2 max-w-md">
|
|
<div class="w-1/2">
|
|
<p id="explicit-content-permissions-toggle">{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
|
|
</div>
|
|
<div class="w-1/2">
|
|
<ui-toggle-switch labeledBy="explicit-content-permissions-toggle" v-model="newUser.permissions.accessExplicitContent" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center my-2 max-w-md">
|
|
<div class="w-1/2">
|
|
<p id="access-all-libs--permissions-toggle">{{ $strings.LabelPermissionsAccessAllLibraries }}</p>
|
|
</div>
|
|
<div class="w-1/2">
|
|
<ui-toggle-switch labeledBy="access-all-libs--permissions-toggle" v-model="newUser.permissions.accessAllLibraries" @input="accessAllLibrariesToggled" />
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!newUser.permissions.accessAllLibraries" class="my-4">
|
|
<ui-multi-select-dropdown v-model="newUser.librariesAccessible" :items="libraryItems" :label="$strings.LabelLibrariesAccessibleToUser" />
|
|
</div>
|
|
|
|
<div class="flex items-cen~ter my-2 max-w-md">
|
|
<div class="w-1/2">
|
|
<p>{{ $strings.LabelPermissionsAccessAllTags }}</p>
|
|
</div>
|
|
<div class="w-1/2">
|
|
<ui-toggle-switch v-model="newUser.permissions.accessAllTags" @input="accessAllTagsToggled" />
|
|
</div>
|
|
</div>
|
|
<div v-if="!newUser.permissions.accessAllTags" class="my-4">
|
|
<ui-multi-select-dropdown v-model="newUser.itemTagsAccessible" :items="itemTags" :label="$strings.LabelTagsAccessibleToUser" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex pt-4 px-2">
|
|
<ui-btn v-if="isEditingRoot" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
|
|
<div class="flex-grow" />
|
|
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</modals-modal>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
props: {
|
|
value: Boolean,
|
|
account: {
|
|
type: Object,
|
|
default: () => null
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
processing: false,
|
|
newUser: {},
|
|
isNew: true,
|
|
tags: [],
|
|
loadingTags: false
|
|
}
|
|
},
|
|
watch: {
|
|
show: {
|
|
handler(newVal) {
|
|
if (newVal) {
|
|
this.init()
|
|
}
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
show: {
|
|
get() {
|
|
return this.value
|
|
},
|
|
set(val) {
|
|
this.$emit('input', val)
|
|
}
|
|
},
|
|
accountTypes() {
|
|
return [
|
|
{
|
|
text: this.$strings.LabelAccountTypeGuest,
|
|
value: 'guest'
|
|
},
|
|
{
|
|
text: this.$strings.LabelAccountTypeUser,
|
|
value: 'user'
|
|
},
|
|
{
|
|
text: this.$strings.LabelAccountTypeAdmin,
|
|
value: 'admin'
|
|
}
|
|
]
|
|
},
|
|
user() {
|
|
return this.$store.state.user.user
|
|
},
|
|
title() {
|
|
return this.isNew ? this.$strings.HeaderNewAccount : this.$strings.HeaderUpdateAccount
|
|
},
|
|
isEditingRoot() {
|
|
return this.account && this.account.type === 'root'
|
|
},
|
|
libraries() {
|
|
return this.$store.state.libraries.libraries
|
|
},
|
|
libraryItems() {
|
|
return this.libraries.map((lib) => ({ text: lib.name, value: lib.id }))
|
|
},
|
|
itemTags() {
|
|
return this.tags.map((t) => {
|
|
return {
|
|
text: t,
|
|
value: t
|
|
}
|
|
})
|
|
}
|
|
},
|
|
methods: {
|
|
close() {
|
|
// Force close when navigating - used in UsersTable
|
|
if (this.$refs.modal) this.$refs.modal.setHide()
|
|
},
|
|
accessAllTagsToggled(val) {
|
|
if (val && this.newUser.itemTagsAccessible.length) {
|
|
this.newUser.itemTagsAccessible = []
|
|
}
|
|
},
|
|
fetchAllTags() {
|
|
this.loadingTags = true
|
|
this.$axios
|
|
.$get(`/api/tags`)
|
|
.then((res) => {
|
|
this.tags = res.tags
|
|
this.loadingTags = false
|
|
})
|
|
.catch((error) => {
|
|
console.error('Failed to load tags', error)
|
|
this.loadingTags = false
|
|
})
|
|
},
|
|
accessAllLibrariesToggled(val) {
|
|
if (!val && !this.newUser.librariesAccessible.length) {
|
|
this.newUser.librariesAccessible = this.libraries.map((l) => l.id)
|
|
} else if (val && this.newUser.librariesAccessible.length) {
|
|
this.newUser.librariesAccessible = []
|
|
}
|
|
},
|
|
submitForm() {
|
|
if (!this.newUser.username) {
|
|
this.$toast.error('Enter a username')
|
|
return
|
|
}
|
|
if (!this.newUser.permissions.accessAllLibraries && !this.newUser.librariesAccessible.length) {
|
|
this.$toast.error('Must select at least one library')
|
|
return
|
|
}
|
|
if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsAccessible.length) {
|
|
this.$toast.error('Must select at least one tag')
|
|
return
|
|
}
|
|
|
|
if (this.isNew) {
|
|
this.submitCreateAccount()
|
|
} else {
|
|
this.submitUpdateAccount()
|
|
}
|
|
},
|
|
submitUpdateAccount() {
|
|
var account = { ...this.newUser }
|
|
if (!account.password || account.type === 'root') {
|
|
delete account.password
|
|
}
|
|
if (account.type === 'root' && !account.isActive) return
|
|
|
|
this.processing = true
|
|
console.log('Calling update', account)
|
|
this.$axios
|
|
.$patch(`/api/users/${this.account.id}`, account)
|
|
.then((data) => {
|
|
this.processing = false
|
|
if (data.error) {
|
|
this.$toast.error(`${this.$strings.ToastAccountUpdateFailed}: ${data.error}`)
|
|
} else {
|
|
console.log('Account updated', data.user)
|
|
|
|
if (data.user.id === this.user.id && data.user.token !== this.user.token) {
|
|
console.log('Current user token was updated')
|
|
this.$store.commit('user/setUserToken', data.user.token)
|
|
}
|
|
|
|
this.$toast.success(this.$strings.ToastAccountUpdateSuccess)
|
|
this.show = false
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
this.processing = false
|
|
console.error('Failed to update account', error)
|
|
var errMsg = error.response ? error.response.data || '' : ''
|
|
this.$toast.error(errMsg || 'Failed to update account')
|
|
})
|
|
},
|
|
submitCreateAccount() {
|
|
if (!this.newUser.password) {
|
|
this.$toast.error('Must have a password, only root user can have an empty password')
|
|
return
|
|
}
|
|
|
|
var account = { ...this.newUser }
|
|
this.processing = true
|
|
this.$axios
|
|
.$post('/api/users', account)
|
|
.then((data) => {
|
|
this.processing = false
|
|
if (data.error) {
|
|
this.$toast.error(`Failed to create account: ${data.error}`)
|
|
} else {
|
|
this.$toast.success('New account created')
|
|
this.show = false
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
this.processing = false
|
|
console.error('Failed to create account', error)
|
|
var errMsg = error.response ? error.response.data || '' : ''
|
|
this.$toast.error(errMsg || 'Failed to create account')
|
|
})
|
|
},
|
|
toggleActive() {
|
|
this.newUser.isActive = !this.newUser.isActive
|
|
},
|
|
userTypeUpdated(type) {
|
|
this.newUser.permissions = {
|
|
download: type !== 'guest',
|
|
update: type === 'admin',
|
|
delete: type === 'admin',
|
|
upload: type === 'admin',
|
|
accessAllLibraries: true,
|
|
accessAllTags: true
|
|
}
|
|
},
|
|
init() {
|
|
this.fetchAllTags()
|
|
|
|
this.isNew = !this.account
|
|
if (this.account) {
|
|
this.newUser = {
|
|
username: this.account.username,
|
|
password: this.account.password,
|
|
type: this.account.type,
|
|
isActive: this.account.isActive,
|
|
permissions: { ...this.account.permissions },
|
|
librariesAccessible: [...(this.account.librariesAccessible || [])],
|
|
itemTagsAccessible: [...(this.account.itemTagsAccessible || [])]
|
|
}
|
|
} else {
|
|
this.newUser = {
|
|
username: null,
|
|
password: null,
|
|
type: 'user',
|
|
isActive: true,
|
|
permissions: {
|
|
download: true,
|
|
update: false,
|
|
delete: false,
|
|
upload: false,
|
|
accessAllLibraries: true,
|
|
accessAllTags: true
|
|
},
|
|
librariesAccessible: []
|
|
}
|
|
}
|
|
}
|
|
},
|
|
mounted() {}
|
|
}
|
|
</script>
|