Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/CustomFieldsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use Relaticle\CustomFields\Providers\FieldTypeServiceProvider;
use Relaticle\CustomFields\Providers\ImportsServiceProvider;
use Relaticle\CustomFields\Providers\ValidationServiceProvider;
use Relaticle\CustomFields\Services\ModelAttributeDiscoveryService;
use Relaticle\CustomFields\Services\TenantContextService;
use Relaticle\CustomFields\Services\ValueResolver\ValueResolver;
use Relaticle\CustomFields\Services\Visibility\BackendVisibilityService;
Expand Down Expand Up @@ -77,6 +78,11 @@ public function bootingPackage(): void
Livewire::component('manage-custom-field', ManageCustomField::class);
Livewire::component('manage-custom-field-width', ManageCustomFieldWidth::class);
Livewire::component('manage-fields-table', ManageFieldsTable::class);

$this->app->terminating(function (): void {
ModelAttributeDiscoveryService::clearCache();
BackendVisibilityService::clearCache();
});
}

public function configurePackage(Package $package): void
Expand Down
9 changes: 8 additions & 1 deletion src/Data/CustomFieldSectionSettingsData.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
<?php

declare(strict_types=1);

namespace Relaticle\CustomFields\Data;

use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;

#[MapName(SnakeCaseMapper::class)]
class CustomFieldSectionSettingsData extends Data {}
class CustomFieldSectionSettingsData extends Data
{
public function __construct(
public ?VisibilityData $visibility = null,
) {}
}
12 changes: 12 additions & 0 deletions src/Data/VisibilityConditionData.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Relaticle\CustomFields\Data;

use Relaticle\CustomFields\Enums\ConditionSource;
use Relaticle\CustomFields\Enums\VisibilityOperator;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
Expand All @@ -16,5 +17,16 @@ public function __construct(
public string $field_code,
public VisibilityOperator $operator,
public mixed $value,
public ConditionSource $source = ConditionSource::CustomField,
) {}

public function isModelAttribute(): bool
{
return $this->source === ConditionSource::ModelAttribute;
}

public function isCustomField(): bool
{
return $this->source === ConditionSource::CustomField;
}
}
49 changes: 44 additions & 5 deletions src/Data/VisibilityData.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

namespace Relaticle\CustomFields\Data;

use Illuminate\Database\Eloquent\Model;
use Relaticle\CustomFields\Enums\CustomFieldsFeature;
use Relaticle\CustomFields\Enums\VisibilityLogic;
use Relaticle\CustomFields\Enums\VisibilityMode;
use Relaticle\CustomFields\FeatureSystem\FeatureManager;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
Expand Down Expand Up @@ -34,17 +37,26 @@ public function requiresConditions(): bool
/**
* @param array<string, mixed> $fieldValues
*/
public function evaluate(array $fieldValues): bool
public function evaluate(array $fieldValues, ?Model $record = null): bool
{
if (! $this->requiresConditions() || ! $this->conditions instanceof DataCollection) {
return $this->mode === VisibilityMode::ALWAYS_VISIBLE;
}

$modelAttributesEnabled = FeatureManager::isEnabled(CustomFieldsFeature::MODEL_ATTRIBUTE_CONDITIONS);

$results = [];

foreach ($this->conditions as $condition) {
$result = $this->evaluateCondition($condition, $fieldValues);
$results[] = $result;
if ($condition->isModelAttribute() && ! $modelAttributesEnabled) {
continue;
}

$results[] = $this->evaluateCondition($condition, $fieldValues, $record);
}

if ($results === []) {
return true;
}

$conditionsMet = $this->logic->evaluate($results);
Expand All @@ -55,8 +67,18 @@ public function evaluate(array $fieldValues): bool
/**
* @param array<string, mixed> $fieldValues
*/
private function evaluateCondition(VisibilityConditionData $condition, array $fieldValues): bool
private function evaluateCondition(VisibilityConditionData $condition, array $fieldValues, ?Model $record = null): bool
{
if ($condition->isModelAttribute()) {
if (! $record instanceof Model) {
return true;
}

$fieldValue = $record->getAttribute($condition->field_code);

return $condition->operator->evaluate($fieldValue, $condition->value);
}

$fieldValue = $fieldValues[$condition->field_code] ?? null;

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

foreach ($this->conditions as $condition) {
$fields[] = $condition->field_code;
if ($condition->isCustomField()) {
$fields[] = $condition->field_code;
}
}

return array_unique($fields);
}

public function hasModelAttributeConditions(): bool
{
if (! $this->conditions instanceof DataCollection) {
return false;
}

foreach ($this->conditions as $condition) {
if ($condition->isModelAttribute()) {
return true;
}
}

return false;
}
}
21 changes: 21 additions & 0 deletions src/Enums/ConditionSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Relaticle\CustomFields\Enums;

use Filament\Support\Contracts\HasLabel;

enum ConditionSource: string implements HasLabel
{
case CustomField = 'custom_field';
case ModelAttribute = 'model_attribute';

public function getLabel(): string
{
return match ($this) {
self::CustomField => 'Custom Field',
self::ModelAttribute => 'Model Attribute',
};
}
}
4 changes: 4 additions & 0 deletions src/Enums/CustomFieldsFeature.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ enum CustomFieldsFeature: string
case FIELD_VALIDATION_RULES = 'field_validation_rules';
case FIELD_DESCRIPTION = 'field_description';

// Visibility features
case MODEL_ATTRIBUTE_CONDITIONS = 'model_attribute_conditions';
case SECTION_CONDITIONAL_VISIBILITY = 'section_conditional_visibility';

// Table/UI integration features
case UI_TABLE_COLUMNS = 'ui_table_columns';
case UI_TABLE_FILTERS = 'ui_table_filters';
Expand Down
7 changes: 5 additions & 2 deletions src/Filament/Integration/Builders/FormBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,16 @@ public function values(): Collection
return $allFields->map($createField);
}

// Resolve record for section visibility (null for create forms)
$record = isset($this->model) && $this->model->exists ? $this->model : null;

return $this->getFilteredSections()
->map(function (CustomFieldSection $section) use ($sectionComponentFactory, $createField) {
->map(function (CustomFieldSection $section) use ($sectionComponentFactory, $createField, $allFields, $record) {
$fields = $section->fields->map($createField);

return $fields->isEmpty()
? null
: $sectionComponentFactory->create($section)->schema($fields->toArray());
: $sectionComponentFactory->create($section, $allFields, $record)->schema($fields->toArray());
})
->filter();
}
Expand Down
75 changes: 71 additions & 4 deletions src/Filament/Integration/Factories/SectionComponentFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,34 @@
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Relaticle\CustomFields\Enums\CustomFieldSectionType;
use Relaticle\CustomFields\Enums\CustomFieldsFeature;
use Relaticle\CustomFields\FeatureSystem\FeatureManager;
use Relaticle\CustomFields\Models\CustomField;
use Relaticle\CustomFields\Models\CustomFieldSection;
use Relaticle\CustomFields\Services\Visibility\BackendVisibilityService;
use Relaticle\CustomFields\Services\Visibility\CoreVisibilityLogicService;
use Relaticle\CustomFields\Services\Visibility\FrontendVisibilityService;

final class SectionComponentFactory
final readonly class SectionComponentFactory
{
public function create(CustomFieldSection $customFieldSection): Section|Fieldset|Grid
{
return match ($customFieldSection->type) {
public function __construct(
private FrontendVisibilityService $frontendVisibilityService,
private BackendVisibilityService $backendVisibilityService,
private CoreVisibilityLogicService $coreLogic,
) {}

/**
* @param Collection<int, CustomField>|null $allFields
*/
public function create(
CustomFieldSection $customFieldSection,
?Collection $allFields = null,
?Model $record = null
): Section|Fieldset|Grid {
$component = match ($customFieldSection->type) {
CustomFieldSectionType::SECTION => Section::make($customFieldSection->name)
->columnSpanFull()
->description($customFieldSection->description)
Expand All @@ -25,5 +45,52 @@ public function create(CustomFieldSection $customFieldSection): Section|Fieldset
->columns(12),
CustomFieldSectionType::HEADLESS => Grid::make(12)->columnSpanFull(),
};

if ($this->shouldApplySectionVisibility($customFieldSection)) {
$this->applySectionVisibility($component, $customFieldSection, $allFields, $record);
}

return $component;
}

private function shouldApplySectionVisibility(CustomFieldSection $section): bool
{
return FeatureManager::isEnabled(CustomFieldsFeature::SECTION_CONDITIONAL_VISIBILITY)
&& $this->coreLogic->hasSectionVisibilityConditions($section);
}

/**
* @param Collection<int, CustomField>|null $allFields
*/
private function applySectionVisibility(
Section|Fieldset|Grid $component,
CustomFieldSection $section,
?Collection $allFields,
?Model $record
): void {
$jsExpression = $this->frontendVisibilityService->buildSectionVisibilityExpression(
$section,
$allFields
);

if ($jsExpression !== null) {
// Use visibleJs only -- do NOT combine with visible()
// Server-side visible(false) prevents the component from rendering,
// which blocks visibleJs from ever executing
$component->visibleJs($jsExpression);

return;
}

// Fallback: server-side evaluation when JS can't be generated
if ($record instanceof Model) {
$component->visible(
fn (): bool => $this->backendVisibilityService->isSectionVisible(
$record,
$section,
$allFields ?? collect()
)
);
}
}
}
Loading