feat(lists): add error handling (#1593)
parent
2daaad90a1
commit
6c1ec2a252
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue