From b0e31aa2b37d557f4d3fc0b706cc206dcc604cbe Mon Sep 17 00:00:00 2001 From: wize-wiz Date: Sat, 13 Jan 2024 23:10:40 +0100 Subject: [PATCH 1/6] Modifications to support dependency fields. See https://github.com/whitecube/nova-flexible-content/issues/492#issuecomment-1890568222 --- src/Http/FlexibleAttribute.php | 6 +++ .../InterceptFlexibleAttributes.php | 5 ++ src/Http/ParsesFlexibleAttributes.php | 46 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/src/Http/FlexibleAttribute.php b/src/Http/FlexibleAttribute.php index b6782c97..0decefae 100644 --- a/src/Http/FlexibleAttribute.php +++ b/src/Http/FlexibleAttribute.php @@ -28,6 +28,12 @@ class FlexibleAttribute */ const FILE_INDICATOR = '___upload-'; + /** + * Offset of a generated flexible attribute where the group + * separator (FlexibleAttribute::GROUP_SEPARATOR) starts + */ + const FLEXIBLE_FIELD_OFFSET = 15; + /** * The original attribute name * diff --git a/src/Http/Middleware/InterceptFlexibleAttributes.php b/src/Http/Middleware/InterceptFlexibleAttributes.php index adc65361..629150c6 100644 --- a/src/Http/Middleware/InterceptFlexibleAttributes.php +++ b/src/Http/Middleware/InterceptFlexibleAttributes.php @@ -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); diff --git a/src/Http/ParsesFlexibleAttributes.php b/src/Http/ParsesFlexibleAttributes.php index 459b69ad..e380cdca 100644 --- a/src/Http/ParsesFlexibleAttributes.php +++ b/src/Http/ParsesFlexibleAttributes.php @@ -3,6 +3,7 @@ namespace Whitecube\NovaFlexibleContent\Http; use Illuminate\Http\Request; +use Illuminate\Support\Str; trait ParsesFlexibleAttributes { @@ -13,6 +14,47 @@ 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->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 * @@ -21,6 +63,10 @@ trait ParsesFlexibleAttributes */ 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)); } From a3920fdfde7441d1f09b3fccc744c727a74f4cc5 Mon Sep 17 00:00:00 2001 From: wize-wiz Date: Sun, 14 Jan 2024 19:11:17 +0100 Subject: [PATCH 2/6] Solution where package resolves everything ootb. --- routes/nova-api.php | 6 ++ src/FieldServiceProvider.php | 21 +++++++ .../Controllers/UpdateFieldController.php | 55 +++++++++++++++++++ src/Http/FlexibleAttribute.php | 15 ++++- src/Http/ParsesFlexibleAttributes.php | 19 ++++--- 5 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 routes/nova-api.php create mode 100644 src/Http/Controllers/UpdateFieldController.php diff --git a/routes/nova-api.php b/routes/nova-api.php new file mode 100644 index 00000000..d635c8cb --- /dev/null +++ b/routes/nova-api.php @@ -0,0 +1,6 @@ +novaApiRouteOverwrite(); } /** @@ -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'); + }); + } } diff --git a/src/Http/Controllers/UpdateFieldController.php b/src/Http/Controllers/UpdateFieldController.php new file mode 100644 index 00000000..84e00c6b --- /dev/null +++ b/src/Http/Controllers/UpdateFieldController.php @@ -0,0 +1,55 @@ +query('field'))) { + // return native response + return parent::sync($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->input('field') === $field->attribute && + $request->input('component') === $field->dependentComponentKey(); + })->each->syncDependsOn($request)->first()); + + } + +} \ No newline at end of file diff --git a/src/Http/FlexibleAttribute.php b/src/Http/FlexibleAttribute.php index 0decefae..893defb8 100644 --- a/src/Http/FlexibleAttribute.php +++ b/src/Http/FlexibleAttribute.php @@ -29,8 +29,9 @@ class FlexibleAttribute const FILE_INDICATOR = '___upload-'; /** - * Offset of a generated flexible attribute where the group - * separator (FlexibleAttribute::GROUP_SEPARATOR) starts + * Offset of a generated flexible part where the group + * separator (FlexibleAttribute::GROUP_SEPARATOR) starts at + * given position. */ const FLEXIBLE_FIELD_OFFSET = 15; @@ -362,4 +363,14 @@ 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); + } } diff --git a/src/Http/ParsesFlexibleAttributes.php b/src/Http/ParsesFlexibleAttributes.php index e380cdca..568cf0b2 100644 --- a/src/Http/ParsesFlexibleAttributes.php +++ b/src/Http/ParsesFlexibleAttributes.php @@ -28,28 +28,31 @@ protected function splitFlexPartsFromFieldName(string $attribute) : array { * @return bool */ protected function parseFlexableFieldForPatchRequest($request) : bool { - $field = $request->input('field'); + $field = $request->query('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)) { + if(! FlexibleAttribute::hasFlexibleGeneratedPart($field)) { return false; } - // flexible keys converted to original and to be merged with the request - $flex_fields = []; + + // 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'. - $request->instance()->query->set('field', $parts['field']); foreach($request->all() as $field => $value) { - // check if we a have a flexible generated field name + // 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) + str_replace($parts['key'] . FlexibleAttribute::GROUP_SEPARATOR, '', $field) ] = $value; } } + $request->merge($flex_fields); return true; From 8011b4f2881084092ade292e0b5cebd040a78122 Mon Sep 17 00:00:00 2001 From: wize-wiz Date: Thu, 18 Jan 2024 21:07:10 +0100 Subject: [PATCH 3/6] Add creation-fields fix --- routes/nova-api.php | 5 +- .../DependentFieldSupportController.php | 87 +++++++++++++++++++ .../Controllers/UpdateFieldController.php | 55 ------------ src/Http/ParsesFlexibleAttributes.php | 9 +- 4 files changed, 98 insertions(+), 58 deletions(-) create mode 100644 src/Http/Controllers/DependentFieldSupportController.php delete mode 100644 src/Http/Controllers/UpdateFieldController.php diff --git a/routes/nova-api.php b/routes/nova-api.php index d635c8cb..a50978fa 100644 --- a/routes/nova-api.php +++ b/routes/nova-api.php @@ -1,6 +1,7 @@ query('field'))) { + // return native response + return App::makeWith(UpdateFieldController::class, [$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) + ); + } + + protected function findFlexibleField($request, $resource) { + return $resource->creationFields($request) + ->map(function($field) use ($resource) { + if($field instanceof Flexible) { + $resolved = $field->jsonSerialize()['layouts']->map(function($layout) { + return $layout->fields(); + })->flatten(); + + return $resolved; + } + return $field; + }) + ->flatten() + ->filter(function ($field) use ($request) { + return $request->input('field') === $field->attribute && + $request->input('component') === $field->dependentComponentKey(); + })->each->syncDependsOn($request) + ->first(); + } + +} \ No newline at end of file diff --git a/src/Http/Controllers/UpdateFieldController.php b/src/Http/Controllers/UpdateFieldController.php deleted file mode 100644 index 84e00c6b..00000000 --- a/src/Http/Controllers/UpdateFieldController.php +++ /dev/null @@ -1,55 +0,0 @@ -query('field'))) { - // return native response - return parent::sync($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->input('field') === $field->attribute && - $request->input('component') === $field->dependentComponentKey(); - })->each->syncDependsOn($request)->first()); - - } - -} \ No newline at end of file diff --git a/src/Http/ParsesFlexibleAttributes.php b/src/Http/ParsesFlexibleAttributes.php index 568cf0b2..b60d0278 100644 --- a/src/Http/ParsesFlexibleAttributes.php +++ b/src/Http/ParsesFlexibleAttributes.php @@ -66,7 +66,14 @@ protected function parseFlexableFieldForPatchRequest($request) : bool { */ protected function requestHasParsableFlexibleInputs(Request $request) { - if($request->method() === 'PATCH' && Str::contains($request->getRequestUri(), '/update-fields?')) { + // 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); } From 786050e4af0f83ac00f4fa7e3817d4b4d46ab45c Mon Sep 17 00:00:00 2001 From: wize-wiz Date: Wed, 10 Apr 2024 16:12:38 +0200 Subject: [PATCH 4/6] Call UpdateFieldController's sync method directly (without __invoke). --- .../Controllers/DependentFieldSupportController.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Http/Controllers/DependentFieldSupportController.php b/src/Http/Controllers/DependentFieldSupportController.php index ae5ca0fe..800b0c88 100644 --- a/src/Http/Controllers/DependentFieldSupportController.php +++ b/src/Http/Controllers/DependentFieldSupportController.php @@ -29,8 +29,8 @@ public function updateFieldSync(ResourceUpdateOrUpdateAttachedRequest $request) { // we only act on flexible fields concerning this package if(! FlexibleAttribute::hasFlexibleGeneratedPart($request->query('field'))) { - // return native response - return App::makeWith(UpdateFieldController::class, [$request]); + // return native response calling sync + return App::make(UpdateFieldController::class)->sync($request); } $resource = UpdateViewResource::make()->newResourceWith($request); @@ -64,9 +64,16 @@ public function creationFieldSync(ResourceCreateOrAttachRequest $request) ); } + /** + * Find flexible field by unpacking all flexible layouts. + * @param $request + * @param $resource + * @return mixed + */ protected function findFlexibleField($request, $resource) { return $resource->creationFields($request) ->map(function($field) use ($resource) { + // we need to unpack each flexible layout if($field instanceof Flexible) { $resolved = $field->jsonSerialize()['layouts']->map(function($layout) { return $layout->fields(); From 2eb9151487d3370055b4625c4d5c0d365c67fd4b Mon Sep 17 00:00:00 2001 From: wize-wiz Date: Wed, 10 Apr 2024 16:14:35 +0200 Subject: [PATCH 5/6] Only check the presence for flexible group separator in hasFlexibleGeneratedPart method. --- src/Http/FlexibleAttribute.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Http/FlexibleAttribute.php b/src/Http/FlexibleAttribute.php index 893defb8..992e33ac 100644 --- a/src/Http/FlexibleAttribute.php +++ b/src/Http/FlexibleAttribute.php @@ -3,6 +3,7 @@ namespace Whitecube\NovaFlexibleContent\Http; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Log; class FlexibleAttribute { @@ -370,7 +371,9 @@ protected function setName() * @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); + strpos($field, self::GROUP_SEPARATOR, self::FLEXIBLE_FIELD_OFFSET) !== false); **/ + return strpos($field, self::GROUP_SEPARATOR) !== false; } } From abcf902a48f30b720c0780521f0f4120530ef01f Mon Sep 17 00:00:00 2001 From: wize-wiz Date: Tue, 23 Apr 2024 00:28:35 +0200 Subject: [PATCH 6/6] Support nested flexible fields recursively unfolding all fields --- .../DependentFieldSupportController.php | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Http/Controllers/DependentFieldSupportController.php b/src/Http/Controllers/DependentFieldSupportController.php index 800b0c88..59996774 100644 --- a/src/Http/Controllers/DependentFieldSupportController.php +++ b/src/Http/Controllers/DependentFieldSupportController.php @@ -72,17 +72,7 @@ public function creationFieldSync(ResourceCreateOrAttachRequest $request) */ protected function findFlexibleField($request, $resource) { return $resource->creationFields($request) - ->map(function($field) use ($resource) { - // we need to unpack each flexible layout - if($field instanceof Flexible) { - $resolved = $field->jsonSerialize()['layouts']->map(function($layout) { - return $layout->fields(); - })->flatten(); - - return $resolved; - } - return $field; - }) + ->map($this->recursiveUnfoldFlexible()) ->flatten() ->filter(function ($field) use ($request) { return $request->input('field') === $field->attribute && @@ -91,4 +81,21 @@ protected function findFlexibleField($request, $resource) { ->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; + }; + } + } \ No newline at end of file