Compare commits

...

4 commits

Author SHA1 Message Date
0f7f872522 License.txt => LICENSE
All checks were successful
ci/woodpecker/manual/code-style Pipeline was successful
ci/woodpecker/manual/functional-tests/1 Pipeline was successful
ci/woodpecker/manual/functional-tests/2 Pipeline was successful
ci/woodpecker/manual/functional-tests/3 Pipeline was successful
ci/woodpecker/manual/functional-tests/4 Pipeline was successful
2024-06-04 23:41:23 +02:00
a68732c45f updating ci pipelines with own images
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests/1 Pipeline was successful
ci/woodpecker/push/functional-tests/3 Pipeline was successful
ci/woodpecker/push/functional-tests/2 Pipeline was successful
ci/woodpecker/push/functional-tests/4 Pipeline was successful
2024-06-04 21:54:21 +02:00
baf5636bd3 adjusting to new storage location of render functions
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests/1 Pipeline was successful
ci/woodpecker/push/functional-tests/2 Pipeline was successful
ci/woodpecker/push/functional-tests/3 Pipeline was successful
ci/woodpecker/push/functional-tests/4 Pipeline was successful
2024-06-02 04:26:53 +02:00
7585baee36 first working version in Flow 6.3
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests/2 Pipeline was successful
ci/woodpecker/push/functional-tests/1 Pipeline was successful
ci/woodpecker/push/functional-tests/3 Pipeline was successful
ci/woodpecker/push/functional-tests/4 Pipeline was successful
2024-06-02 02:33:19 +02:00
12 changed files with 460 additions and 0 deletions

View file

@ -0,0 +1,7 @@
steps:
code-style:
image: git.digital-competence.de/woodpecker-ci/plugin-phpcs
settings:
args: Classes/ Tests/
when:
- event: [push, pull_request, manual]

View file

@ -0,0 +1,30 @@
workspace:
base: /woodpecker
path: package
matrix:
include:
- FLOW_VERSION: 6.3
PHP_VERSION: 7.4
- FLOW_VERSION: 7.3
PHP_VERSION: 7.4
- FLOW_VERSION: 7.3
PHP_VERSION: 8.2
- FLOW_VERSION: 8.2
PHP_VERSION: 8.2
steps:
functional-tests:
image: git.digital-competence.de/woodpecker-ci/plugin-phpunit-flow:${PHP_VERSION}
settings:
flow_version: ${FLOW_VERSION}
ssh_key:
from_secret: deploykey
hostkeys:
- digital-competence.de ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNSjVKJ+SO6wqmDSCgcJDk2ljWlD7qajsTxAuvZpTbJBg2++Zu0VxH0S1WzPVTD/D5UUbK6LVy6YSCnGlv6zmc0=
repositories:
- vcs ssh://git@digital-competence.de/Packages/DigiComp.FluidRenderFunctions
require:
- digicomp/fluid-render-functions:@dev
when:
- event: [ push, pull_request, manual ]

View file

@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace DigiComp\FluidJsonViews\ViewHelpers\Controller;
use DigiComp\FluidRenderFunctions\InvokeRenderFunctionInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Proxy\Proxy;
use Doctrine\ORM\Query\Parameter;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Persistence\Doctrine\Query;
use Neos\FluidAdaptor\Core\Widget\AbstractWidgetController;
use Neos\FluidAdaptor\View\TemplateView;
use Neos\Utility\TypeHandling;
class FluidJsonController extends AbstractWidgetController
{
/**
* @Flow\Inject
* @var EntityManagerInterface
*/
protected $entityManager;
/**
* @inheritDoc
*/
protected $supportedMediaTypes = [
'text/html',
'application/json',
];
/**
* @var Query|null
*/
protected ?Query $query = null;
protected InvokeRenderFunctionInterface $renderFunction;
/**
* @Flow\InjectConfiguration(package="DigiComp.FluidJsonViews", path="limitConfiguration")
* @var array
*/
protected array $limitConfiguration;
/**
* @var array
*/
protected array $searchProperties;
protected function initializeAction(): void
{
parent::initializeAction();
$this->query = clone $this->widgetConfiguration['objects']->getQuery();
$this->renderFunction = $this->widgetConfiguration['renderFunction'];
$this->searchProperties = $this->widgetConfiguration['searchProperties'];
$this->initializeDoctrineSource();
}
/**
* Initializes count, objectType and doctrine's MetaDataFactory
*/
protected function initializeDoctrineSource(): void
{
// As we are working with objects which may be doctrine proxies persisted in session, its metadata have
// never been loaded in this request. So let's see these parameters and load their metadata, so the doctrine
// framework knows how to go on.
if ($this->query instanceof Query) {
$parameters = $this->query->getQueryBuilder()->getParameters();
foreach ($parameters as $parameter) {
if ($parameter instanceof Parameter && $parameter->getValue() instanceof Proxy) {
$this->entityManager->getMetadataFactory()->getMetadataFor(
TypeHandling::getTypeForValue($parameter->getValue())
);
}
}
}
}
public function indexAction()
{
$this->view->assign('entityClassName', $this->query->getType());
if ($this->view instanceof TemplateView) {
$renderingContext = $this->request
->getInternalArgument('__widgetContext')
->getViewHelperChildNodeRenderingContext();
$this->view->assignMultiple($renderingContext->getVariableProvider()->getAll());
}
}
/**
* @see https://select2.org/data-sources/ajax#request-parameters
*/
public function dataAction(
?int $limit = null,
?string $term = null,
?int $page = 0
) {
$query = $this->query;
$result['recordsTotal'] = $query->execute()->count();
if ($term !== null) {
$searchConstraint = $this->getSearchConstraints($term);
$query->matching($query->logicalAnd($searchConstraint));
}
$result['recordsFiltered'] = $query->execute()->count();
if ($limit === null) {
$limit = $this->limitConfiguration['default'];
}
if ($this->limitConfiguration['max'] !== null) {
if ($limit > $this->limitConfiguration['max'] || $limit === null) {
$limit = $this->limitConfiguration['max'];
}
}
if ($limit !== null) {
$data = $query->setLimit($limit)->setOffset($page * $limit)->execute();
} else {
$data = $query->execute();
}
$renderFunc = $this->renderFunction;
foreach ($data as $object) {
$result['results'][] = [
'id' => $this->persistenceManager->getIdentifierByObject($object),
'text' => $renderFunc($object),
];
}
$this->response->setContentType('application/json');
return \json_encode($result, \JSON_THROW_ON_ERROR);
}
protected function getSearchConstraints(
string $term
): object {
$searchConstraints = [];
foreach ($this->searchProperties as $searchProperty) {
$searchConstraints[] = $this->query->like($searchProperty, '%' . $term . '%');
}
return $this->query->logicalOr($searchConstraints);
}
}

View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace DigiComp\FluidJsonViews\ViewHelpers;
use DigiComp\FluidJsonViews\ViewHelpers\Controller\FluidJsonController;
use DigiComp\FluidRenderFunctions\ViewHelpers\Traits\ValidateRenderFunctionTrait;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Exception\InfiniteLoopException;
use Neos\Flow\Mvc\Exception\StopActionException;
use Neos\Flow\Persistence\Doctrine\QueryResult;
use Neos\FluidAdaptor\Core\Widget\AbstractWidgetViewHelper;
use Neos\FluidAdaptor\Core\Widget\Exception\InvalidControllerException;
use Neos\FluidAdaptor\Core\Widget\Exception\MissingControllerException;
class FluidJsonViewHelper extends AbstractWidgetViewHelper
{
use ValidateRenderFunctionTrait;
/**
* @inheritDoc
* @Flow\Inject
* @var FluidJsonController
*/
protected $controller;
/**
* @inheritDoc
*/
protected $ajaxWidget = true;
/**
* @inheritDoc
*/
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('objects', QueryResult::class, 'Objects to show in table.', true);
$this->registerArgument(
'renderFunction',
'string',
'callabe to use to render single object',
true
);
$this->registerArgument(
'searchProperties',
'array',
'an array of pathes, which should be used during search evaluation',
false,
[]
);
}
public function validateArguments()
{
parent::validateArguments();
$this->validateRenderFunctionArgument('renderFunction');
}
/**
* @throws InfiniteLoopException
* @throws InvalidControllerException
* @throws MissingControllerException
* @throws StopActionException
*/
public function render(): string
{
return $this->initiateSubRequest();
}
}

View file

@ -0,0 +1,5 @@
DigiComp:
FluidJsonViews:
limitConfiguration:
default: 1000
max: 1000

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2024 Ferdinand Kuhl <f.kuhl@digital-competence.de>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

28
README.md Normal file
View file

@ -0,0 +1,28 @@
# DigiComp.FluidJsonViews
This package builds upon `DigiComp.FluidRenderFunctions` and uses this, to use a such defined render function to create a simple key/value json view from your QueryResult. Where the key will be the persistence identifier and the value the result of your rendered template.
Let me provide you an example:
```html
<rf:registerRenderFunction as="renderTag">
{subject.name}
</rf:registerRenderFunction>
<fj:fluidJson objects="{tags}" renderFunction="renderTag" searchProperties="{0: 'name'}">
<a href="{dataUri}">jsonView</a>
</fj:fluidJson>
```
If you fetch the jsonView you will see something like this:
```json
{
"recordsTotal": 2,
"recordsFiltered": 2,
"results": [
{"id": "a310057f-869e-419e-b6fe-6c3a00fe444a", "text": "hallo 2"},
{"id": "a310057f-869e-419e-b6fe-6c3a00fe444b", "text": "hallo"}
]
}
```
The provided link will understand three query parameters: `limit`, `term` and `page` - all optional.
If you send a "term" parameter, it will apply an or sql search over all your given search property paths. `limit` and `page` will allow you to paginate the results.
For security and performance reasons, you can define a max limit using the provided values in Settings.yaml. There you can change the default limit (used, if not sent), or disable both (not recommended).

View file

@ -0,0 +1,8 @@
{namespace rf=DigiComp\FluidRenderFunctions\ViewHelpers}
{namespace fj=DigiComp\FluidJsonViews\ViewHelpers}
<rf:registerRenderFunction as="renderTag">
{subject.name}
</rf:registerRenderFunction>
<fj:fluidJson objects="{tags}" renderFunction="renderTag" searchProperties="{0: 'name'}">
<a href="{dataUri}">jsonView</a>
</fj:fluidJson>

View file

@ -0,0 +1,4 @@
<f:renderChildren arguments="{
dataUri: '{f:widget.uri(action: \'data\', format: \'json\', ajax: true)}',
entityClassName: entityClassName
}" />

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace DigiComp\FluidJsonViews\Tests\Functional\Fixtures\Controller;
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\Flow\Persistence\Doctrine\Query;
use Neos\FluidAdaptor\Tests\Functional\Form\Fixtures\Domain\Model\Tag;
class TestController extends ActionController
{
public function indexAction()
{
$this->view->assign('tags', (new Query(Tag::class))->execute());
}
}

View file

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace DigiComp\FluidJsonViews\Tests\Functional;
use Neos\Flow\Mvc\Routing\Route;
use Neos\Flow\Tests\FunctionalTestCase;
use Neos\FluidAdaptor\Tests\Functional\Form\Fixtures\Domain\Model\Tag;
class FluidJsonTest extends FunctionalTestCase
{
/**
* @inheritDoc
*/
protected static $testablePersistenceEnabled = true;
/**
* Initializer
*/
protected function setUp(): void
{
parent::setUp();
$route = new Route();
$route->setUriPattern('test/fluidjsonviews/test(/{@action})');
$route->setDefaults([
'@package' => 'DigiComp.FluidJsonViews',
'@subpackage' => 'Tests\Functional\Fixtures',
'@controller' => 'Test',
'@action' => 'index',
]);
$route->setAppendExceedingArguments(true);
$this->router->addRoute($route);
}
/**
* @test
*/
public function itAllowsFluidToRenderWrappedArray(): void
{
$post1 = new Tag('hallo');
$this->persistenceManager->add($post1);
$post2 = new Tag('hallo 2');
$this->persistenceManager->add($post2);
$this->persistenceManager->persistAll();
$this->browser->request('http://localhost/test/fluidjsonviews/test');
$link = $this->browser->getCrawler()->selectLink('jsonView')->link()->getUri();
$response = $this->browser->request($link);
static::assertEquals('application/json', $response->getHeaderLine('Content-Type'));
$result = \json_decode((string)$response->getBody(), true);
static::assertEquals(2, $result['recordsTotal']);
static::assertCount(2, $result['results']);
}
/**
* @test
*/
public function itFiltersIfTermProvided(): void
{
$post1 = new Tag('hallo');
$this->persistenceManager->add($post1);
$post2 = new Tag('hallo 2');
$this->persistenceManager->add($post2);
$this->persistenceManager->persistAll();
$this->browser->request('http://localhost/test/fluidjsonviews/test');
$link = $this->browser->getCrawler()->selectLink('jsonView')->link()->getUri();
$response = $this->browser->request($link . '&term=2');
$result = \json_decode((string)$response->getBody(), true);
static::assertEquals('application/json', $response->getHeaderLine('Content-Type'));
static::assertEquals(2, $result['recordsTotal']);
static::assertEquals(1, $result['recordsFiltered']);
static::assertCount(1, $result['results']);
}
}

49
composer.json Normal file
View file

@ -0,0 +1,49 @@
{
"name": "digicomp/fluid-json-views",
"description": "create simple json views, with remote search from Fluid",
"type": "neos-package",
"require": {
"digicomp/fluid-render-functions": "^1.0.0",
"ext-json": "*",
"neos/flow": "^6.3.5 | ^7.3 | ^8.3",
"neos/fluid-adaptor": "^6.3.5 | ^7.3 | ^8.3",
"php": ">=7.4"
},
"require-dev": {
"phpunit/phpunit": "~8.5"
},
"autoload": {
"psr-4": {
"DigiComp\\FluidJsonViews\\": "Classes/"
}
},
"autoload-dev": {
"psr-4": {
"DigiComp\\FluidJsonViews\\Tests\\": "Tests/"
}
},
"extra": {
"neos": {
"package-key": "DigiComp.FluidJsonViews"
},
"branch-alias": {
"dev-develop": "1.0.x-dev"
}
},
"authors": [
{
"name": "Ferdinand Kuhl",
"email": "f.kuhl@digital-competence.de",
"homepage": "https://www.digital-competence.de",
"role": "Developer"
}
],
"license": "MIT",
"homepage": "https://git.digital-competence.de/Packages/DigiComp.FluidJsonViews",
"keywords": [
"Neos",
"Flow",
"fluid",
"ajax"
]
}