From c39bd54958a139a8b289dfbadeafb9a2ca6b3cef Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Tue, 28 Apr 2026 21:43:56 +0200 Subject: [PATCH] feat(form-failures): action dialogs (Retry / Resolve / Dismiss) (WS-6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../form-failures/DismissFailureDialog.vue | 149 ++++++++++++++++++ .../form-failures/ResolveFailureDialog.vue | 117 ++++++++++++++ .../form-failures/RetryFailureDialog.vue | 106 +++++++++++++ .../__tests__/DismissFailureDialog.spec.ts | 139 ++++++++++++++++ .../__tests__/ResolveFailureDialog.spec.ts | 98 ++++++++++++ .../__tests__/RetryFailureDialog.spec.ts | 116 ++++++++++++++ 6 files changed, 725 insertions(+) create mode 100644 apps/app/src/components/form-failures/DismissFailureDialog.vue create mode 100644 apps/app/src/components/form-failures/ResolveFailureDialog.vue create mode 100644 apps/app/src/components/form-failures/RetryFailureDialog.vue create mode 100644 apps/app/src/components/form-failures/__tests__/DismissFailureDialog.spec.ts create mode 100644 apps/app/src/components/form-failures/__tests__/ResolveFailureDialog.spec.ts create mode 100644 apps/app/src/components/form-failures/__tests__/RetryFailureDialog.spec.ts diff --git a/apps/app/src/components/form-failures/DismissFailureDialog.vue b/apps/app/src/components/form-failures/DismissFailureDialog.vue new file mode 100644 index 00000000..ee981d39 --- /dev/null +++ b/apps/app/src/components/form-failures/DismissFailureDialog.vue @@ -0,0 +1,149 @@ + + + diff --git a/apps/app/src/components/form-failures/ResolveFailureDialog.vue b/apps/app/src/components/form-failures/ResolveFailureDialog.vue new file mode 100644 index 00000000..4317b0fa --- /dev/null +++ b/apps/app/src/components/form-failures/ResolveFailureDialog.vue @@ -0,0 +1,117 @@ + + + diff --git a/apps/app/src/components/form-failures/RetryFailureDialog.vue b/apps/app/src/components/form-failures/RetryFailureDialog.vue new file mode 100644 index 00000000..05d36edd --- /dev/null +++ b/apps/app/src/components/form-failures/RetryFailureDialog.vue @@ -0,0 +1,106 @@ + + + diff --git a/apps/app/src/components/form-failures/__tests__/DismissFailureDialog.spec.ts b/apps/app/src/components/form-failures/__tests__/DismissFailureDialog.spec.ts new file mode 100644 index 00000000..dfba377e --- /dev/null +++ b/apps/app/src/components/form-failures/__tests__/DismissFailureDialog.spec.ts @@ -0,0 +1,139 @@ +import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query' +import { flushPromises, mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mockPost = vi.fn() +vi.mock('@/lib/axios', () => ({ + apiClient: { + get: vi.fn(), + post: (...args: unknown[]) => mockPost(...args), + }, +})) + +import DismissFailureDialog from '../DismissFailureDialog.vue' +import type { FormFailure } from '@/types/form-failures' + +function makeFailure(overrides: Partial = {}): FormFailure { + return { + id: '01ABC', + form_submission_id: '01SUB', + binding_id: null, + listener_class: 'App\\Listeners\\FormBuilder\\ApplyBindingsOnFormSubmit', + failed_at: '2026-04-28T12:00:00Z', + exception_class: 'RuntimeException', + exception_message: 'boom', + context: null, + retry_count: 0, + resolved_at: null, + resolved_note: null, + dismissed_at: null, + dismissed_reason_type: null, + dismissed_reason_note: null, + state: 'open', + ...overrides, + } +} + +// VRadioGroup's modelValue → emits update; VRadio is selected via parent. +// Stub them as a simple select-like control so tests can set the reason +// without rendering Vuetify's full radio implementation. +const stubs = { + VDialog: { template: '
', props: ['modelValue'] }, + VCard: { template: '
' }, + VCardTitle: { template: '
' }, + VCardText: { template: '
' }, + VCardActions: { template: '
' }, + VAlert: { template: '
' }, + VBtn: { + template: '', + props: ['disabled', 'loading', 'color', 'variant'], + }, + VTextarea: { + template: '