Repository: lunarphp/lunar (php)

Issue:
Product variant attributes can not be edited if product has no options
<!-- DO NOT THROW THIS AWAY -->
<!-- Fill out the FULL versions with patch versions -->

- Lunar version: 1.0.0-beta3
- Laravel Version: 10.48.22
- PHP Version: 8.3.9
- Database Driver & Version: Mysql

### Expected Behaviour:
I should be able to fill in product variant attribute data event though product has only one product variant (e.g. it has no options)

### Actual Behaviour:
I am not able to fill in any product variant attributes.

### Steps To Reproduce:
1. Assign some custom attributes to product variant.
2. Create a product and you won't be able to fill in any custom attribute_data on product_variant.

On lunar 0.8 it was possible to fill in this data, not we can't. Solution might be to create same attributes for the product too, but it create additional code complexity as you must check two places for attributes in a product and in a product variant.


Pull request description:
Add variant attributes to product edit page
Fixes #1996 

Patch:
diff --git a/packages/admin/src/Filament/Resources/ProductResource.php b/packages/admin/src/Filament/Resources/ProductResource.php
index 0ae84e24..7f1826a3 100644
--- a/packages/admin/src/Filament/Resources/ProductResource.php
+++ b/packages/admin/src/Filament/Resources/ProductResource.php
@@ -123,6 +123,7 @@ public static function getDefaultForm(Form $form): Form
                         static::getMainFormComponents(),
                     ),
                 static::getAttributeDataFormComponent(),
+                static::getVariantAttributeDataFormComponent(),
             ])
             ->columns(1);
     }
@@ -225,7 +226,14 @@ protected static function getTagsFormComponent(): Component
 
     protected static function getAttributeDataFormComponent(): Component
     {
-        return Attributes::make()->statePath('attribute_data');
+        return Attributes::make();
+    }
+
+    protected static function getVariantAttributeDataFormComponent(): Component
+    {
+        return Attributes::make()
+            ->using(ProductVariant::class)
+            ->relationship('variant');
     }
 
     public static function getDefaultTable(Table $table): Table
diff --git a/packages/admin/src/Support/Forms/Components/Attributes.php b/packages/admin/src/Support/Forms/Components/Attributes.php
index 4f474aa8..02d96c00 100644
--- a/packages/admin/src/Support/Forms/Components/Attributes.php
+++ b/packages/admin/src/Support/Forms/Components/Attributes.php
@@ -14,15 +14,24 @@
 
 class Attributes extends Forms\Components\Group
 {
+    public ?string $modelClassOverride = null;
+
+    public function using(string $modelClass): self
+    {
+        $this->modelClassOverride = $modelClass;
+
+        return $this;
+    }
+
     protected function setUp(): void
     {
         parent::setUp();
 
-        $this->key('attributeData');
+        $this->key('attributeData'.$this->modelClassOverride);
 
         if (blank($this->childComponents)) {
             $this->schema(function (\Filament\Forms\Get $get, Livewire $livewire, ?Model $record) {
-                $modelClass = $livewire::getResource()::getModel();
+                $modelClass = $this->modelClassOverride ?: $livewire::getResource()::getModel();
 
                 $productTypeId = null;
 
@@ -42,7 +51,12 @@ protected function setUp(): void
                 }
 
                 if ($morphMap == ProductVariant::morphName()) {
-                    $productTypeId = $record?->product?->product_type_id ?: ProductType::first()->id;
+                    if ($record::class === Product::modelClass()) {
+                        $productTypeId = $record?->product_type_id ?: ProductType::first()->id;
+                    } else {
+                        $productTypeId = $record?->product?->product_type_id ?: ProductType::first()->id;
+                    }
+
                     // If we have a product type, the attributes should be based off that.
                     if ($productTypeId) {
                         $attributeQuery = ProductType::find($productTypeId)->variantAttributes();
@@ -72,9 +86,9 @@ protected function setUp(): void
                     foreach ($group['fields'] as $field) {
                         $sectionFields[] = AttributeData::getFilamentComponent($field);
                     }
-
                     $groupComponents[] = Forms\Components\Section::make($group['model']->translate('name'))
-                        ->schema($sectionFields);
+                        ->schema($sectionFields)
+                        ->statePath('attribute_data');
                 }
 
                 return $groupComponents;
diff --git a/packages/core/src/Models/Product.php b/packages/core/src/Models/Product.php
index ea4c335e..ed417326 100644
--- a/packages/core/src/Models/Product.php
+++ b/packages/core/src/Models/Product.php
@@ -10,6 +10,7 @@
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Database\Eloquent\Relations\HasManyThrough;
+use Illuminate\Database\Eloquent\Relations\HasOne;
 use Illuminate\Database\Eloquent\Relations\MorphMany;
 use Illuminate\Database\Eloquent\SoftDeletes;
 use Lunar\Base\BaseModel;
@@ -112,6 +113,11 @@ public function variants(): HasMany
         return $this->hasMany(ProductVariant::modelClass());
     }
 
+    public function variant(): HasOne
+    {
+        return $this->hasOne(ProductVariant::modelClass());
+    }
+
     public function collections(): BelongsToMany
     {
         return $this->belongsToMany(


Test patch:
diff --git a/tests/admin/Feature/Filament/Resources/ProductResource/Pages/EditProductTest.php b/tests/admin/Feature/Filament/Resources/ProductResource/Pages/EditProductTest.php
new file mode 100644
index 00000000..f685860e
--- /dev/null
+++ b/tests/admin/Feature/Filament/Resources/ProductResource/Pages/EditProductTest.php
@@ -0,0 +1,64 @@
+<?php
+
+use Livewire\Livewire;
+
+uses(\Lunar\Tests\Admin\Unit\Filament\TestCase::class)
+    ->group('resource.product');
+
+it('can edit variant attributes', function () {
+    \Lunar\Models\CustomerGroup::factory()->create([
+        'default' => true,
+    ]);
+
+    \Lunar\Models\Language::factory()->create([
+        'default' => true,
+    ]);
+
+    $product = \Lunar\Models\Product::factory()->create();
+    $variant = \Lunar\Models\ProductVariant::factory()->create([
+        'product_id' => $product->id,
+    ]);
+
+    $group = \Lunar\Models\AttributeGroup::factory()->create([
+        'attributable_type' => 'product_variant',
+        'name' => [
+            'en' => 'Variant Details',
+        ],
+        'handle' => 'variant_details',
+        'position' => 1,
+    ]);
+
+    $attribute = \Lunar\Models\Attribute::factory()->create([
+        'attribute_type' => 'product_variant',
+        'attribute_group_id' => $group->id,
+        'position' => 1,
+        'name' => [
+            'en' => 'Test Attribute',
+        ],
+        'handle' => 'test-attribute',
+        'section' => 'main',
+        'required' => false,
+        'system' => false,
+        'searchable' => false,
+    ]);
+
+    \Illuminate\Support\Facades\DB::table('lunar_attributables')->insert([
+        'attribute_id' => $attribute->id,
+        'attributable_type' => 'product_type',
+        'attributable_id' => $product->productType->id,
+    ]);
+
+    $this->asStaff(admin: true);
+
+    $component = Livewire::test(\Lunar\Admin\Filament\Resources\ProductResource\Pages\EditProduct::class, [
+        'record' => $product->id,
+        'pageClass' => 'productEdit',
+    ])->assertSuccessful();
+
+    expect($variant->attr($attribute->handle))->toBeNull();
+
+    $component->set('data.variant.attribute_data.'.$attribute->handle, 'Hello');
+    $component->call('save');
+
+    expect($variant->refresh()->attr($attribute->handle))->toBe('Hello');
+});