Skip to content

Commit 024fbcb

Browse files
committed
feat: add support for saving values to original model
1 parent 0673f3e commit 024fbcb

File tree

8 files changed

+237
-68
lines changed

8 files changed

+237
-68
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Database\Migrations\Migration;
6+
use Illuminate\Database\Schema\Blueprint;
7+
use Illuminate\Support\Facades\Schema;
8+
9+
return new class extends Migration
10+
{
11+
public function up(): void
12+
{
13+
// Add uses_entity_column flag to custom_fields table
14+
Schema::table(config('custom-fields.database.table_names.custom_fields'), function (Blueprint $table): void {
15+
$table->boolean('uses_entity_column')->default(false)->after('system_defined');
16+
});
17+
18+
// Add value column to custom_field_options table
19+
Schema::table(config('custom-fields.database.table_names.custom_field_options'), function (Blueprint $table): void {
20+
$table->string('value')->nullable()->after('name');
21+
});
22+
}
23+
24+
public function down(): void
25+
{
26+
Schema::table(config('custom-fields.database.table_names.custom_fields'), function (Blueprint $table): void {
27+
$table->dropColumn('uses_entity_column');
28+
});
29+
30+
Schema::table(config('custom-fields.database.table_names.custom_field_options'), function (Blueprint $table): void {
31+
$table->dropColumn('value');
32+
});
33+
}
34+
};

src/Data/CustomFieldData.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public function __construct(
2727
public CustomFieldSectionData $section,
2828
public bool $active = true,
2929
public bool $systemDefined = false,
30+
public bool $usesEntityColumn = false,
3031
public CustomFieldWidth $width = CustomFieldWidth::_100,
3132
public ?string $entityType = null,
3233
public ?array $options = null,

src/Filament/Integration/Migrations/CustomFieldsMigrator.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,24 @@ protected function createOptions(
264264
$customField->options()->createMany(
265265
collect($options)
266266
->map(function (mixed $value, mixed $key): array {
267-
$data = [
268-
'name' => $value,
269-
'sort_order' => $key,
270-
];
267+
// Handle both formats:
268+
// 1. Simple: ['M' => 'Male', 'F' => 'Female']
269+
// 2. Detailed: [['value' => 'M', 'label' => 'Male'], ...]
270+
if (is_array($value) && isset($value['value'], $value['label'])) {
271+
// Detailed format
272+
$data = [
273+
'name' => $value['label'],
274+
'value' => $value['value'],
275+
'sort_order' => is_int($key) ? $key : 0,
276+
];
277+
} else {
278+
// Simple format: key => value
279+
$data = [
280+
'name' => $value,
281+
'value' => is_string($key) ? $key : null,
282+
'sort_order' => is_int($key) ? $key : 0,
283+
];
284+
}
271285

272286
if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) {
273287
$data[config(

src/Filament/Management/Schemas/FieldForm.php

Lines changed: 98 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,57 @@ class FieldForm implements FormInterface
4242
public static function schema(bool $withOptionsRelationship = true): array
4343
{
4444
$optionsRepeater = Repeater::make('options')
45-
->table([
46-
TableColumn::make('Color')->width('150px')->hiddenHeaderLabel(),
47-
TableColumn::make('Name')->hiddenHeaderLabel(),
48-
])
49-
->schema([
50-
ColorPicker::make('settings.color')
51-
->columnSpan(3)
52-
->hexColor()
53-
->visible(
54-
fn (
55-
Get $get
56-
): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_OPTION_COLORS) &&
57-
$get('../../settings.enable_option_colors')
58-
),
59-
TextInput::make('name')->required()->columnSpan(9)->distinct(),
60-
])
45+
->table(function (Get $get): array {
46+
$hasColors = FeatureManager::isEnabled(CustomFieldsFeature::FIELD_OPTION_COLORS) &&
47+
$get('settings.enable_option_colors');
48+
49+
$columns = [];
50+
51+
if ($hasColors) {
52+
$columns[] = TableColumn::make('Color')->width('100px');
53+
}
54+
55+
if (! $hasColors) {
56+
$columns[] = TableColumn::make('Value')->width('150px');
57+
}
58+
59+
$columns[] = TableColumn::make('Name');
60+
61+
return $columns;
62+
})
63+
->schema(function (Get $get): array {
64+
$hasColors = FeatureManager::isEnabled(CustomFieldsFeature::FIELD_OPTION_COLORS) &&
65+
$get('settings.enable_option_colors');
66+
67+
$fields = [];
68+
69+
if ($hasColors) {
70+
$fields[] = ColorPicker::make('settings.color')
71+
->hexColor()
72+
->label('Color')
73+
->columnSpan(2)
74+
;
75+
} else {
76+
$fields[] = TextInput::make('value')
77+
->label('Value')
78+
->placeholder('M')
79+
->distinct()
80+
->disabled(fn (Get $get): bool => (bool) $get('../../uses_entity_column'))
81+
->columnSpan(3)
82+
;
83+
}
84+
85+
$fields[] = TextInput::make('name')
86+
->required()
87+
->label('Name')
88+
->placeholder('Male')
89+
->distinct()
90+
->disabled(fn (Get $get): bool => (bool) $get('../../uses_entity_column'))
91+
->columnSpan($hasColors ? 10 : 9)
92+
;
93+
94+
return $fields;
95+
})
6196
->columns(12)
6297
->columnSpanFull()
6398
->requiredUnless('type', function (callable $get) {
@@ -81,6 +116,7 @@ public static function schema(bool $withOptionsRelationship = true): array
81116
&& CustomFieldsType::getFieldType($get('type'))->dataType->isChoiceField()
82117
&& ! CustomFieldsType::getFieldType($get('type'))->withoutUserOptions
83118
)
119+
->disabled(fn (Get $get): bool => (bool) $get('uses_entity_column'))
84120
->mutateRelationshipDataBeforeCreateUsing(function (
85121
array $data
86122
): array {
@@ -89,7 +125,8 @@ public static function schema(bool $withOptionsRelationship = true): array
89125
}
90126

91127
return $data;
92-
});
128+
})
129+
;
93130

94131
if ($withOptionsRelationship) {
95132
$optionsRepeater = $optionsRepeater->relationship();
@@ -156,11 +193,6 @@ public static function schema(bool $withOptionsRelationship = true): array
156193
->live(onBlur: true)
157194
->required()
158195
->maxLength(50)
159-
->disabled(
160-
fn (
161-
?CustomField $record
162-
): bool => (bool) $record?->system_defined
163-
)
164196
->unique(
165197
table: CustomFields::customFieldModel(),
166198
column: 'name',
@@ -257,64 +289,79 @@ public static function schema(bool $withOptionsRelationship = true): array
257289
->columnSpanFull()
258290
->columns(2)
259291
->schema([
260-
// Visibility settings
261-
Toggle::make('settings.visible_in_list')
292+
// Storage settings
293+
Toggle::make('uses_entity_column')
262294
->inline(false)
263295
->live()
296+
->label('Store in Entity Column')
297+
->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.')
298+
->visible(
299+
fn (Get $get): bool => $get('type') !== null
300+
)
301+
->disabled(
302+
fn (
303+
?CustomField $record
304+
): bool => (bool) $record?->exists
305+
)
306+
->default(false),
307+
Toggle::make('settings.list_toggleable_hidden')
308+
->inline(false)
264309
->label(
265310
__(
266-
'custom-fields::custom-fields.field.form.visible_in_list'
311+
'custom-fields::custom-fields.field.form.list_toggleable_hidden'
312+
)
313+
)
314+
->helperText(
315+
__(
316+
'custom-fields::custom-fields.field.form.list_toggleable_hidden_hint'
267317
)
268318
)
319+
->visible(
320+
fn (Get $get): bool => $get(
321+
'settings.visible_in_list'
322+
) &&
323+
FeatureManager::isEnabled(CustomFieldsFeature::UI_TOGGLEABLE_COLUMNS)
324+
)
269325
->afterStateHydrated(function (
270326
Toggle $component,
271327
?Model $record
272328
): void {
273-
if (is_null($record)) {
274-
$component->state(true);
329+
if ($record === null) {
330+
$component->state(
331+
FeatureManager::isEnabled(CustomFieldsFeature::UI_TOGGLEABLE_COLUMNS_HIDDEN_DEFAULT)
332+
);
275333
}
276334
}),
277-
Toggle::make('settings.visible_in_view')
335+
// Visibility settings
336+
Toggle::make('settings.visible_in_list')
278337
->inline(false)
338+
->live()
279339
->label(
280340
__(
281-
'custom-fields::custom-fields.field.form.visible_in_view'
341+
'custom-fields::custom-fields.field.form.visible_in_list'
282342
)
283343
)
284344
->afterStateHydrated(function (
285345
Toggle $component,
286346
?Model $record
287347
): void {
288-
if (is_null($record)) {
348+
if ($record === null) {
289349
$component->state(true);
290350
}
291351
}),
292-
Toggle::make('settings.list_toggleable_hidden')
352+
Toggle::make('settings.visible_in_view')
293353
->inline(false)
294354
->label(
295355
__(
296-
'custom-fields::custom-fields.field.form.list_toggleable_hidden'
297-
)
298-
)
299-
->helperText(
300-
__(
301-
'custom-fields::custom-fields.field.form.list_toggleable_hidden_hint'
356+
'custom-fields::custom-fields.field.form.visible_in_view'
302357
)
303358
)
304-
->visible(
305-
fn (Get $get): bool => $get(
306-
'settings.visible_in_list'
307-
) &&
308-
FeatureManager::isEnabled(CustomFieldsFeature::UI_TOGGLEABLE_COLUMNS)
309-
)
310359
->afterStateHydrated(function (
311360
Toggle $component,
312361
?Model $record
313362
): void {
314-
if (is_null($record)) {
315-
$component->state(
316-
FeatureManager::isEnabled(CustomFieldsFeature::UI_TOGGLEABLE_COLUMNS_HIDDEN_DEFAULT)
317-
);
363+
if ($record === null) {
364+
$component->state(true);
318365
}
319366
}),
320367
// Data settings
@@ -339,7 +386,7 @@ public static function schema(bool $withOptionsRelationship = true): array
339386
Toggle $component,
340387
mixed $state
341388
): void {
342-
if (is_null($state)) {
389+
if ($state === null) {
343390
$component->state(false);
344391
}
345392
}),
@@ -381,7 +428,8 @@ public static function schema(bool $withOptionsRelationship = true): array
381428
->visible(
382429
fn (
383430
Get $get
384-
): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_OPTION_COLORS) &&
431+
): bool => ! $get('uses_entity_column') &&
432+
FeatureManager::isEnabled(CustomFieldsFeature::FIELD_OPTION_COLORS) &&
385433
in_array((string) $get('type'), [
386434
'select',
387435
'multi_select',
@@ -410,7 +458,7 @@ public static function schema(bool $withOptionsRelationship = true): array
410458
'options' => __(
411459
'custom-fields::custom-fields.field.form.options_lookup_type.options'
412460
),
413-
'lookup' => __(
461+
'lookup' => __(
414462
'custom-fields::custom-fields.field.form.options_lookup_type.lookup'
415463
),
416464
])

src/Models/Concerns/UsesCustomFields.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,20 @@ public function scopeWithCustomFieldValues(Builder $query): Builder
9898

9999
public function getCustomFieldValue(CustomField $customField): mixed
100100
{
101+
// If field uses entity column, read directly from model
102+
if ($customField->usesEntityColumn()) {
103+
$value = $this->getAttribute($customField->code);
104+
105+
// For choice fields with options, we need to return the value as-is
106+
// since the form components will use it with pluck('name', 'id')
107+
// where 'id' is actually the 'value' thanks to our accessor
108+
return $value;
109+
}
110+
101111
$fieldValue = $this->customFieldValues
102112
->firstWhere('custom_field_id', $customField->getKey())
103-
?->getValue();
113+
?->getValue()
114+
;
104115

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

118129
public function saveCustomFieldValue(CustomField $customField, mixed $value, ?Model $tenant = null): void
119130
{
131+
// Entity column fields should not use this method
132+
if ($customField->usesEntityColumn()) {
133+
return;
134+
}
135+
120136
$data = ['custom_field_id' => $customField->getKey()];
121137

122138
if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) {
@@ -163,7 +179,11 @@ public function saveCustomFields(array $customFields, ?Model $tenant = null): vo
163179
{
164180
$this->customFields()->each(function (CustomField $customField) use ($customFields, $tenant): void {
165181
$value = $customFields[$customField->code] ?? null;
166-
$this->saveCustomFieldValue($customField, $value, $tenant);
182+
183+
// Skip entity column fields - they're already saved as regular model attributes
184+
if (! $customField->usesEntityColumn()) {
185+
$this->saveCustomFieldValue($customField, $value, $tenant);
186+
}
167187
});
168188
}
169189
}

src/Models/CustomField.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
* @property int $sort_order
4040
* @property bool $active
4141
* @property bool $system_defined
42+
* @property bool $uses_entity_column
4243
* @property FieldTypeData $typeData
4344
* @property CustomFieldWidth $width
4445
*
@@ -115,6 +116,7 @@ protected function casts(): array
115116
'validation_rules' => DataCollection::class.':'.ValidationRuleData::class.',default',
116117
'active' => 'boolean',
117118
'system_defined' => 'boolean',
119+
'uses_entity_column' => 'boolean',
118120
'settings' => CustomFieldSettingsData::class.':default',
119121
];
120122
}
@@ -163,13 +165,26 @@ public function isSystemDefined(): bool
163165
return $this->system_defined === true;
164166
}
165167

168+
/**
169+
* Determine if the field saves to entity column instead of custom_field_values.
170+
*/
171+
public function usesEntityColumn(): bool
172+
{
173+
return $this->uses_entity_column === true;
174+
}
175+
166176
public function getValueColumn(): string
167177
{
168178
return CustomFields::newValueModel()::getValueColumn($this->type);
169179
}
170180

171181
public function getFieldName(): string
172182
{
183+
// Entity column fields are saved directly to model columns, not under custom_fields
184+
if ($this->usesEntityColumn()) {
185+
return $this->code;
186+
}
187+
173188
return 'custom_fields.'.$this->code;
174189
}
175190
}

0 commit comments

Comments
 (0)