Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dependent Fields not working #492

Open
davinyoeun opened this issue Dec 8, 2023 · 4 comments
Open

Dependent Fields not working #492

davinyoeun opened this issue Dec 8, 2023 · 4 comments

Comments

@davinyoeun
Copy link

I have 2 fields below:

  1. I have one select field is called "Type"
  2. I have one select field is called "Unit" that depend on field "Type"
    Action:
  3. I select field type
  4. I select field unit suddenly it was hidden from the form
@wize-wiz
Copy link

I can confirm this is indeed a problem. The field depending on another field will just disappear.

@wize-wiz
Copy link

@davinyoeun As far as I can tell, the support for field dependency was added in times of Laravel 9 and Nova 3 where Nova 4 was just released. I've tested this feature with several early versions of Nova 4 and this feature just doesn't work.

The commit seems be mostly #347

Can anyone even confirm this worked with Laravel 10 / Nova 4? Or at all?

I seem to get an empty PATCH response, can anyone confirm?

@wize-wiz
Copy link

wize-wiz commented Jan 12, 2024

@toonvandenbos @voidgraphics I've been diving into this problem and in dire need to catch up some time I haven't spent with Laravel and Nova the last +/- 3 years.

To my understanding, Nova 4 uses an update-fields PATCH request to get the new state of a field. The middleware intercepting requests using requestHasParsableFlexibleInputs only acts on ['POST', 'PUT'] defined in ParsesFlexibleAttributes.

// InterceptFlexibleAttributes
public function handle(Request $request, Closure $next): Response
{
    if (! $this->requestHasParsableFlexibleInputs($request)) {
        return $next($request);
    }

    ...
}
// ParsesFlexibleAttributes
protected function requestHasParsableFlexibleInputs(Request $request)
{
    return in_array($request->method(), ['POST', 'PUT']) &&
            is_string($request->input(FlexibleAttribute::REGISTER));
}

Now is_string($request->input(FlexibleAttribute::REGISTER) checks if a specific attribute is present FlexibleAttribute::REGISTER which again contains ___nova_flexible_content_fields. But this attribute is never added to the PATCH request by nova. This attribute is added by FormField.vue when doing a fill.

So I'm just wondering, did this ever work? Did Nova 4 started splitting the requests using a PATCH request?

@wize-wiz
Copy link

wize-wiz commented Jan 13, 2024

My final take on this matter, more or less a proof of concept.

The support for field dependency native to Nova isn't supported by the middleware InterceptFlexibleAttributes. It doesn't take into account that a patch request is send to only update the state of the dependent fields, therefor the response of /nova-api/{resource}/{resourceId}/update-fields returns only one field because each dependent field sends out its own syncField request. In its current state it simply returns an empty json response because the field requesting for its current state is never found due to the nature of how this package groups fields (nested) in form of layouts.

The support for dependent fields can only be achieved if the native update-fields Nova offers is replaced with a patched one.

-- edit

I've moved the constant FLEXIBLE_FIELD_OFFSET from the trait to the class FlexibleAttribute because constants in traits is supported from php >= 8.2 and onwards.

The modification

The following three files from this package needs to be modified:

  • Whitecube\NovaFlexibleContent\Http\ParsesFlexibleAttributes
  • Whitecube\NovaFlexibleContent\Http\Middleware\InterceptFlexibleAttributes
  • Whitecube\NovaFlexibleContent\Http\FlexibleAttribute

A new route file should be created in the project routes directory

  • nova-api.php

A controller extending Novas UpdateFieldController controller:

  • App\Http\Controllers\Nova\PatchedUpdateFieldController

A definition in App\Providers\RouteServiceProvider::boot registering the routes/nova-api.php routes file:

namespace App\Providers;

....
use Illuminate\Routing\Middleware\SubstituteBindings;

class RouteServiceProvider extends ServiceProvider
{

    public function boot()
    {
    ....

      $this->routes(function () {
        ....
   
        $this->novaApiRouteOverwrite();
      });

    ....
    }

    /**
     * Project based Nova API route file.
     * @return void
     */
    protected function novaApiRouteOverwrite() {
        Route::group([
            'domain' => config('nova.domain', null),
            'as' => 'nova.api.',
            'prefix' => 'nova-api',
            'middleware' => 'nova:api',
            'excluded_middleware' => [SubstituteBindings::class],
        ], function () {
            $this->loadRoutesFrom(base_path('routes/nova-api.php'));
        });
    }

The nova-api.php route file:

  • file: routes/nova-api.php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Nova\PatchedUpdateFieldController;

Route::patch('/{resource}/{resourceId}/update-fields', [PatchedUpdateFieldController::class, 'sync']);

The PatchedUpdatedFieldController

  • app/Http/Controllers/Nova/PatchedUpdatedFieldController.php
<?php

namespace App\Http\Controllers\Nova;

use Laravel\Nova\Http\Requests\ResourceUpdateOrUpdateAttachedRequest;
use Laravel\Nova\Http\Resources\UpdateViewResource;
use Laravel\Nova\Http\Controllers\UpdateFieldController;
use Whitecube\NovaFlexibleContent\Flexible;

class PatchedUpdateFieldController extends UpdateFieldController {

    /**
     * @param ResourceUpdateOrUpdateAttachedRequest $request
     * @return \Illuminate\Http\JsonResponse
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function sync(ResourceUpdateOrUpdateAttachedRequest $request)
    {
        $resource = UpdateViewResource::make()->newResourceWith($request);

        return response()->json($resource->updateFields($request)
            // we first need to extract all fields from a given group/layout
            ->map(function($field) use ($resource) {
                if($field instanceof Flexible) {
                    $resolved = $field->jsonSerialize()['layouts']->map(function($layout) {
                        return $layout->fields();
                    })->flatten();

                    return $resolved;
                }
                return $field;
            })
            // we need to flatten the collection with all nested collections given previously
            ->flatten()
            // here is everything as usual
            ->filter(function ($field) use ($request) {
                return $request->query('field') === $field->attribute &&
                    $request->query('component') === $field->dependentComponentKey();
            })->each->syncDependsOn($request)->first());

    }

}

Modifications to the InterceptFlexibleAttributes middleware class, concerning only the handle method.

  • file: src/Http/Middleware/
    public function handle(Request $request, Closure $next): Response
    {

        // has been adapted to also parse patch requests of `update-fields`
        if (! $this->requestHasParsableFlexibleInputs($request)) {
            return $next($request);
        }

        // we assume we can quite here because we don't need the rest after if statement
        if($request->method() === 'PATCH') {
            return $next($request);
        }

        $request->merge($this->getParsedFlexibleInputs($request));
        $request->request->remove(FlexibleAttribute::REGISTER);

        $response = $next($request);

        if (! $this->shouldTransformFlexibleErrors($response)) {
            return $response;
        }

        return $this->transformFlexibleErrors($response);
    }

Modifications to the FlexibleAttribute class:

<?php

namespace Whitecube\NovaFlexibleContent\Http;

use Illuminate\Support\Arr;

class FlexibleAttribute
{
....
    /**
     * Offset of a generated flexible attribute where the group
     * separator (FlexibleAttribute::GROUP_SEPARATOR) starts
     * @note: supported from php >= 8.2
     */
    const FLEXIBLE_FIELD_OFFSET = 15;
....
}
?>

Modifications to the ParsesFlexibleAttributes trait:

<?php

namespace Whitecube\NovaFlexibleContent\Http;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

trait ParsesFlexibleAttributes
{

    /**
     * Offset of a generated flexible attribute where the group
     * separator (FlexibleAttribute::GROUP_SEPARATOR) starts
     * @note: supported from php >= 8.2
     */
    // const FLEXIBLE_FIELD_OFFSET = 15;

    /**
     * @param $attribute
     * @return void
     */
    protected function splitFlexPartsFromFieldName(string $attribute) {
        return array_combine(['key', 'field'], explode('__', $attribute));
    }

    /**
     * Modify the request for a PATCH update-fields request
     * @param $request
     * @return bool
     */
    protected function parseFlexableFieldForPatchRequest($request) : bool {
        $field = $request->input('field');
        // we firstly check if the group separator starts at char index 15 (16th char)
        if(!(strlen($field) >= FlexibleAttribute::FLEXIBLE_FIELD_OFFSET &&
             strpos($field, FlexibleAttribute::GROUP_SEPARATOR, FlexibleAttribute::FLEXIBLE_FIELD_OFFSET) !== false)) {
            return false;
        }
        // flexible keys converted to original and to be merged with the request
        $flex_fields = [];
        $parts = $this->splitFlexPartsFromFieldName($field);
        // here we overwrite the query parameter 'field'.
        $request->instance()->query->set('field', $parts['field']);
        foreach($request->all() as $field => $value) {
            // check if we a have a flexible generated field name
            if(Str::startsWith($field, $parts['key'])) {
                // remove flexible generate input
                $request->request->remove($field);
                // pretend it's an original field input
                $flex_fields[
                    str_replace($parts['key'] . FlexibleAttribute::GROUP_SEPARATOR, '', $field)
                ] = $value;
            }
        }
        $request->merge($flex_fields);

        return true;
    }

    /**
     * Check if given request should be handled by the middleware
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    protected function requestHasParsableFlexibleInputs(Request $request)
    {
        if($request->method() === 'PATCH' && Str::contains($request->getRequestUri(), '/update-fields?')) {
            return $this->parseFlexableFieldForPatchRequest($request);
        }

        return in_array($request->method(), ['POST', 'PUT']) &&
               is_string($request->input(FlexibleAttribute::REGISTER));
    }

}

I'm not quite familiar with this package so some parts can certainly be done better. This part is quite ugly $resolved = $field->jsonSerialize()['layouts']->map but I didn't find a better solution where a flexible field could resolve its groups and return a collection of fields. Maybe I just missed it.

Anyhow, it works as it should and I'll update this issue along with any future progress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants