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); | ||
|  |     } | ||
|  | } |