Skip to content

Commit d8e8f13

Browse files
committed
Merge branch 'main' of github.com:backstagephp/fields into feat/file-uploader
2 parents 226e764 + cdb3baf commit d8e8f13

28 files changed

+1467
-36
lines changed

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,31 @@
22

33
All notable changes to `fields` will be documented in this file.
44

5+
## v0.8.0 (Conditional fields) - 2025-09-26
6+
7+
### What's Changed
8+
9+
* fix: handling filament v4 RichEditor data structure by @Baspa in https://github.com/backstagephp/fields/pull/25
10+
* fix: disable array casting by @arduinomaster22 in https://github.com/backstagephp/fields/pull/26
11+
* fix: rich editor form fill issues by @Baspa in https://github.com/backstagephp/fields/pull/27
12+
* feat: auto assign workflow by @Baspa in https://github.com/backstagephp/fields/pull/28
13+
* feat: remove content cleaning by @Baspa in https://github.com/backstagephp/fields/pull/29
14+
* feat: remove live and set json to true by @Baspa in https://github.com/backstagephp/fields/pull/30
15+
* fix: disable indenting and drag n drop by @Baspa in https://github.com/backstagephp/fields/pull/31
16+
* Bump actions/github-script from 7 to 8 by @dependabot[bot] in https://github.com/backstagephp/fields/pull/32
17+
* fix: rich editor state by correcting return by @Baspa in https://github.com/backstagephp/fields/pull/33
18+
* build(deps-dev): bump form-data from 4.0.3 to 4.0.4 by @dependabot[bot] in https://github.com/backstagephp/fields/pull/35
19+
* build(deps-dev): bump esbuild from 0.19.12 to 0.25.0 by @dependabot[bot] in https://github.com/backstagephp/fields/pull/36
20+
* feat: rich editor jump anchor plugin by @Baspa in https://github.com/backstagephp/fields/pull/37
21+
* feat: let user change the relationKey by @Baspa in https://github.com/backstagephp/fields/pull/38
22+
* feat: validation and conditional fields by @Baspa in https://github.com/backstagephp/fields/pull/19
23+
24+
### New Contributors
25+
26+
* @arduinomaster22 made their first contribution in https://github.com/backstagephp/fields/pull/26
27+
28+
**Full Changelog**: https://github.com/backstagephp/fields/compare/v0.7.0...v0.8.0
29+
530
## v0.7.0 (Filament v4) - 2025-08-15
631

732
### What's Changed

README.md

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,41 @@ class ContentResource extends Resource
137137
}
138138
```
139139

140+
### Field Configuration
141+
142+
#### Validation Rules
143+
144+
Each field supports validation rules that can be configured through the admin interface. The package includes support for all standard Laravel and Filament validation rules:
145+
146+
- **Basic Rules**: Required, nullable, filled
147+
- **String Rules**: Min/max length, alpha, alphanumeric, email, URL
148+
- **Numeric Rules**: Min/max values, integer, decimal, numeric
149+
- **Date Rules**: Date format, before/after dates, date equals
150+
- **Comparison Rules**: Same as field, different from field, greater/less than field
151+
- **Conditional Rules**: Required if/unless, prohibited if/unless, required with/without
152+
- **Pattern Rules**: Regex, starts/ends with, in/not in list
153+
- **Database Rules**: Exists, unique
154+
155+
##### Field Dependencies
156+
157+
Validation rules can depend on other fields in the form:
158+
159+
- **Field Comparison**: Compare values with other fields (`same`, `different`, `greater_than`, etc.)
160+
- **Conditional Requirements**: Make fields required based on other field values (`required_if`, `required_unless`)
161+
- **Multi-field Dependencies**: Require fields based on multiple other fields (`required_with_all`, `required_without_all`)
162+
163+
When no other fields are available for dependency rules, the field selection will be disabled and show a helpful message.
164+
165+
#### Visibility Rules
166+
167+
Control when fields are shown or hidden based on conditions:
168+
169+
- **Conditional Display**: Show/hide fields based on other field values
170+
- **Dynamic Forms**: Create adaptive forms that change based on user input
171+
- **Complex Logic**: Support for multiple conditions and logical operators
172+
173+
The visibility system works seamlessly with validation rules to create intelligent, user-friendly forms.
174+
140175
### Making a resource page configurable
141176

142177
To make a resource page configurable, you need to add the `CanMapDynamicFields` trait to your page. For this example, we'll make a `EditContent` page configurable.
@@ -146,11 +181,7 @@ To make a resource page configurable, you need to add the `CanMapDynamicFields`
146181

147182
namespace Backstage\Resources\ContentResource\Pages;
148183

149-
use Filament\Forms\Components\Grid;
150-
use Filament\Forms\Components\Tabs;
151-
use Filament\Forms\Components\Tabs\Tab;
152-
use Filament\Forms\Form;
153-
use Filament\Resources\Pages\EditRecord;
184+
// ...
154185
use Backstage\Fields\Concerns\CanMapDynamicFields;
155186

156187
class EditContent extends EditRecord

src/Concerns/HasSelectableValues.php

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace Backstage\Fields\Concerns;
44

55
use Filament\Forms\Components\CheckboxList;
6-
use Filament\Forms\Components\Hidden;
76
use Filament\Forms\Components\Repeater;
87
use Filament\Forms\Components\Select;
98
use Filament\Forms\Components\TextInput;
@@ -132,7 +131,9 @@ protected static function buildRelationshipOptions(mixed $field): array
132131
continue;
133132
}
134133

135-
$opts = $results->pluck($relation['relationValue'] ?? 'name', $relation['relationKey'])->toArray();
134+
// Fallback to model's primary key for existing records that don't have relationKey set
135+
$relationKey = $relation['relationKey'] ?? $model->getKeyName();
136+
$opts = $results->pluck($relation['relationValue'] ?? 'name', $relationKey)->toArray();
136137

137138
if (count($opts) === 0) {
138139
continue;
@@ -255,8 +256,13 @@ protected function selectableValuesFormFields(string $type, string $label, strin
255256
return [$column => Str::title($column)];
256257
})->toArray();
257258

259+
// Get the primary key of the model
260+
$primaryKey = $model->getKeyName();
261+
258262
$set('relationValue', null);
259263
$set('relationValue_options', $columnOptions);
264+
$set('relationKey_options', $columnOptions);
265+
$set('relationKey', $primaryKey);
260266
})
261267
->options(function () {
262268
$resources = config('backstage.fields.selectable_resources');
@@ -278,20 +284,23 @@ protected function selectableValuesFormFields(string $type, string $label, strin
278284
fn (Get $get): bool => is_array($get("../../config.{$type}")) && in_array('relationship', $get("../../config.{$type}")) ||
279285
$get("../../config.{$type}") === 'relationship'
280286
),
281-
Select::make('relationValue')
282-
->label(__('Column'))
283-
->helperText(__('The column to use as name for the options'))
284-
->options(fn (Get $get) => $get('relationValue_options') ?? [])
287+
Select::make('relationKey')
288+
->label(__('Key Column'))
289+
->helperText(__('The column to use as the unique identifier/value for each option'))
290+
->options(fn (Get $get) => $get('relationKey_options') ?? [])
285291
->searchable()
286292
->visible(fn (Get $get): bool => ! empty($get('resource')))
287-
->required(fn (Get $get): bool => ! empty($get('resource'))),
288-
Hidden::make('relationKey')
289-
->default('ulid')
290-
->label(__('Key'))
291293
->required(
292294
fn (Get $get): bool => is_array($get("../../config.{$type}")) && in_array('relationship', $get("../../config.{$type}")) ||
293295
$get("../../config.{$type}") === 'relationship'
294296
),
297+
Select::make('relationValue')
298+
->label(__('Display Column'))
299+
->helperText(__('The column to use as the display text/label for each option'))
300+
->options(fn (Get $get) => $get('relationValue_options') ?? [])
301+
->searchable()
302+
->visible(fn (Get $get): bool => ! empty($get('resource')))
303+
->required(fn (Get $get): bool => ! empty($get('resource'))),
295304
Repeater::make('relationValue_filters')
296305
->label(__('Filters'))
297306
->visible(fn (Get $get): bool => ! empty($get('resource')))

src/Contracts/FieldContract.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ public function getForm(): array;
1111
public static function make(string $name, Field $field);
1212

1313
public static function getDefaultConfig(): array;
14+
15+
public function getFieldType(): ?string;
1416
}

src/Fields/Base.php

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,34 @@
33
namespace Backstage\Fields\Fields;
44

55
use Backstage\Fields\Contracts\FieldContract;
6+
use Backstage\Fields\Fields\FormSchemas\BasicSettingsSchema;
7+
use Backstage\Fields\Fields\FormSchemas\ValidationRulesSchema;
8+
use Backstage\Fields\Fields\FormSchemas\VisibilityRulesSchema;
9+
use Backstage\Fields\Fields\Logic\ConditionalLogicApplier;
10+
use Backstage\Fields\Fields\Logic\VisibilityLogicApplier;
11+
use Backstage\Fields\Fields\Validation\ValidationRuleApplier;
612
use Backstage\Fields\Models\Field;
713
use Filament\Forms\Components\ColorPicker;
814
use Filament\Forms\Components\TextInput;
915
use Filament\Forms\Components\Toggle;
1016
use Filament\Schemas\Components\Grid;
1117
use Filament\Schemas\Components\Utilities\Get;
1218
use Filament\Support\Colors\Color;
19+
use ReflectionObject;
1320

1421
abstract class Base implements FieldContract
1522
{
1623
public function getForm(): array
1724
{
18-
return $this->getBaseFormSchema();
25+
return BasicSettingsSchema::make();
26+
}
27+
28+
public function getRulesForm(): array
29+
{
30+
return [
31+
...ValidationRulesSchema::make($this->getFieldType()),
32+
...VisibilityRulesSchema::make(),
33+
];
1934
}
2035

2136
protected function getBaseFormSchema(): array
@@ -91,7 +106,7 @@ private function filterExcludedFields(array $schema): array
91106

92107
private function fieldContainsConfigKey($field, string $configKey): bool
93108
{
94-
$reflection = new \ReflectionObject($field);
109+
$reflection = new ReflectionObject($field);
95110
$propertiesToCheck = ['name', 'statePath'];
96111

97112
foreach ($propertiesToCheck as $propertyName) {
@@ -109,6 +124,13 @@ private function fieldContainsConfigKey($field, string $configKey): bool
109124
return false;
110125
}
111126

127+
public function getFieldType(): ?string
128+
{
129+
// This method should be overridden by specific field classes
130+
// to return their field type
131+
return null;
132+
}
133+
112134
public static function getDefaultConfig(): array
113135
{
114136
return [
@@ -119,6 +141,12 @@ public static function getDefaultConfig(): array
119141
'hint' => null,
120142
'hintColor' => null,
121143
'hintIcon' => null,
144+
'conditionalField' => null,
145+
'conditionalOperator' => null,
146+
'conditionalValue' => null,
147+
'conditionalAction' => null,
148+
'validationRules' => [],
149+
'visibilityRules' => [],
122150
'defaultValue' => null,
123151
];
124152
}
@@ -131,25 +159,38 @@ public static function applyDefaultSettings($input, ?Field $field = null)
131159
->hidden($field->config['hidden'] ?? self::getDefaultConfig()['hidden'])
132160
->helperText($field->config['helperText'] ?? self::getDefaultConfig()['helperText'])
133161
->hint($field->config['hint'] ?? self::getDefaultConfig()['hint'])
134-
->hintIcon($field->config['hintIcon'] ?? self::getDefaultConfig()['hintIcon']);
162+
->hintIcon($field->config['hintIcon'] ?? self::getDefaultConfig()['hintIcon'])
163+
->live();
135164

136165
if (isset($field->config['hintColor']) && $field->config['hintColor']) {
137166
$input->hintColor(Color::generateV3Palette($field->config['hintColor']));
138167
}
139168

169+
$input = ConditionalLogicApplier::applyConditionalLogic($input, $field);
170+
$input = ConditionalLogicApplier::applyConditionalValidation($input, $field);
171+
$input = VisibilityLogicApplier::applyVisibilityLogic($input, $field);
172+
173+
$input = self::applyAdditionalValidation($input, $field);
174+
140175
if (isset($field->config['defaultValue'])) {
141176
$input->default($field->config['defaultValue']);
142177
}
143178

144179
return $input;
145180
}
146181

147-
protected static function ensureArray($value, string $delimiter = ','): array
182+
protected static function applyAdditionalValidation($input, ?Field $field = null): mixed
148183
{
149-
if (is_array($value)) {
150-
return $value;
184+
if (! $field || empty($field->config['validationRules'])) {
185+
return $input;
151186
}
152187

153-
return ! empty($value) ? explode($delimiter, $value) : [];
188+
$rules = $field->config['validationRules'];
189+
190+
foreach ($rules as $rule) {
191+
$input = ValidationRuleApplier::applyValidationRule($input, $rule, $field);
192+
}
193+
194+
return $input;
154195
}
155196
}

src/Fields/Checkbox.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212

1313
class Checkbox extends Base implements FieldContract
1414
{
15+
public function getFieldType(): ?string
16+
{
17+
return 'checkbox';
18+
}
19+
1520
public static function getDefaultConfig(): array
1621
{
1722
return [
@@ -68,6 +73,11 @@ public function getForm(): array
6873
->inline(false),
6974
]),
7075
]),
76+
Tab::make('Rules')
77+
->label(__('Rules'))
78+
->schema([
79+
...parent::getRulesForm(),
80+
]),
7181
])->columnSpanFull(),
7282
];
7383
}

src/Fields/CheckboxList.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ class CheckboxList extends Base implements FieldContract
1818
{
1919
use HasOptions;
2020

21+
public function getFieldType(): ?string
22+
{
23+
return 'checkbox-list';
24+
}
25+
2126
public static function getDefaultConfig(): array
2227
{
2328
return [
@@ -112,6 +117,11 @@ public function getForm(): array
112117
->visible(fn (Get $get): bool => $get('config.searchable')),
113118
]),
114119
]),
120+
Tab::make('Rules')
121+
->label(__('Rules'))
122+
->schema([
123+
...parent::getRulesForm(),
124+
]),
115125
])->columnSpanFull(),
116126
];
117127
}

src/Fields/Color.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
*/
2020
class Color extends Base implements FieldContract
2121
{
22+
public function getFieldType(): ?string
23+
{
24+
return 'color';
25+
}
26+
2227
public static function getDefaultConfig(): array
2328
{
2429
return [
@@ -64,6 +69,11 @@ public function getForm(): array
6469
->placeholder(__('Enter a regex pattern')),
6570
]),
6671
]),
72+
Tab::make('Rules')
73+
->label(__('Rules'))
74+
->schema([
75+
...parent::getRulesForm(),
76+
]),
6777
])->columnSpanFull(),
6878
];
6979
}

src/Fields/DateTime.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ class DateTime extends Base implements FieldContract
1818
{
1919
use HasAffixes;
2020

21+
public function getFieldType(): ?string
22+
{
23+
return 'date-time';
24+
}
25+
2126
public static function getDefaultConfig(): array
2227
{
2328
return [
@@ -123,6 +128,11 @@ public function getForm(): array
123128
]),
124129
self::affixFormFields(),
125130
]),
131+
Tab::make('Rules')
132+
->label(__('Rules'))
133+
->schema([
134+
...parent::getRulesForm(),
135+
]),
126136
])->columnSpanFull(),
127137
];
128138
}

0 commit comments

Comments
 (0)