Skip to content

Commit 440c664

Browse files
committed
feat: extend visibility rules to use models as well
1 parent 5a8c826 commit 440c664

File tree

6 files changed

+535
-61
lines changed

6 files changed

+535
-61
lines changed

README.md

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ return [
8383
'selectable_resources' => [
8484
// App\Filament\Resources\ContentResource::class,
8585
],
86+
87+
// Models that can be used for visibility rules
88+
'visibility_models' => [
89+
\App\Models\Content::class,
90+
\App\Models\User::class,
91+
// Add any models you want to use in visibility conditions
92+
],
8693
];
8794
```
8895

@@ -164,13 +171,115 @@ When no other fields are available for dependency rules, the field selection wil
164171

165172
#### Visibility Rules
166173

167-
Control when fields are shown or hidden based on conditions:
174+
Control when fields are shown or hidden based on conditions. The visibility system supports two types of conditions:
175+
176+
##### Field-Based Conditions
177+
178+
Show/hide fields based on other field values in the same form:
179+
180+
- **Field Selection**: Choose from available fields in the current form
181+
- **Dynamic Options**: Field options update automatically based on available fields
182+
- **Self-Exclusion**: Fields cannot reference themselves to prevent infinite loops
183+
184+
##### Model Attribute Conditions
168185

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
186+
Show/hide fields based on properties of the current record:
172187

173-
The visibility system works seamlessly with validation rules to create intelligent, user-friendly forms.
188+
- **Record Properties**: Access any attribute of the current model instance
189+
- **Model Selection**: Choose from configured models in your application
190+
- **Attribute Discovery**: Automatically discover available attributes from database schema
191+
192+
##### Configuration
193+
194+
To enable model attribute conditions, add your models to the `visibility_models` config array:
195+
196+
```php
197+
return [
198+
// ... other config
199+
200+
// Models that can be used for visibility rules
201+
'visibility_models' => [
202+
\App\Models\Content::class,
203+
\App\Models\User::class,
204+
\App\Models\Category::class,
205+
],
206+
];
207+
```
208+
209+
##### Supported Operators
210+
211+
The visibility system supports a comprehensive set of comparison operators:
212+
213+
- **Equality**: `equals`, `not_equals`
214+
- **Text Operations**: `contains`, `not_contains`, `starts_with`, `ends_with`
215+
- **Empty Checks**: `is_empty`, `is_not_empty`
216+
- **Numeric Comparisons**: `greater_than`, `less_than`, `greater_than_or_equal`, `less_than_or_equal`
217+
- **List Operations**: `in`, `not_in` (comma-separated values)
218+
219+
##### Logical Operators
220+
221+
Combine multiple conditions with logical operators:
222+
223+
- **AND Logic**: All conditions must be met for the field to be visible
224+
- **OR Logic**: Any condition can be met for the field to be visible
225+
226+
##### Example Use Cases
227+
228+
**Content Type-Based Fields**:
229+
```json
230+
{
231+
"logic": "AND",
232+
"conditions": [
233+
{
234+
"source": "model_attribute",
235+
"model": "App\\Models\\Content",
236+
"property": "type_slug",
237+
"operator": "equals",
238+
"value": "article"
239+
}
240+
]
241+
}
242+
```
243+
244+
**Multi-Condition Logic**:
245+
```json
246+
{
247+
"logic": "OR",
248+
"conditions": [
249+
{
250+
"source": "model_attribute",
251+
"model": "App\\Models\\Content",
252+
"property": "status",
253+
"operator": "equals",
254+
"value": "published"
255+
},
256+
{
257+
"source": "field",
258+
"property": "field_ulid_here",
259+
"operator": "equals",
260+
"value": "draft"
261+
}
262+
]
263+
}
264+
```
265+
266+
**Hide on Specific Pages**:
267+
```json
268+
{
269+
"logic": "AND",
270+
"conditions": [
271+
{
272+
"source": "model_attribute",
273+
"model": "App\\Models\\Content",
274+
"property": "slug",
275+
"operator": "not_equals",
276+
"value": "home"
277+
}
278+
]
279+
}
280+
```
281+
282+
The visibility system works seamlessly with validation rules to create intelligent, user-friendly forms that adapt to your data and user interactions.
174283

175284
### Making a resource page configurable
176285

@@ -382,6 +491,10 @@ The package includes a powerful Rich Editor with custom plugins:
382491

383492
- **[Jump Anchor Plugin](docs/jump-anchor-plugin.md)** - Add anchor links to selected text for navigation and jumping to specific sections
384493

494+
### Field Configuration
495+
496+
- **[Visibility Rules](docs/visibility-rules.md)** - Comprehensive guide to controlling field visibility based on conditions and record properties
497+
385498
## Testing
386499

387500
```bash

config/backstage/fields.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,11 @@
2121
'selectable_resources' => [
2222
// App\Filament\Resources\ContentResource::class,
2323
],
24+
25+
// Models that can be used for visibility rules
26+
'visibility_models' => [
27+
\Backstage\Models\Content::class,
28+
\Backstage\Models\Type::class,
29+
// \App\Models\User::class,
30+
],
2431
];

src/Fields/FormSchemas/VisibilityRulesSchema.php

Lines changed: 140 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
namespace Backstage\Fields\Fields\FormSchemas;
44

55
use Backstage\Fields\Fields\Helpers\FieldOptionsHelper;
6+
use Backstage\Fields\Fields\Helpers\ModelAttributeHelper;
67
use Backstage\Fields\Models\Field;
78
use Filament\Forms\Components\Repeater;
89
use Filament\Forms\Components\Select;
910
use Filament\Forms\Components\TextInput;
11+
use Filament\Schemas\Components\Grid;
1012
use Filament\Schemas\Components\Section;
1113
use Filament\Schemas\Components\Utilities\Get;
1214

@@ -15,73 +17,163 @@ class VisibilityRulesSchema
1517
public static function make(): array
1618
{
1719
return [
18-
Section::make('Visibility rules')
20+
Section::make('Show/Hide Rules')
1921
->collapsible()
2022
->collapsed(false)
2123
->columnSpanFull()
2224
->compact(true)
23-
->description(__('Show or hide this field based on the value of another field'))
25+
->description(__('Control when this field appears based on other field values or record properties'))
2426
->schema([
2527
Repeater::make('config.visibilityRules')
2628
->hiddenLabel()
2729
->schema([
2830
Select::make('logic')
29-
->label(__('Logic'))
31+
->label(__('When to show this field'))
3032
->options([
31-
'AND' => __('All conditions must be true (AND)'),
32-
'OR' => __('Any condition can be true (OR)'),
33+
'AND' => __('All conditions must be met'),
34+
'OR' => __('Any condition can be met'),
3335
])
3436
->default('AND')
3537
->required(),
3638
Repeater::make('conditions')
3739
->hiddenLabel()
3840
->schema([
39-
Select::make('field')
40-
->label(__('Field'))
41-
->placeholder(__('Select a field'))
42-
->searchable()
43-
->live()
44-
->options(function ($livewire) {
45-
$excludeUlid = null;
46-
if (method_exists($livewire, 'getMountedTableActionRecord')) {
47-
$record = $livewire->getMountedTableActionRecord();
48-
if ($record && isset($record->ulid)) {
49-
$excludeUlid = $record->ulid;
50-
}
51-
}
52-
53-
return FieldOptionsHelper::getFieldOptions($livewire, $excludeUlid);
54-
})
55-
->required(),
56-
Select::make('operator')
57-
->label(__('Condition'))
58-
->live()
59-
->options([
60-
'equals' => __('Equals'),
61-
'not_equals' => __('Does not equal'),
62-
'contains' => __('Contains'),
63-
'not_contains' => __('Does not contain'),
64-
'starts_with' => __('Starts with'),
65-
'ends_with' => __('Ends with'),
66-
'is_empty' => __('Is empty'),
67-
'is_not_empty' => __('Is not empty'),
68-
'greater_than' => __('Greater than'),
69-
'less_than' => __('Less than'),
70-
'greater_than_or_equal' => __('Greater than or equal'),
71-
'less_than_or_equal' => __('Less than or equal'),
72-
'in' => __('In list'),
73-
'not_in' => __('Not in list'),
74-
])
75-
->required(),
76-
TextInput::make('value')
77-
->label(__('Value'))
78-
->visible(fn (Get $get): bool => ! in_array($get('operator'), ['is_empty', 'is_not_empty'])),
41+
Grid::make(2)
42+
->columnSpanFull()
43+
->schema([
44+
Select::make('source')
45+
->label(__('Check'))
46+
->options([
47+
'field' => __('Another field'),
48+
'model_attribute' => __('Record property'),
49+
])
50+
->default('field')
51+
->live()
52+
->required()
53+
->columnSpan(1),
54+
55+
Select::make('field')
56+
->label(__('Which field'))
57+
->placeholder(__('Choose a field'))
58+
->searchable()
59+
->live()
60+
->visible(fn (Get $get): bool => $get('source') === 'field')
61+
->options(function ($livewire) {
62+
$excludeUlid = null;
63+
if (method_exists($livewire, 'getMountedTableActionRecord')) {
64+
$record = $livewire->getMountedTableActionRecord();
65+
if ($record && isset($record->ulid)) {
66+
$excludeUlid = $record->ulid;
67+
}
68+
}
69+
70+
return FieldOptionsHelper::getFieldOptions($livewire, $excludeUlid);
71+
})
72+
->required(fn (Get $get): bool => $get('source') === 'field')
73+
->columnSpan(1),
74+
75+
Select::make('model')
76+
->label(__('Record type'))
77+
->placeholder(__('Choose record type'))
78+
->searchable()
79+
->visible(fn (Get $get): bool => $get('source') === 'model_attribute')
80+
->options(function () {
81+
return ModelAttributeHelper::getAvailableModels();
82+
})
83+
->live()
84+
->required(fn (Get $get): bool => $get('source') === 'model_attribute')
85+
->columnSpan(1),
86+
]),
87+
88+
Grid::make(3)
89+
->columnSpanFull()
90+
->schema([
91+
Select::make('property')
92+
->label(__('Property'))
93+
->placeholder(function (Get $get): string {
94+
return $get('source') === 'field'
95+
? __('Choose a field')
96+
: __('Choose a property');
97+
})
98+
->searchable()
99+
->visible(
100+
fn (Get $get): bool => ($get('source') === 'field') ||
101+
($get('source') === 'model_attribute')
102+
)
103+
->options(function (Get $get, $livewire) {
104+
if ($get('source') === 'field') {
105+
$excludeUlid = null;
106+
if (method_exists($livewire, 'getMountedTableActionRecord')) {
107+
$record = $livewire->getMountedTableActionRecord();
108+
if ($record && isset($record->ulid)) {
109+
$excludeUlid = $record->ulid;
110+
}
111+
}
112+
113+
return FieldOptionsHelper::getFieldOptions($livewire, $excludeUlid);
114+
}
115+
116+
if ($get('source') === 'model_attribute') {
117+
$modelClass = $get('model');
118+
if (! $modelClass) {
119+
return [];
120+
}
121+
122+
return ModelAttributeHelper::getModelAttributesForModel($modelClass);
123+
}
124+
125+
return [];
126+
})
127+
->required(
128+
fn (Get $get): bool => ($get('source') === 'field') ||
129+
($get('source') === 'model_attribute' && $get('model'))
130+
)
131+
->columnSpan(1),
132+
133+
Select::make('operator')
134+
->label(__('Is'))
135+
->live()
136+
->options([
137+
'equals' => __('equal to'),
138+
'not_equals' => __('not equal to'),
139+
'contains' => __('containing'),
140+
'not_contains' => __('not containing'),
141+
'starts_with' => __('starting with'),
142+
'ends_with' => __('ending with'),
143+
'is_empty' => __('empty'),
144+
'is_not_empty' => __('not empty'),
145+
'greater_than' => __('greater than'),
146+
'less_than' => __('less than'),
147+
'greater_than_or_equal' => __('greater than or equal to'),
148+
'less_than_or_equal' => __('less than or equal to'),
149+
'in' => __('one of'),
150+
'not_in' => __('not one of'),
151+
])
152+
->required()
153+
->columnSpan(1),
154+
155+
TextInput::make('value')
156+
->label(__('This value'))
157+
->placeholder(__('Enter the value to check'))
158+
->visible(fn (Get $get): bool => ! in_array($get('operator'), ['is_empty', 'is_not_empty']))
159+
->columnSpan(1),
160+
]),
79161
])
80162
->collapsible()
81163
->itemLabel(function (array $state): ?string {
164+
if (isset($state['source']) && $state['source'] === 'model_attribute') {
165+
if (isset($state['model']) && isset($state['property'])) {
166+
$modelName = class_basename($state['model']);
167+
$attributeName = ucfirst(str_replace('_', ' ', $state['property']));
168+
169+
return "{$modelName} {$attributeName}";
170+
}
171+
172+
return 'Record property';
173+
}
82174

83-
if (isset($state['field'])) {
84-
$field = Field::find($state['field']);
175+
if (isset($state['source']) && $state['source'] === 'field' && isset($state['property'])) {
176+
$field = Field::find($state['property']);
85177

86178
return $field->name ?? null;
87179
}
@@ -94,7 +186,7 @@ public static function make(): array
94186
->columnSpanFull(),
95187
])
96188
->collapsible()
97-
->itemLabel(fn (array $state): string => __('Visibility Rule'))
189+
->itemLabel(fn (array $state): string => __('Show/Hide Rule'))
98190
->defaultItems(0)
99191
->maxItems(1)
100192
->reorderableWithButtons()

0 commit comments

Comments
 (0)