diff --git a/resources/js/Pages/Events/Index.vue b/resources/js/Pages/Events/Index.vue
index dd716d2..141af76 100644
--- a/resources/js/Pages/Events/Index.vue
+++ b/resources/js/Pages/Events/Index.vue
@@ -230,7 +230,7 @@
This JSON template will be used when triggering the event. Use variables like {{user_id}} for dynamic values.
-
+
@@ -245,7 +245,7 @@
Optional. Define expected payload fields using Laravel validation rules. Trigger requests that don't match will be rejected with a 422.
-
+
@@ -257,7 +257,7 @@
{{ form.processing ? 'Saving...' : 'Create' }}
@@ -388,6 +388,8 @@ const managingEvent = ref(null)
const triggeringEvent = ref(null)
const payloadText = ref('')
const schemaText = ref('')
+const payloadJsonError = ref('')
+const schemaJsonError = ref('')
const triggerPayload = ref('')
const triggerProcessing = ref(false)
const selectedEndpoints = ref([])
@@ -433,18 +435,34 @@ const availableEndpoints = computed(() => props.endpoints || [])
// Watch for payload changes
watch(payloadText, (newValue) => {
+ if (!newValue) {
+ form.payload = null
+ payloadJsonError.value = ''
+ return
+ }
+
try {
- form.payload = newValue ? JSON.parse(newValue) : null
+ form.payload = JSON.parse(newValue)
+ payloadJsonError.value = ''
} catch (e) {
- // Invalid JSON - will be handled by backend validation
+ // Invalid JSON - surface the error and stop the stale value from being submitted
+ payloadJsonError.value = 'Invalid JSON: ' + e.message
}
})
watch(schemaText, (newValue) => {
+ if (!newValue) {
+ form.schema = null
+ schemaJsonError.value = ''
+ return
+ }
+
try {
- form.schema = newValue ? JSON.parse(newValue) : null
+ form.schema = JSON.parse(newValue)
+ schemaJsonError.value = ''
} catch (e) {
- // Invalid JSON - will be handled by backend validation
+ // Invalid JSON - surface the error and stop the stale value from being submitted
+ schemaJsonError.value = 'Invalid JSON: ' + e.message
}
})
@@ -455,9 +473,15 @@ function closeModal() {
form.clearErrors()
payloadText.value = ''
schemaText.value = ''
+ payloadJsonError.value = ''
+ schemaJsonError.value = ''
}
function saveEvent() {
+ if (payloadJsonError.value || schemaJsonError.value) {
+ return
+ }
+
form.post(route('events.store'), {
onSuccess: () => closeModal()
})
diff --git a/tests/Feature/DashboardEventCreateTest.php b/tests/Feature/DashboardEventCreateTest.php
new file mode 100644
index 0000000..e4516ca
--- /dev/null
+++ b/tests/Feature/DashboardEventCreateTest.php
@@ -0,0 +1,48 @@
+withPersonalTeam()->create();
+
+ $response = $this->actingAs($user)->post(route('events.store'), [
+ 'name' => 'order.created',
+ 'event_type' => 'order.created',
+ 'description' => 'Fired when an order is created',
+ 'payload' => ['user_id' => 1, 'nested' => ['count' => 2]],
+ 'schema' => ['user_id' => 'required|integer'],
+ ]);
+
+ $response->assertRedirect(route('events'));
+ $response->assertSessionHas('success');
+
+ $event = Event::where('user_id', $user->id)->where('name', 'order.created')->firstOrFail();
+
+ $this->assertSame(['user_id' => 1, 'nested' => ['count' => 2]], $event->payload);
+ $this->assertSame(['user_id' => 'required|integer'], $event->schema);
+ }
+
+ public function test_dashboard_create_rejects_duplicate_name_for_same_user(): void
+ {
+ $user = User::factory()->withPersonalTeam()->create();
+ Event::factory()->for($user)->create(['name' => 'order.created']);
+
+ $response = $this->actingAs($user)->post(route('events.store'), [
+ 'name' => 'order.created',
+ 'payload' => ['user_id' => 1],
+ ]);
+
+ $response->assertSessionHasErrors('name');
+ $this->assertSame(1, Event::where('user_id', $user->id)->where('name', 'order.created')->count());
+ }
+}
diff --git a/tests/e2e/specs/events.spec.ts b/tests/e2e/specs/events.spec.ts
index 3cc0c4a..d6e24f1 100644
--- a/tests/e2e/specs/events.spec.ts
+++ b/tests/e2e/specs/events.spec.ts
@@ -44,6 +44,26 @@ test.describe('Events', () => {
await expect(page.getByText(EVENT_NAME)).toBeVisible({ timeout: 10_000 })
})
+ test('rejects invalid JSON payload in the create modal instead of silently discarding it', async ({ page }) => {
+ await page.getByRole('button', { name: /create event/i }).click()
+
+ const createButton = page.getByRole('button', { name: /^create$/i })
+ await expect(createButton).toBeEnabled()
+
+ // Break the JSON - the Create button must disable and an error must appear,
+ // instead of silently keeping the last valid value and reporting success.
+ await page.fill('#payload', '{ "user_id": 1, ')
+ await expect(page.getByText(/invalid json/i)).toBeVisible()
+ await expect(createButton).toBeDisabled()
+
+ // Fixing the JSON clears the error and re-enables saving.
+ await page.fill('#payload', JSON.stringify({ user_id: 1 }))
+ await expect(page.getByText(/invalid json/i)).not.toBeVisible()
+ await expect(createButton).toBeEnabled()
+
+ await page.getByRole('button', { name: /cancel/i }).click()
+ })
+
test('searches events by name', async ({ page }) => {
await page.fill('input[placeholder*="Search"]', EVENT_NAME)
await expect(page.getByText(EVENT_NAME)).toBeVisible({ timeout: 8_000 })