feat(lists): add error handling (#1593)

This commit is contained in:
Joaquín Sánchez 2023-02-03 13:09:08 +01:00 committed by GitHub
parent 2daaad90a1
commit 6c1ec2a252
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 148 additions and 28 deletions

View file

@ -1,22 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ defineProps<{ describedBy: string }>()
describedBy: string
}>()
defineOptions({
inheritAttrs: false,
})
</script> </script>
<template> <template>
<div <div
role="alert" role="alert"
aria-live="polite"
:aria-describedby="describedBy" :aria-describedby="describedBy"
flex="~ col" flex="~ col"
gap-1 text-sm gap-1 text-sm
pt-1 ps-2 pe-1 pb-2 pt-1 ps-2 pe-1 pb-2
text-red-600 dark:text-red-400 text-red-600 dark:text-red-400
border="~ base rounded red-600 dark:red-400" border="~ base rounded red-600 dark:red-400"
v-bind="$attrs"
> >
<slot /> <slot />
</div> </div>

View file

@ -19,24 +19,28 @@ const client = useMastoClient()
let isEditing = $ref<boolean>(false) let isEditing = $ref<boolean>(false)
let busy = $ref<boolean>(false) let busy = $ref<boolean>(false)
let deleteBusy = $ref<boolean>(false) let deleteBusy = $ref<boolean>(false)
let actionError = $ref<string | undefined>(undefined)
const enableSaveButton = computed(() => list.title !== modelValue.value) const enableSaveButton = computed(() => list.title !== modelValue.value)
const edit = ref() const edit = ref()
const deleteBtn = ref()
const input = ref() const input = ref()
const prepareEdit = () => { const prepareEdit = () => {
isEditing = true isEditing = true
actionError = undefined
nextTick(() => { nextTick(() => {
input.value.focus() input.value?.focus()
}) })
} }
const cancelEdit = (updateTitle = true) => { const cancelEdit = () => {
isEditing = false isEditing = false
if (updateTitle) actionError = undefined
modelValue.value = list.title modelValue.value = list.title
nextTick(() => { nextTick(() => {
edit.value.focus() edit.value?.focus()
}) })
} }
async function finishEditing() { async function finishEditing() {
@ -44,14 +48,22 @@ async function finishEditing() {
return return
busy = true busy = true
actionError = undefined
await nextTick() await nextTick()
try { try {
const updateList = await client.v1.lists.update(list.id, { const updateList = await client.v1.lists.update(list.id, {
title: modelValue.value, title: modelValue.value,
}) })
cancelEdit(false) cancelEdit()
emit('listUpdated', updateList) emit('listUpdated', updateList)
} }
catch (err) {
console.error(err)
actionError = (err as Error).message
nextTick(() => {
input.value?.focus()
})
}
finally { finally {
busy = false busy = false
} }
@ -61,6 +73,7 @@ async function removeList() {
return return
deleteBusy = true deleteBusy = true
actionError = undefined
await nextTick() await nextTick()
const confirmDelete = await openConfirmDialog({ const confirmDelete = await openConfirmDialog({
@ -75,6 +88,13 @@ async function removeList() {
await client.v1.lists.remove(list.id) await client.v1.lists.remove(list.id)
emit('listRemoved', list.id) emit('listRemoved', list.id)
} }
catch (err) {
console.error(err)
actionError = (err as Error).message
nextTick(() => {
deleteBtn.value?.focus()
})
}
finally { finally {
deleteBusy = false deleteBusy = false
} }
@ -83,11 +103,27 @@ async function removeList() {
deleteBusy = false deleteBusy = false
} }
} }
onBeforeUnmount(() => cancelEdit(false))
function clearError() {
actionError = undefined
nextTick(() => {
if (isEditing)
input.value?.focus()
else
deleteBtn.value?.focus()
})
}
onDeactivated(cancelEdit)
</script> </script>
<template> <template>
<form hover:bg-active flex justify-between items-center gap-x-2 @submit.prevent="finishEditing"> <form
hover:bg-active flex justify-between items-center gap-x-2
:aria-describedby="actionError ? `action-list-error-${list.id}` : undefined"
:class="actionError ? 'border border-base border-rounded rounded-be-is-0 rounded-be-ie-0 border-b-unset border-$c-danger-active' : null"
@submit.prevent="finishEditing"
>
<div <div
v-if="isEditing" v-if="isEditing"
bg-base border="~ base" h10 m2 ps-1 pe-4 rounded-3 w-full flex="~ row" bg-base border="~ base" h10 m2 ps-1 pe-4 rounded-3 w-full flex="~ row"
@ -98,7 +134,7 @@ onBeforeUnmount(() => cancelEdit(false))
type="button" type="button"
rounded-full text-sm p2 transition-colors rounded-full text-sm p2 transition-colors
hover:text-primary hover:text-primary
@click="cancelEdit(true)" @click="cancelEdit()"
> >
<span block text-current i-ri:close-fill /> <span block text-current i-ri:close-fill />
</button> </button>
@ -114,7 +150,7 @@ onBeforeUnmount(() => cancelEdit(false))
pb="1px" pb="1px"
flex-1 flex-1
placeholder-text-secondary placeholder-text-secondary
@keydown.esc="cancelEdit(true)" @keydown.esc="cancelEdit()"
> >
</div> </div>
<NuxtLink v-else :to="`list/${list.id}`" block grow p4> <NuxtLink v-else :to="`list/${list.id}`" block grow p4>
@ -151,6 +187,7 @@ onBeforeUnmount(() => cancelEdit(false))
</CommonTooltip> </CommonTooltip>
<CommonTooltip :content="$t('list.delete')" no-auto-focus> <CommonTooltip :content="$t('list.delete')" no-auto-focus>
<button <button
ref="delete"
type="button" type="button"
text-sm p2 border-1 transition-colors text-sm p2 border-1 transition-colors
border-dark hover:text-primary border-dark hover:text-primary
@ -166,4 +203,31 @@ onBeforeUnmount(() => cancelEdit(false))
</CommonTooltip> </CommonTooltip>
</div> </div>
</form> </form>
<CommonErrorMessage
v-if="actionError"
:id="`action-list-error-${list.id}`"
:described-by="`action-list-failed-${list.id}`"
class="rounded-bs-is-0 rounded-bs-ie-0 border-t-dashed m-b-2"
>
<header :id="`action-list-failed-${list.id}`" flex justify-between>
<div flex items-center gap-x-2 font-bold>
<div aria-hidden="true" i-ri:error-warning-fill />
<p>{{ $t(`list.${isEditing ? 'edit_error' : 'delete_error'}`) }}</p>
</div>
<CommonTooltip placement="bottom" :content="$t('list.clear_error')" no-auto-focus>
<button
flex rounded-4 p1 hover:bg-active cursor-pointer transition-100 :aria-label="$t('list.clear_error')"
@click="clearError"
>
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
</button>
</CommonTooltip>
</header>
<ol ps-2 sm:ps-1>
<li flex="~ col sm:row" gap-y-1 sm:gap-x-2>
<strong sr-only>{{ $t('list.error_prefix') }}</strong>
<span>{{ actionError }}</span>
</li>
</ol>
</CommonErrorMessage>
</template> </template>

View file

@ -164,8 +164,8 @@ defineExpose({
> >
</div> </div>
<PublishErrMessage v-if="failedMessages.length > 0" described-by="publish-failed"> <CommonErrorMessage v-if="failedMessages.length > 0" described-by="publish-failed">
<head id="publish-failed" flex justify-between> <header id="publish-failed" flex justify-between>
<div flex items-center gap-x-2 font-bold> <div flex items-center gap-x-2 font-bold>
<div aria-hidden="true" i-ri:error-warning-fill /> <div aria-hidden="true" i-ri:error-warning-fill />
<p>{{ $t('state.publish_failed') }}</p> <p>{{ $t('state.publish_failed') }}</p>
@ -178,14 +178,14 @@ defineExpose({
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line /> <span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
</button> </button>
</CommonTooltip> </CommonTooltip>
</head> </header>
<ol ps-2 sm:ps-1> <ol ps-2 sm:ps-1>
<li v-for="(error, i) in failedMessages" :key="i" flex="~ col sm:row" gap-y-1 sm:gap-x-2> <li v-for="(error, i) in failedMessages" :key="i" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
<strong>{{ i + 1 }}.</strong> <strong>{{ i + 1 }}.</strong>
<span>{{ error }}</span> <span>{{ error }}</span>
</li> </li>
</ol> </ol>
</PublishErrMessage> </CommonErrorMessage>
<div relative flex-1 flex flex-col> <div relative flex-1 flex flex-col>
<EditorContent <EditorContent
@ -201,11 +201,11 @@ defineExpose({
</div> </div>
{{ $t('state.uploading') }} {{ $t('state.uploading') }}
</div> </div>
<PublishErrMessage <CommonErrorMessage
v-else-if="failedAttachments.length > 0" v-else-if="failedAttachments.length > 0"
:described-by="isExceedingAttachmentLimit ? 'upload-failed uploads-per-post' : 'upload-failed'" :described-by="isExceedingAttachmentLimit ? 'upload-failed uploads-per-post' : 'upload-failed'"
> >
<head id="upload-failed" flex justify-between> <header id="upload-failed" flex justify-between>
<div flex items-center gap-x-2 font-bold> <div flex items-center gap-x-2 font-bold>
<div aria-hidden="true" i-ri:error-warning-fill /> <div aria-hidden="true" i-ri:error-warning-fill />
<p>{{ $t('state.upload_failed') }}</p> <p>{{ $t('state.upload_failed') }}</p>
@ -218,7 +218,7 @@ defineExpose({
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line /> <span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
</button> </button>
</CommonTooltip> </CommonTooltip>
</head> </header>
<div v-if="isExceedingAttachmentLimit" id="uploads-per-post" ps-2 sm:ps-1 text-small> <div v-if="isExceedingAttachmentLimit" id="uploads-per-post" ps-2 sm:ps-1 text-small>
{{ $t('state.attachments_exceed_server_limit') }} {{ $t('state.attachments_exceed_server_limit') }}
</div> </div>
@ -228,7 +228,7 @@ defineExpose({
<span>{{ error[0] }}</span> <span>{{ error[0] }}</span>
</li> </li>
</ol> </ol>
</PublishErrMessage> </CommonErrorMessage>
<div v-if="draft.attachments.length" flex="~ col gap-2" overflow-auto> <div v-if="draft.attachments.length" flex="~ col gap-2" overflow-auto>
<PublishAttachment <PublishAttachment

View file

@ -183,9 +183,14 @@
"list": { "list": {
"add_account": "Add account to list", "add_account": "Add account to list",
"cancel_edit": "Cancel editing", "cancel_edit": "Cancel editing",
"clear_error": "Clear error",
"create": "Create", "create": "Create",
"delete": "Delete this list", "delete": "Delete this list",
"delete_error": "There was an error while deleting the list",
"edit": "Edit this list", "edit": "Edit this list",
"edit_error": "There was an error while updating the list",
"error": "There was an error while creating the list",
"error_prefix": "Error: ",
"list_title_placeholder": "List title", "list_title_placeholder": "List title",
"modify_account": "Modify lists with account", "modify_account": "Modify lists with account",
"remove_account": "Remove account from list", "remove_account": "Remove account from list",

View file

@ -183,9 +183,14 @@
"list": { "list": {
"add_account": "Agregar cuenta a la lista", "add_account": "Agregar cuenta a la lista",
"cancel_edit": "Cancelar edición", "cancel_edit": "Cancelar edición",
"clear_error": "Limpiar error",
"create": "Crear", "create": "Crear",
"delete": "Eliminar esta lista", "delete": "Eliminar esta lista",
"delete_error": "Se produjo un error eliminando la lista",
"edit": "Ediar esta lista", "edit": "Ediar esta lista",
"edit_error": "Se produjo un error modificando la lista",
"error": "Se produjo un error creando la lista",
"error_prefix": "Error: ",
"list_title_placeholder": "Título de la lista", "list_title_placeholder": "Título de la lista",
"modify_account": "Modificar listas con cuenta", "modify_account": "Modificar listas con cuenta",
"remove_account": "Eliminar cuenta de la lista", "remove_account": "Eliminar cuenta de la lista",

View file

@ -16,6 +16,8 @@ useHeadFixed({
}) })
const paginatorRef = ref() const paginatorRef = ref()
const inputRef = ref()
let actionError = $ref<string | undefined>(undefined)
let busy = $ref<boolean>(false) let busy = $ref<boolean>(false)
const createText = ref('') const createText = ref('')
const enableSubmit = computed(() => createText.value.length > 0) const enableSubmit = computed(() => createText.value.length > 0)
@ -25,24 +27,42 @@ async function createList() {
return return
busy = true busy = true
actionError = undefined
await nextTick() await nextTick()
try { try {
const newEntry = await client.v1.lists.create({ const newEntry = await client.v1.lists.create({
title: createText.value, title: createText.value,
}) })
paginatorRef.value.createEntry(newEntry) paginatorRef.value?.createEntry(newEntry)
createText.value = '' createText.value = ''
} }
catch (err) {
console.error(err)
actionError = (err as Error).message
nextTick(() => {
inputRef.value?.focus()
})
}
finally { finally {
busy = false busy = false
} }
} }
function clearError(focusBtn: boolean) {
actionError = undefined
focusBtn && nextTick(() => {
inputRef.value?.focus()
})
}
function updateEntry(list: mastodon.v1.List) { function updateEntry(list: mastodon.v1.List) {
paginatorRef.value.updateEntry(list) paginatorRef.value?.updateEntry(list)
} }
function removeEntry(id: string) { function removeEntry(id: string) {
paginatorRef.value.removeEntry(id) paginatorRef.value?.removeEntry(id)
} }
onDeactivated(() => clearError(false))
</script> </script>
<template> <template>
@ -67,6 +87,8 @@ function removeEntry(id: string) {
border="t base" border="t base"
p-4 w-full p-4 w-full
flex="~ wrap" relative gap-3 flex="~ wrap" relative gap-3
:aria-describedby="actionError ? 'create-list-error' : undefined"
:class="actionError ? 'border border-base border-rounded rounded-be-is-0 rounded-be-ie-0 border-b-unset border-$c-danger-active' : null"
@submit.prevent="createList" @submit.prevent="createList"
> >
<div <div
@ -74,6 +96,7 @@ function removeEntry(id: string) {
items-center relative focus-within:box-shadow-outline gap-3 items-center relative focus-within:box-shadow-outline gap-3
> >
<input <input
ref="inputRef"
v-model="createText" v-model="createText"
bg-transparent bg-transparent
outline="focus:none" outline="focus:none"
@ -95,6 +118,33 @@ function removeEntry(id: string) {
</button> </button>
</div> </div>
</form> </form>
<CommonErrorMessage
v-if="actionError"
id="create-list-error"
described-by="create-list-failed"
class="rounded-bs-is-0 rounded-bs-ie-0 border-t-dashed m-b-2"
>
<header id="create-list-failed" flex justify-between>
<div flex items-center gap-x-2 font-bold>
<div aria-hidden="true" i-ri:error-warning-fill />
<p>{{ $t('list.error') }}</p>
</div>
<CommonTooltip placement="bottom" :content="$t('list.clear_error')" no-auto-focus>
<button
flex rounded-4 p1 hover:bg-active cursor-pointer transition-100 :aria-label="$t('list.clear_error')"
@click="clearError(true)"
>
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
</button>
</CommonTooltip>
</header>
<ol ps-2 sm:ps-1>
<li flex="~ col sm:row" gap-y-1 sm:gap-x-2>
<strong sr-only>{{ $t('list.error_prefix') }}</strong>
<span>{{ actionError }}</span>
</li>
</ol>
</CommonErrorMessage>
</template> </template>
</CommonPaginator> </CommonPaginator>
</slot> </slot>