From 96f15d50b007f4a2aa01449608dea0fdb43c5184 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:10:08 +0400 Subject: [PATCH 01/20] feat: add new feature flags for field enhancements --- src/Enums/CustomFieldsFeature.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Enums/CustomFieldsFeature.php b/src/Enums/CustomFieldsFeature.php index b7a5c234..a551486b 100644 --- a/src/Enums/CustomFieldsFeature.php +++ b/src/Enums/CustomFieldsFeature.php @@ -14,6 +14,9 @@ enum CustomFieldsFeature: string case FIELD_CONDITIONAL_VISIBILITY = 'field_conditional_visibility'; case FIELD_ENCRYPTION = 'field_encryption'; case FIELD_OPTION_COLORS = 'field_option_colors'; + case FIELD_CODE_AUTO_GENERATE = 'field_code_auto_generate'; + case FIELD_MULTI_VALUE = 'field_multi_value'; + case FIELD_UNIQUE_VALUE = 'field_unique_value'; // Table/UI integration features case UI_TABLE_COLUMNS = 'ui_table_columns'; @@ -24,4 +27,5 @@ enum CustomFieldsFeature: string // System-level features case SYSTEM_MANAGEMENT_INTERFACE = 'system_management_interface'; case SYSTEM_MULTI_TENANCY = 'system_multi_tenancy'; + case SYSTEM_SECTIONS_DISABLED = 'system_sections_disabled'; } From 05f61d7a53a85b0060658e09820ffdb8bab34b39 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:10:13 +0400 Subject: [PATCH 02/20] feat: add auto-generate code from name feature --- .../Pages/CustomFieldsManagementPage.php | 30 +++++ src/Filament/Management/Schemas/FieldForm.php | 101 ++++++++++++++++- .../Management/Schemas/SectionForm.php | 11 +- src/Livewire/ManageCustomFieldSection.php | 9 ++ src/Support/CodeGenerator.php | 107 ++++++++++++++++++ 5 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 src/Support/CodeGenerator.php diff --git a/src/Filament/Management/Pages/CustomFieldsManagementPage.php b/src/Filament/Management/Pages/CustomFieldsManagementPage.php index 50da4299..0c105c1f 100644 --- a/src/Filament/Management/Pages/CustomFieldsManagementPage.php +++ b/src/Filament/Management/Pages/CustomFieldsManagementPage.php @@ -24,7 +24,9 @@ use Relaticle\CustomFields\FeatureSystem\FeatureManager; use Relaticle\CustomFields\Filament\Management\Schemas\SectionForm; use Relaticle\CustomFields\Models\CustomFieldSection; +use Relaticle\CustomFields\Services\DefaultSectionService; use Relaticle\CustomFields\Services\TenantContextService; +use Relaticle\CustomFields\Support\CodeGenerator; use Relaticle\CustomFields\Support\Utils; class CustomFieldsManagementPage extends Page @@ -46,6 +48,20 @@ public function mount(): void $firstEntity = Entities::withCustomFields()->first(); $this->setCurrentEntityType($firstEntity?->getAlias() ?? ''); } + + // When sections are disabled, ensure a default hidden section exists + if ($this->isSectionsDisabled() && filled($this->currentEntityType)) { + app(DefaultSectionService::class)->getOrCreateDefaultSection($this->currentEntityType); + } + } + + /** + * Check if sections are disabled (single hidden section mode). + */ + #[Computed] + public function isSectionsDisabled(): bool + { + return FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_SECTIONS_DISABLED); } #[Computed] @@ -97,6 +113,11 @@ public function entityTypes(): Collection public function setCurrentEntityType(?string $entityType): void { $this->currentEntityType = $entityType; + + // When sections are disabled, ensure a default hidden section exists for the new entity type + if ($this->isSectionsDisabled() && filled($entityType)) { + app(DefaultSectionService::class)->getOrCreateDefaultSection($entityType); + } } public function createSectionAction(): Action @@ -108,6 +129,7 @@ public function createSectionAction(): Action ->color('gray') ->button() ->outlined() + ->visible(fn (): bool => ! $this->isSectionsDisabled()) ->extraAttributes([ 'class' => 'flex justify-center items-center rounded-lg border-gray-300 hover:border-gray-400 border-dashed', ]) @@ -142,6 +164,14 @@ private function storeSection(array $data): CustomFieldSection $data['type'] ??= CustomFieldSectionType::SECTION->value; $data['entity_type'] = $this->currentEntityType; + // Auto-generate code if feature is enabled and code is empty + if (FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) && blank($data['code'] ?? null)) { + $data['code'] = CodeGenerator::generateUniqueSectionCode( + $data['name'], + $this->currentEntityType + ); + } + return CustomFieldsModel::newSectionModel()->create($data); } diff --git a/src/Filament/Management/Schemas/FieldForm.php b/src/Filament/Management/Schemas/FieldForm.php index 89cc06eb..b859eabb 100644 --- a/src/Filament/Management/Schemas/FieldForm.php +++ b/src/Filament/Management/Schemas/FieldForm.php @@ -26,6 +26,7 @@ use Relaticle\CustomFields\Facades\CustomFieldsType; use Relaticle\CustomFields\Facades\Entities; use Relaticle\CustomFields\FeatureSystem\FeatureManager; +use Relaticle\CustomFields\FieldTypeSystem\FieldManager; use Relaticle\CustomFields\Filament\Management\Forms\Components\CustomFieldValidationComponent; use Relaticle\CustomFields\Filament\Management\Forms\Components\TypeField; use Relaticle\CustomFields\Filament\Management\Forms\Components\VisibilityComponent; @@ -34,6 +35,36 @@ class FieldForm implements FormInterface { + /** + * Get type-specific settings schema components. + * + * @return array + */ + private static function getTypeSettingsSchema(): array + { + $fieldManager = app(FieldManager::class); + + $components = []; + + foreach (CustomFieldsType::toCollection() as $fieldTypeData) { + if ($fieldTypeData->settingsSchema === null) { + continue; + } + + $schema = is_callable($fieldTypeData->settingsSchema) + ? ($fieldTypeData->settingsSchema)() + : $fieldTypeData->settingsSchema; + + foreach ($schema as $component) { + $components[] = $component->visible( + fn (Get $get): bool => $get('type') === $fieldTypeData->key + ); + } + } + + return $components; + } + /** * @return array * @@ -213,7 +244,9 @@ public static function schema(bool $withOptionsRelationship = true): array ) ) ->live(onBlur: true) - ->required() + ->required( + fn (): bool => ! FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) + ) ->alphaDash() ->maxLength(50) ->disabled( @@ -221,6 +254,9 @@ public static function schema(bool $withOptionsRelationship = true): array ?CustomField $record ): bool => (bool) $record?->system_defined ) + ->visible( + fn (): bool => ! FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) + ) ->unique( table: CustomFields::customFieldModel(), column: 'code', @@ -387,8 +423,71 @@ public static function schema(bool $withOptionsRelationship = true): array 'multi_select', ]) ), + // Multi-value settings + Toggle::make('settings.allow_multiple') + ->inline(false) + ->live() + ->label( + __( + 'custom-fields::custom-fields.field.form.allow_multiple' + ) + ) + ->helperText( + __( + 'custom-fields::custom-fields.field.form.allow_multiple_help' + ) + ) + ->visible( + fn ( + Get $get + ): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_MULTI_VALUE) && + CustomFieldsType::getFieldType($get('type'))?->supportsMultiValue === true + ) + ->default(false), + TextInput::make('settings.max_values') + ->label( + __( + 'custom-fields::custom-fields.field.form.max_values' + ) + ) + ->helperText( + __( + 'custom-fields::custom-fields.field.form.max_values_help' + ) + ) + ->numeric() + ->minValue(1) + ->maxValue(20) + ->default(5) + ->visible( + fn ( + Get $get + ): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_MULTI_VALUE) && + CustomFieldsType::getFieldType($get('type'))?->supportsMultiValue === true && + $get('settings.allow_multiple') === true + ), + // Uniqueness constraint + Toggle::make('settings.unique_per_entity_type') + ->inline(false) + ->label( + __( + 'custom-fields::custom-fields.field.form.unique_per_entity_type' + ) + ) + ->helperText( + __( + 'custom-fields::custom-fields.field.form.unique_per_entity_type_help' + ) + ) + ->visible( + fn (): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_UNIQUE_VALUE) + ) + ->default(false), ]), + // Dynamic type-specific settings from field type definition + ...self::getTypeSettingsSchema(), + Select::make('options_lookup_type') ->label( __( diff --git a/src/Filament/Management/Schemas/SectionForm.php b/src/Filament/Management/Schemas/SectionForm.php index 39e1668c..b40036f2 100644 --- a/src/Filament/Management/Schemas/SectionForm.php +++ b/src/Filament/Management/Schemas/SectionForm.php @@ -78,14 +78,21 @@ public static function schema(): array $set('code', Str::of($state)->slug('_')->toString()); }) - ->columnSpan(6), + ->columnSpan( + fn (): int => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) ? 12 : 6 + ), TextInput::make('code') ->label( __('custom-fields::custom-fields.section.form.code') ) - ->required() + ->required( + fn (): bool => ! FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) + ) ->alphaDash() ->maxLength(50) + ->visible( + fn (): bool => ! FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) + ) ->unique( table: CustomFields::sectionModel(), column: 'code', diff --git a/src/Livewire/ManageCustomFieldSection.php b/src/Livewire/ManageCustomFieldSection.php index acde13d8..2ce6f3a9 100644 --- a/src/Livewire/ManageCustomFieldSection.php +++ b/src/Livewire/ManageCustomFieldSection.php @@ -24,6 +24,7 @@ use Relaticle\CustomFields\Filament\Management\Schemas\SectionForm; use Relaticle\CustomFields\Models\CustomFieldSection; use Relaticle\CustomFields\Services\TenantContextService; +use Relaticle\CustomFields\Support\CodeGenerator; final class ManageCustomFieldSection extends Component implements HasActions, HasForms { @@ -170,6 +171,14 @@ public function createFieldAction(): Action $data[config('custom-fields.database.column_names.tenant_foreign_key')] = TenantContextService::getCurrentTenantId(); } + // Auto-generate code if feature is enabled and code is empty + if (FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) && blank($data['code'] ?? null)) { + $data['code'] = CodeGenerator::generateUniqueFieldCode( + $data['name'], + $this->entityType + ); + } + return [ ...$data, 'entity_type' => $this->entityType, diff --git a/src/Support/CodeGenerator.php b/src/Support/CodeGenerator.php new file mode 100644 index 00000000..16923099 --- /dev/null +++ b/src/Support/CodeGenerator.php @@ -0,0 +1,107 @@ +slug('_')->toString(); + } + + /** + * Generate a unique code for a custom field within an entity type. + */ + public static function generateUniqueFieldCode(string $name, string $entityType, ?int $ignoreId = null): string + { + $baseCode = self::generateFromName($name); + + return self::ensureUniqueCode( + $baseCode, + $entityType, + 'field', + $ignoreId + ); + } + + /** + * Generate a unique code for a section within an entity type. + */ + public static function generateUniqueSectionCode(string $name, string $entityType, ?int $ignoreId = null): string + { + $baseCode = self::generateFromName($name); + + return self::ensureUniqueCode( + $baseCode, + $entityType, + 'section', + $ignoreId + ); + } + + /** + * Check if a code already exists and append a counter if needed. + */ + private static function ensureUniqueCode( + string $baseCode, + string $entityType, + string $type, + ?int $ignoreId = null + ): string { + $code = $baseCode; + $counter = 1; + + while (self::codeExists($code, $entityType, $type, $ignoreId)) { + $code = "{$baseCode}_{$counter}"; + $counter++; + } + + return $code; + } + + /** + * Check if a code exists in the database. + */ + private static function codeExists( + string $code, + string $entityType, + string $type, + ?int $ignoreId = null + ): bool { + $model = $type === 'field' + ? CustomFields::newCustomFieldModel() + : CustomFields::newSectionModel(); + + $query = $model->newQuery() + ->withDeactivated() + ->where('code', $code) + ->where('entity_type', $entityType); + + if ($ignoreId !== null) { + $query->where($model->getKeyName(), '!=', $ignoreId); + } + + if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) { + $query->where( + config('custom-fields.database.column_names.tenant_foreign_key'), + TenantContextService::getCurrentTenantId() + ); + } + + return $query->exists(); + } +} From e1bc5356cda3db47060c23f23d2cb6b94f16c56e Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:10:17 +0400 Subject: [PATCH 03/20] feat: add sections disabled mode with hidden default section --- .../pages/custom-fields-management.blade.php | 78 ++++++---- .../manage-fields-without-sections.blade.php | 44 ++++++ src/CustomFieldsServiceProvider.php | 2 + src/Livewire/ManageFieldsWithoutSections.php | 146 ++++++++++++++++++ src/Services/DefaultSectionService.php | 79 ++++++++++ 5 files changed, 317 insertions(+), 32 deletions(-) create mode 100644 resources/views/livewire/manage-fields-without-sections.blade.php create mode 100644 src/Livewire/ManageFieldsWithoutSections.php create mode 100644 src/Services/DefaultSectionService.php diff --git a/resources/views/filament/pages/custom-fields-management.blade.php b/resources/views/filament/pages/custom-fields-management.blade.php index 45d14a57..5ea46836 100644 --- a/resources/views/filament/pages/custom-fields-management.blade.php +++ b/resources/views/filament/pages/custom-fields-management.blade.php @@ -9,43 +9,57 @@
-
- @foreach ($this->sections as $section) - @livewire('manage-custom-field-section', ['entityType' => $this->currentEntityType, 'section' => $section], key($section->id . str()->random(16))) - @endforeach + @if($this->isSectionsDisabled) + {{-- Sections disabled mode: show flat field list with new component --}} + @if($this->sections->first()) + @livewire('manage-fields-without-sections', [ + 'entityType' => $this->currentEntityType, + 'section' => $this->sections->first(), + ], key('fields-without-sections-' . $this->currentEntityType)) + @endif + @else + {{-- Normal sections mode --}} +
+ @foreach ($this->sections as $section) + @livewire('manage-custom-field-section', [ + 'entityType' => $this->currentEntityType, + 'section' => $section, + ], key($section->id . str()->random(16))) + @endforeach - @if(!count($this->sections)) -
-
-
- -
+ @if(!count($this->sections)) +
+
+
+ +
-

- {{ __('custom-fields::custom-fields.empty_states.sections.heading') }} -

+

+ {{ __('custom-fields::custom-fields.empty_states.sections.heading') }} +

-

- {{ __('custom-fields::custom-fields.empty_states.sections.description') }} -

+

+ {{ __('custom-fields::custom-fields.empty_states.sections.description') }} +

-
- {{ $this->createSectionAction }} +
+ {{ $this->createSectionAction }} +
-
- @else -
- {{ $this->createSectionAction }} -
- @endif -
+ @else +
+ {{ $this->createSectionAction }} +
+ @endif +
+ @endif
diff --git a/resources/views/livewire/manage-fields-without-sections.blade.php b/resources/views/livewire/manage-fields-without-sections.blade.php new file mode 100644 index 00000000..73403679 --- /dev/null +++ b/resources/views/livewire/manage-fields-without-sections.blade.php @@ -0,0 +1,44 @@ +
+ @if(count($this->fields)) +
+ @foreach ($this->fields as $field) + @livewire('manage-custom-field', ['field' => $field], key($field->id . $field->width->value . str()->random(16))) + @endforeach +
+ +
+ {{ $this->createFieldAction() }} +
+ @else +
+
+
+ +
+ +

+ {{ __('custom-fields::custom-fields.empty_states.fields_no_sections.heading') }} +

+ +

+ {{ __('custom-fields::custom-fields.empty_states.fields_no_sections.description') }} +

+ +
+ {{ $this->createFieldAction() }} +
+
+
+ @endif + + +
diff --git a/src/CustomFieldsServiceProvider.php b/src/CustomFieldsServiceProvider.php index c9020212..d0c142a5 100644 --- a/src/CustomFieldsServiceProvider.php +++ b/src/CustomFieldsServiceProvider.php @@ -23,6 +23,7 @@ use Relaticle\CustomFields\Livewire\ManageCustomField; use Relaticle\CustomFields\Livewire\ManageCustomFieldSection; use Relaticle\CustomFields\Livewire\ManageCustomFieldWidth; +use Relaticle\CustomFields\Livewire\ManageFieldsWithoutSections; use Relaticle\CustomFields\Models\CustomField; use Relaticle\CustomFields\Models\CustomFieldSection; use Relaticle\CustomFields\Providers\EntityServiceProvider; @@ -73,6 +74,7 @@ public function bootingPackage(): void Livewire::component('manage-custom-field-section', ManageCustomFieldSection::class); Livewire::component('manage-custom-field', ManageCustomField::class); Livewire::component('manage-custom-field-width', ManageCustomFieldWidth::class); + Livewire::component('manage-fields-without-sections', ManageFieldsWithoutSections::class); } public function configurePackage(Package $package): void diff --git a/src/Livewire/ManageFieldsWithoutSections.php b/src/Livewire/ManageFieldsWithoutSections.php new file mode 100644 index 00000000..ba725502 --- /dev/null +++ b/src/Livewire/ManageFieldsWithoutSections.php @@ -0,0 +1,146 @@ +section->fields()->withDeactivated()->orderBy('sort_order')->get(); + } + + #[On('field-width-updated')] + public function fieldWidthUpdated(int|string $fieldId, int $width): void + { + $model = CustomFields::newCustomFieldModel(); + $model->where($model->getKeyName(), $fieldId)->update(['width' => $width]); + + $this->section->refresh(); + } + + #[On('field-deleted')] + public function fieldDeleted(): void + { + $this->section->refresh(); + } + + #[On('fields-reordered')] + public function fieldsReordered(): void + { + unset($this->fields); + } + + public function updateFieldsOrder(array $fields): void + { + $model = CustomFields::newCustomFieldModel(); + foreach ($fields as $index => $field) { + $model->query() + ->withDeactivated() + ->where($model->getKeyName(), $field) + ->update([ + 'custom_field_section_id' => $this->section->getKey(), + 'sort_order' => $index, + ]); + } + + $this->dispatch('fields-reordered')->self(); + } + + public function createFieldAction(): Action + { + return Action::make('createField') + ->size(Size::Small) + ->label(__('custom-fields::custom-fields.field.form.add_field')) + ->icon('heroicon-s-plus') + ->color('gray') + ->button() + ->outlined() + ->extraAttributes([ + 'class' => 'flex justify-center items-center rounded-lg border-gray-300 hover:border-gray-400 border-dashed', + ]) + ->model(CustomFields::customFieldModel()) + ->schema(FieldForm::schema(withOptionsRelationship: false)) + ->fillForm([ + 'entity_type' => $this->entityType, + ]) + ->mutateDataUsing(function (array $data): array { + if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) { + $data[config('custom-fields.database.column_names.tenant_foreign_key')] = TenantContextService::getCurrentTenantId(); + } + + if (FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) && blank($data['code'] ?? null)) { + $data['code'] = CodeGenerator::generateUniqueFieldCode( + $data['name'], + $this->entityType + ); + } + + return [ + ...$data, + 'entity_type' => $this->entityType, + 'custom_field_section_id' => $this->section->getKey(), + ]; + }) + ->action(function (array $data): void { + $options = collect($data['options'] ?? []) + ->filter() + ->map(function (array $option): array { + if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) { + $option[config('custom-fields.database.column_names.tenant_foreign_key')] = TenantContextService::getCurrentTenantId(); + } + + return $option; + }) + ->values(); + + unset($data['options']); + + $customField = CustomFields::newCustomFieldModel()->create($data); + + $customField->options()->createMany($options); + }) + ->modalWidth(Width::ScreenLarge) + ->slideOver(); + } + + public function render(): View + { + return view('custom-fields::livewire.manage-fields-without-sections'); + } +} diff --git a/src/Services/DefaultSectionService.php b/src/Services/DefaultSectionService.php new file mode 100644 index 00000000..67fdcfb2 --- /dev/null +++ b/src/Services/DefaultSectionService.php @@ -0,0 +1,79 @@ +newQuery() + ->withDeactivated() + ->where('code', self::DEFAULT_SECTION_CODE) + ->where('entity_type', $entityType); + + if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) { + $query->where( + config('custom-fields.database.column_names.tenant_foreign_key'), + TenantContextService::getCurrentTenantId() + ); + } + + $section = $query->first(); + + if ($section instanceof CustomFieldSection) { + return $section; + } + + return $this->createDefaultSection($entityType); + } + + /** + * Check if a section is the default hidden section. + */ + public function isDefaultSection(CustomFieldSection $section): bool + { + return $section->code === self::DEFAULT_SECTION_CODE; + } + + /** + * Create the default hidden section for an entity type. + */ + private function createDefaultSection(string $entityType): CustomFieldSection + { + $data = [ + 'name' => __('custom-fields::custom-fields.section.default_section_name'), + 'code' => self::DEFAULT_SECTION_CODE, + 'type' => CustomFieldSectionType::HEADLESS->value, + 'entity_type' => $entityType, + 'sort_order' => 0, + 'active' => true, + ]; + + if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) { + $data[config('custom-fields.database.column_names.tenant_foreign_key')] = TenantContextService::getCurrentTenantId(); + } + + return CustomFields::newSectionModel()->create($data); + } +} From c7659cf5d8c86c85009d25599d82e87cca60015a Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:10:24 +0400 Subject: [PATCH 04/20] feat: add multi-value and unique per entity type settings --- src/Data/CustomFieldSettingsData.php | 3 + src/Data/FieldTypeData.php | 1 + src/FieldTypeSystem/FieldSchema.php | 13 +++ .../Base/AbstractFormComponent.php | 7 +- src/Models/CustomField.php | 29 +++++++ src/Rules/UniqueCustomFieldValue.php | 79 +++++++++++++++++++ src/Services/ValidationService.php | 33 +++++++- 7 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 src/Rules/UniqueCustomFieldValue.php diff --git a/src/Data/CustomFieldSettingsData.php b/src/Data/CustomFieldSettingsData.php index 2ef47d63..993432c0 100644 --- a/src/Data/CustomFieldSettingsData.php +++ b/src/Data/CustomFieldSettingsData.php @@ -20,6 +20,9 @@ public function __construct( public bool $searchable = false, public bool $encrypted = false, public bool $enable_option_colors = false, + public bool $allow_multiple = false, + public int $max_values = 1, + public bool $unique_per_entity_type = false, public VisibilityData $visibility = new VisibilityData, public array $additional = [], ) { diff --git a/src/Data/FieldTypeData.php b/src/Data/FieldTypeData.php index 2ac897b6..7df8d546 100644 --- a/src/Data/FieldTypeData.php +++ b/src/Data/FieldTypeData.php @@ -27,6 +27,7 @@ public function __construct( public bool $encryptable = false, public bool $withoutUserOptions = false, public bool $acceptsArbitraryValues = false, + public bool $supportsMultiValue = false, public array $validationRules = [], public ?string $settingsDataClass = null, public string|Closure|null $settingsSchema = null, diff --git a/src/FieldTypeSystem/FieldSchema.php b/src/FieldTypeSystem/FieldSchema.php index e58502be..bddeb445 100644 --- a/src/FieldTypeSystem/FieldSchema.php +++ b/src/FieldTypeSystem/FieldSchema.php @@ -53,6 +53,8 @@ class FieldSchema private bool $acceptsArbitraryValues = false; + private bool $supportsMultiValue = false; + protected bool $withoutUserOptions = false; private ?string $settingsDataClass = null; @@ -311,6 +313,16 @@ public function withArbitraryValues(bool $accepts = true): self return $this; } + /** + * Configure whether field supports multiple values (e.g., multiple emails, phones) + */ + public function supportsMultiValue(bool $supports = true): self + { + $this->supportsMultiValue = $supports; + + return $this; + } + // ========== Data Type Specific Methods (from DataTypeConfigurators) ========== /** @@ -542,6 +554,7 @@ public function data(): FieldTypeData encryptable: $this->encryptable, withoutUserOptions: $this->withoutUserOptions, acceptsArbitraryValues: $this->acceptsArbitraryValues, + supportsMultiValue: $this->supportsMultiValue, validationRules: $this->availableValidationRules, settingsDataClass: $this->settingsDataClass, settingsSchema: $this->settingsSchema diff --git a/src/Filament/Integration/Base/AbstractFormComponent.php b/src/Filament/Integration/Base/AbstractFormComponent.php index 7dbbed71..aef92769 100644 --- a/src/Filament/Integration/Base/AbstractFormComponent.php +++ b/src/Filament/Integration/Base/AbstractFormComponent.php @@ -66,7 +66,12 @@ protected function configure( filled($state) ) ->required($this->validationService->isRequired($customField)) - ->rules($this->validationService->getValidationRules($customField)) + ->rules( + fn (Field $component): array => $this->validationService->getValidationRules( + $customField, + $component->getRecord()?->getKey() + ) + ) ->columnSpan($customField->width->getSpanValue()) ->when( FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CONDITIONAL_VISIBILITY) && diff --git a/src/Models/CustomField.php b/src/Models/CustomField.php index 0cfb734b..b8242ff5 100644 --- a/src/Models/CustomField.php +++ b/src/Models/CustomField.php @@ -26,6 +26,7 @@ use Relaticle\CustomFields\Models\Scopes\TenantScope; use Relaticle\CustomFields\Observers\CustomFieldObserver; use Relaticle\CustomFields\QueryBuilders\CustomFieldQueryBuilder; +use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; /** @@ -172,4 +173,32 @@ public function getFieldName(): string { return 'custom_fields.'.$this->code; } + + /** + * Get field-type-specific settings as a typed Data object. + * + * This retrieves settings from the `settings.additional.type_settings` path + * and deserializes them using the field type's configured settings data class. + * + * @return Data|null The typed settings data object, or null if not configured + */ + public function getTypeSettings(): ?Data + { + $typeData = $this->typeData; + + if ($typeData === null || $typeData->settingsDataClass === null) { + return null; + } + + $typeSettingsData = $this->settings->additional['type_settings'] ?? []; + + if ($typeSettingsData === []) { + return null; + } + + /** @var class-string $dataClass */ + $dataClass = $typeData->settingsDataClass; + + return $dataClass::from($typeSettingsData); + } } diff --git a/src/Rules/UniqueCustomFieldValue.php b/src/Rules/UniqueCustomFieldValue.php new file mode 100644 index 00000000..053a2678 --- /dev/null +++ b/src/Rules/UniqueCustomFieldValue.php @@ -0,0 +1,79 @@ +customField->getValueColumn(); + + foreach ($values as $singleValue) { + if (blank($singleValue)) { + continue; + } + + $query = $valueModel->newQuery() + ->where('custom_field_id', $this->customField->getKey()) + ->where('entity_type', $this->customField->entity_type); + + // Check the appropriate value column based on field type's storage column + if ($valueColumn === 'json_value') { + // For JSON columns, search within the array + $query->whereJsonContains('json_value', $singleValue); + } else { + // For scalar columns (string_value, integer_value, etc.) + $query->where($valueColumn, $singleValue); + } + + // Apply tenant scope if multi-tenancy is enabled + if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) { + $tenantFk = config('custom-fields.database.column_names.tenant_foreign_key'); + $query->where($tenantFk, TenantContextService::getCurrentTenantId()); + } + + // Exclude the current entity if updating + if ($this->ignoreEntityId !== null) { + $query->where('entity_id', '!=', $this->ignoreEntityId); + } + + if ($query->exists()) { + $fail(__('custom-fields::custom-fields.validation.unique_value', [ + 'value' => $singleValue, + ])); + + return; + } + } + } +} diff --git a/src/Services/ValidationService.php b/src/Services/ValidationService.php index e4d30b34..92840390 100644 --- a/src/Services/ValidationService.php +++ b/src/Services/ValidationService.php @@ -9,6 +9,7 @@ use Relaticle\CustomFields\FieldTypeSystem\FieldManager; use Relaticle\CustomFields\Models\CustomField; use Relaticle\CustomFields\Models\CustomFieldValue; +use Relaticle\CustomFields\Rules\UniqueCustomFieldValue; use Relaticle\CustomFields\Support\DatabaseFieldConstraints; use Spatie\LaravelData\DataCollection; @@ -21,14 +22,16 @@ final class ValidationService * Get all validation rules for a custom field, applying both: * - User-defined validation rules from the field configuration * - Database field constraints based on field type + * - Type-specific settings (e.g., unique per entity type for email fields) * - Special handling for numeric values to prevent database errors * * Returns a combined array of validation rules in Laravel validator format. * * @param CustomField $customField The custom field to get validation rules for - * @return array Combined array of validation rules + * @param int|null $ignoreEntityId Entity ID to ignore for unique checks (when updating) + * @return array Combined array of validation rules */ - public function getValidationRules(CustomField $customField): array + public function getValidationRules(CustomField $customField, ?int $ignoreEntityId = null): array { // Convert user rules to Laravel validator format $userRules = $this->convertUserRulesToValidatorFormat($customField->validation_rules, $customField); @@ -41,7 +44,12 @@ public function getValidationRules(CustomField $customField): array $databaseRules = $this->getDatabaseValidationRules($customField->type, $isEncrypted); // Merge all rule types: field defaults + user rules + database constraints - return $this->mergeAllValidationRules($fieldTypeDefaultRules, $userRules, $databaseRules, $customField->type); + $rules = $this->mergeAllValidationRules($fieldTypeDefaultRules, $userRules, $databaseRules, $customField->type); + + // Add type-specific rules based on settings + $typeSpecificRules = $this->getTypeSpecificRules($customField, $ignoreEntityId); + + return array_merge($rules, $typeSpecificRules); } /** @@ -183,6 +191,25 @@ private function getFieldTypeDefaultRules(string $fieldType): array return []; } + /** + * Get type-specific validation rules based on field settings. + * + * @param CustomField $customField The custom field + * @param int|null $ignoreEntityId Entity ID to ignore for unique checks + * @return array Type-specific validation rules + */ + private function getTypeSpecificRules(CustomField $customField, ?int $ignoreEntityId = null): array + { + $rules = []; + + // Handle unique per entity type setting (available for any field type) + if ($customField->settings->unique_per_entity_type) { + $rules[] = new UniqueCustomFieldValue($customField, $ignoreEntityId); + } + + return $rules; + } + /** * Merge field type defaults, user rules, and database constraints with proper precedence. * Field type defaults are always applied, user rules can add additional constraints, From b7522711f62ff72e6037bcb4c99b977bce2d3e81 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:10:34 +0400 Subject: [PATCH 05/20] feat: refactor email field to use tags input for multi-value support --- .../Definitions/EmailFieldType.php | 11 ++++------ .../Components/Forms/EmailComponent.php | 21 +++++++++---------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/FieldTypeSystem/Definitions/EmailFieldType.php b/src/FieldTypeSystem/Definitions/EmailFieldType.php index 0c3f4be2..fece8f3b 100644 --- a/src/FieldTypeSystem/Definitions/EmailFieldType.php +++ b/src/FieldTypeSystem/Definitions/EmailFieldType.php @@ -19,7 +19,7 @@ class EmailFieldType extends BaseFieldType { public function configure(): FieldSchema { - return FieldSchema::string() + return FieldSchema::multiChoice() ->key('email') ->label('Email') ->icon('heroicon-o-envelope') @@ -27,18 +27,15 @@ public function configure(): FieldSchema ->tableColumn(TextColumn::class) ->infolistEntry(TextEntry::class) ->priority(15) - ->encryptable() - ->searchable() ->sortable() - ->defaultValidationRules([ValidationRule::EMAIL]) + ->supportsMultiValue() + ->withArbitraryValues() + ->withoutUserOptions() ->availableValidationRules([ ValidationRule::REQUIRED, - ValidationRule::EMAIL, ValidationRule::MIN, ValidationRule::MAX, - ValidationRule::REGEX, ValidationRule::UNIQUE, - ValidationRule::EXISTS, ]); } } diff --git a/src/Filament/Integration/Components/Forms/EmailComponent.php b/src/Filament/Integration/Components/Forms/EmailComponent.php index 6cbfc1f9..18dccb55 100644 --- a/src/Filament/Integration/Components/Forms/EmailComponent.php +++ b/src/Filament/Integration/Components/Forms/EmailComponent.php @@ -5,7 +5,7 @@ namespace Relaticle\CustomFields\Filament\Integration\Components\Forms; use Filament\Forms\Components\Field; -use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\TagsInput; use Relaticle\CustomFields\Filament\Integration\Base\AbstractFormComponent; use Relaticle\CustomFields\Models\CustomField; @@ -13,16 +13,15 @@ { public function create(CustomField $customField): Field { - $defaults = [ - 'email' => true, // Client-side validation for UX - 'maxLength' => 255, - 'suffixIcon' => 'heroicon-m-envelope', - 'autocomplete' => 'email', - 'type' => 'email', - ]; + $maxValues = $customField->settings->allow_multiple + ? $customField->settings->max_values + : 1; - $component = TextInput::make($customField->getFieldName()); - - return $this->applySettingsToComponent($component, $defaults); + return TagsInput::make($customField->getFieldName()) + ->placeholder(__('custom-fields::custom-fields.email.add_email_placeholder')) + ->splitKeys(['Tab', ',', 'Enter']) + ->nestedRecursiveRules(['email']) + ->rules(['array', 'max:'.$maxValues]) + ->reorderable(); } } From de7aed02c08fe41c47d7326b4fc427ff231eb77c Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:10:45 +0400 Subject: [PATCH 06/20] fix: add dark mode support and remove empty state borders --- .../views/livewire/manage-custom-field-section.blade.php | 4 ++-- .../views/livewire/manage-custom-field-width.blade.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/views/livewire/manage-custom-field-section.blade.php b/resources/views/livewire/manage-custom-field-section.blade.php index 7715fe0d..1bc961dd 100644 --- a/resources/views/livewire/manage-custom-field-section.blade.php +++ b/resources/views/livewire/manage-custom-field-section.blade.php @@ -40,8 +40,8 @@ class="fi-sc fi-sc-has-gap fi-grid lg:fi-grid-cols" @if(!count($this->fields))
-
-
+
+
-
{{ $selectedWidth }}%
+
{{ $selectedWidth }}%
From b14d37edfb19915db836af9fbaf1a448960a1b02 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:10:55 +0400 Subject: [PATCH 07/20] feat: add translations for new field settings and empty states --- resources/lang/en/custom-fields.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/resources/lang/en/custom-fields.php b/resources/lang/en/custom-fields.php index 9bd318cb..43ce13bd 100644 --- a/resources/lang/en/custom-fields.php +++ b/resources/lang/en/custom-fields.php @@ -24,6 +24,7 @@ 'default' => [ 'new_section' => 'New Section', ], + 'default_section_name' => 'Default', ], 'field' => [ @@ -65,6 +66,12 @@ ], 'add_field' => 'Add Field', 'system_defined_cannot_delete' => 'System-defined fields cannot be deleted.', + 'allow_multiple' => 'Allow Multiple Values', + 'allow_multiple_help' => 'When enabled, users can enter multiple values for this field.', + 'max_values' => 'Maximum Values', + 'max_values_help' => 'The maximum number of values that can be entered.', + 'unique_per_entity_type' => 'Unique Per Entity Type', + 'unique_per_entity_type_help' => 'Each value can only be assigned to one record of this entity type.', 'validation' => [ 'label' => 'Validation', 'rules' => 'Validation Rules', @@ -315,6 +322,7 @@ 'multi_parameter_missing' => 'This validation rule requires multiple parameters. Please add all required parameters.', 'parameter_missing' => 'This validation rule requires exactly :count parameter(s). Please add all required parameters.', 'invalid_rule_for_field_type' => 'The selected rule is not valid for this field type.', + 'unique_value' => 'The value ":value" is already assigned to another record.', ], 'empty_states' => [ @@ -328,9 +336,19 @@ 'description' => 'Drag and drop fields here or click the button below to add your first field.', 'icon' => 'heroicon-o-squares-plus', ], + 'fields_no_sections' => [ + 'heading' => 'No custom fields yet', + 'description' => 'Click the button below to add your first custom field.', + 'icon' => 'heroicon-o-squares-plus', + ], ], 'common' => [ 'inactive' => 'Inactive', ], + + 'email' => [ + 'add_email_placeholder' => 'Add email address...', + ], + ]; From 37e71bb1a5faf6923fc7a34d364e34b9da676d14 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:16:14 +0400 Subject: [PATCH 08/20] refactor: extract field creation logic to shared trait --- src/Livewire/Concerns/CreatesCustomFields.php | 54 +++++++++++++++++++ src/Livewire/ManageCustomFieldSection.php | 49 ++--------------- src/Livewire/ManageFieldsWithoutSections.php | 51 ++---------------- 3 files changed, 64 insertions(+), 90 deletions(-) create mode 100644 src/Livewire/Concerns/CreatesCustomFields.php diff --git a/src/Livewire/Concerns/CreatesCustomFields.php b/src/Livewire/Concerns/CreatesCustomFields.php new file mode 100644 index 00000000..86119bec --- /dev/null +++ b/src/Livewire/Concerns/CreatesCustomFields.php @@ -0,0 +1,54 @@ + $entityType, + 'custom_field_section_id' => $sectionId, + ]; + } + + protected function storeField(array $data): void + { + $options = collect($data['options'] ?? []) + ->filter() + ->map(function (array $option): array { + if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) { + $option[config('custom-fields.database.column_names.tenant_foreign_key')] = TenantContextService::getCurrentTenantId(); + } + + return $option; + }) + ->values(); + + unset($data['options']); + + $customField = CustomFields::newCustomFieldModel()->create($data); + + $customField->options()->createMany($options); + } +} diff --git a/src/Livewire/ManageCustomFieldSection.php b/src/Livewire/ManageCustomFieldSection.php index 2ce6f3a9..42eb26a6 100644 --- a/src/Livewire/ManageCustomFieldSection.php +++ b/src/Livewire/ManageCustomFieldSection.php @@ -18,16 +18,14 @@ use Livewire\Attributes\On; use Livewire\Component; use Relaticle\CustomFields\CustomFields; -use Relaticle\CustomFields\Enums\CustomFieldsFeature; -use Relaticle\CustomFields\FeatureSystem\FeatureManager; use Relaticle\CustomFields\Filament\Management\Schemas\FieldForm; use Relaticle\CustomFields\Filament\Management\Schemas\SectionForm; +use Relaticle\CustomFields\Livewire\Concerns\CreatesCustomFields; use Relaticle\CustomFields\Models\CustomFieldSection; -use Relaticle\CustomFields\Services\TenantContextService; -use Relaticle\CustomFields\Support\CodeGenerator; final class ManageCustomFieldSection extends Component implements HasActions, HasForms { + use CreatesCustomFields; use InteractsWithActions; use InteractsWithForms; @@ -163,46 +161,9 @@ public function createFieldAction(): Action ->label(__('custom-fields::custom-fields.field.form.add_field')) ->model(CustomFields::customFieldModel()) ->schema(FieldForm::schema(withOptionsRelationship: false)) - ->fillForm([ - 'entity_type' => $this->entityType, - ]) - ->mutateDataUsing(function (array $data): array { - if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) { - $data[config('custom-fields.database.column_names.tenant_foreign_key')] = TenantContextService::getCurrentTenantId(); - } - - // Auto-generate code if feature is enabled and code is empty - if (FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) && blank($data['code'] ?? null)) { - $data['code'] = CodeGenerator::generateUniqueFieldCode( - $data['name'], - $this->entityType - ); - } - - return [ - ...$data, - 'entity_type' => $this->entityType, - 'custom_field_section_id' => $this->section->getKey(), - ]; - }) - ->action(function (array $data): void { - $options = collect($data['options'] ?? []) - ->filter() - ->map(function (array $option): array { - if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) { - $option[config('custom-fields.database.column_names.tenant_foreign_key')] = TenantContextService::getCurrentTenantId(); - } - - return $option; - }) - ->values(); - - unset($data['options']); - - $customField = CustomFields::newCustomFieldModel()->create($data); - - $customField->options()->createMany($options); - }) + ->fillForm(['entity_type' => $this->entityType]) + ->mutateDataUsing(fn (array $data): array => $this->mutateFieldData($data, $this->entityType, $this->section->getKey())) + ->action(fn (array $data) => $this->storeField($data)) ->modalWidth(Width::ScreenLarge) ->slideOver(); } diff --git a/src/Livewire/ManageFieldsWithoutSections.php b/src/Livewire/ManageFieldsWithoutSections.php index ba725502..3e20b006 100644 --- a/src/Livewire/ManageFieldsWithoutSections.php +++ b/src/Livewire/ManageFieldsWithoutSections.php @@ -17,21 +17,16 @@ use Livewire\Attributes\On; use Livewire\Component; use Relaticle\CustomFields\CustomFields; -use Relaticle\CustomFields\Enums\CustomFieldsFeature; -use Relaticle\CustomFields\FeatureSystem\FeatureManager; use Relaticle\CustomFields\Filament\Management\Schemas\FieldForm; +use Relaticle\CustomFields\Livewire\Concerns\CreatesCustomFields; use Relaticle\CustomFields\Models\CustomFieldSection; -use Relaticle\CustomFields\Services\TenantContextService; -use Relaticle\CustomFields\Support\CodeGenerator; /** * Livewire component for managing custom fields when sections are disabled. - * - * This component displays fields in a flat grid without any section wrapper, - * while the default hidden section is managed in the background. */ final class ManageFieldsWithoutSections extends Component implements HasActions, HasForms { + use CreatesCustomFields; use InteractsWithActions; use InteractsWithForms; @@ -96,45 +91,9 @@ public function createFieldAction(): Action ]) ->model(CustomFields::customFieldModel()) ->schema(FieldForm::schema(withOptionsRelationship: false)) - ->fillForm([ - 'entity_type' => $this->entityType, - ]) - ->mutateDataUsing(function (array $data): array { - if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) { - $data[config('custom-fields.database.column_names.tenant_foreign_key')] = TenantContextService::getCurrentTenantId(); - } - - if (FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) && blank($data['code'] ?? null)) { - $data['code'] = CodeGenerator::generateUniqueFieldCode( - $data['name'], - $this->entityType - ); - } - - return [ - ...$data, - 'entity_type' => $this->entityType, - 'custom_field_section_id' => $this->section->getKey(), - ]; - }) - ->action(function (array $data): void { - $options = collect($data['options'] ?? []) - ->filter() - ->map(function (array $option): array { - if (FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY)) { - $option[config('custom-fields.database.column_names.tenant_foreign_key')] = TenantContextService::getCurrentTenantId(); - } - - return $option; - }) - ->values(); - - unset($data['options']); - - $customField = CustomFields::newCustomFieldModel()->create($data); - - $customField->options()->createMany($options); - }) + ->fillForm(['entity_type' => $this->entityType]) + ->mutateDataUsing(fn (array $data): array => $this->mutateFieldData($data, $this->entityType, $this->section->getKey())) + ->action(fn (array $data) => $this->storeField($data)) ->modalWidth(Width::ScreenLarge) ->slideOver(); } From b1032b5f4a4518e442c10c31c61ba0b92f7e3b54 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:16:20 +0400 Subject: [PATCH 09/20] chore: remove unused getTypeSettings method and import --- src/Models/CustomField.php | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/Models/CustomField.php b/src/Models/CustomField.php index b8242ff5..0cfb734b 100644 --- a/src/Models/CustomField.php +++ b/src/Models/CustomField.php @@ -26,7 +26,6 @@ use Relaticle\CustomFields\Models\Scopes\TenantScope; use Relaticle\CustomFields\Observers\CustomFieldObserver; use Relaticle\CustomFields\QueryBuilders\CustomFieldQueryBuilder; -use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; /** @@ -173,32 +172,4 @@ public function getFieldName(): string { return 'custom_fields.'.$this->code; } - - /** - * Get field-type-specific settings as a typed Data object. - * - * This retrieves settings from the `settings.additional.type_settings` path - * and deserializes them using the field type's configured settings data class. - * - * @return Data|null The typed settings data object, or null if not configured - */ - public function getTypeSettings(): ?Data - { - $typeData = $this->typeData; - - if ($typeData === null || $typeData->settingsDataClass === null) { - return null; - } - - $typeSettingsData = $this->settings->additional['type_settings'] ?? []; - - if ($typeSettingsData === []) { - return null; - } - - /** @var class-string $dataClass */ - $dataClass = $typeData->settingsDataClass; - - return $dataClass::from($typeSettingsData); - } } From 148372da0255e5e32a34e960606101dbf1b00260 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:16:28 +0400 Subject: [PATCH 10/20] feat: add searchable capability to email field type --- src/FieldTypeSystem/Definitions/EmailFieldType.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/FieldTypeSystem/Definitions/EmailFieldType.php b/src/FieldTypeSystem/Definitions/EmailFieldType.php index fece8f3b..00b1469e 100644 --- a/src/FieldTypeSystem/Definitions/EmailFieldType.php +++ b/src/FieldTypeSystem/Definitions/EmailFieldType.php @@ -27,6 +27,7 @@ public function configure(): FieldSchema ->tableColumn(TextColumn::class) ->infolistEntry(TextEntry::class) ->priority(15) + ->searchable() ->sortable() ->supportsMultiValue() ->withArbitraryValues() From b7d1f79aa44252882351c6577dd8277403fdef8c Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:23:18 +0400 Subject: [PATCH 11/20] chore: remove unused fieldmanager variable and import --- src/Filament/Management/Schemas/FieldForm.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Filament/Management/Schemas/FieldForm.php b/src/Filament/Management/Schemas/FieldForm.php index b859eabb..7d1e9119 100644 --- a/src/Filament/Management/Schemas/FieldForm.php +++ b/src/Filament/Management/Schemas/FieldForm.php @@ -26,7 +26,6 @@ use Relaticle\CustomFields\Facades\CustomFieldsType; use Relaticle\CustomFields\Facades\Entities; use Relaticle\CustomFields\FeatureSystem\FeatureManager; -use Relaticle\CustomFields\FieldTypeSystem\FieldManager; use Relaticle\CustomFields\Filament\Management\Forms\Components\CustomFieldValidationComponent; use Relaticle\CustomFields\Filament\Management\Forms\Components\TypeField; use Relaticle\CustomFields\Filament\Management\Forms\Components\VisibilityComponent; @@ -42,8 +41,6 @@ class FieldForm implements FormInterface */ private static function getTypeSettingsSchema(): array { - $fieldManager = app(FieldManager::class); - $components = []; foreach (CustomFieldsType::toCollection() as $fieldTypeData) { From 7aba8d1f4a0722f738006055aa83a1cec7069901 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:23:22 +0400 Subject: [PATCH 12/20] refactor: extract shared field management methods to trait --- src/Livewire/Concerns/ManagesFields.php | 43 ++++++++++++++++++++ src/Livewire/ManageCustomFieldSection.php | 34 +--------------- src/Livewire/ManageFieldsWithoutSections.php | 32 +-------------- 3 files changed, 47 insertions(+), 62 deletions(-) create mode 100644 src/Livewire/Concerns/ManagesFields.php diff --git a/src/Livewire/Concerns/ManagesFields.php b/src/Livewire/Concerns/ManagesFields.php new file mode 100644 index 00000000..bd45f683 --- /dev/null +++ b/src/Livewire/Concerns/ManagesFields.php @@ -0,0 +1,43 @@ +section->fields()->withDeactivated()->orderBy('sort_order')->get(); + } + + #[On('field-width-updated')] + public function fieldWidthUpdated(int|string $fieldId, int $width): void + { + $model = CustomFields::newCustomFieldModel(); + $model->where($model->getKeyName(), $fieldId)->update(['width' => $width]); + + $this->section->refresh(); + } + + #[On('field-deleted')] + public function fieldDeleted(): void + { + $this->section->refresh(); + } + + #[On('fields-reordered')] + public function fieldsReordered(): void + { + unset($this->fields); + } +} diff --git a/src/Livewire/ManageCustomFieldSection.php b/src/Livewire/ManageCustomFieldSection.php index 42eb26a6..8690b1b2 100644 --- a/src/Livewire/ManageCustomFieldSection.php +++ b/src/Livewire/ManageCustomFieldSection.php @@ -13,14 +13,12 @@ use Filament\Support\Enums\Size; use Filament\Support\Enums\Width; use Illuminate\Contracts\View\View; -use Illuminate\Database\Eloquent\Collection; -use Livewire\Attributes\Computed; -use Livewire\Attributes\On; use Livewire\Component; use Relaticle\CustomFields\CustomFields; use Relaticle\CustomFields\Filament\Management\Schemas\FieldForm; use Relaticle\CustomFields\Filament\Management\Schemas\SectionForm; use Relaticle\CustomFields\Livewire\Concerns\CreatesCustomFields; +use Relaticle\CustomFields\Livewire\Concerns\ManagesFields; use Relaticle\CustomFields\Models\CustomFieldSection; final class ManageCustomFieldSection extends Component implements HasActions, HasForms @@ -28,40 +26,12 @@ final class ManageCustomFieldSection extends Component implements HasActions, Ha use CreatesCustomFields; use InteractsWithActions; use InteractsWithForms; + use ManagesFields; public string $entityType; public CustomFieldSection $section; - #[Computed] - public function fields(): Collection - { - return $this->section->fields()->withDeactivated()->orderBy('sort_order')->get(); - } - - #[On('field-width-updated')] - public function fieldWidthUpdated(int|string $fieldId, int $width): void - { - // Update the width - $model = CustomFields::newCustomFieldModel(); - $model->where($model->getKeyName(), $fieldId)->update(['width' => $width]); - - // Re-fetch the fields - $this->section->refresh(); - } - - #[On('field-deleted')] - public function fieldDeleted(): void - { - $this->section->refresh(); - } - - #[On('fields-reordered')] - public function fieldsReordered(): void - { - unset($this->fields); - } - public function updateFieldsOrder(int|string $sectionId, array $fields): void { $model = CustomFields::newCustomFieldModel(); diff --git a/src/Livewire/ManageFieldsWithoutSections.php b/src/Livewire/ManageFieldsWithoutSections.php index 3e20b006..c6bdf0a9 100644 --- a/src/Livewire/ManageFieldsWithoutSections.php +++ b/src/Livewire/ManageFieldsWithoutSections.php @@ -12,13 +12,11 @@ use Filament\Support\Enums\Size; use Filament\Support\Enums\Width; use Illuminate\Contracts\View\View; -use Illuminate\Database\Eloquent\Collection; -use Livewire\Attributes\Computed; -use Livewire\Attributes\On; use Livewire\Component; use Relaticle\CustomFields\CustomFields; use Relaticle\CustomFields\Filament\Management\Schemas\FieldForm; use Relaticle\CustomFields\Livewire\Concerns\CreatesCustomFields; +use Relaticle\CustomFields\Livewire\Concerns\ManagesFields; use Relaticle\CustomFields\Models\CustomFieldSection; /** @@ -29,38 +27,12 @@ final class ManageFieldsWithoutSections extends Component implements HasActions, use CreatesCustomFields; use InteractsWithActions; use InteractsWithForms; + use ManagesFields; public string $entityType; public CustomFieldSection $section; - #[Computed] - public function fields(): Collection - { - return $this->section->fields()->withDeactivated()->orderBy('sort_order')->get(); - } - - #[On('field-width-updated')] - public function fieldWidthUpdated(int|string $fieldId, int $width): void - { - $model = CustomFields::newCustomFieldModel(); - $model->where($model->getKeyName(), $fieldId)->update(['width' => $width]); - - $this->section->refresh(); - } - - #[On('field-deleted')] - public function fieldDeleted(): void - { - $this->section->refresh(); - } - - #[On('fields-reordered')] - public function fieldsReordered(): void - { - unset($this->fields); - } - public function updateFieldsOrder(array $fields): void { $model = CustomFields::newCustomFieldModel(); From cd9e48cc79b293326ae7260d5be0f992f42f01bb Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:26:39 +0400 Subject: [PATCH 13/20] feat: add command to migrate email field values to array format --- .../MigrateEmailFieldValuesCommand.php | 122 ++++++++++++++++++ src/CustomFieldsServiceProvider.php | 2 + 2 files changed, 124 insertions(+) create mode 100644 src/Console/Commands/MigrateEmailFieldValuesCommand.php diff --git a/src/Console/Commands/MigrateEmailFieldValuesCommand.php b/src/Console/Commands/MigrateEmailFieldValuesCommand.php new file mode 100644 index 00000000..72030fdc --- /dev/null +++ b/src/Console/Commands/MigrateEmailFieldValuesCommand.php @@ -0,0 +1,122 @@ +option('dry-run'); + + if (app()->isProduction() && ! $isDryRun && ! $this->option('force')) { + if (! $this->confirm('You are running in production. Are you sure you want to continue?')) { + $this->info('Migration cancelled.'); + + return self::SUCCESS; + } + } + + $fieldTable = config('custom-fields.database.table_names.custom_fields'); + $valueTable = config('custom-fields.database.table_names.custom_field_values'); + + // Find all email type custom fields + $emailFields = DB::table($fieldTable) + ->where('type', 'email') + ->pluck('id'); + + if ($emailFields->isEmpty()) { + $this->info('No email fields found. Nothing to migrate.'); + + return self::SUCCESS; + } + + $this->info(sprintf('Found %d email field(s).', $emailFields->count())); + + // Find values that need migration (have string_value but no json_value) + $valuesToMigrate = DB::table($valueTable) + ->whereIn('custom_field_id', $emailFields) + ->whereNotNull('string_value') + ->where('string_value', '!=', '') + ->where(function ($query) { + $query->whereNull('json_value') + ->orWhere('json_value', '=', '[]') + ->orWhere('json_value', '=', 'null'); + }) + ->get(); + + if ($valuesToMigrate->isEmpty()) { + $this->info('No email values need migration. All values are already in the correct format.'); + + return self::SUCCESS; + } + + $this->info(sprintf('Found %d email value(s) to migrate.', $valuesToMigrate->count())); + + if ($isDryRun) { + $this->warn('Dry run mode - no changes will be made.'); + $this->newLine(); + + $this->table( + ['ID', 'Entity Type', 'Entity ID', 'Current Value', 'New Format'], + $valuesToMigrate->map(fn ($value) => [ + $value->id, + $value->entity_type, + $value->entity_id, + $value->string_value, + json_encode([$value->string_value]), + ])->toArray() + ); + + return self::SUCCESS; + } + + $bar = $this->output->createProgressBar($valuesToMigrate->count()); + $bar->start(); + + $migrated = 0; + $errors = 0; + + foreach ($valuesToMigrate as $value) { + try { + DB::table($valueTable) + ->where('id', $value->id) + ->update([ + 'json_value' => json_encode([$value->string_value]), + 'string_value' => null, + ]); + + $migrated++; + } catch (\Throwable $e) { + $this->newLine(); + $this->error(sprintf('Failed to migrate value ID %d: %s', $value->id, $e->getMessage())); + $errors++; + } + + $bar->advance(); + } + + $bar->finish(); + $this->newLine(2); + + $this->info(sprintf('Migration complete. Migrated: %d, Errors: %d', $migrated, $errors)); + + return $errors > 0 ? self::FAILURE : self::SUCCESS; + } +} diff --git a/src/CustomFieldsServiceProvider.php b/src/CustomFieldsServiceProvider.php index d0c142a5..02d4b48e 100644 --- a/src/CustomFieldsServiceProvider.php +++ b/src/CustomFieldsServiceProvider.php @@ -15,6 +15,7 @@ use Livewire\Livewire; use Relaticle\CustomFields\Console\Commands\MakeCustomFieldsMigrationCommand; use Relaticle\CustomFields\Console\Commands\MakeFieldTypeCommand; +use Relaticle\CustomFields\Console\Commands\MigrateEmailFieldValuesCommand; use Relaticle\CustomFields\Contracts\CustomsFieldsMigrators; use Relaticle\CustomFields\Contracts\ValueResolvers; use Relaticle\CustomFields\Enums\CustomFieldsFeature; @@ -168,6 +169,7 @@ private function getCommands(): array return [ MakeCustomFieldsMigrationCommand::class, MakeFieldTypeCommand::class, + MigrateEmailFieldValuesCommand::class, ]; } From 297c17427876ee9bcc736265fd13d6bc7b859f5d Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:31:47 +0400 Subject: [PATCH 14/20] refactor: simplify field form layout and hide entity type --- .../MigrateEmailFieldValuesCommand.php | 15 +- src/Filament/Management/Schemas/FieldForm.php | 217 ++++++------------ 2 files changed, 82 insertions(+), 150 deletions(-) diff --git a/src/Console/Commands/MigrateEmailFieldValuesCommand.php b/src/Console/Commands/MigrateEmailFieldValuesCommand.php index 72030fdc..dc8427d1 100644 --- a/src/Console/Commands/MigrateEmailFieldValuesCommand.php +++ b/src/Console/Commands/MigrateEmailFieldValuesCommand.php @@ -6,6 +6,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; +use Throwable; /** * Migrates email field values from string_value to json_value format. @@ -25,12 +26,10 @@ public function handle(): int { $isDryRun = $this->option('dry-run'); - if (app()->isProduction() && ! $isDryRun && ! $this->option('force')) { - if (! $this->confirm('You are running in production. Are you sure you want to continue?')) { - $this->info('Migration cancelled.'); + if (app()->isProduction() && ! $isDryRun && ! $this->option('force') && ! $this->confirm('You are running in production. Are you sure you want to continue?')) { + $this->info('Migration cancelled.'); - return self::SUCCESS; - } + return self::SUCCESS; } $fieldTable = config('custom-fields.database.table_names.custom_fields'); @@ -54,7 +53,7 @@ public function handle(): int ->whereIn('custom_field_id', $emailFields) ->whereNotNull('string_value') ->where('string_value', '!=', '') - ->where(function ($query) { + ->where(function ($query): void { $query->whereNull('json_value') ->orWhere('json_value', '=', '[]') ->orWhere('json_value', '=', 'null'); @@ -75,7 +74,7 @@ public function handle(): int $this->table( ['ID', 'Entity Type', 'Entity ID', 'Current Value', 'New Format'], - $valuesToMigrate->map(fn ($value) => [ + $valuesToMigrate->map(fn ($value): array => [ $value->id, $value->entity_type, $value->entity_id, @@ -103,7 +102,7 @@ public function handle(): int ]); $migrated++; - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->newLine(); $this->error(sprintf('Failed to migrate value ID %d: %s', $value->id, $e->getMessage())); $errors++; diff --git a/src/Filament/Management/Schemas/FieldForm.php b/src/Filament/Management/Schemas/FieldForm.php index 7d1e9119..8e89d155 100644 --- a/src/Filament/Management/Schemas/FieldForm.php +++ b/src/Filament/Management/Schemas/FieldForm.php @@ -14,6 +14,7 @@ use Filament\Forms\Components\Toggle; use Filament\Schemas\Components\Component; use Filament\Schemas\Components\Fieldset; +use Filament\Schemas\Components\Grid; use Filament\Schemas\Components\Tabs; use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Components\Utilities\Get; @@ -131,157 +132,89 @@ public static function schema(bool $withOptionsRelationship = true): array Tab::make( __('custom-fields::custom-fields.field.form.general') )->schema([ - Select::make('entity_type') - ->label( - __( - 'custom-fields::custom-fields.field.form.entity_type' - ) - ) - ->options(Entities::getOptions(onlyCustomFields: true)) - ->disabled() + Hidden::make('entity_type') ->default( fn () => request( 'entityType', (Entities::withCustomFields()->first()?->getAlias()) ?? '' ) - ) - ->required(), - TypeField::make('type') - ->label( - __( - 'custom-fields::custom-fields.field.form.type' - ) - ) - ->disabled( - fn ( - ?CustomField $record - ): bool => (bool) $record?->exists - ) - ->live() - ->afterStateHydrated(function ( - Select $component, - mixed $state, - ?CustomField $record - ): void { - if (blank($state)) { - $component->state( - $record->type ?? CustomFieldsType::toCollection()->first()->key - ); - } - }) - ->required(), - TextInput::make('name') - ->label( - __( - 'custom-fields::custom-fields.field.form.name' - ) - ) - ->helperText( - __( - 'custom-fields::custom-fields.field.form.name_helper_text' - ) - ) - ->live(onBlur: true) - ->required() - ->maxLength(50) - ->disabled( - fn ( - ?CustomField $record - ): bool => (bool) $record?->system_defined - ) - ->unique( - table: CustomFields::customFieldModel(), - column: 'name', - ignoreRecord: true, - modifyRuleUsing: fn ( - Unique $rule, - Get $get - ) => $rule - ->where('entity_type', $get('entity_type')) - ->when( - FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY), - fn (Unique $rule) => $rule->where( - config( - 'custom-fields.database.column_names.tenant_foreign_key' - ), - TenantContextService::getCurrentTenantId() - ) + ), + Grid::make() + ->columns(fn (): int => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) ? 2 : 3) + ->columnSpanFull() + ->schema([ + TypeField::make('type') + ->label(__('custom-fields::custom-fields.field.form.type')) + ->disabled(fn (?CustomField $record): bool => (bool) $record?->exists) + ->live() + ->afterStateHydrated(function ( + Select $component, + mixed $state, + ?CustomField $record + ): void { + if (blank($state)) { + $component->state( + $record->type ?? CustomFieldsType::toCollection()->first()->key + ); + } + }) + ->required(), + TextInput::make('name') + ->label(__('custom-fields::custom-fields.field.form.name')) + ->live(onBlur: true) + ->required() + ->maxLength(50) + ->disabled(fn (?CustomField $record): bool => (bool) $record?->system_defined) + ->unique( + table: CustomFields::customFieldModel(), + column: 'name', + ignoreRecord: true, + modifyRuleUsing: fn (Unique $rule, Get $get) => $rule + ->where('entity_type', $get('entity_type')) + ->when( + FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY), + fn (Unique $rule) => $rule->where( + config('custom-fields.database.column_names.tenant_foreign_key'), + TenantContextService::getCurrentTenantId() + ) + ) ) - ) - ->afterStateUpdated(function ( - Get $get, - Set $set, - ?string $old, - ?string $state - ): void { - $old ??= ''; - $state ??= ''; + ->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state): void { + $old ??= ''; + $state ??= ''; - if ( - ($get('code') ?? '') !== - Str::of($old)->slug('_')->toString() - ) { - return; - } + if (($get('code') ?? '') !== Str::of($old)->slug('_')->toString()) { + return; + } - $set( - 'code', - Str::of($state)->slug('_')->toString() - ); - }), - TextInput::make('code') - ->label( - __( - 'custom-fields::custom-fields.field.form.code' - ) - ) - ->helperText( - __( - 'custom-fields::custom-fields.field.form.code_helper_text' - ) - ) - ->live(onBlur: true) - ->required( - fn (): bool => ! FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) - ) - ->alphaDash() - ->maxLength(50) - ->disabled( - fn ( - ?CustomField $record - ): bool => (bool) $record?->system_defined - ) - ->visible( - fn (): bool => ! FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE) - ) - ->unique( - table: CustomFields::customFieldModel(), - column: 'code', - ignoreRecord: true, - modifyRuleUsing: fn ( - Unique $rule, - Get $get - ) => $rule - ->where('entity_type', $get('entity_type')) - ->when( - FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY), - fn (Unique $rule) => $rule->where( - config( - 'custom-fields.database.column_names.tenant_foreign_key' - ), - TenantContextService::getCurrentTenantId() - ) + $set('code', Str::of($state)->slug('_')->toString()); + }), + TextInput::make('code') + ->label(__('custom-fields::custom-fields.field.form.code')) + ->live(onBlur: true) + ->required(fn (): bool => ! FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE)) + ->alphaDash() + ->maxLength(50) + ->disabled(fn (?CustomField $record): bool => (bool) $record?->system_defined) + ->visible(fn (): bool => ! FeatureManager::isEnabled(CustomFieldsFeature::FIELD_CODE_AUTO_GENERATE)) + ->unique( + table: CustomFields::customFieldModel(), + column: 'code', + ignoreRecord: true, + modifyRuleUsing: fn (Unique $rule, Get $get) => $rule + ->where('entity_type', $get('entity_type')) + ->when( + FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_MULTI_TENANCY), + fn (Unique $rule) => $rule->where( + config('custom-fields.database.column_names.tenant_foreign_key'), + TenantContextService::getCurrentTenantId() + ) + ) ) - ) - ->afterStateUpdated(function ( - Set $set, - ?string $state - ): void { - $set( - 'code', - Str::of($state)->slug('_')->toString() - ); - }), + ->afterStateUpdated(function (Set $set, ?string $state): void { + $set('code', Str::of($state)->slug('_')->toString()); + }), + ]), Fieldset::make( __( 'custom-fields::custom-fields.field.form.settings' From 56ae025a2c46fd15604c5ef472def026e65cfc51 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 02:53:55 +0400 Subject: [PATCH 15/20] fix: show all fields when sections disabled regardless of section --- src/Livewire/ManageFieldsWithoutSections.php | 43 +++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Livewire/ManageFieldsWithoutSections.php b/src/Livewire/ManageFieldsWithoutSections.php index c6bdf0a9..95b8fc63 100644 --- a/src/Livewire/ManageFieldsWithoutSections.php +++ b/src/Livewire/ManageFieldsWithoutSections.php @@ -12,27 +12,66 @@ use Filament\Support\Enums\Size; use Filament\Support\Enums\Width; use Illuminate\Contracts\View\View; +use Illuminate\Database\Eloquent\Collection; +use Livewire\Attributes\Computed; +use Livewire\Attributes\On; use Livewire\Component; use Relaticle\CustomFields\CustomFields; use Relaticle\CustomFields\Filament\Management\Schemas\FieldForm; use Relaticle\CustomFields\Livewire\Concerns\CreatesCustomFields; -use Relaticle\CustomFields\Livewire\Concerns\ManagesFields; use Relaticle\CustomFields\Models\CustomFieldSection; /** * Livewire component for managing custom fields when sections are disabled. + * + * Shows ALL fields for the entity type regardless of which section they belong to. + * New fields are created in the default section. */ final class ManageFieldsWithoutSections extends Component implements HasActions, HasForms { use CreatesCustomFields; use InteractsWithActions; use InteractsWithForms; - use ManagesFields; public string $entityType; + /** The default section used for creating new fields */ public CustomFieldSection $section; + /** + * Get ALL fields for the entity type, regardless of section. + */ + #[Computed] + public function fields(): Collection + { + return CustomFields::newCustomFieldModel() + ->newQuery() + ->withDeactivated() + ->where('entity_type', $this->entityType) + ->orderBy('sort_order') + ->get(); + } + + #[On('field-width-updated')] + public function fieldWidthUpdated(int|string $fieldId, int $width): void + { + $model = CustomFields::newCustomFieldModel(); + $model->where($model->getKeyName(), $fieldId)->update(['width' => $width]); + unset($this->fields); + } + + #[On('field-deleted')] + public function fieldDeleted(): void + { + unset($this->fields); + } + + #[On('fields-reordered')] + public function fieldsReordered(): void + { + unset($this->fields); + } + public function updateFieldsOrder(array $fields): void { $model = CustomFields::newCustomFieldModel(); From 499d73e47dce95947d63cb45df72d4613a46fce1 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 18:11:46 +0400 Subject: [PATCH 16/20] fix: improve field card density and icon styling --- .../views/livewire/manage-custom-field-section.blade.php | 4 +++- resources/views/livewire/manage-custom-field.blade.php | 4 +++- .../views/livewire/manage-fields-without-sections.blade.php | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/resources/views/livewire/manage-custom-field-section.blade.php b/resources/views/livewire/manage-custom-field-section.blade.php index 1bc961dd..b350f1a3 100644 --- a/resources/views/livewire/manage-custom-field-section.blade.php +++ b/resources/views/livewire/manage-custom-field-section.blade.php @@ -10,6 +10,8 @@ @@ -30,7 +32,7 @@ x-sortable-group="fields" data-section-id="{{ $section->id }}" default="12" - class="fi-sc fi-sc-has-gap fi-grid lg:fi-grid-cols" + class="fi-sc fi-sc-has-gap fi-sc-dense fi-grid lg:fi-grid-cols" style="--cols-lg: repeat(12, minmax(0, 12fr)); --cols-default: repeat(2, minmax(0, 1fr));" @end.stop="$wire.updateFieldsOrder($event.to.getAttribute('data-section-id'), $event.to.sortable.toArray())" > diff --git a/resources/views/livewire/manage-custom-field.blade.php b/resources/views/livewire/manage-custom-field.blade.php index 05df5828..5d1bf0f8 100644 --- a/resources/views/livewire/manage-custom-field.blade.php +++ b/resources/views/livewire/manage-custom-field.blade.php @@ -10,11 +10,13 @@ class="fi-section !px-2 fi-compact !py-2 shadow-none fi-grid-col flex justify-be diff --git a/resources/views/livewire/manage-fields-without-sections.blade.php b/resources/views/livewire/manage-fields-without-sections.blade.php index 73403679..de245654 100644 --- a/resources/views/livewire/manage-fields-without-sections.blade.php +++ b/resources/views/livewire/manage-fields-without-sections.blade.php @@ -3,7 +3,7 @@
From 0f9c671fe23ac6ebe504e668b701c00f692b5c01 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 18:15:19 +0400 Subject: [PATCH 17/20] fix: set default max_values when enabling allow_multiple toggle --- src/Filament/Management/Schemas/FieldForm.php | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Filament/Management/Schemas/FieldForm.php b/src/Filament/Management/Schemas/FieldForm.php index 8e89d155..f9f6d441 100644 --- a/src/Filament/Management/Schemas/FieldForm.php +++ b/src/Filament/Management/Schemas/FieldForm.php @@ -357,22 +357,17 @@ public static function schema(bool $withOptionsRelationship = true): array Toggle::make('settings.allow_multiple') ->inline(false) ->live() - ->label( - __( - 'custom-fields::custom-fields.field.form.allow_multiple' - ) - ) - ->helperText( - __( - 'custom-fields::custom-fields.field.form.allow_multiple_help' - ) - ) + ->label(__('custom-fields::custom-fields.field.form.allow_multiple')) + ->helperText(__('custom-fields::custom-fields.field.form.allow_multiple_help')) ->visible( - fn ( - Get $get - ): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_MULTI_VALUE) && + fn (Get $get): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_MULTI_VALUE) && CustomFieldsType::getFieldType($get('type'))?->supportsMultiValue === true ) + ->afterStateUpdated(function (Set $set, bool $state): void { + if ($state) { + $set('settings.max_values', 2); + } + }) ->default(false), TextInput::make('settings.max_values') ->label( From ec790b90bf3a1bfeb88d26947cd2c03134ca2066 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 18:27:38 +0400 Subject: [PATCH 18/20] refactor: rename SYSTEM_SECTIONS_DISABLED to UI_FLAT_FIELD_LAYOUT --- src/Enums/CustomFieldsFeature.php | 2 +- src/Filament/Management/Pages/CustomFieldsManagementPage.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Enums/CustomFieldsFeature.php b/src/Enums/CustomFieldsFeature.php index a551486b..30101c6e 100644 --- a/src/Enums/CustomFieldsFeature.php +++ b/src/Enums/CustomFieldsFeature.php @@ -23,9 +23,9 @@ enum CustomFieldsFeature: string case UI_TABLE_FILTERS = 'ui_table_filters'; case UI_TOGGLEABLE_COLUMNS = 'ui_toggleable_columns'; case UI_TOGGLEABLE_COLUMNS_HIDDEN_DEFAULT = 'ui_toggleable_columns_hidden_default'; + case UI_FLAT_FIELD_LAYOUT = 'ui_flat_field_layout'; // System-level features case SYSTEM_MANAGEMENT_INTERFACE = 'system_management_interface'; case SYSTEM_MULTI_TENANCY = 'system_multi_tenancy'; - case SYSTEM_SECTIONS_DISABLED = 'system_sections_disabled'; } diff --git a/src/Filament/Management/Pages/CustomFieldsManagementPage.php b/src/Filament/Management/Pages/CustomFieldsManagementPage.php index 0c105c1f..50459803 100644 --- a/src/Filament/Management/Pages/CustomFieldsManagementPage.php +++ b/src/Filament/Management/Pages/CustomFieldsManagementPage.php @@ -61,7 +61,7 @@ public function mount(): void #[Computed] public function isSectionsDisabled(): bool { - return FeatureManager::isEnabled(CustomFieldsFeature::SYSTEM_SECTIONS_DISABLED); + return FeatureManager::isEnabled(CustomFieldsFeature::UI_FLAT_FIELD_LAYOUT); } #[Computed] From 10697a8ea8bb4db5a588427e23b0412be39348f2 Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 18:27:48 +0400 Subject: [PATCH 19/20] fix: respect flat layout feature flag in form and infolist builders --- src/Filament/Integration/Builders/FormBuilder.php | 12 +++++++++--- src/Filament/Integration/Builders/FormContainer.php | 10 ++++++++-- .../Integration/Builders/InfolistBuilder.php | 12 +++++++++--- .../Integration/Builders/InfolistContainer.php | 10 ++++++++-- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/Filament/Integration/Builders/FormBuilder.php b/src/Filament/Integration/Builders/FormBuilder.php index c88381cf..2dc6ff44 100644 --- a/src/Filament/Integration/Builders/FormBuilder.php +++ b/src/Filament/Integration/Builders/FormBuilder.php @@ -14,15 +14,21 @@ class FormBuilder extends BaseBuilder { - private bool $withoutSections = false; + private ?bool $withoutSections = null; public function build(): Grid { - return FormContainer::make() + $container = FormContainer::make() ->forModel($this->explicitModel ?? null) - ->withoutSections($this->withoutSections) ->only($this->only) ->except($this->except); + + // Only set withoutSections if explicitly configured + if ($this->withoutSections !== null) { + $container->withoutSections($this->withoutSections); + } + + return $container; } public function withoutSections(bool $withoutSections = true): static diff --git a/src/Filament/Integration/Builders/FormContainer.php b/src/Filament/Integration/Builders/FormContainer.php index 4e9f310a..687dc74a 100644 --- a/src/Filament/Integration/Builders/FormContainer.php +++ b/src/Filament/Integration/Builders/FormContainer.php @@ -4,6 +4,8 @@ use Filament\Schemas\Components\Grid; use Illuminate\Database\Eloquent\Model; +use Relaticle\CustomFields\Enums\CustomFieldsFeature; +use Relaticle\CustomFields\FeatureSystem\FeatureManager; final class FormContainer extends Grid { @@ -13,7 +15,7 @@ final class FormContainer extends Grid private array $only = []; - private bool $withoutSections = false; + private ?bool $withoutSections = null; public static function make(array|int|null $columns = 12): static { @@ -62,11 +64,15 @@ private function generateSchema(): array return []; // Graceful fallback } + // Use explicit setting if provided, otherwise check feature flag + $withoutSections = $this->withoutSections + ?? FeatureManager::isEnabled(CustomFieldsFeature::UI_FLAT_FIELD_LAYOUT); + $builder = app(FormBuilder::class); return $builder ->forModel($model) - ->withoutSections($this->withoutSections) + ->withoutSections($withoutSections) ->only($this->only) ->except($this->except) ->values() diff --git a/src/Filament/Integration/Builders/InfolistBuilder.php b/src/Filament/Integration/Builders/InfolistBuilder.php index d090a882..29ceea0e 100644 --- a/src/Filament/Integration/Builders/InfolistBuilder.php +++ b/src/Filament/Integration/Builders/InfolistBuilder.php @@ -21,17 +21,23 @@ final class InfolistBuilder extends BaseBuilder private bool $visibleWhenFilled = false; - private bool $withoutSections = false; + private ?bool $withoutSections = null; public function build(): Component { - return InfolistContainer::make() + $container = InfolistContainer::make() ->forModel($this->explicitModel ?? null) ->hiddenLabels($this->hiddenLabels) ->visibleWhenFilled($this->visibleWhenFilled) - ->withoutSections($this->withoutSections) ->only($this->only) ->except($this->except); + + // Only set withoutSections if explicitly configured + if ($this->withoutSections !== null) { + $container->withoutSections($this->withoutSections); + } + + return $container; } /** diff --git a/src/Filament/Integration/Builders/InfolistContainer.php b/src/Filament/Integration/Builders/InfolistContainer.php index 2e315525..d8d54c26 100644 --- a/src/Filament/Integration/Builders/InfolistContainer.php +++ b/src/Filament/Integration/Builders/InfolistContainer.php @@ -5,6 +5,8 @@ use Filament\Forms\Components\Field; use Filament\Schemas\Components\Grid; use Illuminate\Database\Eloquent\Model; +use Relaticle\CustomFields\Enums\CustomFieldsFeature; +use Relaticle\CustomFields\FeatureSystem\FeatureManager; final class InfolistContainer extends Grid { @@ -18,7 +20,7 @@ final class InfolistContainer extends Grid private bool $visibleWhenFilled = false; - private bool $withoutSections = false; + private ?bool $withoutSections = null; public static function make(array|int|null $columns = 12): static { @@ -84,13 +86,17 @@ private function generateSchema(): array return []; // Graceful fallback } + // Use explicit setting if provided, otherwise check feature flag + $withoutSections = $this->withoutSections + ?? FeatureManager::isEnabled(CustomFieldsFeature::UI_FLAT_FIELD_LAYOUT); + $builder = app(InfolistBuilder::class) ->forModel($model) ->only($this->only) ->except($this->except) ->hiddenLabels($this->hiddenLabels) ->visibleWhenFilled($this->visibleWhenFilled) - ->withoutSections($this->withoutSections); + ->withoutSections($withoutSections); return $builder->values()->toArray(); } From 67297c0c4b41ef62994a0f3054ca3ded3c812dee Mon Sep 17 00:00:00 2001 From: manukminasyan Date: Tue, 9 Dec 2025 19:01:57 +0400 Subject: [PATCH 20/20] feat: limit unique constraint option to applicable field types --- src/Data/FieldTypeData.php | 1 + src/FieldTypeSystem/Definitions/EmailFieldType.php | 1 + src/FieldTypeSystem/Definitions/LinkFieldType.php | 1 + src/FieldTypeSystem/Definitions/NumberFieldType.php | 1 + src/FieldTypeSystem/Definitions/TextFieldType.php | 1 + .../Definitions/TextareaFieldType.php | 1 + src/FieldTypeSystem/FieldSchema.php | 13 +++++++++++++ src/Filament/Management/Schemas/FieldForm.php | 3 ++- 8 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Data/FieldTypeData.php b/src/Data/FieldTypeData.php index 7df8d546..1d9777ce 100644 --- a/src/Data/FieldTypeData.php +++ b/src/Data/FieldTypeData.php @@ -28,6 +28,7 @@ public function __construct( public bool $withoutUserOptions = false, public bool $acceptsArbitraryValues = false, public bool $supportsMultiValue = false, + public bool $supportsUniqueConstraint = false, public array $validationRules = [], public ?string $settingsDataClass = null, public string|Closure|null $settingsSchema = null, diff --git a/src/FieldTypeSystem/Definitions/EmailFieldType.php b/src/FieldTypeSystem/Definitions/EmailFieldType.php index 00b1469e..fcf72b78 100644 --- a/src/FieldTypeSystem/Definitions/EmailFieldType.php +++ b/src/FieldTypeSystem/Definitions/EmailFieldType.php @@ -30,6 +30,7 @@ public function configure(): FieldSchema ->searchable() ->sortable() ->supportsMultiValue() + ->supportsUniqueConstraint() ->withArbitraryValues() ->withoutUserOptions() ->availableValidationRules([ diff --git a/src/FieldTypeSystem/Definitions/LinkFieldType.php b/src/FieldTypeSystem/Definitions/LinkFieldType.php index 1f62e496..df4b1979 100644 --- a/src/FieldTypeSystem/Definitions/LinkFieldType.php +++ b/src/FieldTypeSystem/Definitions/LinkFieldType.php @@ -26,6 +26,7 @@ public function configure(): FieldSchema ->formComponent(LinkComponent::class) ->tableColumn(TextColumn::class) ->infolistEntry(TextEntry::class) + ->supportsUniqueConstraint() ->priority(60) ->availableValidationRules([ ValidationRule::REQUIRED, diff --git a/src/FieldTypeSystem/Definitions/NumberFieldType.php b/src/FieldTypeSystem/Definitions/NumberFieldType.php index 61ded7b2..f18d228c 100644 --- a/src/FieldTypeSystem/Definitions/NumberFieldType.php +++ b/src/FieldTypeSystem/Definitions/NumberFieldType.php @@ -26,6 +26,7 @@ public function configure(): FieldSchema ->formComponent(NumberComponent::class) ->tableColumn(TextColumn::class) ->infolistEntry(TextEntry::class) + ->supportsUniqueConstraint() ->priority(20) ->availableValidationRules([ ValidationRule::REQUIRED, diff --git a/src/FieldTypeSystem/Definitions/TextFieldType.php b/src/FieldTypeSystem/Definitions/TextFieldType.php index 5e3b7ced..9be16b1f 100644 --- a/src/FieldTypeSystem/Definitions/TextFieldType.php +++ b/src/FieldTypeSystem/Definitions/TextFieldType.php @@ -27,6 +27,7 @@ public function configure(): FieldSchema ->tableColumn(TextColumn::class) ->infolistEntry(TextEntry::class) ->encryptable() + ->supportsUniqueConstraint() ->priority(10) ->availableValidationRules([ ValidationRule::REQUIRED, diff --git a/src/FieldTypeSystem/Definitions/TextareaFieldType.php b/src/FieldTypeSystem/Definitions/TextareaFieldType.php index 886c8e1b..4feea8f1 100644 --- a/src/FieldTypeSystem/Definitions/TextareaFieldType.php +++ b/src/FieldTypeSystem/Definitions/TextareaFieldType.php @@ -26,6 +26,7 @@ public function configure(): FieldSchema ->formComponent(TextareaFormComponent::class) ->tableColumn(TextColumn::class) ->infolistEntry(TextEntry::class) + ->supportsUniqueConstraint() ->priority(15) ->availableValidationRules([ ValidationRule::REQUIRED, diff --git a/src/FieldTypeSystem/FieldSchema.php b/src/FieldTypeSystem/FieldSchema.php index bddeb445..e68ebcbf 100644 --- a/src/FieldTypeSystem/FieldSchema.php +++ b/src/FieldTypeSystem/FieldSchema.php @@ -55,6 +55,8 @@ class FieldSchema private bool $supportsMultiValue = false; + private bool $supportsUniqueConstraint = false; + protected bool $withoutUserOptions = false; private ?string $settingsDataClass = null; @@ -323,6 +325,16 @@ public function supportsMultiValue(bool $supports = true): self return $this; } + /** + * Configure whether field supports unique value constraint per entity type + */ + public function supportsUniqueConstraint(bool $supports = true): self + { + $this->supportsUniqueConstraint = $supports; + + return $this; + } + // ========== Data Type Specific Methods (from DataTypeConfigurators) ========== /** @@ -555,6 +567,7 @@ public function data(): FieldTypeData withoutUserOptions: $this->withoutUserOptions, acceptsArbitraryValues: $this->acceptsArbitraryValues, supportsMultiValue: $this->supportsMultiValue, + supportsUniqueConstraint: $this->supportsUniqueConstraint, validationRules: $this->availableValidationRules, settingsDataClass: $this->settingsDataClass, settingsSchema: $this->settingsSchema diff --git a/src/Filament/Management/Schemas/FieldForm.php b/src/Filament/Management/Schemas/FieldForm.php index f9f6d441..f5250073 100644 --- a/src/Filament/Management/Schemas/FieldForm.php +++ b/src/Filament/Management/Schemas/FieldForm.php @@ -405,7 +405,8 @@ public static function schema(bool $withOptionsRelationship = true): array ) ) ->visible( - fn (): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_UNIQUE_VALUE) + fn (Get $get): bool => FeatureManager::isEnabled(CustomFieldsFeature::FIELD_UNIQUE_VALUE) && + CustomFieldsType::getFieldType($get('type'))?->supportsUniqueConstraint === true ) ->default(false), ]),