Skip to content

Commit a99c605

Browse files
committed
visibility rules (wip)
1 parent 23d4c5a commit a99c605

File tree

3 files changed

+221
-4
lines changed

3 files changed

+221
-4
lines changed

src/Fields/Base.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
use Backstage\Fields\Fields\FormSchemas\BasicSettingsSchema;
77
use Backstage\Fields\Fields\FormSchemas\ConditionalLogicSchema;
88
use Backstage\Fields\Fields\FormSchemas\ValidationRulesSchema;
9-
use Backstage\Fields\Fields\Helpers\FieldOptionsHelper;
9+
use Backstage\Fields\Fields\FormSchemas\VisibilityRulesSchema;
1010
use Backstage\Fields\Fields\Logic\ConditionalLogicApplier;
11+
use Backstage\Fields\Fields\Logic\VisibilityLogicApplier;
1112
use Backstage\Fields\Fields\Validation\ValidationRuleApplier;
1213
use Backstage\Fields\Models\Field;
1314
use Filament\Forms;
@@ -20,16 +21,14 @@ public function getForm(): array
2021
return BasicSettingsSchema::make();
2122
}
2223

23-
/**
24-
* Get the Rules form schema for conditional logic
25-
*/
2624
public function getRulesForm(): array
2725
{
2826
return [
2927
Forms\Components\Grid::make(2)
3028
->schema([
3129
...ConditionalLogicSchema::make(),
3230
...ValidationRulesSchema::make(),
31+
...VisibilityRulesSchema::make(),
3332
]),
3433
];
3534
}
@@ -49,6 +48,7 @@ public static function getDefaultConfig(): array
4948
'conditionalValue' => null,
5049
'conditionalAction' => null,
5150
'validationRules' => [],
51+
'visibilityRules' => [],
5252
];
5353
}
5454

@@ -69,6 +69,7 @@ public static function applyDefaultSettings($input, ?Field $field = null)
6969

7070
$input = ConditionalLogicApplier::applyConditionalLogic($input, $field);
7171
$input = ConditionalLogicApplier::applyConditionalValidation($input, $field);
72+
$input = VisibilityLogicApplier::applyVisibilityLogic($input, $field);
7273
$input = self::applyAdditionalValidation($input, $field);
7374

7475
return $input;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
namespace Backstage\Fields\Fields\FormSchemas;
4+
5+
use Backstage\Fields\Fields\Helpers\FieldOptionsHelper;
6+
use Filament\Forms;
7+
8+
class VisibilityRulesSchema
9+
{
10+
public static function make(): array
11+
{
12+
return [
13+
Forms\Components\Fieldset::make('Visibility rules')
14+
->schema([
15+
Forms\Components\Repeater::make('config.visibilityRules')
16+
->hiddenLabel()
17+
->schema([
18+
Forms\Components\Select::make('logic')
19+
->label(__('Logic'))
20+
->options([
21+
'AND' => __('All conditions must be true (AND)'),
22+
'OR' => __('Any condition can be true (OR)'),
23+
])
24+
->default('AND')
25+
->required(),
26+
Forms\Components\Repeater::make('conditions')
27+
->hiddenLabel()
28+
->schema([
29+
Forms\Components\Select::make('field')
30+
->label(__('Field'))
31+
->placeholder(__('Select a field'))
32+
->searchable()
33+
->live()
34+
->options(function ($livewire) {
35+
$excludeUlid = null;
36+
if (method_exists($livewire, 'getMountedTableActionRecord')) {
37+
$record = $livewire->getMountedTableActionRecord();
38+
if ($record && isset($record->ulid)) {
39+
$excludeUlid = $record->ulid;
40+
}
41+
}
42+
return FieldOptionsHelper::getFieldOptions($livewire, $excludeUlid);
43+
})
44+
->required(),
45+
Forms\Components\Select::make('operator')
46+
->label(__('Condition'))
47+
->options([
48+
'equals' => __('Equals'),
49+
'not_equals' => __('Does not equal'),
50+
'contains' => __('Contains'),
51+
'not_contains' => __('Does not contain'),
52+
'starts_with' => __('Starts with'),
53+
'ends_with' => __('Ends with'),
54+
'is_empty' => __('Is empty'),
55+
'is_not_empty' => __('Is not empty'),
56+
'greater_than' => __('Greater than'),
57+
'less_than' => __('Less than'),
58+
'greater_than_or_equal' => __('Greater than or equal'),
59+
'less_than_or_equal' => __('Less than or equal'),
60+
'in' => __('In list'),
61+
'not_in' => __('Not in list'),
62+
])
63+
->required(),
64+
Forms\Components\TextInput::make('value')
65+
->label(__('Value'))
66+
->visible(fn (Forms\Get $get): bool => !in_array($get('operator'), ['is_empty', 'is_not_empty'])),
67+
])
68+
->collapsible()
69+
->itemLabel(fn (array $state): ?string => $state['field'] ?? null)
70+
->defaultItems(1)
71+
->columns(3)
72+
->reorderableWithButtons()
73+
->columnSpanFull(),
74+
])
75+
->collapsible()
76+
->itemLabel(fn (array $state): ?string => 'Visibility Rule')
77+
->defaultItems(0)
78+
->reorderableWithButtons()
79+
->columnSpanFull(),
80+
]),
81+
];
82+
}
83+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
namespace Backstage\Fields\Fields\Logic;
4+
5+
use Backstage\Fields\Fields\Helpers\FieldOptionsHelper;
6+
use Backstage\Fields\Models\Field;
7+
use Filament\Forms;
8+
9+
class VisibilityLogicApplier
10+
{
11+
public static function applyVisibilityLogic($input, ?Field $field = null): mixed
12+
{
13+
if (! $field || empty($field->config['visibilityRules'])) {
14+
return $input;
15+
}
16+
17+
$visibilityRules = $field->config['visibilityRules'];
18+
19+
$input->visible(function (Forms\Get $get) use ($visibilityRules, $field): bool {
20+
return self::evaluateVisibilityRules($get, $visibilityRules, $field);
21+
});
22+
23+
return $input;
24+
}
25+
26+
protected static function evaluateVisibilityRules(Forms\Get $get, array $visibilityRules, Field $field): bool
27+
{
28+
foreach ($visibilityRules as $rule) {
29+
$logic = $rule['logic'] ?? 'AND';
30+
$conditions = $rule['conditions'] ?? [];
31+
32+
if (empty($conditions)) {
33+
continue;
34+
}
35+
36+
$ruleResult = self::evaluateRuleConditions($get, $conditions, $logic, $field);
37+
38+
// If any rule evaluates to false, the field should be hidden
39+
if (! $ruleResult) {
40+
return false;
41+
}
42+
}
43+
44+
// If all rules evaluate to true, the field should be visible
45+
return true;
46+
}
47+
48+
protected static function evaluateRuleConditions(Forms\Get $get, array $conditions, string $logic, Field $field): bool
49+
{
50+
$results = [];
51+
52+
foreach ($conditions as $condition) {
53+
$fieldUlid = $condition['field'] ?? '';
54+
$operator = $condition['operator'] ?? 'equals';
55+
$value = $condition['value'] ?? null;
56+
57+
$fieldName = FieldOptionsHelper::getFieldNameFromUlid($fieldUlid, $field);
58+
59+
if (! $fieldName) {
60+
continue;
61+
}
62+
63+
$fieldValue = $get($fieldName);
64+
$results[] = self::evaluateCondition($fieldValue, $operator, $value);
65+
}
66+
67+
if (empty($results)) {
68+
return true;
69+
}
70+
71+
return $logic === 'AND'
72+
? !in_array(false, $results, true) // All must be true
73+
: in_array(true, $results, true); // At least one must be true
74+
}
75+
76+
protected static function evaluateCondition($fieldValue, string $operator, $expectedValue): bool
77+
{
78+
switch ($operator) {
79+
case 'equals':
80+
return $fieldValue == $expectedValue;
81+
82+
case 'not_equals':
83+
return $fieldValue != $expectedValue;
84+
85+
case 'contains':
86+
return is_string($fieldValue) && str_contains($fieldValue, $expectedValue);
87+
88+
case 'not_contains':
89+
return is_string($fieldValue) && ! str_contains($fieldValue, $expectedValue);
90+
91+
case 'starts_with':
92+
return is_string($fieldValue) && str_starts_with($fieldValue, $expectedValue);
93+
94+
case 'ends_with':
95+
return is_string($fieldValue) && str_ends_with($fieldValue, $expectedValue);
96+
97+
case 'is_empty':
98+
return empty($fieldValue);
99+
100+
case 'is_not_empty':
101+
return ! empty($fieldValue);
102+
103+
case 'greater_than':
104+
return is_numeric($fieldValue) && is_numeric($expectedValue) && $fieldValue > $expectedValue;
105+
106+
case 'less_than':
107+
return is_numeric($fieldValue) && is_numeric($expectedValue) && $fieldValue < $expectedValue;
108+
109+
case 'greater_than_or_equal':
110+
return is_numeric($fieldValue) && is_numeric($expectedValue) && $fieldValue >= $expectedValue;
111+
112+
case 'less_than_or_equal':
113+
return is_numeric($fieldValue) && is_numeric($expectedValue) && $fieldValue <= $expectedValue;
114+
115+
case 'in':
116+
if (!is_string($expectedValue)) {
117+
return false;
118+
}
119+
$allowedValues = array_map('trim', explode(',', $expectedValue));
120+
return in_array($fieldValue, $allowedValues);
121+
122+
case 'not_in':
123+
if (!is_string($expectedValue)) {
124+
return true;
125+
}
126+
$excludedValues = array_map('trim', explode(',', $expectedValue));
127+
return !in_array($fieldValue, $excludedValues);
128+
129+
default:
130+
return false;
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)