Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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 = new VisibilityData,
) {}
}
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;
}
}
42 changes: 36 additions & 6 deletions src/Data/VisibilityData.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Relaticle\CustomFields\Data;

use Illuminate\Database\Eloquent\Model;
use Relaticle\CustomFields\Enums\ConditionSource;
use Relaticle\CustomFields\Enums\VisibilityLogic;
use Relaticle\CustomFields\Enums\VisibilityMode;
use Spatie\LaravelData\Attributes\DataCollectionOf;
Expand Down Expand Up @@ -34,7 +36,7 @@ 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;
Expand All @@ -43,7 +45,7 @@ public function evaluate(array $fieldValues): bool
$results = [];

foreach ($this->conditions as $condition) {
$result = $this->evaluateCondition($condition, $fieldValues);
$result = $this->evaluateCondition($condition, $fieldValues, $record);
$results[] = $result;
}

Expand All @@ -55,14 +57,22 @@ public function evaluate(array $fieldValues): bool
/**
* @param array<string, mixed> $fieldValues
*/
private function evaluateCondition(VisibilityConditionData $condition, array $fieldValues): bool
{
$fieldValue = $fieldValues[$condition->field_code] ?? null;
private function evaluateCondition(
VisibilityConditionData $condition,
array $fieldValues,
?Model $record = null
): bool {
$fieldValue = match ($condition->source) {
ConditionSource::CustomField => $fieldValues[$condition->field_code] ?? null,
ConditionSource::ModelAttribute => $record?->getAttribute($condition->field_code),
};

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

/**
* Get dependent custom field codes (excludes model attribute conditions).
*
* @return array<int, string>
*/
public function getDependentFields(): array
Expand All @@ -74,9 +84,29 @@ 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);
}

/**
* Check if any conditions reference model attributes.
*/
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 @@ -19,6 +19,10 @@ enum CustomFieldsFeature: string
case FIELD_UNIQUE_VALUE = 'field_unique_value';
case FIELD_VALIDATION_RULES = 'field_validation_rules';

// 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
15 changes: 11 additions & 4 deletions src/Filament/Integration/Base/AbstractFormComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
public function __construct(
protected ValidationService $validationService,
protected CoreVisibilityLogicService $coreVisibilityLogic,
protected FrontendVisibilityService $frontendVisibilityService
protected FrontendVisibilityService $frontendVisibilityService,
) {}

/**
Expand Down Expand Up @@ -141,9 +141,16 @@ private function applyVisibility(
$allFields
);

return in_array($jsExpression, [null, '', '0'], true)
? $field
: $field->live()->visibleJs($jsExpression);
if (in_array($jsExpression, [null, '', '0'], true)) {
return $field;
}

// visibleJs alone handles both initial state (via x-cloak) and reactivity.
// Do NOT combine with visible() — server-side visible(false) prevents the
// component from rendering entirely, which blocks visibleJs from ever executing.
$field->live()->visibleJs($jsExpression);

return $field;
}

/**
Expand Down
9 changes: 7 additions & 2 deletions src/Filament/Integration/Builders/FormBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace Relaticle\CustomFields\Filament\Integration\Builders;

use Filament\Schemas\Components\Grid;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Relaticle\CustomFields\Enums\CustomFieldsFeature;
use Relaticle\CustomFields\FeatureSystem\FeatureManager;
Expand Down Expand Up @@ -78,13 +79,17 @@ public function values(): Collection
return $allFields->map($createField);
}

$record = $this->explicitModel instanceof Model && $this->explicitModel->exists
? $this->explicitModel
: 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
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function __construct(
private Closure $closure,
ValidationService $validationService,
CoreVisibilityLogicService $coreVisibilityLogic,
FrontendVisibilityService $frontendVisibilityService
FrontendVisibilityService $frontendVisibilityService,
) {
parent::__construct($validationService, $coreVisibilityLogic, $frontendVisibilityService);
}
Expand Down
70 changes: 66 additions & 4 deletions src/Filament/Integration/Factories/SectionComponentFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,32 @@
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\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,
) {}

/**
* @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 +43,49 @@ 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 $customFieldSection): bool
{
if (! FeatureManager::isEnabled(CustomFieldsFeature::SECTION_CONDITIONAL_VISIBILITY)) {
return false;
}

$visibility = $customFieldSection->settings->visibility ?? null;

return $visibility?->requiresConditions() ?? false;
}

private function applySectionVisibility(
Section|Fieldset|Grid $component,
CustomFieldSection $section,
?Collection $allFields,
?Model $record
): void {
$jsExpression = $this->frontendVisibilityService->buildSectionVisibilityExpression($section, $allFields);

if (filled($jsExpression)) {
// visibleJs alone handles both initial state (via x-cloak) and reactivity.
// Do NOT combine with visible() — server-side visible(false) prevents the
// component from rendering entirely, which blocks visibleJs from ever executing.
$component->visibleJs($jsExpression);

return;
}

// Fallback to server-side only when no JS expression is available
$visibility = $section->settings->visibility ?? null;

if ($record instanceof Model && $visibility?->hasModelAttributeConditions()) {
$component->visible(
fn () => $this->backendVisibilityService->isSectionVisible($record, $section, $allFields ?? collect())
);
}
}
}
Loading