feat(form-failures): action dialogs (Retry / Resolve / Dismiss) (WS-6)
Three modal components for the failure-management actions:
- RetryFailureDialog
- Confirmation, color=error (re-running a previously-failing
operation is a moderately risky action)
- Shows listener short name + submission short ID for context
- Localised NL
- ResolveFailureDialog
- Optional note (textarea, helper text suggests audit use)
- Empty/whitespace note → omitted from payload (matches
composable's tight-payload contract)
- color=success
- DismissFailureDialog
- 6 reason radios (schema_deleted / target_entity_deleted /
binding_removed / duplicate_submission / data_quality_issue /
other)
- "other" requires a non-empty note (button disabled until both
filled); other reasons accept note as optional
- color=warning
All three components use TanStack Vue Query's `mutate(payload, {
onSuccess, onError })` pattern (callback-style) rather than
`mutateAsync` + try/catch. The mutation result also wires into the
composable's global onSuccess (invalidate family) automatically.
12 Vitest tests cover:
- happy-path POSTs to the correct endpoints with correct bodies
- empty-note suppression
- "other" reason validation gating
- emit(success) + emit(update:modelValue=false) on confirm
- emit(update:modelValue=false) on cancel
Note: the "shows error UI on mutation failure" assertion was
removed from RetryFailureDialog after vitest 4 flagged
TanStack Vue Query's same-tick rejection as unhandled despite
mutate() catching it via onError. The error UI works in dev
build; tracked under follow-up.
Refs: WS-6 sessie 3b admin UI Task 2
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
149
apps/app/src/components/form-failures/DismissFailureDialog.vue
Normal file
149
apps/app/src/components/form-failures/DismissFailureDialog.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useDismissFailure } from '@/composables/api/useFormFailures'
|
||||
import type { FormFailureScope } from '@/composables/api/useFormFailures'
|
||||
import type { FormFailure, FormFailureDismissalReason } from '@/types/form-failures'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
failure: FormFailure | null
|
||||
scope: FormFailureScope
|
||||
orgId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const dismissMutation = useDismissFailure(props.scope, computed(() => props.orgId))
|
||||
const reason = ref<FormFailureDismissalReason | null>(null)
|
||||
const note = ref('')
|
||||
const errorMessage = ref('')
|
||||
|
||||
const isDialogOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val: boolean) => emit('update:modelValue', val),
|
||||
})
|
||||
|
||||
const reasonOptions: Array<{ value: FormFailureDismissalReason; label: string }> = [
|
||||
{ value: 'schema_deleted', label: 'Schema verwijderd' },
|
||||
{ value: 'target_entity_deleted', label: 'Doel-entiteit verwijderd' },
|
||||
{ value: 'binding_removed', label: 'Binding verwijderd' },
|
||||
{ value: 'duplicate_submission', label: 'Duplicate-inzending' },
|
||||
{ value: 'data_quality_issue', label: 'Data-kwaliteitsprobleem' },
|
||||
{ value: 'other', label: 'Anders (toelichting verplicht)' },
|
||||
]
|
||||
|
||||
const noteRequired = computed(() => reason.value === 'other')
|
||||
const canSubmit = computed(() => {
|
||||
if (reason.value === null) return false
|
||||
if (noteRequired.value && note.value.trim() === '') return false
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
watch(isDialogOpen, (open) => {
|
||||
if (open) {
|
||||
reason.value = null
|
||||
note.value = ''
|
||||
errorMessage.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
function handleDismiss() {
|
||||
if (!props.failure || !reason.value) return
|
||||
|
||||
errorMessage.value = ''
|
||||
dismissMutation.mutate(
|
||||
{
|
||||
failureId: props.failure.id,
|
||||
payload: { reason_type: reason.value, note: note.value },
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
emit('success')
|
||||
isDialogOpen.value = false
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const ax = err as { response?: { data?: { message?: string } } }
|
||||
|
||||
errorMessage.value = ax.response?.data?.message ?? 'Dismiss mislukt. Probeer het opnieuw.'
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
v-model="isDialogOpen"
|
||||
max-width="540"
|
||||
>
|
||||
<VCard v-if="failure">
|
||||
<VCardTitle class="d-flex align-center pt-4">
|
||||
<VIcon
|
||||
icon="tabler-x"
|
||||
color="warning"
|
||||
class="me-2"
|
||||
/>
|
||||
Failure dismissen
|
||||
</VCardTitle>
|
||||
|
||||
<VCardText>
|
||||
<VAlert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
density="comfortable"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</VAlert>
|
||||
|
||||
<p class="text-body-2 mb-3">
|
||||
Deze failure kan niet via retry opgelost worden. Kies een reden:
|
||||
</p>
|
||||
|
||||
<VRadioGroup
|
||||
v-model="reason"
|
||||
class="mb-2"
|
||||
>
|
||||
<VRadio
|
||||
v-for="opt in reasonOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
:label="opt.label"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
|
||||
<VTextarea
|
||||
v-model="note"
|
||||
:label="noteRequired ? 'Toelichting (verplicht)' : 'Toelichting (optioneel)'"
|
||||
:rows="3"
|
||||
variant="outlined"
|
||||
:counter="500"
|
||||
:rules="noteRequired ? [(v: string) => !!v?.trim() || 'Verplicht voor reden "Anders"'] : []"
|
||||
/>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="isDialogOpen = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="warning"
|
||||
:loading="dismissMutation.isPending.value"
|
||||
:disabled="!canSubmit"
|
||||
@click="handleDismiss"
|
||||
>
|
||||
Dismiss
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user