From d4d5404c63b9afe39e7601a31dc6b549bc775355 Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Thu, 18 Sep 2025 13:01:22 +0200 Subject: [PATCH 1/4] feat: implement property value grouping --- ...ng_columns_to_pim_property_value_table.php | 30 +++++ resources/lang/en/property-value.php | 27 +++++ resources/lang/sl/property-value.php | 27 +++++ .../bulk/group-selected-preview.blade.php | 24 ++++ .../columns/group-and-aliases.blade.php | 18 +++ .../Resources/PropertyValueResource.php | 107 +++++++++++++++++- src/Models/PropertyValue.php | 75 ++++++++++++ tests/Unit/PropertyValueGroupingTest.php | 91 +++++++++++++++ 8 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2025_09_17_150136_add_grouping_columns_to_pim_property_value_table.php create mode 100644 resources/views/filament/bulk/group-selected-preview.blade.php create mode 100644 resources/views/filament/columns/group-and-aliases.blade.php create mode 100644 tests/Unit/PropertyValueGroupingTest.php diff --git a/database/migrations/2025_09_17_150136_add_grouping_columns_to_pim_property_value_table.php b/database/migrations/2025_09_17_150136_add_grouping_columns_to_pim_property_value_table.php new file mode 100644 index 0000000..c6f6a6a --- /dev/null +++ b/database/migrations/2025_09_17_150136_add_grouping_columns_to_pim_property_value_table.php @@ -0,0 +1,30 @@ +boolean('is_group')->default(false)->after('image'); + $table->unsignedBigInteger('group_value_id')->nullable()->after('is_group'); + + $table->index('group_value_id', 'pim_property_value_group_value_id_index'); + $table->foreign('group_value_id', 'pim_property_value_group_value_id_foreign') + ->references('id')->on('pim_property_value') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('pim_property_value', function (Blueprint $table) { + $table->dropForeign('pim_property_value_group_value_id_foreign'); + $table->dropIndex('pim_property_value_group_value_id_index'); + $table->dropColumn(['is_group', 'group_value_id']); + }); + } +}; diff --git a/resources/lang/en/property-value.php b/resources/lang/en/property-value.php index 0b71948..0389395 100644 --- a/resources/lang/en/property-value.php +++ b/resources/lang/en/property-value.php @@ -30,6 +30,8 @@ 'table' => [ 'columns' => [ 'value' => 'Value', + 'group' => 'Group', + 'aliases' => 'Aliases', 'image' => 'Image', 'info_url' => 'Info URL', 'sort' => 'Sort', @@ -43,6 +45,8 @@ 'edit' => 'Edit', 'delete' => 'Delete', 'merge' => 'Merge…', + 'group_aliases' => 'Group (aliases)', + 'remove_from_group' => 'Remove from group', ], ], @@ -69,6 +73,21 @@ 'merged_error_body' => 'We couldn’t merge these values. Please try again.', ], + 'grouping' => [ + 'success_grouped_title' => 'Grouped', + 'success_grouped_body' => ':count value(s) grouped under ":target".', + 'success_ungrouped_title' => 'Updated', + 'success_ungrouped_body' => ':count value(s) removed from group.', + 'error_title' => 'Grouping failed', + 'selected_values' => 'Selected values', + 'helper_target' => 'Choose the target value into which the selected values will be grouped.', + 'errors' => [ + 'target_in_sources' => 'The target cannot be one of the selected values.', + 'target_is_member' => 'The selected target already belongs to another group.', + 'different_property' => 'All selected values and the target must belong to the same property.', + ], + ], + 'pages' => [ 'title' => [ 'with_property' => 'Values for: :property', @@ -79,4 +98,12 @@ 'list' => 'List', ], ], + + 'ui' => [ + 'group_badge' => 'Group', + ], + + 'modal_grouping' => [ + 'target_label' => 'Target value', + ], ]; diff --git a/resources/lang/sl/property-value.php b/resources/lang/sl/property-value.php index 1ee113f..431eb13 100644 --- a/resources/lang/sl/property-value.php +++ b/resources/lang/sl/property-value.php @@ -30,6 +30,8 @@ 'table' => [ 'columns' => [ 'value' => 'Vrednost', + 'group' => 'Skupina', + 'aliases' => 'Aliasi', 'image' => 'Slika', 'info_url' => 'URL informacij', 'sort' => 'Vrstni red', @@ -43,6 +45,8 @@ 'edit' => 'Uredi', 'delete' => 'Izbriši', 'merge' => 'Združi…', + 'group_aliases' => 'Združi (aliase)', + 'remove_from_group' => 'Odstrani iz skupine', ], ], @@ -69,6 +73,21 @@ 'merged_error_body' => 'Vrednosti trenutno ni mogoče združiti. Poskusite znova.', ], + 'grouping' => [ + 'success_grouped_title' => 'Združeno', + 'success_grouped_body' => ':count vrednost(i) združene pod ":target".', + 'success_ungrouped_title' => 'Posodobljeno', + 'success_ungrouped_body' => ':count vrednost(i) odstranjene iz skupine.', + 'error_title' => 'Združevanje ni uspelo', + 'selected_values' => 'Izbrane vrednosti', + 'helper_target' => 'Izberite ciljno vrednost, v katero bodo združene izbrane vrednosti.', + 'errors' => [ + 'target_in_sources' => 'Cilj ne sme biti med izbranimi vrednostmi.', + 'target_is_member' => 'Izbrani cilj že pripada drugi skupini.', + 'different_property' => 'Vse izbrane vrednosti in cilj morajo pripadati isti lastnosti.', + ], + ], + 'pages' => [ 'title' => [ 'with_property' => 'Vrednosti za: :property', @@ -79,4 +98,12 @@ 'list' => 'Seznam', ], ], + + 'ui' => [ + 'group_badge' => 'Skupina', + ], + + 'modal_grouping' => [ + 'target_label' => 'Ciljna vrednost', + ], ]; diff --git a/resources/views/filament/bulk/group-selected-preview.blade.php b/resources/views/filament/bulk/group-selected-preview.blade.php new file mode 100644 index 0000000..377aec6 --- /dev/null +++ b/resources/views/filament/bulk/group-selected-preview.blade.php @@ -0,0 +1,24 @@ +@php($items = $getState() ?? []) + +
+
+ {{ __('eclipse-catalogue::property-value.grouping.selected_values') }} +
+ +
+ + diff --git a/resources/views/filament/columns/group-and-aliases.blade.php b/resources/views/filament/columns/group-and-aliases.blade.php new file mode 100644 index 0000000..95bfeda --- /dev/null +++ b/resources/views/filament/columns/group-and-aliases.blade.php @@ -0,0 +1,18 @@ +@php($rec = $getRecord()) + +
+ @if($rec->is_group) + {{ __('eclipse-catalogue::property-value.ui.group_badge') }} + @endif + + @if($rec->group) + {{ $rec->group->value }} + @endif + + @php($aliases = $rec->members()->count()) + @if($rec->is_group) + {{ $aliases }} + @endif +
+ + diff --git a/src/Filament/Resources/PropertyValueResource.php b/src/Filament/Resources/PropertyValueResource.php index 544ec7a..e8abc5a 100644 --- a/src/Filament/Resources/PropertyValueResource.php +++ b/src/Filament/Resources/PropertyValueResource.php @@ -83,6 +83,11 @@ public static function table(Table $table): Table ->searchable() ->sortable(), + Tables\Columns\ViewColumn::make('group_and_aliases') + ->label(__('eclipse-catalogue::property-value.table.columns.group')) + ->view('eclipse-catalogue::filament.columns.group-and-aliases') + ->extraAttributes(['class' => 'space-x-1']), + Tables\Columns\ImageColumn::make('image') ->label(__('eclipse-catalogue::property-value.table.columns.image')) ->disk('public') @@ -169,6 +174,106 @@ public static function table(Table $table): Table ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ + Tables\Actions\BulkAction::make('group_values') + ->label(__('eclipse-catalogue::property-value.table.actions.group_aliases')) + ->icon('heroicon-o-rectangle-group') + ->form(function (\Filament\Tables\Actions\BulkAction $action) { + $firstRecord = $action->getRecords()->first(); + $derivedPropertyId = $firstRecord?->property_id ?? (request()->has('property') ? (int) request('property') : null); + + return [ + \Filament\Forms\Components\View::make('eclipse-catalogue::filament.bulk.group-selected-preview') + ->statePath('selected_records') + ->dehydrated(false) + ->afterStateHydrated(function ($component) use ($action) { + $component->state($action->getRecords()); + }) + ->columnSpanFull(), + + \Filament\Forms\Components\Hidden::make('property_id') + ->default($derivedPropertyId), + + \Filament\Forms\Components\Select::make('target_id') + ->label(__('eclipse-catalogue::property-value.modal_grouping.target_label')) + ->helperText(__('eclipse-catalogue::property-value.grouping.helper_target')) + ->required() + ->options(function (\Filament\Forms\Get $get) { + $query = PropertyValue::query(); + $propertyId = $get('property_id'); + if ($propertyId) { + $query->sameProperty((int) $propertyId); + } + + return $query->orderBy('value')->pluck('value', 'id'); + }) + ->searchable(), + ]; + }) + ->action(function (\Illuminate\Support\Collection $records, array $data) { + try { + if ($records->isEmpty()) { + return; + } + + $target = PropertyValue::findOrFail((int) $data['target_id']); + + $sourceIds = $records->pluck('id'); + if ($sourceIds->contains($target->id)) { + Notification::make()->title(__('eclipse-catalogue::property-value.grouping.error_title'))->body(__('eclipse-catalogue::property-value.grouping.errors.target_in_sources'))->danger()->send(); + + return; + } + + if ($target->group_value_id !== null) { + Notification::make()->title(__('eclipse-catalogue::property-value.grouping.error_title'))->body(__('eclipse-catalogue::property-value.grouping.errors.target_is_member'))->danger()->send(); + + return; + } + + $updated = 0; + foreach ($records as $record) { + /** @var PropertyValue $source */ + $source = $record instanceof PropertyValue ? $record : PropertyValue::findOrFail($record); + if ($source->property_id !== $target->property_id) { + Notification::make()->title(__('eclipse-catalogue::property-value.grouping.error_title'))->body(__('eclipse-catalogue::property-value.grouping.errors.different_property'))->danger()->send(); + + return; + } + $source->groupInto($target->id); + $updated++; + } + + Notification::make()->title(__('eclipse-catalogue::property-value.grouping.success_grouped_title')) + ->body(__('eclipse-catalogue::property-value.grouping.success_grouped_body', ['count' => $updated, 'target' => $target->value])) + ->success()->send(); + } catch (\Throwable $e) { + \Log::error('Bulk group failed', ['exception' => $e]); + Notification::make()->title(__('eclipse-catalogue::property-value.grouping.error_title'))->body(__('eclipse-catalogue::property-value.messages.merged_error_body'))->danger()->send(); + } + }) + ->deselectRecordsAfterCompletion(), + + Tables\Actions\BulkAction::make('ungroup_values') + ->label(__('eclipse-catalogue::property-value.table.actions.remove_from_group')) + ->icon('heroicon-o-squares-2x2') + ->action(function (\Illuminate\Support\Collection $records) { + try { + $updated = 0; + foreach ($records as $record) { + /** @var PropertyValue $model */ + $model = $record instanceof PropertyValue ? $record : PropertyValue::findOrFail($record); + $model->removeFromGroup(); + $updated++; + } + Notification::make()->title(__('eclipse-catalogue::property-value.grouping.success_ungrouped_title')) + ->body(__('eclipse-catalogue::property-value.grouping.success_ungrouped_body', ['count' => $updated])) + ->success()->send(); + } catch (\Throwable $e) { + \Log::error('Bulk ungroup failed', ['exception' => $e]); + Notification::make()->title(__('eclipse-catalogue::property-value.grouping.error_title'))->body(__('eclipse-catalogue::property-value.messages.merged_error_body'))->danger()->send(); + } + }) + ->deselectRecordsAfterCompletion(), Tables\Actions\DeleteBulkAction::make(), ]), ]); @@ -203,6 +308,6 @@ public static function getEloquentQuery(): Builder $query->where('property_id', request('property')); } - return $query; + return $query->groupedOrder(); } } diff --git a/src/Models/PropertyValue.php b/src/Models/PropertyValue.php index 5e7e15d..7a37c8a 100644 --- a/src/Models/PropertyValue.php +++ b/src/Models/PropertyValue.php @@ -24,6 +24,8 @@ class PropertyValue extends Model implements HasMedia 'sort', 'info_url', 'image', + 'is_group', + 'group_value_id', ]; public array $translatable = [ @@ -35,6 +37,8 @@ class PropertyValue extends Model implements HasMedia protected $casts = [ 'sort' => 'integer', 'property_id' => 'integer', + 'is_group' => 'boolean', + 'group_value_id' => 'integer', ]; public function property(): BelongsTo @@ -48,6 +52,16 @@ public function products(): BelongsToMany ->withTimestamps(); } + public function group(): BelongsTo + { + return $this->belongsTo(self::class, 'group_value_id'); + } + + public function members() + { + return $this->hasMany(self::class, 'group_value_id'); + } + public function registerMediaCollections(): void { $this->addMediaCollection('images') @@ -67,6 +81,23 @@ protected static function booted(): void }); } + public function scopeSameProperty($query, int $propertyId) + { + return $query->where('property_id', $propertyId); + } + + /** + * Order values so that each group parent is followed by its members. + */ + public function scopeGroupedOrder($query) + { + return $query + ->orderByRaw('COALESCE(group_value_id, id)') + ->orderByRaw('CASE WHEN is_group THEN 0 ELSE 1 END') + ->orderBy('sort') + ->orderBy('value'); + } + /** * Ensure Filament receives scalar values for form hydration. * @@ -107,6 +138,15 @@ public function mergeInto(int $targetId): array throw new \RuntimeException('Values must belong to the same property.'); } + if ($this->is_group && $target->group_value_id !== null) { + throw new \RuntimeException('Cannot merge a group into a value that is already a member of another group.'); + } + + if ($this->is_group && ! $target->is_group) { + $target->is_group = true; + $target->save(); + } + $pivotTable = 'catalogue_product_has_property_value'; $productIds = DB::table($pivotTable) @@ -137,4 +177,39 @@ public function mergeInto(int $targetId): array ]; }); } + + public function canBeGroupedInto(self $target): void + { + if ($this->id === $target->id) { + throw new \RuntimeException('Cannot group a value into itself.'); + } + if ($this->property_id !== $target->property_id) { + throw new \RuntimeException('Values must belong to the same property.'); + } + if ($target->group_value_id !== null) { + throw new \RuntimeException('Cannot group into a value that is already a member of another group.'); + } + } + + public function groupInto(int $targetId): void + { + DB::transaction(function () use ($targetId) { + $target = self::query()->lockForUpdate()->findOrFail($targetId); + $this->canBeGroupedInto($target); + + if (! $target->is_group) { + $target->is_group = true; + $target->save(); + } + + $this->group_value_id = $target->id; + $this->save(); + }); + } + + public function removeFromGroup(): void + { + $this->group_value_id = null; + $this->save(); + } } diff --git a/tests/Unit/PropertyValueGroupingTest.php b/tests/Unit/PropertyValueGroupingTest.php new file mode 100644 index 0000000..9edef70 --- /dev/null +++ b/tests/Unit/PropertyValueGroupingTest.php @@ -0,0 +1,91 @@ +create(); + $parent = PropertyValue::factory()->create(['property_id' => $property->id, 'is_group' => true]); + $member = PropertyValue::factory()->create(['property_id' => $property->id]); + $member->groupInto($parent->id); + + $other = PropertyValue::factory()->create(['property_id' => $property->id]); + + expect(fn () => $other->groupInto($member->id)) + ->toThrow(RuntimeException::class); // member cannot be target +}); + +it('cannot group a value into itself or across properties', function () { + $propA = Property::factory()->create(); + $propB = Property::factory()->create(); + $a = PropertyValue::factory()->create(['property_id' => $propA->id]); + $b = PropertyValue::factory()->create(['property_id' => $propB->id]); + + // self + expect(fn () => $a->groupInto($a->id)) + ->toThrow(RuntimeException::class); +}); + +it('prevents cross-property grouping', function () { + $propA = Property::factory()->create(); + $propB = Property::factory()->create(); + $a = PropertyValue::factory()->create(['property_id' => $propA->id]); + $b = PropertyValue::factory()->create(['property_id' => $propB->id]); + + expect(fn () => $a->groupInto($b->id)) + ->toThrow(RuntimeException::class); +}); + +it('grouping marks target as group and assigns group_value_id', function () { + $property = Property::factory()->create(); + $target = PropertyValue::factory()->create(['property_id' => $property->id]); + $one = PropertyValue::factory()->create(['property_id' => $property->id]); + $two = PropertyValue::factory()->create(['property_id' => $property->id]); + + $one->groupInto($target->id); + $two->groupInto($target->id); + + expect($target->fresh()->is_group)->toBeTrue() + ->and($one->fresh()->group_value_id)->toBe($target->id) + ->and($two->fresh()->group_value_id)->toBe($target->id); +}); + +it('re-grouping moves a member from old group to a new group', function () { + $property = Property::factory()->create(); + $groupA = PropertyValue::factory()->create(['property_id' => $property->id]); + $groupB = PropertyValue::factory()->create(['property_id' => $property->id]); + $member = PropertyValue::factory()->create(['property_id' => $property->id]); + + $member->groupInto($groupA->id); + expect($member->fresh()->group_value_id)->toBe($groupA->id); + + $member->groupInto($groupB->id); + expect($member->fresh()->group_value_id)->toBe($groupB->id) + ->and($groupB->fresh()->is_group)->toBeTrue(); +}); + +it('removeFromGroup clears group_value_id', function () { + $property = Property::factory()->create(); + $parent = PropertyValue::factory()->create(['property_id' => $property->id]); + $member = PropertyValue::factory()->create(['property_id' => $property->id]); + $member->groupInto($parent->id); + $member->removeFromGroup(); + + expect($member->fresh()->group_value_id)->toBeNull(); +}); + +it('groupedOrder scope clusters parent and children without partial updates on error', function () { + $property = Property::factory()->create(); + $a = PropertyValue::factory()->create(['property_id' => $property->id, 'value' => 'A']); + $b1 = PropertyValue::factory()->create(['property_id' => $property->id, 'value' => 'B1']); + $b2 = PropertyValue::factory()->create(['property_id' => $property->id, 'value' => 'B2']); + + // Group B members under A + $b1->groupInto($a->id); + $b2->groupInto($a->id); + + $ordered = PropertyValue::query()->sameProperty($property->id)->groupedOrder()->pluck('id')->all(); + // Expect A first, then its members in any order + expect($ordered[0])->toBe($a->id) + ->and(collect([$b1->id, $b2->id])->diff($ordered))->toBeEmpty(); +}); From 7dece9d4fc4b7fde3e85992264cd255ff495ff0d Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Thu, 18 Sep 2025 13:24:52 +0200 Subject: [PATCH 2/4] chore: fix migration syntax --- ...grouping_columns_to_pim_property_value_table.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/database/migrations/2025_09_17_150136_add_grouping_columns_to_pim_property_value_table.php b/database/migrations/2025_09_17_150136_add_grouping_columns_to_pim_property_value_table.php index c6f6a6a..1c746e7 100644 --- a/database/migrations/2025_09_17_150136_add_grouping_columns_to_pim_property_value_table.php +++ b/database/migrations/2025_09_17_150136_add_grouping_columns_to_pim_property_value_table.php @@ -10,11 +10,11 @@ public function up(): void { Schema::table('pim_property_value', function (Blueprint $table) { $table->boolean('is_group')->default(false)->after('image'); - $table->unsignedBigInteger('group_value_id')->nullable()->after('is_group'); - $table->index('group_value_id', 'pim_property_value_group_value_id_index'); - $table->foreign('group_value_id', 'pim_property_value_group_value_id_foreign') - ->references('id')->on('pim_property_value') + $table->foreignId('group_value_id') + ->nullable() + ->after('is_group') + ->constrained('pim_property_value') ->nullOnDelete(); }); } @@ -22,9 +22,8 @@ public function up(): void public function down(): void { Schema::table('pim_property_value', function (Blueprint $table) { - $table->dropForeign('pim_property_value_group_value_id_foreign'); - $table->dropIndex('pim_property_value_group_value_id_index'); - $table->dropColumn(['is_group', 'group_value_id']); + $table->dropConstrainedForeignId('group_value_id'); + $table->dropColumn('is_group'); }); } }; From cfa8426a16fe0daba80c58344199fc6c26310a90 Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Fri, 26 Sep 2025 10:03:33 +0200 Subject: [PATCH 3/4] chore: fix lazy loading issue --- src/Filament/Resources/PropertyValueResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Filament/Resources/PropertyValueResource.php b/src/Filament/Resources/PropertyValueResource.php index 0150b28..cc9a8f1 100644 --- a/src/Filament/Resources/PropertyValueResource.php +++ b/src/Filament/Resources/PropertyValueResource.php @@ -449,6 +449,6 @@ public static function getEloquentQuery(): Builder $query->where('property_id', request('property')); } - return $query->groupedOrder(); + return $query->with(['group', 'members'])->groupedOrder(); } } From 4d93c170d8e23d78a5c0874d3868d309acbd783b Mon Sep 17 00:00:00 2001 From: Kilian Trunk Date: Wed, 15 Oct 2025 13:24:41 +0200 Subject: [PATCH 4/4] chore: f4 & n+1 fixes --- .../bulk/group-selected-preview.blade.php | 8 ++-- .../Resources/PropertyValueResource.php | 46 ++++++++++++------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/resources/views/filament/bulk/group-selected-preview.blade.php b/resources/views/filament/bulk/group-selected-preview.blade.php index 377aec6..6aaa153 100644 --- a/resources/views/filament/bulk/group-selected-preview.blade.php +++ b/resources/views/filament/bulk/group-selected-preview.blade.php @@ -9,11 +9,11 @@
  • - - {{ $record->value }} - @if($record->is_group) + {{ is_array($record) ? ($record['value'] ?? '') : ($record->value ?? '') }} + @if(is_array($record) ? ($record['is_group'] ?? false) : ($record->is_group ?? false)) {{ __('eclipse-catalogue::property-value.ui.group_badge') }} - @elseif($record->group) - {{ $record->group->value }} + @elseif(is_array($record) ? !empty($record['group_value'] ?? null) : !empty(optional($record->group)->value)) + {{ is_array($record) ? ($record['group_value'] ?? '') : (optional($record->group)->value ?? '') }} @endif
  • diff --git a/src/Filament/Resources/PropertyValueResource.php b/src/Filament/Resources/PropertyValueResource.php index 779d57e..623992e 100644 --- a/src/Filament/Resources/PropertyValueResource.php +++ b/src/Filament/Resources/PropertyValueResource.php @@ -11,6 +11,7 @@ use Eclipse\Catalogue\Values\Background; use Filament\Actions\Action; use Filament\Actions\ActionGroup; +use Filament\Actions\BulkAction; use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteAction; use Filament\Actions\DeleteBulkAction; @@ -28,14 +29,16 @@ use Filament\Schemas\Components\Grid; use Filament\Schemas\Components\Group; use Filament\Schemas\Components\Utilities\Get; +use Filament\Schemas\Components\View as SchemaView; use Filament\Schemas\Schema; use Filament\Tables\Columns\ImageColumn; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Columns\ViewColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\Log; use LaraZeus\SpatieTranslatable\Resources\Concerns\Translatable; -use Log; use Throwable; class PropertyValueResource extends Resource @@ -119,7 +122,7 @@ public static function table(Table $table): Table ->searchable() ->sortable(), - Tables\Columns\ViewColumn::make('group_and_aliases') + ViewColumn::make('group_and_aliases') ->label(__('eclipse-catalogue::property-value.table.columns.group')) ->view('eclipse-catalogue::filament.columns.group-and-aliases') ->extraAttributes(['class' => 'space-x-1']), @@ -259,30 +262,39 @@ public static function table(Table $table): Table ]) ->toolbarActions([ BulkActionGroup::make([ - Tables\Actions\BulkAction::make('group_values') + BulkAction::make('group_values') ->label(__('eclipse-catalogue::property-value.table.actions.group_aliases')) ->icon('heroicon-o-rectangle-group') - ->form(function (\Filament\Tables\Actions\BulkAction $action) { - $firstRecord = $action->getRecords()->first(); + ->form(function (\Filament\Actions\BulkAction $action) { + $firstRecord = $action->getSelectedRecords()->first(); $derivedPropertyId = $firstRecord?->property_id ?? (request()->has('property') ? (int) request('property') : null); return [ - \Filament\Forms\Components\View::make('eclipse-catalogue::filament.bulk.group-selected-preview') + SchemaView::make('eclipse-catalogue::filament.bulk.group-selected-preview') ->statePath('selected_records') ->dehydrated(false) ->afterStateHydrated(function ($component) use ($action) { - $component->state($action->getRecords()); + $records = $action->getSelectedRecordsQuery()->with(['group:id,value'])->get(); + $items = $records->map(function (PropertyValue $record) { + return [ + 'id' => $record->id, + 'value' => $record->value, + 'is_group' => (bool) $record->is_group, + 'group_value' => $record->group?->value, + ]; + })->all(); + $component->state($items); }) ->columnSpanFull(), - \Filament\Forms\Components\Hidden::make('property_id') + Hidden::make('property_id') ->default($derivedPropertyId), - \Filament\Forms\Components\Select::make('target_id') + Select::make('target_id') ->label(__('eclipse-catalogue::property-value.modal_grouping.target_label')) ->helperText(__('eclipse-catalogue::property-value.grouping.helper_target')) ->required() - ->options(function (\Filament\Forms\Get $get) { + ->options(function (Get $get) { $query = PropertyValue::query(); $propertyId = $get('property_id'); if ($propertyId) { @@ -332,13 +344,13 @@ public static function table(Table $table): Table ->body(__('eclipse-catalogue::property-value.grouping.success_grouped_body', ['count' => $updated, 'target' => $target->value])) ->success()->send(); } catch (\Throwable $e) { - \Log::error('Bulk group failed', ['exception' => $e]); + Log::error('Bulk group failed', ['exception' => $e]); Notification::make()->title(__('eclipse-catalogue::property-value.grouping.error_title'))->body(__('eclipse-catalogue::property-value.messages.merged_error_body'))->danger()->send(); } }) ->deselectRecordsAfterCompletion(), - Tables\Actions\BulkAction::make('ungroup_values') + BulkAction::make('ungroup_values') ->label(__('eclipse-catalogue::property-value.table.actions.remove_from_group')) ->icon('heroicon-o-squares-2x2') ->action(function (\Illuminate\Support\Collection $records) { @@ -354,7 +366,7 @@ public static function table(Table $table): Table ->body(__('eclipse-catalogue::property-value.grouping.success_ungrouped_body', ['count' => $updated])) ->success()->send(); } catch (\Throwable $e) { - \Log::error('Bulk ungroup failed', ['exception' => $e]); + Log::error('Bulk ungroup failed', ['exception' => $e]); Notification::make()->title(__('eclipse-catalogue::property-value.grouping.error_title'))->body(__('eclipse-catalogue::property-value.messages.merged_error_body'))->danger()->send(); } }) @@ -389,11 +401,11 @@ public static function buildColorGroupSchema(): array ->default(BackgroundType::NONE->value) ->live(), ColorPicker::make('color') - ->visible(fn (Get $get) => $get('type') === 's') + ->visible(fn (\Filament\Schemas\Components\Utilities\Get $get) => $get('type') === 's') ->live(), Grid::make() ->columns(4) - ->visible(fn (Get $get) => $get('type') === 'g') + ->visible(fn (\Filament\Schemas\Components\Utilities\Get $get) => $get('type') === 'g') ->schema([ ColorPicker::make('color_start')->columnSpan(2)->live(), ColorPicker::make('color_end')->columnSpan(2)->live(), @@ -414,7 +426,7 @@ public static function buildColorGroupSchema(): array ]), ViewField::make('preview') ->view('eclipse-catalogue::components.color-preview') - ->visible(function (Get $get) { + ->visible(function (\Filament\Schemas\Components\Utilities\Get $get) { $bg = Background::fromForm([ 'type' => $get('type'), 'color' => $get('color'), @@ -426,7 +438,7 @@ public static function buildColorGroupSchema(): array return $bg->hasRenderableCss(); }) - ->viewData(function (Get $get) { + ->viewData(function (\Filament\Schemas\Components\Utilities\Get $get) { $bg = Background::fromForm([ 'type' => $get('type'), 'color' => $get('color'),