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

Fix dependent field support #497

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions routes/nova-api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

use Illuminate\Support\Facades\Route;
use Whitecube\NovaFlexibleContent\Http\Controllers\DependentFieldSupportController;

Route::patch('/{resource}/{resourceId}/update-fields', [DependentFieldSupportController::class, 'updateFieldSync']);
Route::patch('/{resource}/creation-fields', [DependentFieldSupportController::class, 'creationFieldSync']);
21 changes: 21 additions & 0 deletions src/FieldServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Whitecube\NovaFlexibleContent;

use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Laravel\Nova\Events\ServingNova;
use Laravel\Nova\Nova;
Expand All @@ -26,6 +28,9 @@ public function boot()
Nova::script('nova-flexible-content', __DIR__.'/../dist/js/field.js');
Nova::style('nova-flexible-content', __DIR__.'/../dist/css/field.css');
});

// We can overwrite the nova.api.update-fields route safely here.
$this->novaApiRouteOverwrite();
}

/**
Expand Down Expand Up @@ -69,4 +74,20 @@ public function addMiddleware()
));
}
}

/**
*
* @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(__DIR__.'/../routes/nova-api.php');
});
}
}
101 changes: 101 additions & 0 deletions src/Http/Controllers/DependentFieldSupportController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace Whitecube\NovaFlexibleContent\Http\Controllers;

use Illuminate\Routing\Controller;
use Laravel\Nova\Http\Requests\ResourceCreateOrAttachRequest;
use Laravel\Nova\Http\Requests\ResourceUpdateOrUpdateAttachedRequest;
use Laravel\Nova\Http\Resources\CreateViewResource;
use Laravel\Nova\Http\Resources\ReplicateViewResource;
use Laravel\Nova\Http\Resources\UpdateViewResource;
use Laravel\Nova\Http\Controllers\UpdateFieldController;
use Laravel\Nova\Http\Controllers\CreationFieldSyncController;
use Whitecube\NovaFlexibleContent\Flexible;
use Whitecube\NovaFlexibleContent\Http\FlexibleAttribute;

use Illuminate\Support\Facades\App;

/**
* Controller collecting methods for supporting dependent fields in update and creation mode.
*/
class DependentFieldSupportController extends Controller {

/**
* @param ResourceUpdateOrUpdateAttachedRequest $request
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function updateFieldSync(ResourceUpdateOrUpdateAttachedRequest $request)
{
// we only act on flexible fields concerning this package
if(! FlexibleAttribute::hasFlexibleGeneratedPart($request->query('field'))) {
// return native response calling sync
return App::make(UpdateFieldController::class)->sync($request);
}

$resource = UpdateViewResource::make()->newResourceWith($request);

return response()->json(
$this->findFlexibleField($request, $resource)
);

}

/**
* Synchronize the field for creation view.
*
* @param \Laravel\Nova\Http\Requests\ResourceCreateOrAttachRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function creationFieldSync(ResourceCreateOrAttachRequest $request)
{
// we only act on flexible fields concerning this package
if(! FlexibleAttribute::hasFlexibleGeneratedPart($request->query('field'))) {
// return native response (using __invoke)
return App::call(CreationFieldSyncController::class, [$request]);
}

$resource = $request->has('fromResourceId')
? ReplicateViewResource::make($request->fromResourceId)->newResourceWith($request)
: CreateViewResource::make()->newResourceWith($request);

return response()->json(
$this->findFlexibleField($request, $resource)
);
}

/**
* Find flexible field by unpacking all flexible layouts.
* @param $request
* @param $resource
* @return mixed
*/
protected function findFlexibleField($request, $resource) {
return $resource->creationFields($request)
->map($this->recursiveUnfoldFlexible())
->flatten()
->filter(function ($field) use ($request) {
return $request->input('field') === $field->attribute &&
$request->input('component') === $field->dependentComponentKey();
})->each->syncDependsOn($request)
->first();
}

/**
* Recursively unfold Flexible fields.
*
* @return \Closure
*/
protected function recursiveUnfoldFlexible() {
return function($field) {
// we need to unpack each flexible layout
if($field instanceof Flexible) {
return $field->jsonSerialize()['layouts']->map(function($layout) {
return collect($layout->fields())->map($this->recursiveUnfoldFlexible());
})->flatten();
}
return $field;
};
}

}
20 changes: 20 additions & 0 deletions src/Http/FlexibleAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Whitecube\NovaFlexibleContent\Http;

use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;

class FlexibleAttribute
{
Expand All @@ -28,6 +29,13 @@ class FlexibleAttribute
*/
const FILE_INDICATOR = '___upload-';

/**
* Offset of a generated flexible part where the group
* separator (FlexibleAttribute::GROUP_SEPARATOR) starts at
* given position.
*/
const FLEXIBLE_FIELD_OFFSET = 15;

/**
* The original attribute name
*
Expand Down Expand Up @@ -356,4 +364,16 @@ protected function setName()

$this->name = $name;
}

/**
* If field name starts with a generated flexible part.
* @param string $field
* @return bool
*/
public static function hasFlexibleGeneratedPart(string $field) : bool {
/**
return (strlen($field) >= self::FLEXIBLE_FIELD_OFFSET &&
strpos($field, self::GROUP_SEPARATOR, self::FLEXIBLE_FIELD_OFFSET) !== false); **/
return strpos($field, self::GROUP_SEPARATOR) !== false;
}
}
5 changes: 5 additions & 0 deletions src/Http/Middleware/InterceptFlexibleAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public function handle(Request $request, Closure $next): Response
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);

Expand Down
56 changes: 56 additions & 0 deletions src/Http/ParsesFlexibleAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Whitecube\NovaFlexibleContent\Http;

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

trait ParsesFlexibleAttributes
{
Expand All @@ -13,6 +14,50 @@ trait ParsesFlexibleAttributes
*/
protected $registered = [];

/**
* @param $attribute
* @return array
*/
protected function splitFlexPartsFromFieldName(string $attribute) : array {
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->query('field');
// we firstly check if the group separator starts at char index 15 (16th char)
if(! FlexibleAttribute::hasFlexibleGeneratedPart($field)) {
return false;
}

// Flexible keys converted to original and to be merged with the request
$parts = $this->splitFlexPartsFromFieldName($field);
// From here, $request->query('field') will be used to keep track
// of the original flexible generated field name and $request->input('field')
// well be used for the original field name
$flex_fields = ['field' => $parts['field']];
// here we overwrite the query parameter 'field'.
foreach($request->all() as $field => $value) {
// check if there is 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
*
Expand All @@ -21,6 +66,17 @@ trait ParsesFlexibleAttributes
*/
protected function requestHasParsableFlexibleInputs(Request $request)
{
// dependent field requests (PATCH only).
if($request->method() === 'PATCH' && Str::contains($request->getRequestUri(), [
'/update-fields?', // Laravel\Nova\Http\Controllers\UpdateFieldController
'/creation-fields?', // Laravel\Nova\Http\Controllers\CreationFieldSyncController,
// @todo: find out scenario where these two routes are used.
// '/creation-pivot-fields/'
// '/update-pivot-fields/'
])) {
return $this->parseFlexableFieldForPatchRequest($request);
}

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