Skip to content
Open
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
34 changes: 34 additions & 0 deletions database/migrations/add_entity_column_support.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
// Add uses_entity_column flag to custom_fields table
Schema::table(config('custom-fields.database.table_names.custom_fields'), function (Blueprint $table): void {
$table->boolean('uses_entity_column')->default(false)->after('system_defined');
});

// Add value column to custom_field_options table
Schema::table(config('custom-fields.database.table_names.custom_field_options'), function (Blueprint $table): void {
$table->string('value')->nullable()->after('name');
});
}

public function down(): void
{
Schema::table(config('custom-fields.database.table_names.custom_fields'), function (Blueprint $table): void {
$table->dropColumn('uses_entity_column');
});

Schema::table(config('custom-fields.database.table_names.custom_field_options'), function (Blueprint $table): void {
$table->dropColumn('value');
});
}
};
1 change: 1 addition & 0 deletions src/Data/CustomFieldData.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function __construct(
public CustomFieldSectionData $section,
public bool $active = true,
public bool $systemDefined = false,
public bool $usesEntityColumn = false,
public CustomFieldWidth $width = CustomFieldWidth::_100,
public ?string $entityType = null,
public ?array $options = null,
Expand Down
22 changes: 18 additions & 4 deletions src/Filament/Integration/Migrations/CustomFieldsMigrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,24 @@ protected function createOptions(
$customField->options()->createMany(
collect($options)
->map(function (mixed $value, mixed $key): array {
$data = [
'name' => $value,
'sort_order' => $key,
];
// Handle both formats:
// 1. Simple: ['M' => 'Male', 'F' => 'Female']
// 2. Detailed: [['value' => 'M', 'label' => 'Male'], ...]
if (is_array($value) && isset($value['value'], $value['label'])) {
// Detailed format
$data = [
'name' => $value['label'],
'value' => $value['value'],
'sort_order' => is_int($key) ? $key : 0,
];
} else {
// Simple format: key => value
$data = [
'name' => $value,
'value' => is_string($key) ? $key : null,
'sort_order' => is_int($key) ? $key : 0,
];
}

if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) {
$data[config(
Expand Down
148 changes: 98 additions & 50 deletions src/Filament/Management/Schemas/FieldForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,57 @@ class FieldForm implements FormInterface
public static function schema(bool $withOptionsRelationship = true): array
{
$optionsRepeater = Repeater::make('options')
->table([
TableColumn::make('Color')->width('150px')->hiddenHeaderLabel(),
TableColumn::make('Name')->hiddenHeaderLabel(),
])
->schema([
ColorPicker::make('settings.color')
->columnSpan(3)
->hexColor()
->visible(
fn (
Get $get
): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_OPTION_COLORS) &&
$get('../../settings.enable_option_colors')
),
TextInput::make('name')->required()->columnSpan(9)->distinct(),
])
->table(function (Get $get): array {
$hasColors = FeatureManager::isEnabled(CustomFieldsFeature::FIELD_OPTION_COLORS) &&
$get('settings.enable_option_colors');

$columns = [];

if ($hasColors) {
$columns[] = TableColumn::make('Color')->width('100px');
}

if (! $hasColors) {
$columns[] = TableColumn::make('Value')->width('150px');
}

$columns[] = TableColumn::make('Name');

return $columns;
})
->schema(function (Get $get): array {
$hasColors = FeatureManager::isEnabled(CustomFieldsFeature::FIELD_OPTION_COLORS) &&
$get('settings.enable_option_colors');

$fields = [];

if ($hasColors) {
$fields[] = ColorPicker::make('settings.color')
->hexColor()
->label('Color')
->columnSpan(2)
;
} else {
$fields[] = TextInput::make('value')
->label('Value')
->placeholder('M')
->distinct()
->disabled(fn (Get $get): bool => (bool) $get('../../uses_entity_column'))
->columnSpan(3)
;
}

$fields[] = TextInput::make('name')
->required()
->label('Name')
->placeholder('Male')
->distinct()
->disabled(fn (Get $get): bool => (bool) $get('../../uses_entity_column'))
->columnSpan($hasColors ? 10 : 9)
;

return $fields;
})
->columns(12)
->columnSpanFull()
->requiredUnless('type', function (callable $get) {
Expand All @@ -81,6 +116,7 @@ public static function schema(bool $withOptionsRelationship = true): array
&& CustomFieldsType::getFieldType($get('type'))->dataType->isChoiceField()
&& ! CustomFieldsType::getFieldType($get('type'))->withoutUserOptions
)
->disabled(fn (Get $get): bool => (bool) $get('uses_entity_column'))
->mutateRelationshipDataBeforeCreateUsing(function (
array $data
): array {
Expand All @@ -89,7 +125,8 @@ public static function schema(bool $withOptionsRelationship = true): array
}

return $data;
});
})
;

if ($withOptionsRelationship) {
$optionsRepeater = $optionsRepeater->relationship();
Expand Down Expand Up @@ -156,11 +193,6 @@ public static function schema(bool $withOptionsRelationship = true): array
->live(onBlur: true)
->required()
->maxLength(50)
->disabled(
fn (
?CustomField $record
): bool => (bool) $record?->system_defined
)
->unique(
table: CustomFields::customFieldModel(),
column: 'name',
Expand Down Expand Up @@ -257,64 +289,79 @@ public static function schema(bool $withOptionsRelationship = true): array
->columnSpanFull()
->columns(2)
->schema([
// Visibility settings
Toggle::make('settings.visible_in_list')
// Storage settings
Toggle::make('uses_entity_column')
->inline(false)
->live()
->label('Store in Entity Column')
->helperText('When enabled, this field will store its value directly in a column on the entity model instead of the custom_field_values table. The column name must match the field code.')
->visible(
fn (Get $get): bool => $get('type') !== null
)
->disabled(
fn (
?CustomField $record
): bool => (bool) $record?->exists
)
->default(false),
Toggle::make('settings.list_toggleable_hidden')
->inline(false)
->label(
__(
'custom-fields::custom-fields.field.form.visible_in_list'
'custom-fields::custom-fields.field.form.list_toggleable_hidden'
)
)
->helperText(
__(
'custom-fields::custom-fields.field.form.list_toggleable_hidden_hint'
)
)
->visible(
fn (Get $get): bool => $get(
'settings.visible_in_list'
) &&
FeatureManager::isEnabled(CustomFieldsFeature::UI_TOGGLEABLE_COLUMNS)
)
->afterStateHydrated(function (
Toggle $component,
?Model $record
): void {
if (is_null($record)) {
$component->state(true);
if ($record === null) {
$component->state(
FeatureManager::isEnabled(CustomFieldsFeature::UI_TOGGLEABLE_COLUMNS_HIDDEN_DEFAULT)
);
}
}),
Toggle::make('settings.visible_in_view')
// Visibility settings
Toggle::make('settings.visible_in_list')
->inline(false)
->live()
->label(
__(
'custom-fields::custom-fields.field.form.visible_in_view'
'custom-fields::custom-fields.field.form.visible_in_list'
)
)
->afterStateHydrated(function (
Toggle $component,
?Model $record
): void {
if (is_null($record)) {
if ($record === null) {
$component->state(true);
}
}),
Toggle::make('settings.list_toggleable_hidden')
Toggle::make('settings.visible_in_view')
->inline(false)
->label(
__(
'custom-fields::custom-fields.field.form.list_toggleable_hidden'
)
)
->helperText(
__(
'custom-fields::custom-fields.field.form.list_toggleable_hidden_hint'
'custom-fields::custom-fields.field.form.visible_in_view'
)
)
->visible(
fn (Get $get): bool => $get(
'settings.visible_in_list'
) &&
FeatureManager::isEnabled(CustomFieldsFeature::UI_TOGGLEABLE_COLUMNS)
)
->afterStateHydrated(function (
Toggle $component,
?Model $record
): void {
if (is_null($record)) {
$component->state(
FeatureManager::isEnabled(CustomFieldsFeature::UI_TOGGLEABLE_COLUMNS_HIDDEN_DEFAULT)
);
if ($record === null) {
$component->state(true);
}
}),
// Data settings
Expand All @@ -339,7 +386,7 @@ public static function schema(bool $withOptionsRelationship = true): array
Toggle $component,
mixed $state
): void {
if (is_null($state)) {
if ($state === null) {
$component->state(false);
}
}),
Expand Down Expand Up @@ -381,7 +428,8 @@ public static function schema(bool $withOptionsRelationship = true): array
->visible(
fn (
Get $get
): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_OPTION_COLORS) &&
): bool => ! $get('uses_entity_column') &&
FeatureManager::isEnabled(CustomFieldsFeature::FIELD_OPTION_COLORS) &&
in_array((string) $get('type'), [
'select',
'multi_select',
Expand Down Expand Up @@ -410,7 +458,7 @@ public static function schema(bool $withOptionsRelationship = true): array
'options' => __(
'custom-fields::custom-fields.field.form.options_lookup_type.options'
),
'lookup' => __(
'lookup' => __(
'custom-fields::custom-fields.field.form.options_lookup_type.lookup'
),
])
Expand Down
24 changes: 22 additions & 2 deletions src/Models/Concerns/UsesCustomFields.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,20 @@ public function scopeWithCustomFieldValues(Builder $query): Builder

public function getCustomFieldValue(CustomField $customField): mixed
{
// If field uses entity column, read directly from model
if ($customField->usesEntityColumn()) {
$value = $this->getAttribute($customField->code);

// For choice fields with options, we need to return the value as-is
// since the form components will use it with pluck('name', 'id')
// where 'id' is actually the 'value' thanks to our accessor
return $value;
}

$fieldValue = $this->customFieldValues
->firstWhere('custom_field_id', $customField->getKey())
?->getValue();
?->getValue()
;

if (empty($fieldValue)) {
return $fieldValue;
Expand All @@ -117,6 +128,11 @@ public function getCustomFieldValue(CustomField $customField): mixed

public function saveCustomFieldValue(CustomField $customField, mixed $value, ?Model $tenant = null): void
{
// Entity column fields should not use this method
if ($customField->usesEntityColumn()) {
return;
}

$data = ['custom_field_id' => $customField->getKey()];

if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) {
Expand Down Expand Up @@ -163,7 +179,11 @@ public function saveCustomFields(array $customFields, ?Model $tenant = null): vo
{
$this->customFields()->each(function (CustomField $customField) use ($customFields, $tenant): void {
$value = $customFields[$customField->code] ?? null;
$this->saveCustomFieldValue($customField, $value, $tenant);

// Skip entity column fields - they're already saved as regular model attributes
if (! $customField->usesEntityColumn()) {
$this->saveCustomFieldValue($customField, $value, $tenant);
}
});
}
}
15 changes: 15 additions & 0 deletions src/Models/CustomField.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
* @property int $sort_order
* @property bool $active
* @property bool $system_defined
* @property bool $uses_entity_column
* @property FieldTypeData $typeData
* @property CustomFieldWidth $width
*
Expand Down Expand Up @@ -115,6 +116,7 @@ protected function casts(): array
'validation_rules' => DataCollection::class.':'.ValidationRuleData::class.',default',
'active' => 'boolean',
'system_defined' => 'boolean',
'uses_entity_column' => 'boolean',
'settings' => CustomFieldSettingsData::class.':default',
];
}
Expand Down Expand Up @@ -163,13 +165,26 @@ public function isSystemDefined(): bool
return $this->system_defined === true;
}

/**
* Determine if the field saves to entity column instead of custom_field_values.
*/
public function usesEntityColumn(): bool
{
return $this->uses_entity_column === true;
}

public function getValueColumn(): string
{
return CustomFields::newValueModel()::getValueColumn($this->type);
}

public function getFieldName(): string
{
// Entity column fields are saved directly to model columns, not under custom_fields
if ($this->usesEntityColumn()) {
return $this->code;
}

return 'custom_fields.'.$this->code;
}
}
Loading