• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

contributte / datagrid / 15346293061

30 May 2025 12:00PM UTC coverage: 35.433% (-0.06%) from 35.492%
15346293061

push

github

f3l1x
QA: fix phpstan

4 of 7 new or added lines in 2 files covered. (57.14%)

127 existing lines in 9 files now uncovered.

1198 of 3381 relevant lines covered (35.43%)

0.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

35.75
/src/Datagrid.php
1
<?php declare(strict_types = 1);
2

3
namespace Contributte\Datagrid;
4

5
use Contributte\Datagrid\AggregationFunction\TDatagridAggregationFunction;
6
use Contributte\Datagrid\Column\Action;
7
use Contributte\Datagrid\Column\ActionCallback;
8
use Contributte\Datagrid\Column\Column;
9
use Contributte\Datagrid\Column\ColumnDateTime;
10
use Contributte\Datagrid\Column\ColumnLink;
11
use Contributte\Datagrid\Column\ColumnNumber;
12
use Contributte\Datagrid\Column\ColumnStatus;
13
use Contributte\Datagrid\Column\ColumnText;
14
use Contributte\Datagrid\Column\ItemDetail;
15
use Contributte\Datagrid\Column\MultiAction;
16
use Contributte\Datagrid\Components\DatagridPaginator\DatagridPaginator;
17
use Contributte\Datagrid\DataSource\IDataSource;
18
use Contributte\Datagrid\Exception\DatagridColumnNotFoundException;
19
use Contributte\Datagrid\Exception\DatagridException;
20
use Contributte\Datagrid\Exception\DatagridFilterNotFoundException;
21
use Contributte\Datagrid\Exception\DatagridHasToBeAttachedToPresenterComponentException;
22
use Contributte\Datagrid\Export\Export;
23
use Contributte\Datagrid\Export\ExportCsv;
24
use Contributte\Datagrid\Filter\Filter;
25
use Contributte\Datagrid\Filter\FilterDate;
26
use Contributte\Datagrid\Filter\FilterDateRange;
27
use Contributte\Datagrid\Filter\FilterMultiSelect;
28
use Contributte\Datagrid\Filter\FilterRange;
29
use Contributte\Datagrid\Filter\FilterSelect;
30
use Contributte\Datagrid\Filter\FilterText;
31
use Contributte\Datagrid\Filter\IFilterDate;
32
use Contributte\Datagrid\Filter\SubmitButton;
33
use Contributte\Datagrid\GroupAction\GroupAction;
34
use Contributte\Datagrid\GroupAction\GroupActionCollection;
35
use Contributte\Datagrid\GroupAction\GroupButtonAction;
36
use Contributte\Datagrid\InlineEdit\InlineAdd;
37
use Contributte\Datagrid\InlineEdit\InlineEdit;
38
use Contributte\Datagrid\Localization\SimpleTranslator;
39
use Contributte\Datagrid\Toolbar\ToolbarButton;
40
use Contributte\Datagrid\Utils\ArraysHelper;
41
use Contributte\Datagrid\Utils\ItemDetailForm;
42
use Contributte\Datagrid\Utils\Sorting;
43
use DateTime;
44
use InvalidArgumentException;
45
use Nette\Application\ForbiddenRequestException;
46
use Nette\Application\IPresenter;
47
use Nette\Application\Request;
48
use Nette\Application\UI\Component;
49
use Nette\Application\UI\Control;
50
use Nette\Application\UI\Form;
51
use Nette\Application\UI\Link;
52
use Nette\Application\UI\Presenter;
53
use Nette\Bridges\ApplicationLatte\Template;
54
use Nette\ComponentModel\IContainer;
55
use Nette\Forms\Container;
56
use Nette\Forms\Control as FormControl;
57
use Nette\Forms\Controls\SubmitButton as FormsSubmitButton;
58
use Nette\Forms\Form as NetteForm;
59
use Nette\Http\SessionSection;
60
use Nette\Localization\Translator;
61
use Nette\Utils\ArrayHash;
62
use UnexpectedValueException;
63

64
/**
65
 * @method onRedraw()
66
 * @method onRender(Datagrid $dataGrid)
67
 * @method onColumnAdd(string $key, Column $column)
68
 * @method onColumnShow(string $key)
69
 * @method onColumnHide(string $key)
70
 * @method onShowDefaultColumns()
71
 * @method onShowAllColumns()
72
 * @method onExport(Datagrid $dataGrid)
73
 * @method onFiltersAssembled(Filter[] $filters)
74
 */
75
class Datagrid extends Control
76
{
77

78
        use TDatagridAggregationFunction;
79

80
        private const HIDEABLE_COLUMNS_SESSION_KEYS = [
81
                '_grid_hidden_columns',
82
                '_grid_hidden_columns_manipulated',
83
        ];
84

85
        public static string $iconPrefix = 'fa fa-';
86

87
        public static string $btnSecondaryClass = 'btn-default btn-secondary';
88

89
        /**
90
         * Default form method
91
         */
92
        public static string $formMethod = 'post';
93

94
        /** @var array|callable[] */
95
        public array $onRedraw = [];
96

97
        /** @var array|callable[] */
98
        public array $onRender = [];
99

100
        /** @var array|callable[] */
101
        public array $onExport = [];
102

103
        /** @var array|callable[] */
104
        public array $onColumnAdd = [];
105

106
        /** @var array|callable[] */
107
        public array $onColumnShow = [];
108

109
        /** @var array|callable[] */
110
        public array $onColumnHide = [];
111

112
        /** @var array|callable[] */
113
        public array $onShowDefaultColumns = [];
114

115
        /** @var array|callable[] */
116
        public array $onShowAllColumns = [];
117

118
        /** @var array|callable[] */
119
        public array $onFiltersAssembled = [];
120

121
        /**
122
         * When set to TRUE, datagrid throws an exception
123
         *  when tring to get related entity within join and entity does not exist
124
         */
125
        public bool $strictEntityProperty = false;
126

127
        /**
128
         * When set to TRUE, datagrid throws an exception
129
         *  when tring to set filter value, that does not exist (select, multiselect, etc)
130
         */
131
        public bool $strictSessionFilterValues = true;
132

133
        /** @persistent */
134
        public int $page = 1;
135

136
        /** @persistent */
137
        public string|int|null $perPage = null;
138

139
        /** @persistent */
140
        public array $sort = [];
141

142
        public array $defaultSort = [];
143

144
        public array $defaultFilter = [];
145

146
        public bool $defaultFilterUseOnReset = true;
147

148
        public bool $defaultSortUseOnReset = true;
149

150
        /** @persistent */
151
        public array $filter = [];
152

153
        /** @var callable|null */
154
        protected $sortCallback = null;
155

156
        protected bool $useHappyComponents = true;
157

158
        /** @var callable */
159
        protected $rowCallback;
160

161
        protected array $itemsPerPageList = [10, 20, 50, 'all'];
162

163
        protected ?int $defaultPerPage = null;
164

165
        protected ?string $templateFile = null;
166

167
        /** @var array<Column> */
168
        protected array $columns = [];
169

170
        /** @var array<Action>|array<MultiAction> */
171
        protected array $actions = [];
172

173
        protected ?GroupActionCollection $groupActionCollection = null;
174

175
        /** @var array<Filter> */
176
        protected array $filters = [];
177

178
        /** @var array<Export> */
179
        protected array $exports = [];
180

181
        /** @var array<ToolbarButton> */
182
        protected array $toolbarButtons = [];
183

184
        protected ?DataModel $dataModel = null;
185

186
        protected string $primaryKey = 'id';
187

188
        protected bool $doPaginate = true;
189

190
        protected bool $csvExport = true;
191

192
        protected bool $csvExportFiltered = true;
193

194
        protected bool $sortable = false;
195

196
        protected bool $multiSort = false;
197

198
        protected string $sortableHandler = 'sort!';
199

200
        protected ?string $originalTemplate = null;
201

202
        protected array $redrawItem = [];
203

204
        protected ?Translator $translator = null;
205

206
        protected bool $forceFilterActive = false;
207

208
        /** @var callable|null */
209
        protected $treeViewChildrenCallback = null;
210

211
        /** @var callable|null */
212
        protected $treeViewHasChildrenCallback = null;
213

214
        protected ?string $treeViewHasChildrenColumn = null;
215

216
        protected bool $outerFilterRendering = false;
217

218
        protected int $outerFilterColumnsCount = 2;
219

220
        protected bool $collapsibleOuterFilters = true;
221

222
        /** @var array|string[] */
223
        protected array $columnsExportOrder = [];
224

225
        protected bool $rememberState = true;
226

227
        protected bool $rememberHideableColumnsState = true;
228

229
        protected bool $refreshURL = true;
230

231
        protected SessionSection $gridSession;
232

233
        protected ?ItemDetail $itemsDetail = null;
234

235
        protected array $rowConditions = [
236
                'group_action' => false,
237
                'action' => [],
238
        ];
239

240
        protected array $columnCallbacks = [];
241

242
        protected bool $canHideColumns = false;
243

244
        protected array $columnsVisibility = [];
245

246
        protected ?InlineEdit $inlineEdit = null;
247

248
        protected ?InlineAdd $inlineAdd = null;
249

250
        protected bool $snippetsSet = false;
251

252
        protected bool $someColumnDefaultHide = false;
253

254
        protected ?ColumnsSummary $columnsSummary = null;
255

256
        protected bool $autoSubmit = true;
257

258
        protected ?SubmitButton $filterSubmitButton = null;
259

260
        protected bool $hasColumnReset = true;
261

262
        protected bool $showSelectedRowsCount = true;
263

264
        private ?string $customPaginatorTemplate = null;
265

266
        private ?string $componentFullName = null;
267

268
        public function __construct(?IContainer $parent = null, ?string $name = null)
1✔
269
        {
270
                if ($parent !== null) {
1✔
271
                        $parent->addComponent($this, $name);
1✔
272
                }
273

274
                /**
275
                 * Try to find previous filters, pagination, perPage and other values in session
276
                 */
277
                $this->onRender[] = [$this, 'findSessionValues'];
1✔
278
                $this->onExport[] = [$this, 'findSessionValues'];
1✔
279

280
                /**
281
                 * Find default filter values
282
                 */
283
                $this->onRender[] = [$this, 'findDefaultFilter'];
1✔
284
                $this->onExport[] = [$this, 'findDefaultFilter'];
1✔
285

286
                /**
287
                 * Find default sort
288
                 */
289
                $this->onRender[] = [$this, 'findDefaultSort'];
1✔
290
                $this->onExport[] = [$this, 'findDefaultSort'];
1✔
291

292
                /**
293
                 * Find default items per page
294
                 */
295
                $this->onRender[] = [$this, 'findDefaultPerPage'];
1✔
296

297
                /**
298
                 * Notify about that json js extension
299
                 */
300
                $this->onFiltersAssembled[] = [$this, 'sendNonEmptyFiltersInPayload'];
1✔
301

302
                $this->monitor(
1✔
303
                        Presenter::class,
1✔
304
                        function (Presenter $presenter): void {
1✔
305
                                /**
306
                                 * Get session
307
                                 */
308
                                if ($this->rememberState || $this->canHideColumns()) {
1✔
309
                                        $this->gridSession = $presenter->getSession($this->getSessionSectionName());
1✔
310
                                }
311

312
                                $this->componentFullName = $this->lookupPath();
1✔
313
                        }
1✔
314
                );
315
        }
1✔
316

317
        /********************************************************************************
318
         *                                  RENDERING *
319
         ********************************************************************************/
320
        public function render(): void
321
        {
322
                /**
323
                 * Check whether datagrid has set some columns, initiated data source, etc
324
                 */
325
                if (!($this->dataModel instanceof DataModel)) {
×
326
                        throw new DatagridException('You have to set a data source first.');
×
327
                }
328

329
                if ($this->columns === []) {
×
330
                        throw new DatagridException('You have to add at least one column.');
×
331
                }
332

333
                $template = $this->getTemplate();
×
334

335
                if (!$template instanceof Template) {
×
336
                        throw new UnexpectedValueException();
×
337
                }
338

339
                $template->setTranslator($this->getTranslator());
×
340

341
                /**
342
                 * Invoke possible events
343
                 */
344
                $this->onRender($this);
×
345

346
                /**
347
                 * Prepare data for rendering (datagrid may render just one item)
348
                 */
349
                $rows = [];
×
350

351
                $items = $this->redrawItem !== [] ? $this->dataModel->filterRow($this->redrawItem) : $this->dataModel->filterData(
×
352
                        $this->getPaginator(),
×
353
                        $this->createSorting($this->sort, $this->sortCallback),
×
354
                        $this->assembleFilters()
×
355
                );
356

357
                $hasGroupActionOnRows = false;
×
358

359
                foreach ($items as $item) {
×
360
                        $rows[] = $row = new Row($this, $item, $this->getPrimaryKey());
×
361

362
                        if (!$hasGroupActionOnRows && $row->hasGroupAction()) {
×
363
                                $hasGroupActionOnRows = true;
×
364
                        }
365

366
                        if ($this->rowCallback !== null) {
×
367
                                ($this->rowCallback)($item, $row->getControl());
×
368
                        }
369

370
                        /**
371
                         * Walkaround for item snippet - snippet is the <tr> element and its class has to be also updated
372
                         */
373
                        if ($this->redrawItem !== []) {
×
374
                                $this->getPresenterInstance()->payload->_datagrid_redrawItem_class = $row->getControlClass();
×
375
                                $this->getPresenterInstance()->payload->_datagrid_redrawItem_id = $row->getId();
×
376
                        }
377
                }
378

379
                if ($hasGroupActionOnRows) {
×
380
                        $hasGroupActionOnRows = $this->hasGroupActions();
×
381
                }
382

383
                if ($this->isTreeView()) {
×
384
                        $template->add('treeViewHasChildrenColumn', $this->treeViewHasChildrenColumn);
×
385
                }
386

387
                $template->rows = $rows;
×
388

389
                $template->columns = $this->getColumns();
×
390
                $template->actions = $this->actions;
×
391
                $template->exports = $this->exports;
×
392
                $template->filters = $this->filters;
×
393
                $template->toolbarButtons = $this->toolbarButtons;
×
394
                $template->aggregationFunctions = $this->getAggregationFunctions();
×
395
                $template->multipleAggregationFunction = $this->getMultipleAggregationFunction();
×
396

397
                $template->filter_active = $this->isFilterActive();
×
398
                $template->originalTemplate = $this->getOriginalTemplateFile();
×
399
                $template->iconPrefix = static::$iconPrefix;
×
400
                $template->btnSecondaryClass = static::$btnSecondaryClass;
×
401
                $template->itemsDetail = $this->itemsDetail;
×
402
                $template->columnsVisibility = $this->getColumnsVisibility();
×
403
                $template->columnsSummary = $this->columnsSummary;
×
404

405
                $template->inlineEdit = $this->inlineEdit;
×
406
                $template->inlineAdd = $this->inlineAdd;
×
407

408
                $template->hasGroupActions = $this->hasGroupActions();
×
409
                $template->hasGroupActionOnRows = $hasGroupActionOnRows;
×
410

411
                /**
412
                 * Walkaround for Latte (does not know $form in snippet in {form} etc)
413
                 */
414
                $template->filter = $this['filter'];
×
415

416
                /**
417
                 * Set template file and render it
418
                 */
419
                $template->setFile($this->getTemplateFile());
×
420
                $template->render();
×
421
        }
422

423

424
        /********************************************************************************
425
         *                                 ROW CALLBACK *
426
         ********************************************************************************/
427

428
        /**
429
         * Each row can be modified with user defined callback
430
         *
431
         * @return static
432
         */
433
        public function setRowCallback(callable $callback): self
434
        {
435
                $this->rowCallback = $callback;
×
436

437
                return $this;
×
438
        }
439

440

441
        /********************************************************************************
442
         *                                 DATA SOURCE *
443
         ********************************************************************************/
444

445
        /**
446
         * @return static
447
         */
448
        public function setPrimaryKey(string $primaryKey): self
449
        {
450
                if ($this->dataModel instanceof DataModel) {
×
451
                        throw new DatagridException('Please set datagrid primary key before setting datasource.');
×
452
                }
453

454
                $this->primaryKey = $primaryKey;
×
455

456
                return $this;
×
457
        }
458

459
        /**
460
         * @return static
461
         * @throws InvalidArgumentException
462
         */
463
        public function setDataSource(mixed $source): self
1✔
464
        {
465
                $this->dataModel = new DataModel($source, $this->primaryKey);
1✔
466

467
                $this->dataModel->onBeforeFilter[] = [$this, 'beforeDataModelFilter'];
1✔
468
                $this->dataModel->onAfterFilter[] = [$this, 'afterDataModelFilter'];
1✔
469
                $this->dataModel->onAfterPaginated[] = [$this, 'afterDataModelPaginated'];
1✔
470

471
                return $this;
1✔
472
        }
473

474
        public function getDataSource(): IDataSource|array|null
475
        {
476
                return isset($this->dataModel)
×
477
                        ? $this->dataModel->getDataSource()
×
478
                        : null;
×
479
        }
480

481

482
        /********************************************************************************
483
         *                                  TEMPLATING *
484
         ********************************************************************************/
485

486
        /**
487
         * @return static
488
         */
489
        public function setTemplateFile(string $templateFile): self
490
        {
491
                $this->templateFile = $templateFile;
×
492

493
                return $this;
×
494
        }
495

496
        public function getTemplateFile(): string
497
        {
498
                return $this->templateFile ?? $this->getOriginalTemplateFile();
×
499
        }
500

501
        public function getOriginalTemplateFile(): string
502
        {
503
                return __DIR__ . '/templates/datagrid.latte';
×
504
        }
505

506
        /**
507
         * @return static
508
         */
509
        public function useHappyComponents(bool $useHappyComponents): self
510
        {
511
                $this->useHappyComponents = $useHappyComponents;
×
512

513
                return $this;
×
514
        }
515

516
        public function shouldUseHappyComponents(): bool
517
        {
518
                return $this->useHappyComponents;
×
519
        }
520

521

522
        /********************************************************************************
523
         *                                   SORTING *
524
         ********************************************************************************/
525

526
 /**
527
  * @return static
528
  */
529
        public function setDefaultSort(string|array $sort, bool $useOnReset = true): self
530
        {
531
                $sort = is_string($sort)
×
532
                        ? [$sort => 'ASC']
×
533
                        : $sort;
×
534

535
                $this->defaultSort = $sort;
×
536
                $this->defaultSortUseOnReset = $useOnReset;
×
537

538
                return $this;
×
539
        }
540

541
        /**
542
         * Return default sort for column, if specified
543
         */
544
        public function getColumnDefaultSort(string $columnKey): ?string
545
        {
546
                if (isset($this->defaultSort[$columnKey])) {
×
547
                        return $this->defaultSort[$columnKey];
×
548
                }
549

550
                return null;
×
551
        }
552

553
        /**
554
         * User may set default sorting, apply it
555
         */
556
        public function findDefaultSort(): void
557
        {
558
                if ($this->sort !== []) {
1✔
559
                        return;
×
560
                }
561

562
                if ((bool) $this->getSessionData('_grid_has_sorted')) {
1✔
563
                        return;
×
564
                }
565

566
                if ($this->defaultSort !== []) {
1✔
567
                        $this->sort = $this->defaultSort;
×
568
                }
569

570
                $this->saveSessionData('_grid_sort', $this->sort);
1✔
571
        }
1✔
572

573
        /**
574
         * @return static
575
         * @throws DatagridException
576
         */
577
        public function setSortable(bool $sortable = true): self
578
        {
579
                if ($this->getItemsDetail() !== null) {
×
580
                        throw new DatagridException('You can not use both sortable datagrid and items detail.');
×
581
                }
582

583
                $this->sortable = $sortable;
×
584

585
                return $this;
×
586
        }
587

588
        public function isSortable(): bool
589
        {
590
                return $this->sortable;
×
591
        }
592

593
        /**
594
         * @return static
595
         */
596
        public function setMultiSortEnabled(bool $multiSort = true): self
597
        {
598
                $this->multiSort = $multiSort;
×
599

600
                return $this;
×
601
        }
602

603
        public function isMultiSortEnabled(): bool
604
        {
605
                return $this->multiSort;
×
606
        }
607

608
        /**
609
         * @return static
610
         */
611
        public function setSortableHandler(string $handler = 'sort!'): self
612
        {
613
                $this->sortableHandler = $handler;
×
614

615
                return $this;
×
616
        }
617

618
        public function getSortableHandler(): string
619
        {
620
                return $this->sortableHandler;
×
621
        }
622

623
        public function getSortNext(Column $column): array
624
        {
625
                $sort = $column->getSortNext();
×
626

627
                if ($this->isMultiSortEnabled()) {
×
628
                        $sort = array_merge($this->sort, $sort);
×
629
                }
630

631
                return array_filter($sort);
×
632
        }
633

634
        /********************************************************************************
635
         *                                  TREE VIEW *
636
         ********************************************************************************/
637
        public function isTreeView(): bool
638
        {
639
                return $this->treeViewChildrenCallback !== null;
×
640
        }
641

642
        /**
643
         * @return static
644
         */
645
        public function setTreeView(
646
                callable $getChildrenCallback,
647
                string|callable $treeViewHasChildrenColumn = 'has_children'
648
        ): self
649
        {
650
                if (is_callable($treeViewHasChildrenColumn)) {
×
651
                        $this->treeViewHasChildrenCallback = $treeViewHasChildrenColumn;
×
652
                        $treeViewHasChildrenColumn = null;
×
653
                }
654

655
                $this->treeViewChildrenCallback = $getChildrenCallback;
×
656
                $this->treeViewHasChildrenColumn = $treeViewHasChildrenColumn;
×
657

658
                /**
659
                 * Torn off pagination
660
                 */
661
                $this->setPagination(false);
×
662

663
                /**
664
                 * Set tree view template file
665
                 */
666
                if ($this->templateFile === null) {
×
667
                        $this->setTemplateFile(__DIR__ . '/templates/datagrid_tree.latte');
×
668
                }
669

670
                return $this;
×
671
        }
672

673
        public function hasTreeViewChildrenCallback(): bool
674
        {
675
                return is_callable($this->treeViewHasChildrenCallback);
×
676
        }
677

678
        public function treeViewChildrenCallback(mixed $item): bool
679
        {
680
                if ($this->treeViewHasChildrenCallback === null) {
×
681
                        throw new UnexpectedValueException();
×
682
                }
683

684
                return (bool) call_user_func($this->treeViewHasChildrenCallback, $item);
×
685
        }
686

687
        /********************************************************************************
688
         *                                    COLUMNS *
689
         ********************************************************************************/
690
        public function addColumnText(
1✔
691
                string $key,
692
                string $name,
693
                ?string $column = null
694
        ): ColumnText
695
        {
696
                $column ??= $key;
1✔
697

698
                $columnText = new ColumnText($this, $key, $column, $name);
1✔
699
                $this->addColumn($key, $columnText);
1✔
700

701
                return $columnText;
1✔
702
        }
703

704
        public function addColumnLink(
1✔
705
                string $key,
706
                string $name,
707
                ?string $href = null,
708
                ?string $column = null,
709
                ?array $params = null
710
        ): ColumnLink
711
        {
712
                $column ??= $key;
1✔
713
                $href ??= $key;
1✔
714

715
                if ($params === null) {
1✔
716
                        $params = [$this->primaryKey];
1✔
717
                }
718

719
                $columnLink = new ColumnLink($this, $key, $column, $name, $href, $params);
1✔
720
                $this->addColumn($key, $columnLink);
1✔
721

722
                return $columnLink;
1✔
723
        }
724

725
        public function addColumnNumber(
1✔
726
                string $key,
727
                string $name,
728
                ?string $column = null
729
        ): ColumnNumber
730
        {
731
                $column ??= $key;
1✔
732

733
                $columnNumber = new ColumnNumber($this, $key, $column, $name);
1✔
734
                $this->addColumn($key, $columnNumber);
1✔
735

736
                return $columnNumber;
1✔
737
        }
738

739
        public function addColumnDateTime(
1✔
740
                string $key,
741
                string $name,
742
                ?string $column = null
743
        ): ColumnDateTime
744
        {
745
                $column ??= $key;
1✔
746

747
                $columnDateTime = new ColumnDateTime($this, $key, $column, $name);
1✔
748
                $this->addColumn($key, $columnDateTime);
1✔
749

750
                return $columnDateTime;
1✔
751
        }
752

753
        public function addColumnStatus(
1✔
754
                string $key,
755
                string $name,
756
                ?string $column = null
757
        ): ColumnStatus
758
        {
759
                $column ??= $key;
1✔
760

761
                $columnStatus = new ColumnStatus($this, $key, $column, $name);
1✔
762
                $this->addColumn($key, $columnStatus);
1✔
763

764
                return $columnStatus;
1✔
765
        }
766

767
        /**
768
         * @throws DatagridColumnNotFoundException
769
         */
770
        public function getColumn(string $key): Column
1✔
771
        {
772
                if (!isset($this->columns[$key])) {
1✔
773
                        throw new DatagridColumnNotFoundException(
1✔
774
                                sprintf('There is no column at key [%s] defined.', $key)
1✔
775
                        );
776
                }
777

778
                return $this->columns[$key];
1✔
779
        }
780

781
        /**
782
         * @return static
783
         */
784
        public function removeColumn(string $key): self
1✔
785
        {
786
                unset($this->columnsVisibility[$key], $this->columns[$key]);
1✔
787

788
                return $this;
1✔
789
        }
790

791
        /********************************************************************************
792
         *                                    ACTIONS *
793
         ********************************************************************************/
794
        public function addAction(
1✔
795
                string $key,
796
                string $name,
797
                ?string $href = null,
798
                ?array $params = null
799
        ): Action
800
        {
801
                $this->addActionCheck($key);
1✔
802

803
                $href ??= $key;
1✔
804

805
                if ($params === null) {
1✔
806
                        $params = [$this->primaryKey];
1✔
807
                }
808

809
                return $this->actions[$key] = new Action($this, $key, $href, $name, $params);
1✔
810
        }
811

812
        public function addActionCallback(
813
                string $key,
814
                string $name,
815
                ?callable $callback = null
816
        ): ActionCallback
817
        {
818
                $this->addActionCheck($key);
×
819

820
                $params = ['__id' => $this->primaryKey];
×
821

822
                $this->actions[$key] = $action = new ActionCallback($this, $key, $key, $name, $params);
×
823

824
                if ($callback !== null) {
×
825
                        $action->onClick[] = $callback;
×
826
                }
827

828
                return $action;
×
829
        }
830

831
        public function addMultiAction(string $key, string $name): MultiAction
832
        {
833
                $this->addActionCheck($key);
×
834

835
                $action = new MultiAction($this, $key, $name);
×
836

837
                $this->actions[$key] = $action;
×
838

839
                return $action;
×
840
        }
841

842
        /**
843
         * @throws DatagridException
844
         */
845
        public function getAction(string $key): Action|MultiAction
846
        {
847
                if (!isset($this->actions[$key])) {
×
848
                        throw new DatagridException(sprintf('There is no action at key [%s] defined.', $key));
×
849
                }
850

851
                return $this->actions[$key];
×
852
        }
853

854
        /**
855
         * @return static
856
         */
857
        public function removeAction(string $key): self
858
        {
859
                unset($this->actions[$key]);
×
860

861
                return $this;
×
862
        }
863

864
        public function addFilterText(
1✔
865
                string $key,
866
                string $name,
867
                array|string|null $columns = null
868
        ): FilterText
869
        {
870
                $columns = $columns === null ? [$key] : (is_string($columns) ? [$columns] : $columns);
1✔
871

872
                $this->addFilterCheck($key);
1✔
873

874
                return $this->filters[$key] = new FilterText($this, $key, $name, $columns);
1✔
875
        }
876

877
        public function addFilterSelect(
878
                string $key,
879
                string $name,
880
                array $options,
881
                ?string $column = null
882
        ): FilterSelect
883
        {
884
                $column ??= $key;
×
885

886
                $this->addFilterCheck($key);
×
887

888
                return $this->filters[$key] = new FilterSelect($this, $key, $name, $options, $column);
×
889
        }
890

891
        public function addFilterMultiSelect(
892
                string $key,
893
                string $name,
894
                array $options,
895
                ?string $column = null
896
        ): FilterMultiSelect
897
        {
898
                $column ??= $key;
×
899

900
                $this->addFilterCheck($key);
×
901

902
                return $this->filters[$key] = new FilterMultiSelect($this, $key, $name, $options, $column);
×
903
        }
904

905
        public function addFilterDate(string $key, string $name, ?string $column = null): FilterDate
906
        {
907
                $column ??= $key;
×
908

909
                $this->addFilterCheck($key);
×
910

911
                return $this->filters[$key] = new FilterDate($this, $key, $name, $column);
×
912
        }
913

914
        public function addFilterRange(
915
                string $key,
916
                string $name,
917
                ?string $column = null,
918
                string $nameSecond = '-'
919
        ): FilterRange
920
        {
921
                $column ??= $key;
×
922

923
                $this->addFilterCheck($key);
×
924

925
                return $this->filters[$key] = new FilterRange($this, $key, $name, $column, $nameSecond);
×
926
        }
927

928
        /**
929
         * @throws DatagridException
930
         */
931
        public function addFilterDateRange(
932
                string $key,
933
                string $name,
934
                ?string $column = null,
935
                string $nameSecond = '-'
936
        ): FilterDateRange
937
        {
938
                $column ??= $key;
×
939

940
                $this->addFilterCheck($key);
×
941

942
                return $this->filters[$key] = new FilterDateRange($this, $key, $name, $column, $nameSecond);
×
943
        }
944

945
        /**
946
         * Fill array of Filter\Filter[] with values from $this->filter persistent parameter
947
         * Fill array of Column\Column[] with values from $this->sort persistent parameter
948
         *
949
         * @return array<Filter>
950
         */
951
        public function assembleFilters(): array
952
        {
953
                foreach ($this->filter as $key => $value) {
1✔
954
                        if (!isset($this->filters[$key])) {
×
955
                                $this->deleteSessionData($key);
×
956

957
                                continue;
×
958
                        }
959

960
                        if (is_iterable($value)) {
×
961
                                if (!ArraysHelper::testEmpty($value)) {
×
962
                                        $this->filters[$key]->setValue($value);
×
963
                                }
964
                        } else {
965
                                if ($value !== '' && $value !== null) {
×
966
                                        $this->filters[$key]->setValue($value);
×
967
                                }
968
                        }
969
                }
970

971
                foreach ($this->columns as $key => $column) {
1✔
972
                        if (isset($this->sort[$key])) {
×
973
                                $column->setSort($this->sort[$key]);
×
974
                        }
975
                }
976

977
                $this->onFiltersAssembled($this->filters);
1✔
978

979
                return $this->filters;
1✔
980
        }
981

982
        /**
983
         * @return static
984
         */
985
        public function removeFilter(string $key): self
986
        {
987
                unset($this->filters[$key]);
×
988

989
                return $this;
×
990
        }
991

992
        public function getFilter(string $key): Filter
1✔
993
        {
994
                if (!isset($this->filters[$key])) {
1✔
995
                        throw new DatagridException(sprintf('Filter [%s] is not defined', $key));
×
996
                }
997

998
                return $this->filters[$key];
1✔
999
        }
1000

1001
        /**
1002
         * @return static
1003
         */
1004
        public function setStrictSessionFilterValues(bool $strictSessionFilterValues = true): self
1005
        {
1006
                $this->strictSessionFilterValues = $strictSessionFilterValues;
×
1007

1008
                return $this;
×
1009
        }
1010

1011
        /********************************************************************************
1012
         *                                  FILTERING *
1013
         ********************************************************************************/
1014
        public function isFilterActive(): bool
1015
        {
1016
                $is_filter = ArraysHelper::testTruthy($this->filter);
×
1017

1018
                return $is_filter || $this->forceFilterActive;
×
1019
        }
1020

1021
        public function isFilterDefault(): bool
1022
        {
1023
                return $this->filter === $this->defaultFilter;
1✔
1024
        }
1025

1026
        /**
1027
         * Tell that filter is active from whatever reasons
1028
         *
1029
         * @return static
1030
         */
1031
        public function setFilterActive(): self
1032
        {
1033
                $this->forceFilterActive = true;
×
1034

1035
                return $this;
×
1036
        }
1037

1038
        /**
1039
         * Set filter values (force - overwrite user data)
1040
         *
1041
         * @return static
1042
         */
1043
        public function setFilter(array $filter): self
1✔
1044
        {
1045
                $this->filter = $filter;
1✔
1046

1047
                $this->saveSessionData('_grid_has_filtered', 1);
1✔
1048

1049
                return $this;
1✔
1050
        }
1051

1052
        /**
1053
         * If we want to sent some initial filter
1054
         *
1055
         * @return static
1056
         * @throws DatagridException
1057
         */
1058
        public function setDefaultFilter(array $defaultFilter, bool $useOnReset = true): self
1✔
1059
        {
1060
                foreach ($defaultFilter as $key => $value) {
1✔
1061
                        /** @var Filter|null $filter */
1062
                        $filter = $this->getFilter($key);
1✔
1063

1064
                        if ($filter === null) {
1✔
1065
                                throw new DatagridException(
×
1066
                                        sprintf('Can not set default value to nonexisting filter [%s]', $key)
×
1067
                                );
1068
                        }
1069

1070
                        if ($filter instanceof FilterMultiSelect && !is_array($value)) {
1✔
1071
                                throw new DatagridException(
×
1072
                                        sprintf('Default value of filter [%s] - MultiSelect has to be an array', $key)
×
1073
                                );
1074
                        }
1075

1076
                        if ($filter instanceof FilterRange || $filter instanceof FilterDateRange) {
1✔
1077
                                if (!is_array($value)) {
×
1078
                                        throw new DatagridException(
×
1079
                                                sprintf('Default value of filter [%s] - Range/DateRange has to be an array [from/to => ...]', $key)
×
1080
                                        );
1081
                                }
1082

1083
                                $temp = $value;
×
1084
                                unset($temp['from'], $temp['to']);
×
1085

1086
                                if (count($temp) > 0) {
×
1087
                                        throw new DatagridException(
×
1088
                                                sprintf(
×
1089
                                                        'Default value of filter [%s] - Range/DateRange can contain only [from/to => ...] values',
×
1090
                                                        $key
1091
                                                )
1092
                                        );
1093
                                }
1094
                        }
1095
                }
1096

1097
                $this->defaultFilter = $defaultFilter;
1✔
1098
                $this->defaultFilterUseOnReset = $useOnReset;
1✔
1099

1100
                return $this;
1✔
1101
        }
1102

1103
        public function findDefaultFilter(): void
1104
        {
1105
                if ($this->filter !== []) {
1✔
1106
                        return;
×
1107
                }
1108

1109
                if ((bool) $this->getSessionData('_grid_has_filtered')) {
1✔
1110
                        return;
×
1111
                }
1112

1113
                if ($this->defaultFilter !== []) {
1✔
1114
                        $this->filter = $this->defaultFilter;
×
1115
                }
1116

1117
                foreach ($this->filter as $key => $value) {
1✔
1118
                        $this->saveSessionData($key, $value);
×
1119
                }
1120
        }
1✔
1121

1122
        public function createComponentFilter(): Form
1123
        {
1124
                $form = new Form($this, 'filter');
1✔
1125

1126
                $form->setMethod(static::$formMethod);
1✔
1127

1128
                $form->setTranslator($this->getTranslator());
1✔
1129

1130
                /**
1131
                 * InlineEdit part
1132
                 */
1133
                $inline_edit_container = $form->addContainer('inline_edit');
1✔
1134

1135
                if ($this->inlineEdit instanceof InlineEdit) {
1✔
1136
                        $inline_edit_container->addSubmit('submit', 'contributte_datagrid.save')
×
1137
                                ->setValidationScope([$inline_edit_container]);
×
1138
                        $inline_edit_container->addSubmit('cancel', 'contributte_datagrid.cancel')
×
1139
                                ->setValidationScope(null);
×
1140

1141
                        $this->inlineEdit->onControlAdd($inline_edit_container);
×
1142
                        $this->inlineEdit->onControlAfterAdd($inline_edit_container);
×
1143
                }
1144

1145
                /**
1146
                 * InlineAdd part
1147
                 */
1148
                $inlineAddContainer = $form->addContainer('inline_add');
1✔
1149

1150
                if ($this->inlineAdd instanceof InlineAdd) {
1✔
1151
                        $inlineAddContainer->addSubmit('submit', 'contributte_datagrid.save')
1✔
1152
                                ->setValidationScope([$inlineAddContainer]);
1✔
1153
                        $inlineAddContainer->addSubmit('cancel', 'contributte_datagrid.cancel')
1✔
1154
                                ->setValidationScope(null)
1✔
1155
                                ->setHtmlAttribute('data-datagrid-cancel-inline-add', true);
1✔
1156

1157
                        $this->inlineAdd->onControlAdd($inlineAddContainer);
1✔
1158
                        $this->inlineAdd->onControlAfterAdd($inlineAddContainer);
1✔
1159
                }
1160

1161
                /**
1162
                 * ItemDetail form part
1163
                 */
1164
                $itemsDetailForm = $this->getItemDetailForm();
1✔
1165

1166
                if ($itemsDetailForm instanceof Container) {
1✔
1167
                        $form['items_detail_form'] = $itemsDetailForm;
×
1168
                }
1169

1170
                /**
1171
                 * Filter part
1172
                 */
1173
                $filterContainer = $form->addContainer('filter');
1✔
1174

1175
                foreach ($this->filters as $filter) {
1✔
1176
                        $filter->addToFormContainer($filterContainer);
×
1177
                }
1178

1179
                if (!$this->hasAutoSubmit()) {
1✔
1180
                        $filterContainer['submit'] = $this->getFilterSubmitButton();
×
1181
                }
1182

1183
                /**
1184
                 * Group action part
1185
                 */
1186
                $groupActionContainer = $form->addContainer('group_action');
1✔
1187

1188
                if ($this->hasGroupActions()) {
1✔
1189
                        $this->getGroupActionCollection()->addToFormContainer($groupActionContainer);
×
1190
                }
1191

1192
                if ($form->isSubmitted() === false) {
1✔
1193
                        $this->setFilterContainerDefaults($filterContainer, $this->filter);
1✔
1194
                }
1195

1196
                /**
1197
                 * Per page part
1198
                 */
1199
                if ($this->isPaginated()) {
1✔
1200
                        $select = $form->addSelect('perPage', '', $this->getItemsPerPageList())
1✔
1201
                                ->setTranslator(null);
1✔
1202

1203
                        if ($form->isSubmitted() === false) {
1✔
1204
                                $select->setValue($this->getPerPage());
1✔
1205
                        }
1206

1207
                        $form->addSubmit('perPage_submit', 'contributte_datagrid.per_page_submit')
1✔
1208
                                ->setValidationScope([$select]);
1✔
1209
                }
1210

1211
                $form->onSubmit[] = function (NetteForm $form): void {
1✔
1212
                        $this->filterSucceeded($form);
1213
                };
1214

1215
                return $form;
1✔
1216
        }
1217

1218
        public function setFilterContainerDefaults(Container $container, array $values, ?string $parentKey = null): void
1✔
1219
        {
1220
                foreach ($container->getComponents() as $key => $control) {
1✔
1221
                        if (!isset($values[$key])) {
×
1222
                                continue;
×
1223
                        }
1224

1225
                        if ($control instanceof Container) {
×
1226
                                $this->setFilterContainerDefaults($control, (array) $values[$key], (string) $key);
×
1227

1228
                                continue;
×
1229
                        }
1230

1231
                        $value = $values[$key];
×
1232

1233
                        if ($value instanceof DateTime) {
×
1234
                                $filter = $parentKey !== null ? $this->getFilter($parentKey) : $this->getFilter((string) $key);
×
1235

1236
                                if ($filter instanceof IFilterDate) {
×
1237
                                        $value = $value->format($filter->getPhpFormat());
×
1238
                                }
1239
                        }
1240

1241
                        try {
1242
                                if (!$control instanceof FormControl) {
×
1243
                                        throw new UnexpectedValueException();
×
1244
                                }
1245

1246
                                $control->setValue($value);
×
1247

1248
                        } catch (InvalidArgumentException $e) {
×
1249
                                if ($this->strictSessionFilterValues) {
×
1250
                                        throw $e;
×
1251
                                }
1252
                        }
1253
                }
1254
        }
1✔
1255

1256
        /**
1257
         * Set $this->filter values after filter form submitted
1258
         */
1259
        public function filterSucceeded(NetteForm $form): void
1✔
1260
        {
1261
                if ($this->snippetsSet) {
1✔
1262
                        return;
×
1263
                }
1264

1265
                if ($this->getPresenterInstance()->isAjax()) {
1✔
1266
                        if (isset($form['group_action']['submit']) && $form['group_action']['submit']->isSubmittedBy()) {
×
1267
                                return;
×
1268
                        }
1269
                }
1270

1271
                /**
1272
                 * Per page
1273
                 */
1274
                $perPage = $form->getComponent('perPage')->getValue();
1✔
1275

1276
                if (isset($perPage)) {
1✔
1277
                        $this->saveSessionData('_grid_perPage', $perPage);
1✔
1278
                        $this->perPage = $perPage;
1✔
1279
                }
1280

1281
                /**
1282
                 * Inline edit
1283
                 */
1284
                if (
1285
                        isset($form['inline_edit'])
1✔
1286
                        && isset($form['inline_edit']['submit'])
1✔
1287
                        && isset($form['inline_edit']['cancel'])
1✔
1288
                        && $this->inlineEdit !== null
1✔
1289
                ) {
1290
                        $edit = $form['inline_edit'];
×
1291

1292
                        if (
1293
                                !$edit instanceof Container
×
1294
                                || !$edit['submit'] instanceof FormsSubmitButton
×
1295
                                || !$edit['cancel'] instanceof FormsSubmitButton
×
1296
                        ) {
1297
                                throw new UnexpectedValueException();
×
1298
                        }
1299

1300
                        if ($edit['submit']->isSubmittedBy() || $edit['cancel']->isSubmittedBy()) {
×
1301
                                /** @var string $id */
1302
                                $id = $form->getHttpData(Form::DataLine, 'inline_edit[_id]');
×
1303
                                $primaryWhereColumn = $form->getHttpData(Form::DataLine, 'inline_edit[_primary_where_column]');
×
1304

NEW
1305
                                if ($edit->getComponent('submit')->isSubmittedBy() && $edit->getErrors() === []) {
×
NEW
1306
                                        $this->inlineEdit->onSubmit($id, $form->getComponent('inline_edit')->getValues());
×
1307
                                        $this->getPresenterInstance()->payload->_datagrid_inline_edited = $id;
×
1308
                                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1309
                                } else {
1310
                                        $this->getPresenterInstance()->payload->_datagrid_inline_edit_cancel = $id;
×
1311
                                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1312
                                }
1313

1314
                                if ($edit['submit']->isSubmittedBy() && $this->inlineEdit->onCustomRedraw !== []) {
×
1315
                                        $this->inlineEdit->onCustomRedraw('submit');
×
1316
                                } elseif ($edit['cancel']->isSubmittedBy() && $this->inlineEdit->onCustomRedraw !== []) {
×
1317
                                        $this->inlineEdit->onCustomRedraw('cancel');
×
1318
                                } else {
1319
                                        $this->redrawItem($id, $primaryWhereColumn);
×
1320
                                        $this->redrawControl('summary');
×
1321
                                }
1322

1323
                                return;
×
1324
                        }
1325
                }
1326

1327
                /**
1328
                 * Inline add
1329
                 */
1330
                if (
1331
                        isset($form['inline_add'])
1✔
1332
                        && isset($form['inline_add']['submit'])
1✔
1333
                        && isset($form['inline_add']['cancel'])
1✔
1334
                        && $this->inlineAdd !== null
1✔
1335
                ) {
1336
                        $add = $form['inline_add'];
1✔
1337

1338
                        if (
1339
                                !$add instanceof Container
1✔
1340
                                || !$add['submit'] instanceof FormsSubmitButton
1✔
1341
                                || !$add['cancel'] instanceof FormsSubmitButton
1✔
1342
                        ) {
1343
                                throw new UnexpectedValueException();
×
1344
                        }
1345

1346
                        if ($add['submit']->isSubmittedBy() || $add['cancel']->isSubmittedBy()) {
1✔
1347
                                if ($add['submit']->isSubmittedBy() && $add->getErrors() === []) {
×
NEW
1348
                                        $this->inlineAdd->onSubmit($form->getComponent('inline_add')->getValues());
×
1349
                                }
1350

1351
                                $this->redrawControl('tbody');
×
1352

1353
                                $this->onRedraw();
×
1354

1355
                                return;
×
1356
                        }
1357
                }
1358

1359
                /**
1360
                 * Filter itself
1361
                 */
1362
                $values = $form->getComponent('filter')->getValues();
1✔
1363

1364
                if (!$values instanceof ArrayHash) {
1✔
1365
                        throw new UnexpectedValueException();
×
1366
                }
1367

1368
                foreach ($values as $key => $value) {
1✔
1369
                        /**
1370
                         * Session stuff
1371
                         */
1372
                        if ($this->rememberState && $this->getSessionData((string) $key) !== $value) {
×
1373
                                /**
1374
                                 * Has been filter changed?
1375
                                 */
1376
                                $this->page = 1;
×
1377
                                $this->saveSessionData('_grid_page', 1);
×
1378
                        }
1379

1380
                        $this->saveSessionData((string) $key, $value);
×
1381

1382
                        /**
1383
                         * Other stuff
1384
                         */
1385
                        $this->filter[$key] = $value;
×
1386
                }
1387

1388
                if ($values->count() > 0) {
1✔
1389
                        $this->saveSessionData('_grid_has_filtered', 1);
×
1390
                }
1391

1392
                if ($this->getPresenterInstance()->isAjax()) {
1✔
1393
                        $this->getPresenterInstance()->payload->_datagrid_sort = [];
×
1394

1395
                        foreach ($this->columns as $key => $column) {
×
1396
                                if ($column->isSortable()) {
×
1397
                                        $this->getPresenterInstance()->payload->_datagrid_sort[$key] = $this->link('sort!', [
×
1398
                                                'sort' => $column->getSortNext(),
×
1399
                                        ]);
1400
                                }
1401
                        }
1402
                }
1403

1404
                $this->reload();
1✔
1405
        }
1406

1407
        /**
1408
         * @return static
1409
         */
1410
        public function setOuterFilterRendering(bool $outerFilterRendering = true): self
1411
        {
1412
                $this->outerFilterRendering = $outerFilterRendering;
×
1413

1414
                return $this;
×
1415
        }
1416

1417
        public function hasOuterFilterRendering(): bool
1418
        {
1419
                return $this->outerFilterRendering;
×
1420
        }
1421

1422
        /**
1423
         * @return static
1424
         * @throws InvalidArgumentException
1425
         */
1426
        public function setOuterFilterColumnsCount(int $count): self
1427
        {
1428
                $columnsCounts = [1, 2, 3, 4, 6, 12];
×
1429

1430
                if (!in_array($count, $columnsCounts, true)) {
×
1431
                        throw new InvalidArgumentException(sprintf(
×
1432
                                'Columns count must be one of following values: %s. Value %s given.',
×
1433
                                implode(', ', $columnsCounts),
×
1434
                                $count
1435
                        ));
1436
                }
1437

1438
                $this->outerFilterColumnsCount = $count;
×
1439

1440
                return $this;
×
1441
        }
1442

1443
        public function getOuterFilterColumnsCount(): int
1444
        {
1445
                return $this->outerFilterColumnsCount;
×
1446
        }
1447

1448
        /**
1449
         * @return static
1450
         */
1451
        public function setCollapsibleOuterFilters(bool $collapsibleOuterFilters = true): self
1452
        {
1453
                $this->collapsibleOuterFilters = $collapsibleOuterFilters;
×
1454

1455
                return $this;
×
1456
        }
1457

1458
        public function hasCollapsibleOuterFilters(): bool
1459
        {
1460
                return $this->collapsibleOuterFilters;
×
1461
        }
1462

1463
        /**
1464
         * Try to restore session stuff
1465
         *
1466
         * @throws DatagridFilterNotFoundException
1467
         */
1468
        public function findSessionValues(): void
1469
        {
1470
                if (!ArraysHelper::testEmpty($this->filter) || ($this->page !== 1) || $this->sort !== []) {
1✔
1471
                        return;
×
1472
                }
1473

1474
                if (!$this->rememberState) {
1✔
1475
                        return;
×
1476
                }
1477

1478
                $page = $this->getSessionData('_grid_page');
1✔
1479

1480
                if ($page !== null) {
1✔
1481
                        $this->page = (int) $page;
×
1482
                }
1483

1484
                $perPage = $this->getSessionData('_grid_perPage');
1✔
1485

1486
                if ($perPage !== null) {
1✔
1487
                        $this->perPage = $perPage;
×
1488
                }
1489

1490
                $sort = $this->getSessionData('_grid_sort');
1✔
1491

1492
                if (is_array($sort) && $sort !== []) {
1✔
1493
                        $this->sort = $sort;
×
1494
                }
1495

1496
                foreach ($this->getSessionData() as $key => $value) {
1✔
1497
                        $other_session_keys = [
1✔
1498
                                '_grid_perPage',
1499
                                '_grid_sort',
1500
                                '_grid_page',
1501
                                '_grid_has_sorted',
1502
                                '_grid_has_filtered',
1503
                                '_grid_hidden_columns',
1504
                                '_grid_hidden_columns_manipulated',
1505
                        ];
1506

1507
                        if (!in_array($key, $other_session_keys, true)) {
1✔
1508
                                try {
1509
                                        $this->getFilter($key);
×
1510

1511
                                        $this->filter[$key] = $value;
×
1512

1513
                                } catch (DatagridException) {
×
1514
                                        if ($this->strictSessionFilterValues) {
×
1515
                                                throw new DatagridFilterNotFoundException(
×
1516
                                                        sprintf('Session filter: Filter [%s] not found', $key)
×
1517
                                                );
1518
                                        }
1519
                                }
1520
                        }
1521
                }
1522

1523
                /**
1524
                 * When column is sorted via custom callback, apply it
1525
                 */
1526
                if ($this->sortCallback === null && $this->sort !== []) {
1✔
1527
                        foreach (array_keys($this->sort) as $key) {
×
1528
                                try {
1529
                                        $column = $this->getColumn((string) $key);
×
1530

1531
                                } catch (DatagridColumnNotFoundException) {
×
1532
                                        $this->deleteSessionData('_grid_sort');
×
1533
                                        $this->sort = [];
×
1534

1535
                                        return;
×
1536
                                }
1537

1538
                                if ($column->isSortable() && is_callable($column->getSortableCallback())) {
×
1539
                                        $this->sortCallback = $column->getSortableCallback();
×
1540
                                }
1541
                        }
1542
                }
1543
        }
1✔
1544

1545
        /********************************************************************************
1546
         *                                    EXPORTS *
1547
         ********************************************************************************/
1548
        public function addExportCallback(
1✔
1549
                string $text,
1550
                callable $callback,
1551
                bool $filtered = false
1552
        ): Export
1553
        {
1554
                return $this->addToExports(new Export($this, $text, $callback, $filtered));
1✔
1555
        }
1556

1557
        public function addExportCsvFiltered(
1558
                string $text,
1559
                string $csvFileName,
1560
                string $outputEncoding = 'utf-8',
1561
                string $delimiter = ';',
1562
                bool $includeBom = false
1563
        ): ExportCsv
1564
        {
1565
                return $this->addExportCsv($text, $csvFileName, $outputEncoding, $delimiter, $includeBom, true);
×
1566
        }
1567

1568
        public function addExportCsv(
1✔
1569
                string $text,
1570
                string $csvFileName,
1571
                string $outputEncoding = 'utf-8',
1572
                string $delimiter = ';',
1573
                bool $includeBom = false,
1574
                bool $filtered = false
1575
        ): ExportCsv
1576
        {
1577
                $exportCsv = new ExportCsv($this, $text, $csvFileName, $filtered, $outputEncoding, $delimiter, $includeBom);
1✔
1578

1579
                $this->addToExports($exportCsv);
1✔
1580

1581
                return $exportCsv;
1✔
1582
        }
1583

1584
        public function resetExportsLinks(): void
1585
        {
1586
                foreach ($this->exports as $id => $export) {
×
1587
                        $link = new Link($this, 'export!', ['id' => $id]);
×
1588

1589
                        $export->setLink($link);
×
1590
                }
1591
        }
1592

1593

1594
        /********************************************************************************
1595
         *                                TOOLBAR BUTTONS *
1596
         ********************************************************************************/
1597

1598
        /**
1599
         * @throws DatagridException
1600
         */
1601
        public function addToolbarButton(
1602
                string $href,
1603
                string $text = '',
1604
                array $params = []
1605
        ): ToolbarButton
1606
        {
1607
                if (isset($this->toolbarButtons[$href])) {
×
1608
                        throw new DatagridException(
×
1609
                                sprintf('There is already toolbar button at key [%s] defined.', $href)
×
1610
                        );
1611
                }
1612

1613
                return $this->toolbarButtons[$href] = new ToolbarButton($this, $href, $text, $params);
×
1614
        }
1615

1616
        /**
1617
         * @throws DatagridException
1618
         */
1619
        public function getToolbarButton(string $key): ToolbarButton
1620
        {
1621
                if (!isset($this->toolbarButtons[$key])) {
×
1622
                        throw new DatagridException(
×
1623
                                sprintf('There is no toolbar button at key [%s] defined.', $key)
×
1624
                        );
1625
                }
1626

1627
                return $this->toolbarButtons[$key];
×
1628
        }
1629

1630
        /**
1631
         * @return static
1632
         */
1633
        public function removeToolbarButton(string $key): self
1634
        {
1635
                unset($this->toolbarButtons[$key]);
×
1636

1637
                return $this;
×
1638
        }
1639

1640
        /********************************************************************************
1641
         *                                 GROUP ACTIONS *
1642
         ********************************************************************************/
1643
        public function addGroupAction(string $title, array $options = []): GroupAction
1644
        {
1645
                return $this->getGroupActionCollection()->addGroupSelectAction($title, $options);
×
1646
        }
1647

1648
        public function addGroupButtonAction(string $title, ?string $class = null): GroupButtonAction
1649
        {
1650
                return $this->getGroupActionCollection()->addGroupButtonAction($title, $class);
×
1651
        }
1652

1653
        public function addGroupSelectAction(string $title, array $options = []): GroupAction
1654
        {
1655
                return $this->getGroupActionCollection()->addGroupSelectAction($title, $options);
×
1656
        }
1657

1658
        public function addGroupMultiSelectAction(string $title, array $options = []): GroupAction
1659
        {
1660
                return $this->getGroupActionCollection()->addGroupMultiSelectAction($title, $options);
×
1661
        }
1662

1663
        public function addGroupTextAction(string $title): GroupAction
1664
        {
1665
                return $this->getGroupActionCollection()->addGroupTextAction($title);
×
1666
        }
1667

1668
        public function addGroupTextareaAction(string $title): GroupAction
1669
        {
1670
                return $this->getGroupActionCollection()->addGroupTextareaAction($title);
×
1671
        }
1672

1673
        public function getGroupActionCollection(): GroupActionCollection
1674
        {
1675
                if ($this->groupActionCollection === null) {
×
1676
                        $this->groupActionCollection = new GroupActionCollection($this);
×
1677
                }
1678

1679
                return $this->groupActionCollection;
×
1680
        }
1681

1682
        public function hasGroupActions(): bool
1683
        {
1684
                return $this->groupActionCollection instanceof GroupActionCollection;
1✔
1685
        }
1686

1687
        public function shouldShowSelectedRowsCount(): bool
1688
        {
1689
                return $this->showSelectedRowsCount;
×
1690
        }
1691

1692
        /**
1693
         * @return static
1694
         */
1695
        public function setShowSelectedRowsCount(bool $show = true): self
1696
        {
1697
                $this->showSelectedRowsCount = $show;
×
1698

1699
                return $this;
×
1700
        }
1701

1702
        /********************************************************************************
1703
         *                                   HANDLERS *
1704
         ********************************************************************************/
1705
        public function handlePage(int $page): void
1706
        {
1707
                $this->page = $page;
×
1708
                $this->saveSessionData('_grid_page', $page);
×
1709

1710
                $this->reload(['table']);
×
1711
        }
1712

1713
        /**
1714
         * @throws DatagridColumnNotFoundException
1715
         */
1716
        public function handleSort(array $sort): void
1717
        {
1718
                if (count($sort) === 0) {
×
1719
                        $sort = $this->defaultSort;
×
1720
                }
1721

1722
                foreach (array_keys($sort) as $key) {
×
1723
                        try {
1724
                                $column = $this->getColumn($key);
×
1725

1726
                        } catch (DatagridColumnNotFoundException) {
×
1727
                                unset($sort[$key]);
×
1728

1729
                                continue;
×
1730
                        }
1731

1732
                        if ($column->sortableResetPagination()) {
×
1733
                                $this->saveSessionData('_grid_page', $this->page = 1);
×
1734
                        }
1735

1736
                        if ($column->getSortableCallback() !== null) {
×
1737
                                $this->sortCallback = $column->getSortableCallback();
×
1738
                        }
1739
                }
1740

1741
                $this->saveSessionData('_grid_has_sorted', 1);
×
1742
                $this->saveSessionData('_grid_sort', $this->sort = $sort);
×
1743

1744
                $this->reloadTheWholeGrid();
×
1745
        }
1746

1747
        public function handleResetFilter(): void
1748
        {
1749
                /**
1750
                 * Session stuff
1751
                 */
1752
                $this->deleteSessionData('_grid_page');
1✔
1753

1754
                if ($this->defaultFilterUseOnReset) {
1✔
1755
                        $this->deleteSessionData('_grid_has_filtered');
1✔
1756
                }
1757

1758
                if ($this->defaultSortUseOnReset) {
1✔
1759
                        $this->deleteSessionData('_grid_has_sorted');
1✔
1760
                }
1761

1762
                $sessionData = is_array($this->getSessionData())
1✔
1763
                        ? $this->getSessionData()
1✔
1764
                        : iterator_to_array($this->getSessionData());
1✔
1765

1766
                foreach (array_keys($sessionData) as $key) {
1✔
1767
                        if (!in_array($key, ['_grid_perPage', '_grid_sort', '_grid_page', '_grid_has_filtered', '_grid_has_sorted', '_grid_hidden_columns', '_grid_hidden_columns_manipulated'], true)) {
×
1768
                                $this->deleteSessionData((string) $key);
×
1769
                        }
1770
                }
1771

1772
                $this->filter = [];
1✔
1773

1774
                $this->reloadTheWholeGrid();
1✔
1775
        }
1776

1777
        public function handleResetColumnFilter(string $key): void
1778
        {
1779
                $this->deleteSessionData($key);
×
1780
                unset($this->filter[$key]);
×
1781

1782
                $this->reloadTheWholeGrid();
×
1783
        }
1784

1785
        /**
1786
         * @return static
1787
         */
1788
        public function setColumnReset(bool $reset = true): self
1789
        {
1790
                $this->hasColumnReset = $reset;
×
1791

1792
                return $this;
×
1793
        }
1794

1795
        public function hasColumnReset(): bool
1796
        {
1797
                return $this->hasColumnReset;
1✔
1798
        }
1799

1800
        /**
1801
         * @param array<Filter> $filters
1802
         */
1803
        public function sendNonEmptyFiltersInPayload(array $filters): void
1✔
1804
        {
1805
                if (!$this->hasColumnReset()) {
1✔
1806
                        return;
×
1807
                }
1808

1809
                $non_empty_filters = [];
1✔
1810

1811
                foreach ($filters as $filter) {
1✔
1812
                        if ($filter->isValueSet()) {
1✔
1813
                                $non_empty_filters[] = $filter->getKey();
×
1814
                        }
1815
                }
1816

1817
                $this->getPresenterInstance()->payload->non_empty_filters = $non_empty_filters;
1✔
1818
        }
1✔
1819

1820
        public function handleExport(mixed $id): void
1✔
1821
        {
1822
                if (!isset($this->exports[$id])) {
1✔
1823
                        throw new ForbiddenRequestException();
×
1824
                }
1825

1826
                if ($this->columnsExportOrder !== []) {
1✔
1827
                        $this->setColumnsOrder($this->columnsExportOrder);
×
1828
                }
1829

1830
                $export = $this->exports[$id];
1✔
1831

1832
                /**
1833
                 * Invoke possible events
1834
                 */
1835
                $this->onExport($this);
1✔
1836

1837
                if ($export->isFiltered()) {
1✔
1838
                        $sort = $this->sort;
1✔
1839
                        $filter = $this->assembleFilters();
1✔
1840
                } else {
1841
                        $sort = [$this->primaryKey => 'ASC'];
1✔
1842
                        $filter = [];
1✔
1843
                }
1844

1845
                if ($this->dataModel === null) {
1✔
1846
                        throw new DatagridException('You have to set a data source first.');
1✔
1847
                }
1848

1849
                $rows = [];
1✔
1850

1851
                $items = $this->dataModel->filterData(
1✔
1852
                        null,
1✔
1853
                        $this->createSorting($sort, $this->sortCallback),
1✔
1854
                        $filter
1855
                );
1856

1857
                foreach ($items as $item) {
1✔
1858
                        $rows[] = new Row($this, $item, $this->getPrimaryKey());
1✔
1859
                }
1860

1861
                if ($export instanceof ExportCsv) {
1✔
1862
                        $export->invoke($rows);
1✔
1863
                } else {
1864
                        $export->invoke($items);
1✔
1865
                }
1866

1867
                if ($export->isAjax()) {
1✔
1868
                        $this->reload();
×
1869
                }
1870
        }
1✔
1871

1872
        public function handleGetChildren(mixed $parent): void
1873
        {
1874
                if (!is_callable($this->treeViewChildrenCallback)) {
×
1875
                        throw new UnexpectedValueException();
×
1876
                }
1877

1878
                $this->setDataSource(call_user_func($this->treeViewChildrenCallback, $parent));
×
1879

1880
                if ($this->getPresenterInstance()->isAjax()) {
×
1881
                        $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
1882
                        $this->getPresenterInstance()->payload->_datagrid_tree = $parent;
×
1883

1884
                        $this->redrawControl('items');
×
1885

1886
                        $this->onRedraw();
×
1887
                } else {
1888
                        $this->getPresenterInstance()->redirect('this');
×
1889
                }
1890
        }
1891

1892
        public function handleGetItemDetail(mixed $id): void
1893
        {
1894
                $template = $this->getTemplate();
×
1895

1896
                if (!$template instanceof Template) {
×
1897
                        throw new UnexpectedValueException();
×
1898
                }
1899

1900
                $template->add('toggle_detail', $id);
×
1901

1902
                if ($this->itemsDetail === null) {
×
1903
                        throw new UnexpectedValueException();
×
1904
                }
1905

1906
                $this->redrawItem = [$this->itemsDetail->getPrimaryWhereColumn() => $id];
×
1907

1908
                if ($this->getPresenterInstance()->isAjax()) {
×
1909
                        $this->getPresenterInstance()->payload->_datagrid_toggle_detail = $id;
×
1910
                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1911
                        $this->redrawControl('items');
×
1912

1913
                        /**
1914
                         * Only for nette 2.4
1915
                         */
1916
                        if (method_exists($template->getLatte(), 'addProvider')) {
×
1917
                                $this->redrawControl('gridSnippets');
×
1918
                        }
1919

1920
                        $this->onRedraw();
×
1921
                } else {
1922
                        $this->getPresenterInstance()->redirect('this');
×
1923
                }
1924
        }
1925

1926
        public function handleEdit(mixed $id, mixed $key): void
1927
        {
1928
                $column = $this->getColumn($key);
×
1929
                $request = $this->getPresenterInstance()->getRequest();
×
1930

1931
                if (!$request instanceof Request) {
×
1932
                        throw new UnexpectedValueException();
×
1933
                }
1934

1935
                $value = $request->getPost('value');
×
1936

1937
                // Could be null of course
1938
                if ($column->getEditableCallback() === null) {
×
1939
                        throw new UnexpectedValueException();
×
1940
                }
1941

1942
                $newValue = $column->getEditableCallback()($id, $value);
×
1943

1944
                $this->getPresenterInstance()->payload->_datagrid_editable_new_value = $newValue;
×
1945
                $this->getPresenterInstance()->payload->postGet = true;
×
1946
                $this->getPresenterInstance()->payload->url = $this->link('this');
×
1947

1948
                if (!$this->getPresenterInstance()->isControlInvalid(null)) {
×
1949
                        $this->getPresenterInstance()->sendPayload();
×
1950
                }
1951
        }
1952

1953
        /**
1954
         * @param array|string[] $snippets
1955
         */
1956
        public function reload(array $snippets = []): void
1✔
1957
        {
1958
                if ($this->getPresenterInstance()->isAjax()) {
1✔
1959
                        $this->redrawControl('tbody');
×
1960
                        $this->redrawControl('pagination');
×
1961
                        $this->redrawControl('summary');
×
1962
                        $this->redrawControl('thead-group-action');
×
1963

1964
                        /**
1965
                         * manualy reset exports links...
1966
                         */
1967
                        $this->resetExportsLinks();
×
1968
                        $this->redrawControl('exports');
×
1969

1970
                        foreach ($snippets as $snippet) {
×
1971
                                $this->redrawControl($snippet);
×
1972
                        }
1973

1974
                        $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
1975
                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1976

1977
                        $this->onRedraw();
×
1978
                } else {
1979
                        $this->getPresenterInstance()->redirect('this');
1✔
1980
                }
1981
        }
1982

1983
        public function reloadTheWholeGrid(): void
1984
        {
1985
                if ($this->getPresenterInstance()->isAjax()) {
1✔
1986
                        $this->redrawControl('grid');
×
1987

1988
                        $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
1989
                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1990

1991
                        $this->onRedraw();
×
1992
                } else {
1993
                        $this->getPresenterInstance()->redirect('this');
1✔
1994
                }
1995
        }
1996

1997
        public function handleChangeStatus(string $id, string $key, string $value): void
1998
        {
1999
                if (!isset($this->columns[$key])) {
×
2000
                        throw new DatagridException(sprintf('ColumnStatus[%s] does not exist', $key));
×
2001
                }
2002

2003
                if (!$this->columns[$key] instanceof ColumnStatus) {
×
2004
                        throw new UnexpectedValueException();
×
2005
                }
2006

2007
                $this->columns[$key]->onChange($id, $value);
×
2008
        }
2009

2010
        public function redrawItem(string|int $id, mixed $primaryWhereColumn = null): void
2011
        {
2012
                $this->snippetsSet = true;
×
2013

2014
                $this->redrawItem = [($primaryWhereColumn ?? $this->primaryKey) => $id];
×
2015

2016
                $this->redrawControl('items');
×
2017

2018
                $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
2019

2020
                $this->onRedraw();
×
2021
        }
2022

2023
        public function handleShowAllColumns(): void
2024
        {
2025
                $this->deleteSessionData('_grid_hidden_columns');
×
2026
                $this->saveSessionData('_grid_hidden_columns_manipulated', true);
×
2027

2028
                $this->redrawControl();
×
2029

2030
                $this->onShowAllColumns();
×
2031
                $this->onRedraw();
×
2032
        }
2033

2034
        public function handleShowDefaultColumns(): void
2035
        {
2036
                $this->deleteSessionData('_grid_hidden_columns');
×
2037
                $this->saveSessionData('_grid_hidden_columns_manipulated', false);
×
2038

2039
                $this->redrawControl();
×
2040

2041
                $this->onShowDefaultColumns();
×
2042
                $this->onRedraw();
×
2043
        }
2044

2045
        public function handleShowColumn(string $column): void
2046
        {
2047
                $columns = $this->getSessionData('_grid_hidden_columns');
×
2048

2049
                if ($columns !== [] && $columns !== null) {
×
2050
                        $pos = array_search($column, $columns, true);
×
2051

2052
                        if ($pos !== false) {
×
2053
                                unset($columns[$pos]);
×
2054
                        }
2055
                }
2056

2057
                $this->saveSessionData('_grid_hidden_columns', $columns);
×
2058
                $this->saveSessionData('_grid_hidden_columns_manipulated', true);
×
2059

2060
                $this->redrawControl();
×
2061

2062
                $this->onColumnShow($column);
×
2063
                $this->onRedraw();
×
2064
        }
2065

2066
        public function handleHideColumn(string $column): void
2067
        {
2068
                /**
2069
                 * Store info about hiding a column to session
2070
                 */
2071
                $columns = $this->getSessionData('_grid_hidden_columns');
×
2072

2073
                if ($columns === [] || $columns === null) {
×
2074
                        $columns = [$column];
×
2075
                } elseif (!in_array($column, $columns, true)) {
×
2076
                        array_push($columns, $column);
×
2077
                }
2078

2079
                $this->saveSessionData('_grid_hidden_columns', $columns);
×
2080
                $this->saveSessionData('_grid_hidden_columns_manipulated', true);
×
2081

2082
                $this->redrawControl();
×
2083

2084
                $this->onColumnHide($column);
×
2085
                $this->onRedraw();
×
2086
        }
2087

2088
        public function handleActionCallback(mixed $__key, mixed $__id): void
2089
        {
2090
                $action = $this->getAction($__key);
×
2091

2092
                if (!($action instanceof ActionCallback)) {
×
2093
                        throw new DatagridException(
×
2094
                                sprintf('Action [%s] does not exist or is not an callback aciton.', $__key)
×
2095
                        );
2096
                }
2097

2098
                $action->onClick($__id);
×
2099
        }
2100

2101

2102
        /********************************************************************************
2103
         *                                  PAGINATION *
2104
         ********************************************************************************/
2105

2106
        /**
2107
         * @param array|array|int[]|array|string[] $itemsPerPageList
2108
         * @return static
2109
         */
2110
        public function setItemsPerPageList(array $itemsPerPageList, bool $includeAll = true): self
1✔
2111
        {
2112
                if ($itemsPerPageList === []) {
1✔
2113
                        throw new InvalidArgumentException('$itemsPerPageList can not be an empty array');
×
2114
                }
2115

2116
                $this->itemsPerPageList = $itemsPerPageList;
1✔
2117

2118
                if ($includeAll) {
1✔
2119
                        $this->itemsPerPageList[] = 'all';
1✔
2120
                }
2121

2122
                return $this;
1✔
2123
        }
2124

2125
        /**
2126
         * @return static
2127
         */
2128
        public function setDefaultPerPage(int $count): self
2129
        {
2130
                $this->defaultPerPage = $count;
×
2131

2132
                return $this;
×
2133
        }
2134

2135
        /**
2136
         * User may set default "items per page" value, apply it
2137
         */
2138
        public function findDefaultPerPage(): void
2139
        {
2140
                if ($this->perPage !== null) {
×
2141
                        return;
×
2142
                }
2143

2144
                if ($this->defaultPerPage !== null) {
×
2145
                        $this->perPage = $this->defaultPerPage;
×
2146
                }
2147

2148
                $this->saveSessionData('_grid_perPage', $this->perPage);
×
2149
        }
2150

2151
        public function createComponentPaginator(): DatagridPaginator
2152
        {
2153
                $component = new DatagridPaginator(
×
2154
                        $this->getTranslator(),
×
2155
                        static::$iconPrefix,
×
2156
                        static::$btnSecondaryClass
×
2157
                );
2158
                $paginator = $component->getPaginator();
×
2159

2160
                $paginator->setPage($this->page);
×
2161

2162
                if (is_int($this->getPerPage())) {
×
2163
                        $paginator->setItemsPerPage($this->getPerPage());
×
2164
                }
2165

2166
                if ($this->customPaginatorTemplate !== null) {
×
2167
                        $component->setTemplateFile($this->customPaginatorTemplate);
×
2168
                }
2169

2170
                return $component;
×
2171
        }
2172

2173
        public function getPerPage(): int|string
2174
        {
2175
                $itemsPerPageList = array_keys($this->getItemsPerPageList());
1✔
2176

2177
                $perPage = $this->perPage ?? reset($itemsPerPageList);
1✔
2178

2179
                if (($perPage !== 'all' && !in_array((int) $this->perPage, $itemsPerPageList, true))
1✔
2180
                        || ($perPage === 'all' && !in_array($this->perPage, $itemsPerPageList, true))) {
1✔
2181
                        $perPage = reset($itemsPerPageList);
1✔
2182
                }
2183

2184
                return $perPage === 'all'
1✔
2185
                        ? 'all'
1✔
2186
                        : (int) $perPage;
1✔
2187
        }
2188

2189
        /**
2190
         * @return array|array|int[]|array|string[]|\Stringable[]
2191
         */
2192
        public function getItemsPerPageList(): array
2193
        {
2194
                $list = array_flip($this->itemsPerPageList);
1✔
2195

2196
                foreach (array_keys($list) as $key) {
1✔
2197
                        $list[$key] = $key;
1✔
2198
                }
2199

2200
                if (array_key_exists('all', $list)) {
1✔
2201
                        $list['all'] = $this->getTranslator()->translate('contributte_datagrid.all');
1✔
2202
                }
2203

2204
                return $list;
1✔
2205
        }
2206

2207
        /**
2208
         * @return static
2209
         */
2210
        public function setPagination(bool $doPaginate): self
2211
        {
2212
                $this->doPaginate = $doPaginate;
×
2213

2214
                return $this;
×
2215
        }
2216

2217
        public function isPaginated(): bool
2218
        {
2219
                return $this->doPaginate;
1✔
2220
        }
2221

2222
        public function getPaginator(): ?DatagridPaginator
2223
        {
2224
                if ($this->isPaginated() && $this->perPage !== 'all') {
×
2225
                        return $this['paginator'];
×
2226
                }
2227

2228
                return null;
×
2229
        }
2230

2231

2232
        /********************************************************************************
2233
         *                                     I18N *
2234
         ********************************************************************************/
2235

2236
        /**
2237
         * @return static
2238
         */
2239
        public function setTranslator(Translator $translator): self
1✔
2240
        {
2241
                $this->translator = $translator;
1✔
2242

2243
                return $this;
1✔
2244
        }
2245

2246
        public function getTranslator(): Translator
2247
        {
2248
                if ($this->translator === null) {
1✔
2249
                        $this->translator = new SimpleTranslator();
1✔
2250
                }
2251

2252
                return $this->translator;
1✔
2253
        }
2254

2255

2256
        /********************************************************************************
2257
         *                                 COLUMNS ORDER *
2258
         ********************************************************************************/
2259

2260
        /**
2261
         * Set order of datagrid columns
2262
         *
2263
         * @param array|string[] $order
2264
         * @return static
2265
         */
2266
        public function setColumnsOrder(array $order): self
2267
        {
2268
                $new_order = [];
×
2269

2270
                foreach ($order as $key) {
×
2271
                        if (isset($this->columns[$key])) {
×
2272
                                $new_order[$key] = $this->columns[$key];
×
2273
                        }
2274
                }
2275

2276
                if (count($new_order) === count($this->columns)) {
×
2277
                        $this->columns = $new_order;
×
2278
                } else {
2279
                        throw new DatagridException('When changing columns order, you have to specify all columns');
×
2280
                }
2281

2282
                return $this;
×
2283
        }
2284

2285
        /**
2286
         * Columns order may be different for export and normal grid
2287
         *
2288
         * @param array|string[] $order
2289
         * @return static
2290
         */
2291
        public function setColumnsExportOrder(array $order): self
2292
        {
2293
                $this->columnsExportOrder = $order;
×
2294

2295
                return $this;
×
2296
        }
2297

2298
        /********************************************************************************
2299
         *                                SESSION & URL *
2300
         ********************************************************************************/
2301
        public function getSessionSectionName(): string
2302
        {
2303
                $presenter = $this->getPresenterInstance();
1✔
2304

2305
                return $presenter->getName() . ':' . $this->getUniqueId();
1✔
2306
        }
2307

2308
        /**
2309
         * @return static
2310
         */
2311
        public function setRememberState(bool $remember = true, bool $rememberHideableColumnsState = false): self
1✔
2312
        {
2313
                $this->rememberState = $remember;
1✔
2314
                $this->rememberHideableColumnsState = $rememberHideableColumnsState;
1✔
2315

2316
                return $this;
1✔
2317
        }
2318

2319
        /**
2320
         * @return static
2321
         */
2322
        public function setRefreshUrl(bool $refresh = true): self
2323
        {
2324
                $this->refreshURL = $refresh;
×
2325

2326
                return $this;
×
2327
        }
2328

2329
        public function getSessionData(?string $key = null, mixed $defaultValue = null): mixed
1✔
2330
        {
2331
                $getValue = fn () => ($key !== null ? $this->gridSession[$key] : $this->gridSession) ?? $defaultValue;
1✔
2332

2333
                if ($this->rememberState) {
1✔
2334
                        return ($getValue)();
1✔
2335
                }
2336

2337
                if ($this->rememberHideableColumnsState && in_array($key, self::HIDEABLE_COLUMNS_SESSION_KEYS, true)) {
1✔
2338
                        return ($getValue)();
×
2339
                }
2340

2341
                return $key === null
1✔
2342
                        ? []
1✔
2343
                        : $defaultValue;
1✔
2344
        }
2345

2346
        public function saveSessionData(string $key, mixed $value): void
1✔
2347
        {
2348
                if ($this->rememberState) {
1✔
2349
                        $this->gridSession[$key] = $value;
1✔
2350
                } elseif ($this->rememberHideableColumnsState && in_array($key, self::HIDEABLE_COLUMNS_SESSION_KEYS, true)) {
×
2351
                        $this->gridSession[$key] = $value;
×
2352
                }
2353
        }
1✔
2354

2355
        public function deleteSessionData(string $key): void
1✔
2356
        {
2357
                unset($this->gridSession[$key]);
1✔
2358
        }
1✔
2359

2360

2361
        /********************************************************************************
2362
         *                                  ITEM DETAIL *
2363
         ********************************************************************************/
2364

2365
        /**
2366
         * Get items detail parameters
2367
         */
2368
        public function getItemsDetail(): ?ItemDetail
2369
        {
2370
                return $this->itemsDetail;
×
2371
        }
2372

2373
        /**
2374
         * @param mixed $detail callable|string|bool
2375
         */
2376
        public function setItemsDetail(mixed $detail = true, ?string $primaryWhereColumn = null): ItemDetail
2377
        {
2378
                if ($this->isSortable()) {
×
2379
                        throw new DatagridException('You can not use both sortable datagrid and items detail.');
×
2380
                }
2381

2382
                $this->itemsDetail = new ItemDetail($this, $primaryWhereColumn ?? $this->primaryKey);
×
2383

2384
                if (is_string($detail)) {
×
2385
                        /**
2386
                         * Item detail will be in separate template
2387
                         */
2388
                        $this->itemsDetail->setType('template');
×
2389
                        $this->itemsDetail->setTemplate($detail);
×
2390

2391
                } elseif (is_callable($detail)) {
×
2392
                        /**
2393
                         * Item detail will be rendered via custom callback renderer
2394
                         */
2395
                        $this->itemsDetail->setType('renderer');
×
2396
                        $this->itemsDetail->setRenderer($detail);
×
2397

2398
                } elseif ($detail === true) {
×
2399
                        /**
2400
                         * Item detail will be rendered probably via block #detail
2401
                         */
2402
                        $this->itemsDetail->setType('block');
×
2403

2404
                } else {
2405
                        throw new DatagridException('::setItemsDetail() can be called either with no parameters or with parameter = template path or callable renderer.');
×
2406
                }
2407

2408
                return $this->itemsDetail;
×
2409
        }
2410

2411
        /**
2412
         * @return static
2413
         */
2414
        public function setItemsDetailForm(callable $callableSetContainer): self
2415
        {
2416
                if ($this->itemsDetail instanceof ItemDetail) {
×
2417
                        $this->itemsDetail->setForm(
×
2418
                                new ItemDetailForm($callableSetContainer)
×
2419
                        );
2420

2421
                        return $this;
×
2422
                }
2423

2424
                throw new DatagridException('Please set the ItemDetail first.');
×
2425
        }
2426

2427
        public function getItemDetailForm(): ?Container
2428
        {
2429
                if ($this->itemsDetail instanceof ItemDetail) {
1✔
2430
                        return $this->itemsDetail->getForm();
×
2431
                }
2432

2433
                return null;
1✔
2434
        }
2435

2436
        /********************************************************************************
2437
         *                                ROW PRIVILEGES *
2438
         ********************************************************************************/
2439
        public function allowRowsGroupAction(callable $condition): void
2440
        {
2441
                $this->rowConditions['group_action'] = $condition;
×
2442
        }
2443

2444
        public function allowRowsInlineEdit(callable $condition): void
2445
        {
2446
                $this->rowConditions['inline_edit'] = $condition;
×
2447
        }
2448

2449
        public function allowRowsAction(string $key, callable $condition): void
2450
        {
2451
                $this->rowConditions['action'][$key] = $condition;
×
2452
        }
2453

2454
        /**
2455
         * @throws DatagridException
2456
         */
2457
        public function allowRowsMultiAction(
2458
                string $multiActionKey,
2459
                string $actionKey,
2460
                callable $condition
2461
        ): void
2462
        {
2463
                if (!isset($this->actions[$multiActionKey])) {
×
2464
                        throw new DatagridException(
×
2465
                                sprintf('There is no action at key [%s] defined.', $multiActionKey)
×
2466
                        );
2467
                }
2468

2469
                if (!$this->actions[$multiActionKey] instanceof MultiAction) {
×
2470
                        throw new DatagridException(
×
2471
                                sprintf('Action at key [%s] is not a MultiAction.', $multiActionKey)
×
2472
                        );
2473
                }
2474

2475
                $this->actions[$multiActionKey]->setRowCondition($actionKey, $condition);
×
2476
        }
2477

2478
        public function getRowCondition(string $name, ?string $key = null): bool|callable
2479
        {
2480
                if (!isset($this->rowConditions[$name])) {
×
2481
                        return false;
×
2482
                }
2483

2484
                $condition = $this->rowConditions[$name];
×
2485

2486
                if ($key === null) {
×
2487
                        return $condition;
×
2488
                }
2489

2490
                return $condition[$key] ?? false;
×
2491
        }
2492

2493
        /********************************************************************************
2494
         *                               COLUMN CALLBACK *
2495
         ********************************************************************************/
2496
        public function addColumnCallback(string $key, callable $callback): void
2497
        {
2498
                $this->columnCallbacks[$key] = $callback;
×
2499
        }
2500

2501
        public function getColumnCallback(string $key): ?callable
2502
        {
2503
                return $this->columnCallbacks[$key] ?? null;
×
2504
        }
2505

2506
        /********************************************************************************
2507
         *                                 INLINE EDIT *
2508
         ********************************************************************************/
2509
        public function addInlineEdit(?string $primaryWhereColumn = null): InlineEdit
2510
        {
2511
                $this->inlineEdit = new InlineEdit($this, $primaryWhereColumn ?? $this->primaryKey);
×
2512

2513
                return $this->inlineEdit;
×
2514
        }
2515

2516
        public function getInlineEdit(): ?InlineEdit
2517
        {
2518
                return $this->inlineEdit;
×
2519
        }
2520

2521
        public function handleShowInlineAdd(): void
2522
        {
2523
                if ($this->inlineAdd !== null) {
×
2524
                        $this->inlineAdd->setShouldBeRendered(true);
×
2525
                }
2526

2527
                $presenter = $this->getPresenterInstance();
×
2528

2529
                if ($presenter->isAjax()) {
×
2530
                        $presenter->payload->_datagrid_inline_adding = true;
×
2531
                        $presenter->payload->_datagrid_name = $this->getFullName();
×
2532

2533
                        $this->redrawControl('tbody');
×
2534

2535
                        $this->onRedraw();
×
2536
                }
2537
        }
2538

2539
        public function handleInlineEdit(mixed $id): void
2540
        {
2541
                if ($this->inlineEdit !== null) {
×
2542
                        $this->inlineEdit->setItemId($id);
×
2543

2544
                        $primaryWhereColumn = $this->inlineEdit->getPrimaryWhereColumn();
×
2545

2546
                        $filterContainer = $this['filter'];
×
2547
                        $inlineEditContainer = $filterContainer['inline_edit'];
×
2548

2549
                        if (!$inlineEditContainer instanceof Container) {
×
2550
                                throw new UnexpectedValueException();
×
2551
                        }
2552

2553
                        $inlineEditContainer->addHidden('_id', $id);
×
2554
                        $inlineEditContainer->addHidden('_primary_where_column', $primaryWhereColumn);
×
2555

2556
                        $presenter = $this->getPresenterInstance();
×
2557

2558
                        if ($presenter->isAjax()) {
×
2559
                                $presenter->payload->_datagrid_inline_editing = true;
×
2560
                                $presenter->payload->_datagrid_name = $this->getFullName();
×
2561
                        }
2562

2563
                        $this->redrawItem($id, $primaryWhereColumn);
×
2564
                }
2565
        }
2566

2567
        /********************************************************************************
2568
         *                                  INLINE ADD *
2569
         ********************************************************************************/
2570
        public function addInlineAdd(): InlineAdd
2571
        {
2572
                $this->inlineAdd = new InlineAdd($this);
1✔
2573

2574
                $this->inlineAdd
1✔
2575
                        ->setTitle('contributte_datagrid.add')
1✔
2576
                        ->setIcon('plus');
1✔
2577

2578
                return $this->inlineAdd;
1✔
2579
        }
2580

2581
        public function getInlineAdd(): ?InlineAdd
2582
        {
2583
                return $this->inlineAdd;
×
2584
        }
2585

2586

2587
        /********************************************************************************
2588
         *                               COLUMNS HIDING *
2589
         ********************************************************************************/
2590

2591
        /**
2592
         * Can datagrid hide colums?
2593
         */
2594
        public function canHideColumns(): bool
2595
        {
2596
                return $this->canHideColumns;
×
2597
        }
2598

2599
        /**
2600
         * Order Grid to set columns hideable.
2601
         *
2602
         * @return static
2603
         */
2604
        public function setColumnsHideable(bool $columnsHideable = true): self
2605
        {
2606
                $this->canHideColumns = $columnsHideable;
×
2607

2608
                return $this;
×
2609
        }
2610

2611
        /********************************************************************************
2612
         *                                COLUMNS SUMMARY *
2613
         ********************************************************************************/
2614
        public function hasColumnsSummary(): bool
2615
        {
2616
                return $this->columnsSummary instanceof ColumnsSummary;
×
2617
        }
2618

2619
        /**
2620
         * @param array|string[] $columns
2621
         */
2622
        public function setColumnsSummary(array $columns, ?callable $rowCallback = null): ColumnsSummary
2623
        {
2624
                if ($this->hasSomeAggregationFunction()) {
×
2625
                        throw new DatagridException('You can use either ColumnsSummary or AggregationFunctions');
×
2626
                }
2627

2628
                $this->columnsSummary = new ColumnsSummary($this, $columns, $rowCallback);
×
2629

2630
                return $this->columnsSummary;
×
2631
        }
2632

2633
        public function getColumnsSummary(): ?ColumnsSummary
2634
        {
2635
                return $this->columnsSummary;
1✔
2636
        }
2637

2638

2639
        /********************************************************************************
2640
         *                                   INTERNAL *
2641
         ********************************************************************************/
2642

2643
        /**
2644
         * Gets component's full name in component tree
2645
         *
2646
         * @throws DatagridHasToBeAttachedToPresenterComponentException
2647
         */
2648
        public function getFullName(): string
2649
        {
2650
                if ($this->componentFullName === null) {
×
2651
                        throw new DatagridHasToBeAttachedToPresenterComponentException('Datagrid needs to be attached to presenter in order to get its full name.');
×
2652
                }
2653

2654
                return $this->componentFullName;
×
2655
        }
2656

2657
        /**
2658
         * Tell grid filters to by submitted automatically
2659
         *
2660
         * @return static
2661
         */
2662
        public function setAutoSubmit(bool $autoSubmit = true): self
2663
        {
2664
                $this->autoSubmit = $autoSubmit;
×
2665

2666
                return $this;
×
2667
        }
2668

2669
        public function hasAutoSubmit(): bool
2670
        {
2671
                return $this->autoSubmit;
1✔
2672
        }
2673

2674
        public function getFilterSubmitButton(): SubmitButton
2675
        {
2676
                if ($this->hasAutoSubmit()) {
×
2677
                        throw new DatagridException('Datagrid has auto-submit. Turn it off before setting filter submit button.');
×
2678
                }
2679

2680
                if ($this->filterSubmitButton === null) {
×
2681
                        $this->filterSubmitButton = new SubmitButton($this);
×
2682
                }
2683

2684
                return $this->filterSubmitButton;
×
2685
        }
2686

2687

2688
        /********************************************************************************
2689
         *                                   INTERNAL *
2690
         ********************************************************************************/
2691

2692
        /**
2693
         * @internal
2694
         */
2695
        public function getColumnsCount(): int
2696
        {
2697
                $count = count($this->getColumns());
×
2698

2699
                if ($this->actions !== []
×
2700
                        || $this->isSortable()
×
2701
                        || $this->getItemsDetail() !== null
×
2702
                        || $this->getInlineEdit() !== null
×
2703
                        || $this->getInlineAdd() !== null) {
×
2704
                        $count++;
×
2705
                }
2706

2707
                if ($this->hasGroupActions()) {
×
2708
                        $count++;
×
2709
                }
2710

2711
                return $count;
×
2712
        }
2713

2714
        /**
2715
         * @internal
2716
         */
2717
        public function getPrimaryKey(): string
2718
        {
2719
                return $this->primaryKey;
1✔
2720
        }
2721

2722
        /**
2723
         * @return array<Column>
2724
         * @internal
2725
         */
2726
        public function getColumns(): array
2727
        {
2728
                $return = $this->columns;
1✔
2729

2730
                try {
2731
                        $this->getParentComponent();
1✔
2732

2733
                        if (! (bool) $this->getSessionData('_grid_hidden_columns_manipulated', false)) {
1✔
2734
                                $columns_to_hide = [];
1✔
2735

2736
                                foreach ($this->columns as $key => $column) {
1✔
2737
                                        if ($column->getDefaultHide()) {
×
2738
                                                $columns_to_hide[] = $key;
×
2739
                                        }
2740
                                }
2741

2742
                                if ($columns_to_hide !== []) {
1✔
2743
                                        $this->saveSessionData('_grid_hidden_columns', $columns_to_hide);
×
2744
                                        $this->saveSessionData('_grid_hidden_columns_manipulated', true);
×
2745
                                }
2746
                        }
2747

2748
                        $hidden_columns = $this->getSessionData('_grid_hidden_columns', []);
1✔
2749

2750
                        foreach ($hidden_columns as $column) {
1✔
2751
                                if (isset($this->columns[$column])) {
×
2752
                                        $this->columnsVisibility[$column] = [
×
2753
                                                'visible' => false,
2754
                                        ];
2755

2756
                                        unset($return[$column]);
×
2757
                                }
2758
                        }
2759
                } catch (DatagridHasToBeAttachedToPresenterComponentException) {
×
2760
                        // No need to worry
2761
                }
2762

2763
                return $return;
1✔
2764
        }
2765

2766
        /**
2767
         * @internal
2768
         */
2769
        public function getColumnsVisibility(): array
2770
        {
2771
                $return = $this->columnsVisibility;
1✔
2772

2773
                foreach (array_keys($this->columnsVisibility) as $key) {
1✔
2774
                        $return[$key]['column'] = $this->columns[$key];
×
2775
                }
2776

2777
                return $return;
1✔
2778
        }
2779

2780
        /**
2781
         * @internal
2782
         */
2783
        public function getParentComponent(): Component
2784
        {
2785
                $parent = parent::getParent();
1✔
2786

2787
                if (!$parent instanceof Component) {
1✔
2788
                        throw new DatagridHasToBeAttachedToPresenterComponentException(
×
2789
                                sprintf(
×
2790
                                        'Datagrid is attached to: "%s", but instance of %s is needed.',
×
2791
                                        ($parent !== null ? $parent::class : 'null'),
×
2792
                                        Component::class
×
2793
                                )
2794
                        );
2795
                }
2796

2797
                return $parent;
1✔
2798
        }
2799

2800
        /**
2801
         * @internal
2802
         * @throws UnexpectedValueException
2803
         */
2804
        public function getSortableParentPath(): string
2805
        {
2806
                if ($this->getParentComponent() instanceof IPresenter) {
×
2807
                        return '';
×
2808
                }
2809

2810
                $presenter = $this->getParentComponent()->lookupPath(IPresenter::class, false);
×
2811

2812
                if ($presenter === null) {
×
2813
                        throw new UnexpectedValueException(
×
2814
                                sprintf('%s needs %s', self::class, IPresenter::class)
×
2815
                        );
2816
                }
2817

2818
                return $presenter;
×
2819
        }
2820

2821
        /**
2822
         * Some of datagrid columns may be hidden by default
2823
         *
2824
         * @internal
2825
         * @return static
2826
         */
2827
        public function setSomeColumnDefaultHide(bool $defaultHide): self
2828
        {
2829
                $this->someColumnDefaultHide = $defaultHide;
×
2830

2831
                return $this;
×
2832
        }
2833

2834
        /**
2835
         * Are some of columns hidden bydefault?
2836
         *
2837
         * @internal
2838
         */
2839
        public function hasSomeColumnDefaultHide(): bool
2840
        {
2841
                return $this->someColumnDefaultHide;
×
2842
        }
2843

2844
        /**
2845
         * Simply refresh url
2846
         *
2847
         * @internal
2848
         */
2849
        public function handleRefreshState(): void
2850
        {
2851
                $this->findSessionValues();
×
2852
                $this->findDefaultFilter();
×
2853
                $this->findDefaultSort();
×
2854
                $this->findDefaultPerPage();
×
2855

2856
                $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
2857
                $this->redrawControl('non-existing-snippet');
×
2858
        }
2859

2860
        /**
2861
         * @internal
2862
         */
2863
        public function setCustomPaginatorTemplate(string $templateFile): void
2864
        {
2865
                $this->customPaginatorTemplate = $templateFile;
×
2866
        }
2867

2868
        protected function createSorting(array $sort, ?callable $sortCallback = null): Sorting
1✔
2869
        {
2870
                foreach ($sort as $key => $order) {
1✔
2871
                        unset($sort[$key]);
1✔
2872

2873
                        if ($order !== 'ASC' && $order !== 'DESC') {
1✔
2874
                                continue;
×
2875
                        }
2876

2877
                        try {
2878
                                $column = $this->getColumn($key);
1✔
2879

2880
                        } catch (DatagridColumnNotFoundException) {
1✔
2881
                                continue;
1✔
2882
                        }
2883

2884
                        $sort[$column->getSortingColumn()] = $order;
×
2885
                }
2886

2887
                if ($sortCallback === null && isset($column)) {
1✔
2888
                        $sortCallback = $column->getSortableCallback();
×
2889
                }
2890

2891
                return new Sorting($sort, $sortCallback);
1✔
2892
        }
2893

2894
        /**
2895
         * @throws DatagridException
2896
         */
2897
        protected function addColumn(string $key, Column $column): Column
1✔
2898
        {
2899
                if (isset($this->columns[$key])) {
1✔
2900
                        throw new DatagridException(
×
2901
                                sprintf('There is already column at key [%s] defined.', $key)
×
2902
                        );
2903
                }
2904

2905
                $this->onColumnAdd($key, $column);
1✔
2906

2907
                $this->columnsVisibility[$key] = ['visible' => true];
1✔
2908

2909
                return $this->columns[$key] = $column;
1✔
2910
        }
2911

2912
        /**
2913
         * Check whether given key already exists in $this->filters
2914
         *
2915
         * @throws DatagridException
2916
         */
2917
        protected function addActionCheck(string $key): void
1✔
2918
        {
2919
                if (isset($this->actions[$key])) {
1✔
2920
                        throw new DatagridException(
1✔
2921
                                sprintf('There is already action at key [%s] defined.', $key)
1✔
2922
                        );
2923
                }
2924
        }/********************************************************************************
1✔
2925
          *                                    FILTERS *
2926
          ********************************************************************************/
2927

2928
        /**
2929
         * Check whether given key already exists in $this->filters
2930
         *
2931
         * @throws DatagridException
2932
         */
2933
        protected function addFilterCheck(string $key): void
1✔
2934
        {
2935
                if (isset($this->filters[$key])) {
1✔
2936
                        throw new DatagridException(
×
2937
                                sprintf('There is already action at key [%s] defined.', $key)
×
2938
                        );
2939
                }
2940
        }
1✔
2941

2942
        protected function addToExports(Export $export): Export
1✔
2943
        {
2944
                $id = count($this->exports) > 0 ? count($this->exports) + 1 : 1;
1✔
2945

2946
                $link = new Link($this, 'export!', ['id' => $id]);
1✔
2947

2948
                $export->setLink($link);
1✔
2949

2950
                return $this->exports[$id] = $export;
1✔
2951
        }
2952

2953
        private function getPresenterInstance(): Presenter
2954
        {
2955
                return $this->getPresenter();
1✔
2956
        }
2957

2958
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc