Skip to content

Commit c23c51f

Browse files
Merge pull request #74 from relaticle/feat/model-attribute-conditions-v4-compat
feat: model attribute conditions + section conditional visibility
2 parents e1238dd + 1542d4b commit c23c51f

22 files changed

Lines changed: 1963 additions & 100 deletions

src/CustomFieldsServiceProvider.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Relaticle\CustomFields\Providers\FieldTypeServiceProvider;
3333
use Relaticle\CustomFields\Providers\ImportsServiceProvider;
3434
use Relaticle\CustomFields\Providers\ValidationServiceProvider;
35+
use Relaticle\CustomFields\Services\ModelAttributeDiscoveryService;
3536
use Relaticle\CustomFields\Services\TenantContextService;
3637
use Relaticle\CustomFields\Services\ValueResolver\ValueResolver;
3738
use Relaticle\CustomFields\Services\Visibility\BackendVisibilityService;
@@ -77,6 +78,11 @@ public function bootingPackage(): void
7778
Livewire::component('manage-custom-field', ManageCustomField::class);
7879
Livewire::component('manage-custom-field-width', ManageCustomFieldWidth::class);
7980
Livewire::component('manage-fields-table', ManageFieldsTable::class);
81+
82+
$this->app->terminating(function (): void {
83+
ModelAttributeDiscoveryService::clearCache();
84+
BackendVisibilityService::clearCache();
85+
});
8086
}
8187

8288
public function configurePackage(Package $package): void
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace Relaticle\CustomFields\Data;
46

57
use Spatie\LaravelData\Attributes\MapName;
68
use Spatie\LaravelData\Data;
79
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
810

911
#[MapName(SnakeCaseMapper::class)]
10-
class CustomFieldSectionSettingsData extends Data {}
12+
class CustomFieldSectionSettingsData extends Data
13+
{
14+
public function __construct(
15+
public ?VisibilityData $visibility = null,
16+
) {}
17+
}

src/Data/VisibilityConditionData.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Relaticle\CustomFields\Data;
66

7+
use Relaticle\CustomFields\Enums\ConditionSource;
78
use Relaticle\CustomFields\Enums\VisibilityOperator;
89
use Spatie\LaravelData\Attributes\MapName;
910
use Spatie\LaravelData\Data;
@@ -16,5 +17,16 @@ public function __construct(
1617
public string $field_code,
1718
public VisibilityOperator $operator,
1819
public mixed $value,
20+
public ConditionSource $source = ConditionSource::CustomField,
1921
) {}
22+
23+
public function isModelAttribute(): bool
24+
{
25+
return $this->source === ConditionSource::ModelAttribute;
26+
}
27+
28+
public function isCustomField(): bool
29+
{
30+
return $this->source === ConditionSource::CustomField;
31+
}
2032
}

src/Data/VisibilityData.php

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
namespace Relaticle\CustomFields\Data;
66

7+
use Illuminate\Database\Eloquent\Model;
8+
use Relaticle\CustomFields\Enums\CustomFieldsFeature;
79
use Relaticle\CustomFields\Enums\VisibilityLogic;
810
use Relaticle\CustomFields\Enums\VisibilityMode;
11+
use Relaticle\CustomFields\FeatureSystem\FeatureManager;
912
use Spatie\LaravelData\Attributes\DataCollectionOf;
1013
use Spatie\LaravelData\Attributes\MapName;
1114
use Spatie\LaravelData\Data;
@@ -34,17 +37,26 @@ public function requiresConditions(): bool
3437
/**
3538
* @param array<string, mixed> $fieldValues
3639
*/
37-
public function evaluate(array $fieldValues): bool
40+
public function evaluate(array $fieldValues, ?Model $record = null): bool
3841
{
3942
if (! $this->requiresConditions() || ! $this->conditions instanceof DataCollection) {
4043
return $this->mode === VisibilityMode::ALWAYS_VISIBLE;
4144
}
4245

46+
$modelAttributesEnabled = FeatureManager::isEnabled(CustomFieldsFeature::MODEL_ATTRIBUTE_CONDITIONS);
47+
4348
$results = [];
4449

4550
foreach ($this->conditions as $condition) {
46-
$result = $this->evaluateCondition($condition, $fieldValues);
47-
$results[] = $result;
51+
if ($condition->isModelAttribute() && ! $modelAttributesEnabled) {
52+
continue;
53+
}
54+
55+
$results[] = $this->evaluateCondition($condition, $fieldValues, $record);
56+
}
57+
58+
if ($results === []) {
59+
return true;
4860
}
4961

5062
$conditionsMet = $this->logic->evaluate($results);
@@ -55,8 +67,18 @@ public function evaluate(array $fieldValues): bool
5567
/**
5668
* @param array<string, mixed> $fieldValues
5769
*/
58-
private function evaluateCondition(VisibilityConditionData $condition, array $fieldValues): bool
70+
private function evaluateCondition(VisibilityConditionData $condition, array $fieldValues, ?Model $record = null): bool
5971
{
72+
if ($condition->isModelAttribute()) {
73+
if (! $record instanceof Model) {
74+
return true;
75+
}
76+
77+
$fieldValue = $record->getAttribute($condition->field_code);
78+
79+
return $condition->operator->evaluate($fieldValue, $condition->value);
80+
}
81+
6082
$fieldValue = $fieldValues[$condition->field_code] ?? null;
6183

6284
return $condition->operator->evaluate($fieldValue, $condition->value);
@@ -74,9 +96,26 @@ public function getDependentFields(): array
7496
$fields = [];
7597

7698
foreach ($this->conditions as $condition) {
77-
$fields[] = $condition->field_code;
99+
if ($condition->isCustomField()) {
100+
$fields[] = $condition->field_code;
101+
}
78102
}
79103

80104
return array_unique($fields);
81105
}
106+
107+
public function hasModelAttributeConditions(): bool
108+
{
109+
if (! $this->conditions instanceof DataCollection) {
110+
return false;
111+
}
112+
113+
foreach ($this->conditions as $condition) {
114+
if ($condition->isModelAttribute()) {
115+
return true;
116+
}
117+
}
118+
119+
return false;
120+
}
82121
}

src/Enums/ConditionSource.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Relaticle\CustomFields\Enums;
6+
7+
use Filament\Support\Contracts\HasLabel;
8+
9+
enum ConditionSource: string implements HasLabel
10+
{
11+
case CustomField = 'custom_field';
12+
case ModelAttribute = 'model_attribute';
13+
14+
public function getLabel(): string
15+
{
16+
return match ($this) {
17+
self::CustomField => 'Custom Field',
18+
self::ModelAttribute => 'Model Attribute',
19+
};
20+
}
21+
}

src/Enums/CustomFieldsFeature.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ enum CustomFieldsFeature: string
2020
case FIELD_VALIDATION_RULES = 'field_validation_rules';
2121
case FIELD_DESCRIPTION = 'field_description';
2222

23+
// Visibility features
24+
case MODEL_ATTRIBUTE_CONDITIONS = 'model_attribute_conditions';
25+
case SECTION_CONDITIONAL_VISIBILITY = 'section_conditional_visibility';
26+
2327
// Table/UI integration features
2428
case UI_TABLE_COLUMNS = 'ui_table_columns';
2529
case UI_TABLE_FILTERS = 'ui_table_filters';

src/Filament/Integration/Builders/FormBuilder.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,16 @@ public function values(): Collection
8888
return $allFields->map($createField);
8989
}
9090

91+
// Resolve record for section visibility (null for create forms)
92+
$record = isset($this->model) && $this->model->exists ? $this->model : null;
93+
9194
return $this->getFilteredSections()
92-
->map(function (CustomFieldSection $section) use ($sectionComponentFactory, $createField) {
95+
->map(function (CustomFieldSection $section) use ($sectionComponentFactory, $createField, $allFields, $record) {
9396
$fields = $section->fields->map($createField);
9497

9598
return $fields->isEmpty()
9699
? null
97-
: $sectionComponentFactory->create($section)->schema($fields->toArray());
100+
: $sectionComponentFactory->create($section, $allFields, $record)->schema($fields->toArray());
98101
})
99102
->filter();
100103
}

src/Filament/Integration/Factories/SectionComponentFactory.php

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,34 @@
77
use Filament\Schemas\Components\Fieldset;
88
use Filament\Schemas\Components\Grid;
99
use Filament\Schemas\Components\Section;
10+
use Illuminate\Database\Eloquent\Model;
11+
use Illuminate\Support\Collection;
1012
use Relaticle\CustomFields\Enums\CustomFieldSectionType;
13+
use Relaticle\CustomFields\Enums\CustomFieldsFeature;
14+
use Relaticle\CustomFields\FeatureSystem\FeatureManager;
15+
use Relaticle\CustomFields\Models\CustomField;
1116
use Relaticle\CustomFields\Models\CustomFieldSection;
17+
use Relaticle\CustomFields\Services\Visibility\BackendVisibilityService;
18+
use Relaticle\CustomFields\Services\Visibility\CoreVisibilityLogicService;
19+
use Relaticle\CustomFields\Services\Visibility\FrontendVisibilityService;
1220

13-
final class SectionComponentFactory
21+
final readonly class SectionComponentFactory
1422
{
15-
public function create(CustomFieldSection $customFieldSection): Section|Fieldset|Grid
16-
{
17-
return match ($customFieldSection->type) {
23+
public function __construct(
24+
private FrontendVisibilityService $frontendVisibilityService,
25+
private BackendVisibilityService $backendVisibilityService,
26+
private CoreVisibilityLogicService $coreLogic,
27+
) {}
28+
29+
/**
30+
* @param Collection<int, CustomField>|null $allFields
31+
*/
32+
public function create(
33+
CustomFieldSection $customFieldSection,
34+
?Collection $allFields = null,
35+
?Model $record = null
36+
): Section|Fieldset|Grid {
37+
$component = match ($customFieldSection->type) {
1838
CustomFieldSectionType::SECTION => Section::make($customFieldSection->name)
1939
->columnSpanFull()
2040
->description($customFieldSection->description)
@@ -25,5 +45,52 @@ public function create(CustomFieldSection $customFieldSection): Section|Fieldset
2545
->columns(12),
2646
CustomFieldSectionType::HEADLESS => Grid::make(12)->columnSpanFull(),
2747
};
48+
49+
if ($this->shouldApplySectionVisibility($customFieldSection)) {
50+
$this->applySectionVisibility($component, $customFieldSection, $allFields, $record);
51+
}
52+
53+
return $component;
54+
}
55+
56+
private function shouldApplySectionVisibility(CustomFieldSection $section): bool
57+
{
58+
return FeatureManager::isEnabled(CustomFieldsFeature::SECTION_CONDITIONAL_VISIBILITY)
59+
&& $this->coreLogic->hasSectionVisibilityConditions($section);
60+
}
61+
62+
/**
63+
* @param Collection<int, CustomField>|null $allFields
64+
*/
65+
private function applySectionVisibility(
66+
Section|Fieldset|Grid $component,
67+
CustomFieldSection $section,
68+
?Collection $allFields,
69+
?Model $record
70+
): void {
71+
$jsExpression = $this->frontendVisibilityService->buildSectionVisibilityExpression(
72+
$section,
73+
$allFields
74+
);
75+
76+
if ($jsExpression !== null) {
77+
// Use visibleJs only -- do NOT combine with visible()
78+
// Server-side visible(false) prevents the component from rendering,
79+
// which blocks visibleJs from ever executing
80+
$component->visibleJs($jsExpression);
81+
82+
return;
83+
}
84+
85+
// Fallback: server-side evaluation when JS can't be generated
86+
if ($record instanceof Model) {
87+
$component->visible(
88+
fn (): bool => $this->backendVisibilityService->isSectionVisible(
89+
$record,
90+
$section,
91+
$allFields ?? collect()
92+
)
93+
);
94+
}
2895
}
2996
}

0 commit comments

Comments
 (0)