Skip to content
5 changes: 5 additions & 0 deletions resources/lang/en/custom-fields.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
'code' => 'Code',
'code_helper_text' => 'Unique code to identify this field throughout the resource.',
'description' => 'Description',
'description_position' => 'Description Position',
'description_position_options' => [
'below' => 'Below field',
'above' => 'Above field',
],
'settings' => 'Settings',
'encrypted' => 'Encrypted',
'encrypted_help' => 'When enabled, this field\'s values will be stored securely using encryption.',
Expand Down
2 changes: 2 additions & 0 deletions src/Data/CustomFieldSettingsData.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Relaticle\CustomFields\Data;

use Relaticle\CustomFields\Enums\CustomFieldsFeature;
use Relaticle\CustomFields\Enums\DescriptionPosition;
use Relaticle\CustomFields\FeatureSystem\FeatureManager;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
Expand All @@ -24,6 +25,7 @@ public function __construct(
public int $max_values = 1,
public bool $unique_per_entity_type = false,
public ?string $description = null,
public ?DescriptionPosition $descriptionPosition = null,
public VisibilityData $visibility = new VisibilityData,
public array $additional = [],
) {
Expand Down
1 change: 1 addition & 0 deletions src/Enums/CustomFieldsFeature.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum CustomFieldsFeature: string
case FIELD_UNIQUE_VALUE = 'field_unique_value';
case FIELD_VALIDATION_RULES = 'field_validation_rules';
case FIELD_DESCRIPTION = 'field_description';
case FIELD_DESCRIPTION_POSITION = 'field_description_position';

// Visibility features
case MODEL_ATTRIBUTE_CONDITIONS = 'model_attribute_conditions';
Expand Down
21 changes: 21 additions & 0 deletions src/Enums/DescriptionPosition.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 DescriptionPosition: string implements HasLabel
{
case BELOW = 'below';
case ABOVE = 'above';

public function getLabel(): string
{
return match ($this) {
self::BELOW => __('custom-fields::custom-fields.field.form.description_position_options.below'),
self::ABOVE => __('custom-fields::custom-fields.field.form.description_position_options.above'),
};
}
}
20 changes: 16 additions & 4 deletions src/Filament/Integration/Base/AbstractFormComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
namespace Relaticle\CustomFields\Filament\Integration\Base;

use Filament\Forms\Components\Field;
use Filament\Schemas\Components\Text;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Relaticle\CustomFields\Contracts\FormComponentInterface;
use Relaticle\CustomFields\Enums\CustomFieldsFeature;
use Relaticle\CustomFields\Enums\DescriptionPosition;
use Relaticle\CustomFields\FeatureSystem\FeatureManager;
use Relaticle\CustomFields\Models\CustomField;
use Relaticle\CustomFields\Services\ValidationService;
Expand Down Expand Up @@ -55,10 +57,20 @@ protected function configure(
$field
->name($customField->getFieldName())
->label($customField->name)
->helperText(
FeatureManager::isEnabled(CustomFieldsFeature::FIELD_DESCRIPTION)
? ($customField->settings->description ?? null)
: null
->when(
FeatureManager::isEnabled(CustomFieldsFeature::FIELD_DESCRIPTION) &&
filled($customField->settings->description),
function (Field $field) use ($customField): Field {
$description = $customField->settings->description;
$position = $customField->settings->descriptionPosition;

if (FeatureManager::isEnabled(CustomFieldsFeature::FIELD_DESCRIPTION_POSITION) &&
$position === DescriptionPosition::ABOVE) {
return $field->aboveContent(fn (): Text => Text::make($description));
}

return $field->helperText($description);
}
)
->afterStateHydrated(
fn (mixed $component, mixed $state, mixed $record): mixed => $component->state(
Expand Down
10 changes: 10 additions & 0 deletions src/Filament/Management/Schemas/FieldForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Relaticle\CustomFields\Contracts\ValidationCapability;
use Relaticle\CustomFields\CustomFields;
use Relaticle\CustomFields\Enums\CustomFieldsFeature;
use Relaticle\CustomFields\Enums\DescriptionPosition;
use Relaticle\CustomFields\Facades\CustomFieldsType;
use Relaticle\CustomFields\Facades\Entities;
use Relaticle\CustomFields\FeatureSystem\FeatureManager;
Expand Down Expand Up @@ -273,8 +274,17 @@ public static function schema(bool $withOptionsRelationship = true): array
->label(__('custom-fields::custom-fields.field.form.description'))
->maxLength(255)
->rows(2)
->live(onBlur: true)
->columnSpanFull()
->visible(fn (): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_DESCRIPTION)),
Select::make('settings.description_position')
->label(__('custom-fields::custom-fields.field.form.description_position'))
->options(DescriptionPosition::class)
->placeholder(__('custom-fields::custom-fields.field.form.description_position_options.below'))
->visible(fn (Get $get): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_DESCRIPTION) &&
FeatureManager::isEnabled(CustomFieldsFeature::FIELD_DESCRIPTION_POSITION) &&
filled($get('settings.description'))
),
Fieldset::make(
__(
'custom-fields::custom-fields.field.form.settings'
Expand Down
4 changes: 2 additions & 2 deletions src/Models/CustomField.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,9 @@ public function getCurrencySettings(): CurrencyFieldSettingsData
return CurrencyFieldSettingsData::fromAdditional($additional);
}

public function getDecimalPlaces(int $default = 2): int
public function getDecimalPlaces(): int
{
return $this->getCurrencySettings()->decimalPlaces ?? $default;
return $this->getCurrencySettings()->decimalPlaces;
}

public function getCurrencyCode(): string
Expand Down
36 changes: 10 additions & 26 deletions src/Services/Visibility/CoreVisibilityLogicService.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,17 @@
* Extract visibility data from a custom field.
* This is the authoritative method for getting visibility configuration.
*/
public function getVisibilityData(CustomField $field): ?VisibilityData
public function getVisibilityData(CustomField $field): VisibilityData
{
$settings = $field->settings;

return $settings->visibility ?? null;
return $field->settings->visibility;
}

/**
* Extract visibility data from a section.
*/
public function getVisibilityDataFromSection(CustomFieldSection $section): ?VisibilityData
{
$settings = $section->settings;

return $settings->visibility ?? null;
return $section->settings->visibility;
}

/**
Expand All @@ -54,9 +50,7 @@ public function getVisibilityDataFromSection(CustomFieldSection $section): ?Visi
*/
public function hasVisibilityConditions(CustomField $field): bool
{
$visibility = $this->getVisibilityData($field);

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

/**
Expand All @@ -77,9 +71,7 @@ public function hasSectionVisibilityConditions(CustomFieldSection $section): boo
*/
public function getDependentFields(CustomField $field): array
{
$visibility = $this->getVisibilityData($field);

return $visibility?->getDependentFields() ?? [];
return $this->getVisibilityData($field)->getDependentFields();
}

/**
Expand All @@ -90,9 +82,7 @@ public function getDependentFields(CustomField $field): array
*/
public function evaluateVisibility(CustomField $field, array $fieldValues, ?Model $record = null): bool
{
$visibility = $this->getVisibilityData($field);

return $visibility?->evaluate($fieldValues, $record) ?? true;
return $this->getVisibilityData($field)->evaluate($fieldValues, $record);
}

/**
Expand Down Expand Up @@ -185,9 +175,7 @@ private function evaluateVisibilityWithCascadingInternal(
*/
public function getVisibilityMode(CustomField $field): VisibilityMode
{
$visibility = $this->getVisibilityData($field);

return $visibility->mode ?? VisibilityMode::ALWAYS_VISIBLE;
return $this->getVisibilityData($field)->mode;
}

/**
Expand All @@ -196,9 +184,7 @@ public function getVisibilityMode(CustomField $field): VisibilityMode
*/
public function getVisibilityLogic(CustomField $field): VisibilityLogic
{
$visibility = $this->getVisibilityData($field);

return $visibility->logic ?? VisibilityLogic::ALL;
return $this->getVisibilityData($field)->logic;
}

/**
Expand All @@ -211,7 +197,7 @@ public function getVisibilityConditions(CustomField $field): array
{
$visibility = $this->getVisibilityData($field);

if (! $visibility instanceof VisibilityData || ! $visibility->conditions instanceof DataCollection) {
if (! $visibility->conditions instanceof DataCollection) {
return [];
}

Expand All @@ -223,9 +209,7 @@ public function getVisibilityConditions(CustomField $field): array
*/
public function shouldAlwaysSave(CustomField $field): bool
{
$visibility = $this->getVisibilityData($field);

return $visibility->alwaysSave ?? false;
return $this->getVisibilityData($field)->alwaysSave;
}

/**
Expand Down
156 changes: 156 additions & 0 deletions tests/Feature/DescriptionPositionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php

declare(strict_types=1);

use Relaticle\CustomFields\Data\CustomFieldSettingsData;
use Relaticle\CustomFields\Enums\CustomFieldsFeature;
use Relaticle\CustomFields\Enums\DescriptionPosition;
use Relaticle\CustomFields\FeatureSystem\FeatureConfigurator;
use Relaticle\CustomFields\FeatureSystem\FeatureManager;
use Relaticle\CustomFields\Models\CustomField;
use Relaticle\CustomFields\Models\CustomFieldSection;
use Relaticle\CustomFields\Tests\Fixtures\Models\Post;
use Relaticle\CustomFields\Tests\Fixtures\Models\User;
use Relaticle\CustomFields\Tests\Fixtures\Resources\Posts\Pages\CreatePost;

it('has the correct backing values', function (): void {
expect(DescriptionPosition::BELOW->value)->toBe('below');
expect(DescriptionPosition::ABOVE->value)->toBe('above');
});

it('returns translatable labels', function (): void {
expect(DescriptionPosition::BELOW->getLabel())->toBeString()->not->toBeEmpty();
expect(DescriptionPosition::ABOVE->getLabel())->toBeString()->not->toBeEmpty();
});

it('has FIELD_DESCRIPTION_POSITION feature flag disabled by default', function (): void {
expect(FeatureManager::isEnabled(CustomFieldsFeature::FIELD_DESCRIPTION_POSITION))->toBeFalse();
});

it('can enable FIELD_DESCRIPTION_POSITION feature flag', function (): void {
$config = FeatureConfigurator::configure()
->enable(CustomFieldsFeature::FIELD_DESCRIPTION_POSITION);

config(['custom-fields.features' => $config]);

expect(FeatureManager::isEnabled(CustomFieldsFeature::FIELD_DESCRIPTION_POSITION))->toBeTrue();
});

it('includes descriptionPosition as null by default in settings', function (): void {
$settings = new CustomFieldSettingsData;

expect($settings->descriptionPosition)->toBeNull();
});

it('stores descriptionPosition enum in settings', function (): void {
$settings = new CustomFieldSettingsData(
descriptionPosition: DescriptionPosition::ABOVE,
);

expect($settings->descriptionPosition)->toBe(DescriptionPosition::ABOVE);
});

it('serializes and deserializes descriptionPosition through settings', function (): void {
$settings = new CustomFieldSettingsData(
descriptionPosition: DescriptionPosition::ABOVE,
);

$array = $settings->toArray();
$restored = CustomFieldSettingsData::from($array);

expect($restored->descriptionPosition)->toBe(DescriptionPosition::ABOVE);
});

it('renders description below field by default', function (): void {
$this->actingAs(User::factory()->create());

$config = FeatureConfigurator::configure()
->enable(
CustomFieldsFeature::FIELD_DESCRIPTION,
CustomFieldsFeature::SYSTEM_SECTIONS,
);
config(['custom-fields.features' => $config]);

$section = CustomFieldSection::factory()->create([
'entity_type' => Post::class,
]);

CustomField::factory()->create([
'custom_field_section_id' => $section->id,
'name' => 'Test Field',
'code' => 'test_field',
'type' => 'text',
'entity_type' => Post::class,
'settings' => new CustomFieldSettingsData(
description: 'Help text below',
),
]);

livewire(CreatePost::class)
->assertSuccessful()
->assertSeeHtml('Help text below');
});
Comment thread
ManukMinasyan marked this conversation as resolved.

it('renders description above field when position is ABOVE and feature enabled', function (): void {
$this->actingAs(User::factory()->create());

$config = FeatureConfigurator::configure()
->enable(
CustomFieldsFeature::FIELD_DESCRIPTION,
CustomFieldsFeature::FIELD_DESCRIPTION_POSITION,
CustomFieldsFeature::SYSTEM_SECTIONS,
);
config(['custom-fields.features' => $config]);

$section = CustomFieldSection::factory()->create([
'entity_type' => Post::class,
]);

CustomField::factory()->create([
'custom_field_section_id' => $section->id,
'name' => 'Test Field Above',
'code' => 'test_field_above',
'type' => 'text',
'entity_type' => Post::class,
'settings' => new CustomFieldSettingsData(
description: 'Help text above',
descriptionPosition: DescriptionPosition::ABOVE,
),
]);

livewire(CreatePost::class)
->assertSuccessful()
->assertSeeHtml('Help text above');
});

it('ignores description position when FIELD_DESCRIPTION_POSITION feature is disabled', function (): void {
$this->actingAs(User::factory()->create());

$config = FeatureConfigurator::configure()
->enable(
CustomFieldsFeature::FIELD_DESCRIPTION,
CustomFieldsFeature::SYSTEM_SECTIONS,
)
->disable(CustomFieldsFeature::FIELD_DESCRIPTION_POSITION);
config(['custom-fields.features' => $config]);

$section = CustomFieldSection::factory()->create([
'entity_type' => Post::class,
]);

CustomField::factory()->create([
'custom_field_section_id' => $section->id,
'name' => 'Test Field Fallback',
'code' => 'test_field_fallback',
'type' => 'text',
'entity_type' => Post::class,
'settings' => new CustomFieldSettingsData(
description: 'Should render below despite ABOVE setting',
descriptionPosition: DescriptionPosition::ABOVE,
),
]);

livewire(CreatePost::class)
->assertSuccessful()
->assertSeeHtml('Should render below despite ABOVE setting');
});