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>
150 lines
3.8 KiB
Vue
150 lines
3.8 KiB
Vue
<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>
|