diff --git a/packages/admin/src/Support/Forms/AttributeData.php b/packages/admin/src/Support/Forms/AttributeData.php index d580868a90..49ea986fa5 100644 --- a/packages/admin/src/Support/Forms/AttributeData.php +++ b/packages/admin/src/Support/Forms/AttributeData.php @@ -3,6 +3,7 @@ namespace Lunar\Admin\Support\Forms; use Filament\Schemas\Components\Component; +use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Collection; use Lunar\Admin\Support\FieldTypes\Dropdown; use Lunar\Admin\Support\FieldTypes\File; @@ -24,6 +25,8 @@ use Lunar\FieldTypes\Vimeo as VimeoFieldType; use Lunar\FieldTypes\YouTube as YouTubeFieldType; use Lunar\Models\Attribute; +use Lunar\Models\Language; +use Tiptap\Editor; class AttributeData { @@ -53,26 +56,30 @@ public function getFilamentComponent(Attribute $attribute): Component $attribute->translate('name') ) ->formatStateUsing(function ($state) use ($attribute) { - $value = $state instanceof FieldType ? $state->getValue() : $state; + $value = $this->unwrapFieldValue($state); if ($value === null) { - $value = (new $attribute->type)->getValue(); + $value = $this->unwrapFieldValue((new $attribute->type)->getValue()); + } + + if ($attribute->type === TranslatedTextFieldType::class) { + return $this->normalizeTranslatedTextValue($value); } return is_string($value) && blank($value) ? null : $value; }) ->mutateStateForValidationUsing(function ($state) { - if ($state instanceof FieldType) { - return $state->getValue(); - } - - return $state; + return $this->unwrapFieldValue($state); }) ->mutateDehydratedStateUsing(function ($state) use ($attribute) { if ($state instanceof FieldType) { return $state; } + if ($attribute->type === TranslatedTextFieldType::class) { + $state = $this->normalizeTranslatedTextValueForStorage($state); + } + $instance = new $attribute->type; if (! blank($state)) { @@ -110,4 +117,91 @@ public function synthesizeLivewireProperties(): void $fieldType::synthesize(); } } + + protected function unwrapFieldValue(mixed $value): mixed + { + if ($value instanceof FieldType) { + return $this->unwrapFieldValue($value->getValue()); + } + + if ($value instanceof Arrayable) { + $value = $value->toArray(); + } + + if (! is_array($value)) { + return $value; + } + + return collect($value) + ->map(fn (mixed $item): mixed => $this->unwrapFieldValue($item)) + ->all(); + } + + protected function normalizeTranslatedTextValue(mixed $value): array + { + $defaults = $this->getTranslatedTextDefaults(); + + if ($value === null || $value === []) { + return $defaults; + } + + if (! is_array($value)) { + $defaultLanguage = array_key_first($defaults); + + if ($defaultLanguage === null) { + return []; + } + + return array_replace($defaults, [ + $defaultLanguage => $value, + ]); + } + + return array_replace( + $defaults, + collect($value) + ->map(fn (mixed $item): string => filled($item) ? (string) $item : '') + ->all(), + ); + } + + protected function getTranslatedTextDefaults(): array + { + static $defaults = null; + + if (is_array($defaults)) { + return $defaults; + } + + $defaults = Language::query() + ->orderBy('default', 'desc') + ->get(['code']) + ->mapWithKeys(fn (Language $language): array => [$language->code => '']) + ->all(); + + return $defaults; + } + + protected function normalizeTranslatedTextValueForStorage(mixed $value): mixed + { + if ($value instanceof Arrayable) { + $value = $value->toArray(); + } + + if (! is_array($value)) { + return $value; + } + + return collect($value) + ->map(function (mixed $item): mixed { + $item = $this->unwrapFieldValue($item); + + if (is_array($item)) { + return (new Editor)->setContent($item)->getHTML(); + } + + return $item ?? ''; + }) + ->all(); + } } diff --git a/tests/admin/Feature/Filament/Resources/ProductResource/Pages/EditProductTest.php b/tests/admin/Feature/Filament/Resources/ProductResource/Pages/EditProductTest.php index f692b68505..d1de879757 100644 --- a/tests/admin/Feature/Filament/Resources/ProductResource/Pages/EditProductTest.php +++ b/tests/admin/Feature/Filament/Resources/ProductResource/Pages/EditProductTest.php @@ -6,6 +6,7 @@ use Lunar\FieldTypes\Number; use Lunar\FieldTypes\Text; use Lunar\FieldTypes\Toggle; +use Lunar\FieldTypes\TranslatedText as TranslatedTextField; use Lunar\Models\Attribute; use Lunar\Models\AttributeGroup; use Lunar\Models\CustomerGroup; @@ -191,3 +192,145 @@ expect($record->refresh()->attr('name'))->toBe('New Product Name'); }); + +it('hydrates translated rich text fields with all locale keys', function () { + CustomerGroup::factory()->create([ + 'default' => true, + ]); + + Language::factory()->create([ + 'code' => 'en', + 'default' => true, + ]); + + Language::factory()->create([ + 'code' => 'es', + 'default' => false, + ]); + + TaxClass::factory()->create([ + 'default' => true, + ]); + + $product = Product::factory()->create([ + 'attribute_data' => collect([ + 'description' => new TranslatedTextField(collect()), + ]), + ]); + + ProductVariant::factory()->create([ + 'product_id' => $product->id, + ]); + + createTranslatedRichTextProductAttribute($product, 'description'); + + $this->asStaff(admin: true); + + Livewire::test(EditProduct::class, [ + 'record' => $product->getRouteKey(), + 'pageClass' => 'productEdit', + ]) + ->assertSuccessful() + ->assertSet('data.attribute_data.description.en', '') + ->assertSet('data.attribute_data.description.es', ''); +}); + +it('saves translated rich text fields from rich editor document payloads', function () { + CustomerGroup::factory()->create([ + 'default' => true, + ]); + + Language::factory()->create([ + 'code' => 'en', + 'default' => true, + ]); + + Language::factory()->create([ + 'code' => 'es', + 'default' => false, + ]); + + TaxClass::factory()->create([ + 'default' => true, + ]); + + $product = Product::factory()->create([ + 'attribute_data' => collect([ + 'description' => new TranslatedTextField(collect()), + ]), + ]); + + ProductVariant::factory()->create([ + 'product_id' => $product->id, + ]); + + createTranslatedRichTextProductAttribute($product, 'description'); + + $this->asStaff(admin: true); + + Livewire::test(EditProduct::class, [ + 'record' => $product->getRouteKey(), + 'pageClass' => 'productEdit', + ]) + ->fillForm([ + 'attribute_data' => [ + 'description' => [ + 'en' => [ + 'type' => 'doc', + 'content' => [ + [ + 'type' => 'paragraph', + 'content' => [ + [ + 'type' => 'text', + 'text' => 'Rich description', + ], + ], + ], + ], + ], + 'es' => '', + ], + ], + ]) + ->call('save') + ->assertHasNoFormErrors(); + + expect($product->refresh()->attr('description', 'en'))->toBe('

Rich description

'); +}); + +function createTranslatedRichTextProductAttribute(Product $product, string $handle): void +{ + $group = AttributeGroup::factory()->create([ + 'attributable_type' => 'product', + 'name' => [ + 'en' => 'Details', + ], + 'handle' => 'details', + 'position' => 1, + ]); + + $attribute = Attribute::factory()->create([ + 'attribute_type' => 'product', + 'attribute_group_id' => $group->id, + 'position' => 1, + 'name' => [ + 'en' => 'Description', + ], + 'handle' => $handle, + 'section' => 'main', + 'type' => TranslatedTextField::class, + 'required' => false, + 'system' => false, + 'searchable' => false, + 'configuration' => [ + 'richtext' => true, + ], + ]); + + DB::table('lunar_attributables')->insert([ + 'attribute_id' => $attribute->id, + 'attributable_type' => 'product_type', + 'attributable_id' => $product->productType->id, + ]); +}