whitecube / nova-flexible-content

Flexible Content & Repeater Fields for Laravel Nova
MIT License
787 stars 234 forks source link

resolveUsing support #224

Open keizah7 opened 4 years ago

keizah7 commented 4 years ago

Flexible content don't work with resolveUsing.

Flexible::make('Lėšų savininkai', 'client[funds_owners]') I need fill custom value in resource update page and I can't do it.

Also method hideWhenUpdating don't work with this field

hayatbiralem commented 1 year ago

I was trying to use Nova Flexible Content in a multilingual context without using outl1ne/nova-translatable package so I needed to use resolveUsing for this matter.

Until there will be a built-in support for this, I manage to solve my issue by extending the Flexible class like following:

<?php

namespace App\Nova\Flexible\Fields;

use Laravel\Nova\Http\Requests\NovaRequest;
use Whitecube\NovaFlexibleContent\Flexible;
use Whitecube\NovaFlexibleContent\Value\Resolver;

class MyFlexible extends Flexible
{
    /**
     * The currently defined layout groups
     *
     * @var Illuminate\Support\Collection
     */
    public $groups;

    /**
     * Resolve the field's value.
     *
     * @param  mixed  $resource
     * @param  string|null  $attribute
     * @return void
     */
    public function resolve($resource, $attribute = null)
    {
        if ($this->resolveCallback && is_callable($this->resolveCallback)) {
            $this->value = call_user_func($this->resolveCallback, $this, $resource, $attribute);
            return;
        }

        parent::resolve($resource, $attribute);
    }

    /**
     * Hydrate the given attribute on the model based on the incoming request.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @param  string  $requestAttribute
     * @param  object  $model
     * @param  string  $attribute
     * @return null|Closure
     */
    protected function fillAttribute(NovaRequest $request, $requestAttribute, $model, $attribute)
    {
        if (isset($this->fillCallback)) {
            return call_user_func($this->fillCallback, $request, $model, $attribute, $requestAttribute);
        }

        return parent::fillAttribute($request, $requestAttribute, $model, $attribute);
    }

    /**
     * Resolve all contained groups and their fields
     *
     * @param  Illuminate\Support\Collection  $groups
     * @return Illuminate\Support\Collection
     */
    public function resolveGroups($groups)
    {
        return $groups->map(function ($group) {
            return $group->getResolved();
        });
    }

    /**
     * Define the field's actual layout groups (as "base models") based
     * on the field's current model & attribute
     *
     * @param  mixed  $resource
     * @param  string  $attribute
     * @return Illuminate\Support\Collection
     */
    public function buildGroups($resource, $attribute)
    {
        if (!$this->resolver) {
            $this->resolver(Resolver::class);
        }

        return $this->groups = $this->resolver->get($resource, $attribute, $this->layouts);
    }

    /**
     * Registers a reference to the origin model for nested & contained fields
     *
     * @param  mixed  $model
     * @return void
     */
    public function registerOriginModel($model)
    {
        if (is_a($model, \Laravel\Nova\Resource::class)) {
            $model = $model->model();
        } elseif (is_a($model, \Whitecube\NovaPage\Pages\Template::class)) {
            $model = $model->getOriginal();
        }

        if (! is_a($model, \Illuminate\Database\Eloquent\Model::class)) {
            return;
        }

        static::$model = $model;
    }
}

And I used it in my Nova Resource like following:

<?php

namespace App\Nova;

use App\Nova\Flexible\Fields\MyFlexible;
use App\Nova\Flexible\Layouts\HTML;
use Eminiarts\Tabs\Tab;
use Eminiarts\Tabs\Tabs;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Resource;

class Page extends Resource
{
    // [...]

    /**
     * Get the fields displayed by the resource.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @return array
     */
    public function fields(NovaRequest $request)
    {
        $locales = config('localized-routes.supported-locales');

        $fields = [
            ID::make()->sortable(),
        ];

        $tabs = [];
        foreach ($locales as $locale) {
            $tabs[] = Tab::make(\Str::upper($locale), $this->translatableFields($locale));
        }
        $fields[] = Tabs::make('locale', $tabs);

        return $fields;
    }

    /**
     * Get translatable fields.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @return array
     */
    public function translatableFields($locale)
    {
        return [
            MyFlexible::make('Sections', 'sections->' . $locale)
                ->resolveUsing(function ($flexible, $resource, $attribute) use ($locale) {
                    $currentLocale = app()->getLocale();
                    app()->setLocale($locale);
                    $attribute = $attribute ?? $this->attribute;
                    $flexible->registerOriginModel($resource);
                    $flexible->buildGroups($resource, (explode('->', $attribute ?? $flexible->attribute))[0]);
                    $flexible->value = $flexible->resolveGroups($flexible->groups);
                    $value = $flexible->value;
                    app()->setLocale($currentLocale);
                    return $value;
                })
                ->hideFromDetail()
                ->addLayout(Hero::class)
                ->addLayout(HTML::class),
        ];
    }

    // [...]
}

Of-course this approach is not ideal and comes with maintenance cost but for now I can move forward.

I hope it helps.

hayatbiralem commented 1 year ago

Probably we can (or should) use a custom resolver class to achieve what we want.

hayatbiralem commented 1 year ago

I managed to solve my issue with using a custom resolver class like following:

<?php

// app\Nova\Flexible\Resolvers\MultilingualSectionsResolver.php

namespace App\Nova\Flexible\Resolvers;

use Whitecube\NovaFlexibleContent\Value\ResolverInterface;
use Whitecube\NovaFlexibleContent\Value\Resolver;

class MultilingualSectionsResolver extends Resolver implements ResolverInterface
{
    public $locale = null;
    public $attribute_separator = '->';

    public function __construct($locale, $attribute_separator = null)
    {
        $this->locale = $locale;

        if(!is_null($attribute_separator)) {
            $this->attribute_separator = $attribute_separator;
        }
    }

    /**
     * get the field's value
     *
     * @param  mixed  $resource
     * @param  string $attribute
     * @param  \Whitecube\NovaFlexibleContent\Layouts\Collection $layouts
     * @return \Illuminate\Support\Collection
     */
    public function get($resource, $attribute, $layouts)
    {
        $currentLocale = app()->getLocale();
        app()->setLocale($this->locale);
        $value = parent::get($resource, (explode($this->attribute_separator, $attribute))[0], $layouts);
        app()->setLocale($currentLocale);
        return $value;
    }
}

And I used it in my Nova Resource like following:

<?php

// app\Nova\Page.php

namespace App\Nova;

use App\Nova\Flexible\Layouts\HTML;
use App\Nova\Flexible\Layouts\Hero;
use App\Nova\Flexible\Resolvers\MultilingualSectionsResolver;
use Eminiarts\Tabs\Tab;
use Eminiarts\Tabs\Tabs;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Resource;
use Whitecube\NovaFlexibleContent\Flexible;

class Page extends Resource
{
    // [...]

    /**
     * Get the fields displayed by the resource.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @return array
     */
    public function fields(NovaRequest $request)
    {
        $locales = config('localized-routes.supported-locales');

        $fields = [
            ID::make()->sortable(),
        ];

        $tabs = [];
        foreach ($locales as $locale) {
            $tabs[] = Tab::make(\Str::upper($locale), $this->translatableFields($locale));
        }
        $fields[] = Tabs::make('locale', $tabs);

        return $fields;
    }

    /**
     * Get translatable fields.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @return array
     */
    public function translatableFields($locale)
    {
        return [
            Flexible::make('Sections', 'sections->' . $locale)
                ->resolver( new MultilingualSectionsResolver($locale) )
                ->hideFromDetail()
                ->addLayout(Hero::class)
                ->addLayout(HTML::class),
        ];
    }

    // [...]
}

This is much cleaner than my previous work around.

Of course that would be cool if we can use the resolveUsing function also but a custom resolver class will do just fine.

Kudos to @whitecube team and all contributors of this beautiful package, thank you for good work.

schniper commented 1 year ago

First of all, thank you for pointing me in the right direction. Indeed, the best thing would be to support the built in transformer methods (resolveUsing, etc.).

In my case, I am editing parts of a larger JSON, which I would not want to polute with the layouts syntax, nor can I use casters, as, again, it's about only parts of a larger JSON.

So here is my basic Resolver I whipped up just now. Its role is to take a JSON array and inject a specified layout. It does the job, at least in my usecaes.

You use it like ->resolver(new SimpleJsonResolver('my_layout_name')).

Hope this helps someone. Maybe it could even find its place in the base code, as it would be a good helper for basic use cases, where you only need to edit a piece of random JSON.

`<?php

namespace App\Nova\Flexible\Resolvers;

use Illuminate\Support\Arr; use Whitecube\NovaFlexibleContent\Value\Resolver;

class SimpleJsonResolver extends Resolver { private string $layout = '';

public function __construct(string $layout)
{
    $this->layout = $layout;
}

/**
 * get the field's value
 *
 * @param  mixed  $resource
 * @param  string  $attribute
 * @param  \Whitecube\NovaFlexibleContent\Layouts\Collection  $layouts
 * @return \Illuminate\Support\Collection
 */
public function get($resource, $attribute, $layouts)
{
    $rawValue = $this->extractValueFromResource($resource, $attribute);

    $value = Arr::map($rawValue, fn ($v, $k) => (object) [
        'key' => $k,
        'attributes' => $v,
    ]);

    return collect($value)->map(function ($item) use ($layouts) {
        $layout = $layouts->find($this->layout);

        if (! $layout) {
            return;
        }

        return $layout->duplicateAndHydrate($item->key, (array) $item->attributes);
    })->filter()->values();
}

/**
 * Set the field's value
 *
 * @param  mixed  $model
 * @param  string  $attribute
 * @param  \Illuminate\Support\Collection  $groups
 * @return string
 */
public function set($model, $attribute, $groups)
{
    return $model->$attribute = $groups->map(function ($group) {
        return $group->getAttributes();
    });
}

}`