nova-issues
nova-issues copied to clipboard
Image field "values" don't persist on update after initial upload when used with a Repeater
- Laravel Version: 10.30.0
- Nova Version: 4.29.0
Description:
As per this issue's title, it appears as though Image field "values", when used in conjunction with Repeater fields, don't persist. I realise that Repeater fields are still in beta, but don't see anything in the documentation at https://nova.laravel.com/docs/resources/repeater-fields.html covering that.
My table structure is as follows ...
products
- id
product_images
- id
- description
- image
- product_id
My classes are as follows ...
app/Models/Product.php:
<?php
namespace App\Models;
...
class Product extends Model
{
...
public function images()
{
return $this->hasMany(ProductImage::class);
}
...
}
app/Models/ProductImage.php:
<?php
namespace App\Models;
...
class ProductImage extends Model
{
}
app/Nova/Product.php:
<?php
namespace App\Nova;
...
class Product extends Resource
{
...
public function fields(NovaRequest $request)
{
return [
...
new Panel('Images', [
Repeater::make('Images')->repeatables([
ProductImage::make(),
])->asHasMany(),
]),
...
];
}
app/Nova/Repeater/ProductImage.php:
<?php
namespace App\Nova\Repeater;
...
class ProductImage extends Repeatable
{
public static $model = \App\Models\ProductImage::class;
public function fields(NovaRequest $request)
{
return [
ID::make(),
Text::make('Description')->rules('max:255'),
Image::make('Image')->deletable(false)->disk('public'),
];
}
}
Detailed steps to reproduce the issue on a fresh Nova installation:
-
Add a product image (repeatable) ...
-
Select image (file) ...
-
Update product (model) ...
-
Confirm
product_images.imagecolumn contains correct value ... -
Update product (model) again ...
-
Notice the
product_images.imagecolumn no longer has a value ...
In closing
I suspect that the image column's value is empty after subsequent saves because those rows are deleted and recreated (as per https://nova.laravel.com/docs/resources/repeater-fields.html#upserting-repeatables-using-unique-fields), for some reason the original value isn't included in the XHR payload, and it looks like that image attribute will therefore only be "filled" if a new file is uploaded.
I have been able to get that image column's values to persist by specifying a unique field so that upserts are run instead (ie. ->uniqueField('uuid')), but then the reordering of images (repeatables) no longer work.
Any update here? same happening for me
It is still not resolved, can't save image in repeatable after first save. Also, when saving two images in repeatable, the same error
Any updates or timeline on this?
Bouncing this, just purchased a new Nova license and this still seems to be a bug
Absolutely not a final solution but I've searched where the issue was and it's kind of complex.
My use case is using the JSON preset and to me, the issue come from the fact that a clean Fluent is created inside of the preset, meaning that the File field can't keep track of a precedent eventual value of the data.
ATM for those who really need to keep files data after an update request, I've found this temporary solution:
I created a custom File field and JSON preset to modify these few lines
// In JSON::set method, replace the $callbacks variable initialization by this one
$callbacks = $fields
->withoutUnfillable()
->withoutMissingValues()
->map(function (Field $field) use ($model, $attribute, $request, $requestAttribute, $data, $blockKey) {
$originalAttribute = $model->getOriginal($attribute);
if (!is_null($originalAttribute) && isset($originalAttribute[$blockKey])) {
$field->resolve($originalAttribute[$blockKey]['fields'], $field->attribute);
}
return $field->fillInto($request, $data, $field->attribute, "{$requestAttribute}.{$blockKey}.fields.{$field->attribute}");
})
->filter(function ($callback) {
return is_callable($callback);
});
// In File::fillAttribute method, move up the $hasExistingFile initialization and add this condition
protected function fillAttribute(NovaRequest $request, $requestAttribute, $model, $attribute)
{
// This line was just moved from a later section of the method to be used earlier
$hasExistingFile = ! is_null($this->getStoragePath());
if (is_null($file = $this->retrieveFileFromRequest($request, $requestAttribute))) {
// Add this condition to fill the attribute from the initial now-known value
if ($hasExistingFile) {
return $model->{$attribute} = $this->getStoragePath();
}
return;
}
// Leave the rest unchanged
I've created a gist here to see the full code example. Please share if you succeed further than me :)
This adds more support for the File field and if needed you can override its descendant to make extends your File field class for Audio, Image, Avatar and so on... as needed.
If you dont want to override a bunch of classes, you can even make this fix "like native" with this composer trick to load your File and JSON preset instead of the vendor one.
You won't lose your file paths on updating anymore, but there is remaining cons to this temporary solution:
- The file deletion remains impossible because the
nullvalue will resolve to the previous value instead, but you can delete the row, save the form and repopulate the row fields. - The repeater ordering is broken: because the form still send
nullvalues for the files, each file will be resolved from its previous position in the model attribute. Moving the first row to the second will move the other fields (number, text, etc..) but will keep the file at their initial position.
Hoping that get fixed by a proper solution quickly 🤞🏻.
Here is my approach, which doesn't require modifying other files and may even be easy to integrate in the framework:
Add a hidden field to your Repeatable:
Hidden::make('Key') ->default(fn () => uniqid()),
In your model, do something on the lines of `protected static function boot() { parent::boot();
static::updating(function ($model) {
$originalDocuments = Arr::keyBy($model->getOriginal('documents'), 'fields.key');
$model->documents = Arr::map($model->documents, function ($document) use ($originalDocuments) {
if (! empty($document['fields']['file'])) {
return $document;
}
if (! isset($originalDocuments[$document['fields']['key']])) {
return $document;
}
$document['fields']['file'] = $originalDocuments[$document['fields']['key']]['fields']['file'] ?? '';
return $document;
});
});
}`
This assumes my DB json field is called documents, and my repeatable has a file field called "file", which I want to retain. As it probably is obvious, I am using those generated keys to put back the file paths, when updating the model.
Fixed by this section Link
just add ->uniqueField('id')
Repeater::make('Line Items')
->asHasMany()
->uniqueField('id')
->repeatables([
\App\Nova\Repeater\LineItem::make()
])
and add ID::hidden(), // The unique ID field
Fixed by this section Link
just add ->uniqueField('id')
Repeater::make('Line Items') ->asHasMany() ->uniqueField('id') ->repeatables([ \App\Nova\Repeater\LineItem::make() ])and add ID::hidden(), // The unique ID field
This is working perfect, thanks!
Fixed by this section Link
just add ->uniqueField('id')
Repeater::make('Line Items') ->asHasMany() ->uniqueField('id') ->repeatables([ \App\Nova\Repeater\LineItem::make() ])and add ID::hidden(), // The unique ID field
Agree that this works when using a distinct model via the asHasMany() method, but the issue still persists when storing the data as JSON. For example, my model has an images JSON column, and the repeater allows the storage of path, caption, and alt_text fields.
Looks like I have the same problem: #6410
Any ETA on this?
Hi @tonnyorg, we are working on the next version, Nova 5 which will contain this fix. No set ETA yet but very soon, which is why we decided not to add beta related fixes to 4👍
@jeremynikolic ahhh got it, tyvm!