From 04b019ac3b7c63129a0feda92ad397288d1c50e7 Mon Sep 17 00:00:00 2001 From: Manuk Date: Wed, 15 Apr 2026 20:29:51 +0400 Subject: [PATCH 1/3] feat: configurable description maxLength and boolean required validation fix Make the description textarea maxLength configurable via config('custom-fields.fields.description_max_length') with a 255 default, allowing longer descriptions for field types like Agreement. Fix required validation for checkbox/toggle fields: Laravel's `required` rule passes on `false`, so a required boolean could be submitted unchecked. Now uses `accepted` rule instead, and `markAsRequired()` for the visual asterisk without injecting the broken `required` validation rule. --- config/custom-fields.php | 12 ++++++ .../Base/AbstractFormComponent.php | 6 ++- src/Filament/Management/Schemas/FieldForm.php | 2 +- .../Management/Schemas/SectionForm.php | 2 +- src/Services/ValidationService.php | 4 ++ .../ValidationCapabilitiesTest.php | 42 +++++++++++++++++++ 6 files changed, 65 insertions(+), 3 deletions(-) diff --git a/config/custom-fields.php b/config/custom-fields.php index 61b06045..a47c8242 100644 --- a/config/custom-fields.php +++ b/config/custom-fields.php @@ -79,6 +79,18 @@ 'cluster' => null, ], + /* + |-------------------------------------------------------------------------- + | Field Settings + |-------------------------------------------------------------------------- + | + | Configure default settings for custom fields. + | + */ + 'fields' => [ + 'description_max_length' => 255, + ], + /* |-------------------------------------------------------------------------- | Database Configuration diff --git a/src/Filament/Integration/Base/AbstractFormComponent.php b/src/Filament/Integration/Base/AbstractFormComponent.php index 132492bc..45b00e80 100644 --- a/src/Filament/Integration/Base/AbstractFormComponent.php +++ b/src/Filament/Integration/Base/AbstractFormComponent.php @@ -85,7 +85,11 @@ function (Field $field) use ($customField): Field { $this->coreVisibilityLogic->shouldAlwaysSave($customField) || filled($state) ) - ->required($this->validationService->isRequired($customField)) + ->when( + $this->validationService->isRequired($customField) && $customField->typeData->dataType->isBoolean(), + fn (Field $field): Field => $field->markAsRequired(), + fn (Field $field): Field => $field->required($this->validationService->isRequired($customField)), + ) ->rules(fn (Field $component): array => $this->getFieldValidationRules( $customField, $component->getRecord()?->getKey() diff --git a/src/Filament/Management/Schemas/FieldForm.php b/src/Filament/Management/Schemas/FieldForm.php index 226f7aaa..97609db5 100644 --- a/src/Filament/Management/Schemas/FieldForm.php +++ b/src/Filament/Management/Schemas/FieldForm.php @@ -272,7 +272,7 @@ public static function schema(bool $withOptionsRelationship = true): array ]), Textarea::make('settings.description') ->label(__('custom-fields::custom-fields.field.form.description')) - ->maxLength(255) + ->maxLength(config('custom-fields.fields.description_max_length', 255)) ->rows(2) ->live(onBlur: true) ->columnSpanFull() diff --git a/src/Filament/Management/Schemas/SectionForm.php b/src/Filament/Management/Schemas/SectionForm.php index 5e21918e..bc9bb3fe 100644 --- a/src/Filament/Management/Schemas/SectionForm.php +++ b/src/Filament/Management/Schemas/SectionForm.php @@ -147,7 +147,7 @@ public static function schema(): array fn (Get $get): bool => $get('type') === CustomFieldSectionType::SECTION ) - ->maxLength(255) + ->maxLength(config('custom-fields.fields.description_max_length', 255)) ->nullable() ->columnSpan(12), ...self::visibilitySchema(), diff --git a/src/Services/ValidationService.php b/src/Services/ValidationService.php index 912aaacf..8b5c8b82 100644 --- a/src/Services/ValidationService.php +++ b/src/Services/ValidationService.php @@ -209,6 +209,10 @@ private function getTypeSpecificRules(CustomField $customField, string|int|null $rules[] = $decimalPlaces === 0 ? 'integer' : 'decimal:0,'.$decimalPlaces; } + if ($this->isRequired($customField) && $customField->typeData->dataType->isBoolean()) { + $rules[] = 'accepted'; + } + return $rules; } diff --git a/tests/Feature/Integration/ValidationCapabilitiesTest.php b/tests/Feature/Integration/ValidationCapabilitiesTest.php index 7915af22..077005e1 100644 --- a/tests/Feature/Integration/ValidationCapabilitiesTest.php +++ b/tests/Feature/Integration/ValidationCapabilitiesTest.php @@ -458,3 +458,45 @@ function validationSubmitEdit(Post $post, array $customFields): Testable ->assertHasNoFormErrors() ->assertRedirect(); }); + +// =========================================================================== +// Required boolean (checkbox/toggle) validation +// =========================================================================== + +it('rejects required checkbox with false value', function (): void { + createField($this, 'agree_terms', 'checkbox', ['required' => true]); + + validationSubmitCreate($this, ['agree_terms' => false]) + ->assertHasFormErrors(['custom_fields.agree_terms']); +}); + +it('accepts required checkbox with true value', function (): void { + createField($this, 'agree_terms', 'checkbox', ['required' => true]); + + validationSubmitCreate($this, ['agree_terms' => true]) + ->assertHasNoFormErrors() + ->assertRedirect(); +}); + +it('rejects required toggle with false value', function (): void { + createField($this, 'opt_in', 'toggle', ['required' => true]); + + validationSubmitCreate($this, ['opt_in' => false]) + ->assertHasFormErrors(['custom_fields.opt_in']); +}); + +it('accepts required toggle with true value', function (): void { + createField($this, 'opt_in', 'toggle', ['required' => true]); + + validationSubmitCreate($this, ['opt_in' => true]) + ->assertHasNoFormErrors() + ->assertRedirect(); +}); + +it('accepts non-required checkbox with false value', function (): void { + createField($this, 'newsletter', 'checkbox'); + + validationSubmitCreate($this, ['newsletter' => false]) + ->assertHasNoFormErrors() + ->assertRedirect(); +}); From 677012fdadf53982bfc5a53bef1c432285bdab67 Mon Sep 17 00:00:00 2001 From: Manuk Date: Wed, 15 Apr 2026 20:46:30 +0400 Subject: [PATCH 2/3] refactor: split description maxLength config into field and section keys Section descriptions use a VARCHAR(255) column, so sharing the same config key with field descriptions (stored in JSON) could cause DB errors if increased beyond 255. Separate keys make the constraint explicit. --- config/custom-fields.php | 1 + src/Filament/Management/Schemas/SectionForm.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config/custom-fields.php b/config/custom-fields.php index a47c8242..c184f67a 100644 --- a/config/custom-fields.php +++ b/config/custom-fields.php @@ -89,6 +89,7 @@ */ 'fields' => [ 'description_max_length' => 255, + 'section_description_max_length' => 255, ], /* diff --git a/src/Filament/Management/Schemas/SectionForm.php b/src/Filament/Management/Schemas/SectionForm.php index bc9bb3fe..a703dffd 100644 --- a/src/Filament/Management/Schemas/SectionForm.php +++ b/src/Filament/Management/Schemas/SectionForm.php @@ -147,7 +147,7 @@ public static function schema(): array fn (Get $get): bool => $get('type') === CustomFieldSectionType::SECTION ) - ->maxLength(config('custom-fields.fields.description_max_length', 255)) + ->maxLength(config('custom-fields.fields.section_description_max_length', 255)) ->nullable() ->columnSpan(12), ...self::visibilitySchema(), From f760d0c39d41416e8ff0c2c13ef476ce43b9fec6 Mon Sep 17 00:00:00 2001 From: Manuk Date: Wed, 15 Apr 2026 21:00:21 +0400 Subject: [PATCH 3/3] refactor: move section description config to top-level sections key Sections aren't fields, so section config shouldn't live under the fields key. Follows the existing pattern of top-level keys for separate concerns (management, currency, database). --- config/custom-fields.php | 13 ++++++++++++- src/Filament/Management/Schemas/SectionForm.php | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/config/custom-fields.php b/config/custom-fields.php index c184f67a..b2e1051c 100644 --- a/config/custom-fields.php +++ b/config/custom-fields.php @@ -89,7 +89,18 @@ */ 'fields' => [ 'description_max_length' => 255, - 'section_description_max_length' => 255, + ], + + /* + |-------------------------------------------------------------------------- + | Section Settings + |-------------------------------------------------------------------------- + | + | Configure default settings for custom field sections. + | + */ + 'sections' => [ + 'description_max_length' => 255, ], /* diff --git a/src/Filament/Management/Schemas/SectionForm.php b/src/Filament/Management/Schemas/SectionForm.php index a703dffd..1d0bb5ba 100644 --- a/src/Filament/Management/Schemas/SectionForm.php +++ b/src/Filament/Management/Schemas/SectionForm.php @@ -147,7 +147,7 @@ public static function schema(): array fn (Get $get): bool => $get('type') === CustomFieldSectionType::SECTION ) - ->maxLength(config('custom-fields.fields.section_description_max_length', 255)) + ->maxLength(config('custom-fields.sections.description_max_length', 255)) ->nullable() ->columnSpan(12), ...self::visibilitySchema(),