feat: registration form field display_width and option descriptions

Add configurable column widths (full/half) and optional descriptions
for radio/select/checkbox options on registration form fields.

- Migration adds display_width column to both tables
- FieldDisplayWidth enum with smart defaults per field type
- normalized_options accessor for backwards-compatible option format
- Portal form renderer uses display_width for VRow/VCol grid layout
- Radio/select/checkbox options render with descriptions
- Admin field editor supports display_width toggle and description input
- System templates updated with appropriate widths and descriptions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 07:46:36 +02:00
parent c4a23b6763
commit 9718e27029
25 changed files with 634 additions and 84 deletions

View File

@@ -360,4 +360,136 @@ class RegistrationFormFieldTest extends TestCase
$response->assertUnauthorized();
}
public function test_store_field_with_display_width(): void
{
Sanctum::actingAs($this->orgAdmin);
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
'label' => 'Noodcontact naam',
'field_type' => 'text',
'display_width' => 'half',
]);
$response->assertCreated()
->assertJsonPath('data.display_width', 'half');
$this->assertDatabaseHas('registration_form_fields', [
'event_id' => $this->event->id,
'slug' => 'noodcontact-naam',
'display_width' => 'half',
]);
}
public function test_store_field_defaults_display_width_by_type(): void
{
Sanctum::actingAs($this->orgAdmin);
// Text fields default to 'half'
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
'label' => 'Korte tekst',
'field_type' => 'text',
]);
$response->assertCreated()
->assertJsonPath('data.display_width', 'half');
// Textarea fields default to 'full'
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
'label' => 'Opmerkingen',
'field_type' => 'textarea',
]);
$response->assertCreated()
->assertJsonPath('data.display_width', 'full');
}
public function test_update_field_display_width(): void
{
$field = RegistrationFormField::factory()->create([
'event_id' => $this->event->id,
'display_width' => 'full',
]);
Sanctum::actingAs($this->orgAdmin);
$response = $this->putJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields/{$field->id}", [
'display_width' => 'half',
]);
$response->assertOk()
->assertJsonPath('data.display_width', 'half');
}
public function test_store_field_with_option_descriptions(): void
{
Sanctum::actingAs($this->orgAdmin);
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
'label' => 'Vergoeding',
'field_type' => 'radio',
'options' => [
['label' => 'Pro Deo', 'description' => 'Geen vergoeding'],
['label' => 'Entreeticket', 'description' => 'Gratis ticket'],
],
]);
$response->assertCreated()
->assertJsonPath('data.normalized_options.0.label', 'Pro Deo')
->assertJsonPath('data.normalized_options.0.description', 'Geen vergoeding')
->assertJsonPath('data.normalized_options.1.label', 'Entreeticket')
->assertJsonPath('data.normalized_options.1.description', 'Gratis ticket');
}
public function test_options_backwards_compatible_with_string_array(): void
{
Sanctum::actingAs($this->orgAdmin);
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
'label' => 'Shirtmaat',
'field_type' => 'select',
'options' => ['XS', 'S', 'M', 'L'],
]);
$response->assertCreated()
->assertJsonPath('data.normalized_options.0.label', 'XS')
->assertJsonPath('data.normalized_options.0.description', null)
->assertJsonPath('data.normalized_options.3.label', 'L');
}
public function test_normalized_options_converts_strings_to_objects(): void
{
$field = RegistrationFormField::factory()->selectField()->create([
'event_id' => $this->event->id,
]);
$this->assertNotNull($field->normalized_options);
$this->assertIsArray($field->normalized_options);
// Each option should be an array with label and description keys
foreach ($field->normalized_options as $option) {
$this->assertArrayHasKey('label', $option);
$this->assertArrayHasKey('description', $option);
$this->assertIsString($option['label']);
}
}
public function test_index_returns_display_width_and_normalized_options(): void
{
RegistrationFormField::factory()->radioField()->create([
'event_id' => $this->event->id,
'sort_order' => 0,
]);
Sanctum::actingAs($this->orgAdmin);
$response = $this->getJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields");
$response->assertOk();
$field = $response->json('data.0');
$this->assertArrayHasKey('display_width', $field);
$this->assertArrayHasKey('normalized_options', $field);
$this->assertEquals('full', $field['display_width']);
$this->assertNotNull($field['normalized_options']);
}
}