411 lines
11 KiB
PHP
411 lines
11 KiB
PHP
|
<?php
|
||
|
|
||
|
declare(strict_types=1);
|
||
|
|
||
|
namespace Grav\Plugin\FlexObjects\Table;
|
||
|
|
||
|
use Grav\Common\Debugger;
|
||
|
use Grav\Common\Grav;
|
||
|
use Grav\Framework\Collection\CollectionInterface;
|
||
|
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
|
||
|
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
|
||
|
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||
|
use JsonSerializable;
|
||
|
use Throwable;
|
||
|
use Twig\Environment;
|
||
|
use Twig\Error\LoaderError;
|
||
|
use Twig\Error\RuntimeError;
|
||
|
use Twig\Error\SyntaxError;
|
||
|
use function is_array;
|
||
|
use function is_string;
|
||
|
|
||
|
/**
|
||
|
* Class DataTable
|
||
|
* @package Grav\Plugin\Gitea
|
||
|
*
|
||
|
* https://github.com/ratiw/vuetable-2/wiki/Data-Format-(JSON)
|
||
|
* https://github.com/ratiw/vuetable-2/wiki/Sorting
|
||
|
*/
|
||
|
class DataTable implements JsonSerializable
|
||
|
{
|
||
|
/** @var string */
|
||
|
private $url;
|
||
|
/** @var int */
|
||
|
private $limit;
|
||
|
/** @var int */
|
||
|
private $page;
|
||
|
/** @var array */
|
||
|
private $sort;
|
||
|
/** @var string */
|
||
|
private $search;
|
||
|
/** @var FlexCollectionInterface */
|
||
|
private $collection;
|
||
|
/** @var FlexCollectionInterface */
|
||
|
private $filteredCollection;
|
||
|
/** @var array */
|
||
|
private $columns;
|
||
|
/** @var Environment */
|
||
|
private $twig;
|
||
|
/** @var array */
|
||
|
private $twig_context;
|
||
|
|
||
|
/**
|
||
|
* DataTable constructor.
|
||
|
* @param array $params
|
||
|
*/
|
||
|
public function __construct(array $params)
|
||
|
{
|
||
|
$this->setUrl($params['url'] ?? '');
|
||
|
$this->setLimit((int)($params['limit'] ?? 10));
|
||
|
$this->setPage((int)($params['page'] ?? 1));
|
||
|
$this->setSort($params['sort'] ?? ['id' => 'asc']);
|
||
|
$this->setSearch($params['search'] ?? '');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $url
|
||
|
* @return void
|
||
|
*/
|
||
|
public function setUrl(string $url): void
|
||
|
{
|
||
|
$this->url = $url;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param int $limit
|
||
|
* @return void
|
||
|
*/
|
||
|
public function setLimit(int $limit): void
|
||
|
{
|
||
|
$this->limit = max(1, $limit);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param int $page
|
||
|
* @return void
|
||
|
*/
|
||
|
public function setPage(int $page): void
|
||
|
{
|
||
|
$this->page = max(1, $page);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string|string[] $sort
|
||
|
* @return void
|
||
|
*/
|
||
|
public function setSort($sort): void
|
||
|
{
|
||
|
if (is_string($sort)) {
|
||
|
$sort = $this->decodeSort($sort);
|
||
|
} elseif (!is_array($sort)) {
|
||
|
$sort = [];
|
||
|
}
|
||
|
|
||
|
$this->sort = $sort;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $search
|
||
|
* @return void
|
||
|
*/
|
||
|
public function setSearch(string $search): void
|
||
|
{
|
||
|
$this->search = $search;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param CollectionInterface $collection
|
||
|
* @return void
|
||
|
*/
|
||
|
public function setCollection(CollectionInterface $collection): void
|
||
|
{
|
||
|
$this->collection = $collection;
|
||
|
$this->filteredCollection = null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return int
|
||
|
*/
|
||
|
public function getLimit(): int
|
||
|
{
|
||
|
return $this->limit;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return int
|
||
|
*/
|
||
|
public function getPage(): int
|
||
|
{
|
||
|
return $this->page;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return int
|
||
|
*/
|
||
|
public function getLastPage(): int
|
||
|
{
|
||
|
return 1 + (int)floor(max(0, $this->getTotal()-1) / $this->getLimit());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return int
|
||
|
*/
|
||
|
public function getTotal(): int
|
||
|
{
|
||
|
$collection = $this->filteredCollection ?? $this->getCollection();
|
||
|
|
||
|
return $collection ? $collection->count() : 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return array
|
||
|
*/
|
||
|
public function getSort(): array
|
||
|
{
|
||
|
return $this->sort;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return FlexCollectionInterface|null
|
||
|
*/
|
||
|
public function getCollection(): ?FlexCollectionInterface
|
||
|
{
|
||
|
return $this->collection;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param int $page
|
||
|
* @return string|null
|
||
|
*/
|
||
|
public function getUrl(int $page): ?string
|
||
|
{
|
||
|
if ($page < 1 || $page > $this->getLastPage()) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return "{$this->url}.json?page={$page}&per_page={$this->getLimit()}&sort={$this->encodeSort()}";
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return array
|
||
|
*/
|
||
|
public function getColumns(): array
|
||
|
{
|
||
|
if (null === $this->columns) {
|
||
|
$collection = $this->getCollection();
|
||
|
if (!$collection) {
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
$blueprint = $collection->getFlexDirectory()->getBlueprint();
|
||
|
$schema = $blueprint->schema();
|
||
|
$columns = $blueprint->get('config/admin/views/list/fields') ?? $blueprint->get('config/admin/list/fields', []);
|
||
|
|
||
|
$list = [];
|
||
|
foreach ($columns as $key => $options) {
|
||
|
if (!isset($options['field'])) {
|
||
|
$options['field'] = $schema->get($options['alias'] ?? $key);
|
||
|
}
|
||
|
if (!$options['field'] || !empty($options['field']['ignore'])) {
|
||
|
continue;
|
||
|
}
|
||
|
$list[$key] = $options;
|
||
|
}
|
||
|
|
||
|
$this->columns = $list;
|
||
|
}
|
||
|
|
||
|
return $this->columns;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return array
|
||
|
*/
|
||
|
public function getData(): array
|
||
|
{
|
||
|
$grav = Grav::instance();
|
||
|
|
||
|
/** @var Debugger $debugger */
|
||
|
$debugger = $grav['debugger'];
|
||
|
$debugger->startTimer('datatable', 'Data Table');
|
||
|
|
||
|
$collection = $this->getCollection();
|
||
|
if (!$collection) {
|
||
|
return [];
|
||
|
}
|
||
|
if ($this->search !== '') {
|
||
|
$collection = $collection->search($this->search);
|
||
|
}
|
||
|
|
||
|
$columns = $this->getColumns();
|
||
|
|
||
|
$collection = $collection->sort($this->getSort());
|
||
|
|
||
|
$this->filteredCollection = $collection;
|
||
|
|
||
|
$limit = $this->getLimit();
|
||
|
$page = $this->getPage();
|
||
|
$to = $page * $limit;
|
||
|
$from = $to - $limit + 1;
|
||
|
|
||
|
if ($from < 1 || $from > $this->getTotal()) {
|
||
|
$debugger->stopTimer('datatable');
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
$array = $collection->slice($from-1, $limit);
|
||
|
|
||
|
$twig = $grav['twig'];
|
||
|
$grav->fireEvent('onTwigSiteVariables');
|
||
|
|
||
|
$this->twig = $twig->twig;
|
||
|
$this->twig_context = $twig->twig_vars;
|
||
|
|
||
|
$list = [];
|
||
|
/** @var FlexObjectInterface $object */
|
||
|
foreach ($array as $object) {
|
||
|
$item = [
|
||
|
'id' => $object->getKey(),
|
||
|
'timestamp' => $object->getTimestamp()
|
||
|
];
|
||
|
foreach ($columns as $name => $column) {
|
||
|
$item[str_replace('.', '_', $name)] = $this->renderColumn($name, $column, $object);
|
||
|
}
|
||
|
$item['_actions_'] = $this->renderActions($object);
|
||
|
|
||
|
$list[] = $item;
|
||
|
}
|
||
|
|
||
|
$debugger->stopTimer('datatable');
|
||
|
|
||
|
return $list;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return array
|
||
|
*/
|
||
|
public function jsonSerialize(): array
|
||
|
{
|
||
|
$data = $this->getData();
|
||
|
$total = $this->getTotal();
|
||
|
$limit = $this->getLimit();
|
||
|
$page = $this->getPage();
|
||
|
$to = $page * $limit;
|
||
|
$from = $to - $limit + 1;
|
||
|
|
||
|
$empty = empty($data);
|
||
|
|
||
|
return [
|
||
|
'links' => [
|
||
|
'pagination' => [
|
||
|
'total' => $total,
|
||
|
'per_page' => $limit,
|
||
|
'current_page' => $page,
|
||
|
'last_page' => $this->getLastPage(),
|
||
|
'next_page_url' => $this->getUrl($page+1),
|
||
|
'prev_page_url' => $this->getUrl($page-1),
|
||
|
'from' => $empty ? null : $from,
|
||
|
'to' => $empty ? null : min($to, $total),
|
||
|
]
|
||
|
],
|
||
|
'data' => $data
|
||
|
];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $name
|
||
|
* @param array $column
|
||
|
* @param FlexObjectInterface $object
|
||
|
* @return false|string
|
||
|
* @throws Throwable
|
||
|
* @throws LoaderError
|
||
|
* @throws RuntimeError
|
||
|
* @throws SyntaxError
|
||
|
*/
|
||
|
protected function renderColumn(string $name, array $column, FlexObjectInterface $object)
|
||
|
{
|
||
|
$grav = Grav::instance();
|
||
|
$flex = $grav['flex_objects'];
|
||
|
|
||
|
$value = $object->getFormValue($name) ?? $object->getNestedProperty($name, $column['field']['default'] ?? null);
|
||
|
$type = $column['field']['type'] ?? 'text';
|
||
|
$hasLink = $column['link'] ?? null;
|
||
|
$link = null;
|
||
|
|
||
|
$authorized = $object instanceof FlexAuthorizeInterface
|
||
|
? ($object->isAuthorized('read') || $object->isAuthorized('update')) : true;
|
||
|
|
||
|
if ($hasLink && $authorized) {
|
||
|
$route = $grav['route']->withExtension('');
|
||
|
$link = $route->withAddedPath($object->getKey())->withoutParams()->getUri();
|
||
|
}
|
||
|
|
||
|
$template = $this->twig->resolveTemplate(["forms/fields/{$type}/edit_list.html.twig", 'forms/fields/text/edit_list.html.twig']);
|
||
|
|
||
|
return $this->twig->load($template)->render([
|
||
|
'value' => $value,
|
||
|
'link' => $link,
|
||
|
'field' => $column['field'],
|
||
|
'object' => $object,
|
||
|
'flex' => $flex,
|
||
|
'route' => $grav['route']->withExtension('')
|
||
|
] + $this->twig_context);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param FlexObjectInterface $object
|
||
|
* @return false|string
|
||
|
* @throws Throwable
|
||
|
* @throws LoaderError
|
||
|
* @throws RuntimeError
|
||
|
* @throws SyntaxError
|
||
|
*/
|
||
|
protected function renderActions(FlexObjectInterface $object)
|
||
|
{
|
||
|
$grav = Grav::instance();
|
||
|
$type = $object->getFlexType();
|
||
|
$template = $this->twig->resolveTemplate(["flex-objects/types/{$type}/list/list_actions.html.twig", 'flex-objects/types/default/list/list_actions.html.twig']);
|
||
|
|
||
|
return $this->twig->load($template)->render([
|
||
|
'object' => $object,
|
||
|
'flex' => $grav['flex_objects'],
|
||
|
'route' => $grav['route']->withExtension('')
|
||
|
] + $this->twig_context);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $sort
|
||
|
* @param string $fieldSeparator
|
||
|
* @param string $orderSeparator
|
||
|
* @return array
|
||
|
*/
|
||
|
protected function decodeSort(string $sort, string $fieldSeparator = ',', string $orderSeparator = '|'): array
|
||
|
{
|
||
|
$strings = explode($fieldSeparator, $sort);
|
||
|
$list = [];
|
||
|
foreach ($strings as $string) {
|
||
|
$item = explode($orderSeparator, $string, 2);
|
||
|
$key = array_shift($item);
|
||
|
$order = array_shift($item) === 'desc' ? 'desc' : 'asc';
|
||
|
$list[$key] = $order;
|
||
|
}
|
||
|
|
||
|
return $list;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $fieldSeparator
|
||
|
* @param string $orderSeparator
|
||
|
* @return string
|
||
|
*/
|
||
|
protected function encodeSort(string $fieldSeparator = ',', string $orderSeparator = '|'): string
|
||
|
{
|
||
|
$list = [];
|
||
|
foreach ($this->getSort() as $key => $order) {
|
||
|
$list[] = $key . $orderSeparator . ($order ?: 'asc');
|
||
|
}
|
||
|
|
||
|
return implode($fieldSeparator, $list);
|
||
|
}
|
||
|
}
|