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

contributte / datagrid / 16111598389

04 Jul 2025 09:53AM UTC coverage: 46.757% (-0.05%) from 46.81%
16111598389

push

github

web-flow
Fix: Reset column filter without deleting all grid filters (#1187)

0 of 4 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

1240 of 2652 relevant lines covered (46.76%)

0.47 hits per line

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

37.78
/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\Storage\IStateStorage;
40
use Contributte\Datagrid\Storage\NoopStateStorage;
41
use Contributte\Datagrid\Storage\SessionStateStorage;
42
use Contributte\Datagrid\Toolbar\ToolbarButton;
43
use Contributte\Datagrid\Utils\ArraysHelper;
44
use Contributte\Datagrid\Utils\ItemDetailForm;
45
use Contributte\Datagrid\Utils\Sorting;
46
use DateTime;
47
use InvalidArgumentException;
48
use Nette\Application\ForbiddenRequestException;
49
use Nette\Application\IPresenter;
50
use Nette\Application\Request;
51
use Nette\Application\UI\Component;
52
use Nette\Application\UI\Control;
53
use Nette\Application\UI\Form;
54
use Nette\Application\UI\Link;
55
use Nette\Application\UI\Presenter;
56
use Nette\Bridges\ApplicationLatte\Template;
57
use Nette\ComponentModel\IContainer;
58
use Nette\Forms\Container;
59
use Nette\Forms\Control as FormControl;
60
use Nette\Forms\Controls\SubmitButton as FormsSubmitButton;
61
use Nette\Forms\Form as NetteForm;
62
use Nette\Http\SessionSection;
63
use Nette\Localization\Translator;
64
use Nette\Utils\ArrayHash;
65
use UnexpectedValueException;
66

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

81
        use TDatagridAggregationFunction;
82

83
        private const HIDEABLE_COLUMNS_STORAGE_KEYS = [
84
                '_grid_hidden_columns',
85
                '_grid_hidden_columns_manipulated',
86
        ];
87

88
        public static string $iconPrefix = 'fa fa-';
89

90
        public static string $btnSecondaryClass = 'btn-default btn-secondary';
91

92
        /**
93
         * Default form method
94
         */
95
        public static string $formMethod = 'post';
96

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

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

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

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

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

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

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

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

121
        /** @var array|callable[] */
122
        public array $onFiltersAssembled = [];
123

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

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

136
        /** @persistent */
137
        public int $page = 1;
138

139
        /** @persistent */
140
        public string|int|null $perPage = null;
141

142
        /** @persistent */
143
        public array $sort = [];
144

145
        public array $defaultSort = [];
146

147
        public array $defaultFilter = [];
148

149
        public bool $defaultFilterUseOnReset = true;
150

151
        public bool $defaultSortUseOnReset = true;
152

153
        /** @persistent */
154
        public array $filter = [];
155

156
        /** @var callable|null */
157
        protected $sortCallback = null;
158

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

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

164
        protected ?int $defaultPerPage = null;
165

166
        protected ?string $templateFile = null;
167

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

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

174
        protected ?GroupActionCollection $groupActionCollection = null;
175

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

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

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

185
        protected ?DataModel $dataModel = null;
186

187
        protected string $primaryKey = 'id';
188

189
        protected bool $doPaginate = true;
190

191
        protected bool $csvExport = true;
192

193
        protected bool $csvExportFiltered = true;
194

195
        protected bool $sortable = false;
196

197
        protected bool $multiSort = false;
198

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

201
        protected ?string $originalTemplate = null;
202

203
        protected array $redrawItem = [];
204

205
        protected ?Translator $translator = null;
206

207
        protected bool $forceFilterActive = false;
208

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

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

215
        protected ?string $treeViewHasChildrenColumn = null;
216

217
        protected bool $outerFilterRendering = false;
218

219
        protected int $outerFilterColumnsCount = 2;
220

221
        protected bool $collapsibleOuterFilters = true;
222

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

226
        protected bool $rememberState = true;
227

228
        protected bool $rememberHideableColumnsState = true;
229

230
        protected bool $refreshURL = true;
231

232
        protected SessionSection $gridSession;
233

234
        protected ?ItemDetail $itemsDetail = null;
235

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

241
        protected array $columnCallbacks = [];
242

243
        protected bool $canHideColumns = false;
244

245
        protected array $columnsVisibility = [];
246

247
        protected ?InlineEdit $inlineEdit = null;
248

249
        protected ?InlineAdd $inlineAdd = null;
250

251
        protected bool $snippetsSet = false;
252

253
        protected bool $someColumnDefaultHide = false;
254

255
        protected ?ColumnsSummary $columnsSummary = null;
256

257
        protected bool $autoSubmit = true;
258

259
        protected ?SubmitButton $filterSubmitButton = null;
260

261
        protected bool $hasColumnReset = true;
262

263
        protected bool $showSelectedRowsCount = true;
264

265
        protected ?IStateStorage $stateStorage = null;
266

267
        private ?string $customPaginatorTemplate = null;
268

269
        private ?string $componentFullName = null;
270

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

277
                /**
278
                 * Try to find previous filters, pagination, perPage and other values in storage
279
                 */
280
                $this->onRender[] = [$this, 'findStorageValues'];
1✔
281
                $this->onExport[] = [$this, 'findStorageValues'];
1✔
282

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

289
                /**
290
                 * Find default sort
291
                 */
292
                $this->onRender[] = [$this, 'findDefaultSort'];
1✔
293
                $this->onExport[] = [$this, 'findDefaultSort'];
1✔
294

295
                /**
296
                 * Find default items per page
297
                 */
298
                $this->onRender[] = [$this, 'findDefaultPerPage'];
1✔
299

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

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

315
                                $this->componentFullName = $this->lookupPath();
1✔
316
                        }
1✔
317
                );
318
        }
1✔
319

320
        public function getStateStorage(): IStateStorage
321
        {
322
                if ($this->stateStorage === null) {
1✔
323
                        return $this->rememberState || $this->canHideColumns()
1✔
324
                                ? new SessionStateStorage($this->gridSession)
1✔
325
                                : new NoopStateStorage();
1✔
326
                }
327

328
                return $this->stateStorage;
×
329
        }
330

331
        public function setStateStorage(IStateStorage $stateStorage): self
332
        {
333
                $this->stateStorage = $stateStorage;
×
334

335
                return $this;
×
336
        }
337

338
        /********************************************************************************
339
         *                                  RENDERING *
340
         ********************************************************************************/
341
        public function render(): void
342
        {
343
                /**
344
                 * Check whether datagrid has set some columns, initiated data source, etc
345
                 */
346
                if (!($this->dataModel instanceof DataModel)) {
×
347
                        throw new DatagridException('You have to set a data source first.');
×
348
                }
349

350
                if ($this->columns === []) {
×
351
                        throw new DatagridException('You have to add at least one column.');
×
352
                }
353

354
                $template = $this->getTemplate();
×
355

356
                if (!$template instanceof Template) {
×
357
                        throw new UnexpectedValueException();
×
358
                }
359

360
                $template->setTranslator($this->getTranslator());
×
361

362
                /**
363
                 * Invoke possible events
364
                 */
365
                $this->onRender($this);
×
366

367
                /**
368
                 * Prepare data for rendering (datagrid may render just one item)
369
                 */
370
                $rows = [];
×
371

372
                $items = $this->redrawItem !== [] ? $this->dataModel->filterRow($this->redrawItem) : $this->dataModel->filterData(
×
373
                        $this->getPaginator(),
×
374
                        $this->createSorting($this->sort, $this->sortCallback),
×
375
                        $this->assembleFilters()
×
376
                );
377

378
                $hasGroupActionOnRows = false;
×
379

380
                foreach ($items as $item) {
×
381
                        $rows[] = $row = new Row($this, $item, $this->getPrimaryKey());
×
382

383
                        if (!$hasGroupActionOnRows && $row->hasGroupAction()) {
×
384
                                $hasGroupActionOnRows = true;
×
385
                        }
386

387
                        if ($this->rowCallback !== null) {
×
388
                                ($this->rowCallback)($item, $row->getControl());
×
389
                        }
390

391
                        /**
392
                         * Walkaround for item snippet - snippet is the <tr> element and its class has to be also updated
393
                         */
394
                        if ($this->redrawItem !== []) {
×
395
                                $this->getPresenterInstance()->payload->_datagrid_redrawItem_class = $row->getControlClass();
×
396
                                $this->getPresenterInstance()->payload->_datagrid_redrawItem_id = $row->getId();
×
397
                        }
398
                }
399

400
                if ($hasGroupActionOnRows) {
×
401
                        $hasGroupActionOnRows = $this->hasGroupActions();
×
402
                }
403

404
                if ($this->isTreeView()) {
×
405
                        $template->add('treeViewHasChildrenColumn', $this->treeViewHasChildrenColumn);
×
406
                }
407

408
                $template->rows = $rows;
×
409

410
                $template->columns = $this->getColumns();
×
411
                $template->actions = $this->actions;
×
412
                $template->exports = $this->exports;
×
413
                $template->filters = $this->filters;
×
414
                $template->toolbarButtons = $this->toolbarButtons;
×
415
                $template->aggregationFunctions = $this->getAggregationFunctions();
×
416
                $template->multipleAggregationFunction = $this->getMultipleAggregationFunction();
×
417

418
                $template->filter_active = $this->isFilterActive();
×
419
                $template->originalTemplate = $this->getOriginalTemplateFile();
×
420
                $template->iconPrefix = static::$iconPrefix;
×
421
                $template->btnSecondaryClass = static::$btnSecondaryClass;
×
422
                $template->itemsDetail = $this->itemsDetail;
×
423
                $template->columnsVisibility = $this->getColumnsVisibility();
×
424
                $template->columnsSummary = $this->columnsSummary;
×
425

426
                $template->inlineEdit = $this->inlineEdit;
×
427
                $template->inlineAdd = $this->inlineAdd;
×
428

429
                $template->hasGroupActions = $this->hasGroupActions();
×
430
                $template->hasGroupActionOnRows = $hasGroupActionOnRows;
×
431

432
                /**
433
                 * Walkaround for Latte (does not know $form in snippet in {form} etc)
434
                 */
435
                $template->filter = $this['filter'];
×
436

437
                /**
438
                 * Set template file and render it
439
                 */
440
                $template->setFile($this->getTemplateFile());
×
441
                $template->render();
×
442
        }
443

444

445
        /********************************************************************************
446
         *                                 ROW CALLBACK *
447
         ********************************************************************************/
448

449
        /**
450
         * Each row can be modified with user defined callback
451
         *
452
         * @return static
453
         */
454
        public function setRowCallback(callable $callback): self
455
        {
456
                $this->rowCallback = $callback;
×
457

458
                return $this;
×
459
        }
460

461

462
        /********************************************************************************
463
         *                                 DATA SOURCE *
464
         ********************************************************************************/
465

466
        /**
467
         * @return static
468
         */
469
        public function setPrimaryKey(string $primaryKey): self
470
        {
471
                if ($this->dataModel instanceof DataModel) {
×
472
                        throw new DatagridException('Please set datagrid primary key before setting datasource.');
×
473
                }
474

475
                $this->primaryKey = $primaryKey;
×
476

477
                return $this;
×
478
        }
479

480
        /**
481
         * @return static
482
         * @throws InvalidArgumentException
483
         */
484
        public function setDataSource(mixed $source): self
1✔
485
        {
486
                $this->dataModel = new DataModel($source, $this->primaryKey);
1✔
487

488
                $this->dataModel->onBeforeFilter[] = [$this, 'beforeDataModelFilter'];
1✔
489
                $this->dataModel->onAfterFilter[] = [$this, 'afterDataModelFilter'];
1✔
490
                $this->dataModel->onAfterPaginated[] = [$this, 'afterDataModelPaginated'];
1✔
491

492
                return $this;
1✔
493
        }
494

495
        public function getDataSource(): IDataSource|array|null
496
        {
497
                return isset($this->dataModel)
×
498
                        ? $this->dataModel->getDataSource()
×
499
                        : null;
×
500
        }
501

502

503
        /********************************************************************************
504
         *                                  TEMPLATING *
505
         ********************************************************************************/
506

507
        /**
508
         * @return static
509
         */
510
        public function setTemplateFile(string $templateFile): self
511
        {
512
                $this->templateFile = $templateFile;
×
513

514
                return $this;
×
515
        }
516

517
        public function getTemplateFile(): string
518
        {
519
                return $this->templateFile ?? $this->getOriginalTemplateFile();
×
520
        }
521

522
        public function getOriginalTemplateFile(): string
523
        {
524
                return __DIR__ . '/templates/datagrid.latte';
×
525
        }
526

527
        /********************************************************************************
528
         *                                   SORTING *
529
         ********************************************************************************/
530

531
        /**
532
         * @return static
533
         */
534
        public function setDefaultSort(string|array $sort, bool $useOnReset = true): self
535
        {
536
                $sort = is_string($sort)
×
537
                        ? [$sort => 'ASC']
×
538
                        : $sort;
×
539

540
                $this->defaultSort = $sort;
×
541
                $this->defaultSortUseOnReset = $useOnReset;
×
542

543
                return $this;
×
544
        }
545

546
        /**
547
         * Return default sort for column, if specified
548
         */
549
        public function getColumnDefaultSort(string $columnKey): ?string
550
        {
551
                if (isset($this->defaultSort[$columnKey])) {
×
552
                        return $this->defaultSort[$columnKey];
×
553
                }
554

555
                return null;
×
556
        }
557

558
        /**
559
         * User may set default sorting, apply it
560
         */
561
        public function findDefaultSort(): void
562
        {
563
                if ($this->sort !== []) {
1✔
564
                        return;
×
565
                }
566

567
                if ((bool) $this->getStorageData('_grid_has_sorted')) {
1✔
568
                        return;
×
569
                }
570

571
                if ($this->defaultSort !== []) {
1✔
572
                        $this->sort = $this->defaultSort;
×
573
                }
574

575
                $this->saveStorageData('_grid_sort', $this->sort);
1✔
576
        }
1✔
577

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

588
                $this->sortable = $sortable;
×
589

590
                return $this;
×
591
        }
592

593
        public function isSortable(): bool
594
        {
595
                return $this->sortable;
×
596
        }
597

598
        /**
599
         * @return static
600
         */
601
        public function setMultiSortEnabled(bool $multiSort = true): self
602
        {
603
                $this->multiSort = $multiSort;
×
604

605
                return $this;
×
606
        }
607

608
        public function isMultiSortEnabled(): bool
609
        {
610
                return $this->multiSort;
×
611
        }
612

613
        /**
614
         * @return static
615
         */
616
        public function setSortableHandler(string $handler = 'sort!'): self
617
        {
618
                $this->sortableHandler = $handler;
×
619

620
                return $this;
×
621
        }
622

623
        public function getSortableHandler(): string
624
        {
625
                return $this->sortableHandler;
×
626
        }
627

628
        public function getSortNext(Column $column): array
629
        {
630
                $sort = $column->getSortNext();
×
631

632
                if ($this->isMultiSortEnabled()) {
×
633
                        $sort = array_merge($this->sort, $sort);
×
634
                }
635

636
                return array_filter($sort);
×
637
        }
638

639
        /********************************************************************************
640
         *                                  TREE VIEW *
641
         ********************************************************************************/
642
        public function isTreeView(): bool
643
        {
644
                return $this->treeViewChildrenCallback !== null;
×
645
        }
646

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

660
                $this->treeViewChildrenCallback = $getChildrenCallback;
×
661
                $this->treeViewHasChildrenColumn = $treeViewHasChildrenColumn;
×
662

663
                /**
664
                 * Torn off pagination
665
                 */
666
                $this->setPagination(false);
×
667

668
                /**
669
                 * Set tree view template file
670
                 */
671
                if ($this->templateFile === null) {
×
672
                        $this->setTemplateFile(__DIR__ . '/templates/datagrid_tree.latte');
×
673
                }
674

675
                return $this;
×
676
        }
677

678
        public function hasTreeViewChildrenCallback(): bool
679
        {
680
                return is_callable($this->treeViewHasChildrenCallback);
×
681
        }
682

683
        public function treeViewChildrenCallback(mixed $item): bool
684
        {
685
                if ($this->treeViewHasChildrenCallback === null) {
×
686
                        throw new UnexpectedValueException();
×
687
                }
688

689
                return (bool) call_user_func($this->treeViewHasChildrenCallback, $item);
×
690
        }
691

692
        /********************************************************************************
693
         *                                    COLUMNS *
694
         ********************************************************************************/
695
        public function addColumnText(
1✔
696
                string $key,
697
                string $name,
698
                ?string $column = null
699
        ): ColumnText
700
        {
701
                $column ??= $key;
1✔
702

703
                $columnText = new ColumnText($this, $key, $column, $name);
1✔
704
                $this->addColumn($key, $columnText);
1✔
705

706
                return $columnText;
1✔
707
        }
708

709
        public function addColumnLink(
1✔
710
                string $key,
711
                string $name,
712
                ?string $href = null,
713
                ?string $column = null,
714
                ?array $params = null
715
        ): ColumnLink
716
        {
717
                $column ??= $key;
1✔
718
                $href ??= $key;
1✔
719

720
                if ($params === null) {
1✔
721
                        $params = [$this->primaryKey];
1✔
722
                }
723

724
                $columnLink = new ColumnLink($this, $key, $column, $name, $href, $params);
1✔
725
                $this->addColumn($key, $columnLink);
1✔
726

727
                return $columnLink;
1✔
728
        }
729

730
        public function addColumnNumber(
1✔
731
                string $key,
732
                string $name,
733
                ?string $column = null
734
        ): ColumnNumber
735
        {
736
                $column ??= $key;
1✔
737

738
                $columnNumber = new ColumnNumber($this, $key, $column, $name);
1✔
739
                $this->addColumn($key, $columnNumber);
1✔
740

741
                return $columnNumber;
1✔
742
        }
743

744
        public function addColumnDateTime(
1✔
745
                string $key,
746
                string $name,
747
                ?string $column = null
748
        ): ColumnDateTime
749
        {
750
                $column ??= $key;
1✔
751

752
                $columnDateTime = new ColumnDateTime($this, $key, $column, $name);
1✔
753
                $this->addColumn($key, $columnDateTime);
1✔
754

755
                return $columnDateTime;
1✔
756
        }
757

758
        public function addColumnStatus(
1✔
759
                string $key,
760
                string $name,
761
                ?string $column = null
762
        ): ColumnStatus
763
        {
764
                $column ??= $key;
1✔
765

766
                $columnStatus = new ColumnStatus($this, $key, $column, $name);
1✔
767
                $this->addColumn($key, $columnStatus);
1✔
768

769
                return $columnStatus;
1✔
770
        }
771

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

783
                return $this->columns[$key];
1✔
784
        }
785

786
        /**
787
         * @return static
788
         */
789
        public function removeColumn(string $key): self
1✔
790
        {
791
                unset($this->columnsVisibility[$key], $this->columns[$key]);
1✔
792

793
                return $this;
1✔
794
        }
795

796
        /********************************************************************************
797
         *                                    ACTIONS *
798
         ********************************************************************************/
799
        public function addAction(
1✔
800
                string $key,
801
                string $name,
802
                ?string $href = null,
803
                ?array $params = null
804
        ): Action
805
        {
806
                $this->addActionCheck($key);
1✔
807

808
                $href ??= $key;
1✔
809

810
                if ($params === null) {
1✔
811
                        $params = [$this->primaryKey];
1✔
812
                }
813

814
                return $this->actions[$key] = new Action($this, $key, $href, $name, $params);
1✔
815
        }
816

817
        public function addActionCallback(
818
                string $key,
819
                string $name,
820
                ?callable $callback = null
821
        ): ActionCallback
822
        {
823
                $this->addActionCheck($key);
×
824

825
                $params = ['__id' => $this->primaryKey];
×
826

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

829
                if ($callback !== null) {
×
830
                        $action->onClick[] = $callback;
×
831
                }
832

833
                return $action;
×
834
        }
835

836
        public function addMultiAction(string $key, string $name): MultiAction
837
        {
838
                $this->addActionCheck($key);
×
839

840
                $action = new MultiAction($this, $key, $name);
×
841

842
                $this->actions[$key] = $action;
×
843

844
                return $action;
×
845
        }
846

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

856
                return $this->actions[$key];
×
857
        }
858

859
        /**
860
         * @return static
861
         */
862
        public function removeAction(string $key): self
863
        {
864
                unset($this->actions[$key]);
×
865

866
                return $this;
×
867
        }
868

869
        public function addFilterText(
1✔
870
                string $key,
871
                string $name,
872
                array|string|null $columns = null
873
        ): FilterText
874
        {
875
                $columns = $columns === null ? [$key] : (is_string($columns) ? [$columns] : $columns);
1✔
876

877
                $this->addFilterCheck($key);
1✔
878

879
                return $this->filters[$key] = new FilterText($this, $key, $name, $columns);
1✔
880
        }
881

882
        public function addFilterSelect(
883
                string $key,
884
                string $name,
885
                array $options,
886
                ?string $column = null
887
        ): FilterSelect
888
        {
889
                $column ??= $key;
×
890

891
                $this->addFilterCheck($key);
×
892

893
                return $this->filters[$key] = new FilterSelect($this, $key, $name, $options, $column);
×
894
        }
895

896
        public function addFilterMultiSelect(
897
                string $key,
898
                string $name,
899
                array $options,
900
                ?string $column = null
901
        ): FilterMultiSelect
902
        {
903
                $column ??= $key;
×
904

905
                $this->addFilterCheck($key);
×
906

907
                return $this->filters[$key] = new FilterMultiSelect($this, $key, $name, $options, $column);
×
908
        }
909

910
        public function addFilterDate(string $key, string $name, ?string $column = null): FilterDate
911
        {
912
                $column ??= $key;
×
913

914
                $this->addFilterCheck($key);
×
915

916
                return $this->filters[$key] = new FilterDate($this, $key, $name, $column);
×
917
        }
918

919
        public function addFilterRange(
920
                string $key,
921
                string $name,
922
                ?string $column = null,
923
                string $nameSecond = '-'
924
        ): FilterRange
925
        {
926
                $column ??= $key;
×
927

928
                $this->addFilterCheck($key);
×
929

930
                return $this->filters[$key] = new FilterRange($this, $key, $name, $column, $nameSecond);
×
931
        }
932

933
        /**
934
         * @throws DatagridException
935
         */
936
        public function addFilterDateRange(
937
                string $key,
938
                string $name,
939
                ?string $column = null,
940
                string $nameSecond = '-'
941
        ): FilterDateRange
942
        {
943
                $column ??= $key;
×
944

945
                $this->addFilterCheck($key);
×
946

947
                return $this->filters[$key] = new FilterDateRange($this, $key, $name, $column, $nameSecond);
×
948
        }
949

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

962
                                continue;
×
963
                        }
964

965
                        if (is_iterable($value)) {
×
966
                                if (!ArraysHelper::testEmpty($value)) {
×
967
                                        $this->filters[$key]->setValue($value);
×
968
                                }
969
                        } else {
970
                                if ($value !== '' && $value !== null) {
×
971
                                        $this->filters[$key]->setValue($value);
×
972
                                }
973
                        }
974
                }
975

976
                foreach ($this->columns as $key => $column) {
1✔
977
                        if (isset($this->sort[$key])) {
×
978
                                $column->setSort($this->sort[$key]);
×
979
                        }
980
                }
981

982
                $this->onFiltersAssembled($this->filters);
1✔
983

984
                return $this->filters;
1✔
985
        }
986

987
        /**
988
         * @return static
989
         */
990
        public function removeFilter(string $key): self
991
        {
992
                unset($this->filters[$key]);
×
993

994
                return $this;
×
995
        }
996

997
        public function getFilter(string $key): Filter
1✔
998
        {
999
                if (!isset($this->filters[$key])) {
1✔
1000
                        throw new DatagridException(sprintf('Filter [%s] is not defined', $key));
×
1001
                }
1002

1003
                return $this->filters[$key];
1✔
1004
        }
1005

1006
        /**
1007
         * @deprecated Use setStrictStorageFilterValues() instead
1008
         * @return static
1009
         */
1010
        public function setStrictSessionFilterValues(bool $strictStorageFilterValues = true): self
1011
        {
1012
                return $this->setStrictStorageFilterValues($strictStorageFilterValues);
×
1013
        }
1014

1015
        /**
1016
         * @return static
1017
         */
1018
        public function setStrictStorageFilterValues(bool $strictStorageFilterValues = true): self
1019
        {
1020
                $this->strictStorageFilterValues = $strictStorageFilterValues;
×
1021

1022
                return $this;
×
1023
        }
1024

1025
        /********************************************************************************
1026
         *                                  FILTERING *
1027
         ********************************************************************************/
1028
        public function isFilterActive(): bool
1029
        {
1030
                $is_filter = ArraysHelper::testTruthy($this->filter);
×
1031

1032
                return $is_filter || $this->forceFilterActive;
×
1033
        }
1034

1035
        public function isFilterDefault(): bool
1036
        {
1037
                return $this->filter === $this->defaultFilter;
1✔
1038
        }
1039

1040
        /**
1041
         * Tell that filter is active from whatever reasons
1042
         *
1043
         * @return static
1044
         */
1045
        public function setFilterActive(): self
1046
        {
1047
                $this->forceFilterActive = true;
×
1048

1049
                return $this;
×
1050
        }
1051

1052
        /**
1053
         * Set filter values (force - overwrite user data)
1054
         *
1055
         * @return static
1056
         */
1057
        public function setFilter(array $filter): self
1✔
1058
        {
1059
                $this->filter = $filter;
1✔
1060

1061
                $this->saveStorageData('_grid_has_filtered', 1);
1✔
1062

1063
                return $this;
1✔
1064
        }
1065

1066
        /**
1067
         * If we want to sent some initial filter
1068
         *
1069
         * @return static
1070
         * @throws DatagridException
1071
         */
1072
        public function setDefaultFilter(array $defaultFilter, bool $useOnReset = true): self
1✔
1073
        {
1074
                foreach ($defaultFilter as $key => $value) {
1✔
1075
                        /** @var Filter|null $filter */
1076
                        $filter = $this->getFilter($key);
1✔
1077

1078
                        if ($filter === null) {
1✔
1079
                                throw new DatagridException(
×
1080
                                        sprintf('Can not set default value to nonexisting filter [%s]', $key)
×
1081
                                );
1082
                        }
1083

1084
                        if ($filter instanceof FilterMultiSelect && !is_array($value)) {
1✔
1085
                                throw new DatagridException(
×
1086
                                        sprintf('Default value of filter [%s] - MultiSelect has to be an array', $key)
×
1087
                                );
1088
                        }
1089

1090
                        if ($filter instanceof FilterRange || $filter instanceof FilterDateRange) {
1✔
1091
                                if (!is_array($value)) {
×
1092
                                        throw new DatagridException(
×
1093
                                                sprintf('Default value of filter [%s] - Range/DateRange has to be an array [from/to => ...]', $key)
×
1094
                                        );
1095
                                }
1096

1097
                                $temp = $value;
×
1098
                                unset($temp['from'], $temp['to']);
×
1099

1100
                                if (count($temp) > 0) {
×
1101
                                        throw new DatagridException(
×
1102
                                                sprintf(
×
1103
                                                        'Default value of filter [%s] - Range/DateRange can contain only [from/to => ...] values',
×
1104
                                                        $key
1105
                                                )
1106
                                        );
1107
                                }
1108
                        }
1109
                }
1110

1111
                $this->defaultFilter = $defaultFilter;
1✔
1112
                $this->defaultFilterUseOnReset = $useOnReset;
1✔
1113

1114
                return $this;
1✔
1115
        }
1116

1117
        public function findDefaultFilter(): void
1118
        {
1119
                if ($this->filter !== []) {
1✔
1120
                        return;
×
1121
                }
1122

1123
                if ((bool) $this->getStorageData('_grid_has_filtered')) {
1✔
1124
                        return;
×
1125
                }
1126

1127
                if ($this->defaultFilter !== []) {
1✔
1128
                        $this->filter = $this->defaultFilter;
×
1129
                }
1130

1131
                $storedFilters = $this->getStorageData('_grid_filters', []);
1✔
1132
                foreach ($this->filter as $key => $value) {
1✔
1133
                        $storedFilters[(string) $key] = $value;
×
1134
                }
1135

1136
                $this->saveStorageData('_grid_filters', $storedFilters);
1✔
1137
        }
1✔
1138

1139
        public function createComponentFilter(): Form
1140
        {
1141
                $form = new Form($this, 'filter');
1✔
1142

1143
                $form->setMethod(static::$formMethod);
1✔
1144

1145
                $form->setTranslator($this->getTranslator());
1✔
1146

1147
                /**
1148
                 * InlineEdit part
1149
                 */
1150
                $inline_edit_container = $form->addContainer('inline_edit');
1✔
1151

1152
                if ($this->inlineEdit instanceof InlineEdit) {
1✔
1153
                        $inline_edit_container->addSubmit('submit', 'contributte_datagrid.save')
×
1154
                                ->setValidationScope([$inline_edit_container]);
×
1155
                        $inline_edit_container->addSubmit('cancel', 'contributte_datagrid.cancel')
×
1156
                                ->setValidationScope(null);
×
1157

1158
                        $this->inlineEdit->onControlAdd($inline_edit_container);
×
1159
                        $this->inlineEdit->onControlAfterAdd($inline_edit_container);
×
1160
                }
1161

1162
                /**
1163
                 * InlineAdd part
1164
                 */
1165
                $inlineAddContainer = $form->addContainer('inline_add');
1✔
1166

1167
                if ($this->inlineAdd instanceof InlineAdd) {
1✔
1168
                        $inlineAddContainer->addSubmit('submit', 'contributte_datagrid.save')
1✔
1169
                                ->setValidationScope([$inlineAddContainer]);
1✔
1170
                        $inlineAddContainer->addSubmit('cancel', 'contributte_datagrid.cancel')
1✔
1171
                                ->setValidationScope(null)
1✔
1172
                                ->setHtmlAttribute('data-datagrid-cancel-inline-add', true);
1✔
1173

1174
                        $this->inlineAdd->onControlAdd($inlineAddContainer);
1✔
1175
                        $this->inlineAdd->onControlAfterAdd($inlineAddContainer);
1✔
1176
                }
1177

1178
                /**
1179
                 * ItemDetail form part
1180
                 */
1181
                $itemsDetailForm = $this->getItemDetailForm();
1✔
1182

1183
                if ($itemsDetailForm instanceof Container) {
1✔
1184
                        $form['items_detail_form'] = $itemsDetailForm;
×
1185
                }
1186

1187
                /**
1188
                 * Filter part
1189
                 */
1190
                $filterContainer = $form->addContainer('filter');
1✔
1191

1192
                foreach ($this->filters as $filter) {
1✔
1193
                        $filter->addToFormContainer($filterContainer);
1✔
1194
                }
1195

1196
                if (!$this->hasAutoSubmit()) {
1✔
1197
                        $filterContainer['submit'] = $this->getFilterSubmitButton();
×
1198
                }
1199

1200
                /**
1201
                 * Group action part
1202
                 */
1203
                $groupActionContainer = $form->addContainer('group_action');
1✔
1204

1205
                if ($this->hasGroupActions()) {
1✔
1206
                        $this->getGroupActionCollection()->addToFormContainer($groupActionContainer);
×
1207
                }
1208

1209
                if ($form->isSubmitted() === false) {
1✔
1210
                        $this->setFilterContainerDefaults($filterContainer, $this->filter);
1✔
1211
                }
1212

1213
                /**
1214
                 * Per page part
1215
                 */
1216
                if ($this->isPaginated()) {
1✔
1217
                        $select = $form->addSelect('perPage', '', $this->getItemsPerPageList())
1✔
1218
                                ->setTranslator(null);
1✔
1219

1220
                        if ($form->isSubmitted() === false) {
1✔
1221
                                $select->setValue($this->getPerPage());
1✔
1222
                        }
1223

1224
                        $form->addSubmit('perPage_submit', 'contributte_datagrid.per_page_submit')
1✔
1225
                                ->setValidationScope([$select]);
1✔
1226
                }
1227

1228
                $form->onSubmit[] = function (NetteForm $form): void {
1✔
1229
                        $this->filterSucceeded($form);
1230
                };
1231

1232
                return $form;
1✔
1233
        }
1234

1235
        public function setFilterContainerDefaults(Container $container, array $values, ?string $parentKey = null): void
1✔
1236
        {
1237
                foreach ($container->getComponents() as $key => $control) {
1✔
1238
                        if (!isset($values[$key])) {
1✔
1239
                                continue;
×
1240
                        }
1241

1242
                        if ($control instanceof Container) {
1✔
1243
                                $this->setFilterContainerDefaults($control, (array) $values[$key], (string) $key);
×
1244

1245
                                continue;
×
1246
                        }
1247

1248
                        $value = $values[$key];
1✔
1249

1250
                        if ($value instanceof DateTime) {
1✔
1251
                                $filter = $parentKey !== null ? $this->getFilter($parentKey) : $this->getFilter((string) $key);
×
1252

1253
                                if ($filter instanceof IFilterDate) {
×
1254
                                        $value = $value->format($filter->getPhpFormat());
×
1255
                                }
1256
                        }
1257

1258
                        try {
1259
                                if (!$control instanceof FormControl) {
1✔
1260
                                        throw new UnexpectedValueException();
×
1261
                                }
1262

1263
                                $control->setValue($value);
1✔
1264

1265
                        } catch (InvalidArgumentException $e) {
×
1266
                                if ($this->strictStorageFilterValues) {
×
1267
                                        throw $e;
×
1268
                                }
1269
                        }
1270
                }
1271
        }
1✔
1272

1273
        /**
1274
         * Set $this->filter values after filter form submitted
1275
         */
1276
        public function filterSucceeded(NetteForm $form): void
1✔
1277
        {
1278
                if ($this->snippetsSet) {
1✔
1279
                        return;
×
1280
                }
1281

1282
                if ($this->getPresenterInstance()->isAjax()) {
1✔
1283
                        if (isset($form['group_action']['submit']) && $form['group_action']['submit']->isSubmittedBy()) {
×
1284
                                return;
×
1285
                        }
1286
                }
1287

1288
                /**
1289
                 * Per page
1290
                 */
1291
                if ($form->getComponent('perPage', false) !== null) {
1✔
1292
                        $perPage = $form->getComponent('perPage')->getValue();
1✔
1293
                        $this->saveStorageData('_grid_perPage', $perPage);
1✔
1294
                        $this->perPage = $perPage;
1✔
1295
                }
1296

1297
                /**
1298
                 * Inline edit
1299
                 */
1300
                if (
1301
                        isset($form['inline_edit'])
1✔
1302
                        && isset($form['inline_edit']['submit'])
1✔
1303
                        && isset($form['inline_edit']['cancel'])
1✔
1304
                        && $this->inlineEdit !== null
1✔
1305
                ) {
1306
                        $edit = $form['inline_edit'];
×
1307

1308
                        if (
1309
                                !$edit instanceof Container
×
1310
                                || !$edit['submit'] instanceof FormsSubmitButton
×
1311
                                || !$edit['cancel'] instanceof FormsSubmitButton
×
1312
                        ) {
1313
                                throw new UnexpectedValueException();
×
1314
                        }
1315

1316
                        if ($edit['submit']->isSubmittedBy() || $edit['cancel']->isSubmittedBy()) {
×
1317
                                /** @var string $id */
1318
                                $id = $form->getHttpData(Form::DataLine, 'inline_edit[_id]');
×
1319
                                $primaryWhereColumn = $form->getHttpData(Form::DataLine, 'inline_edit[_primary_where_column]');
×
1320

1321
                                if ($edit->getComponent('submit')->isSubmittedBy() && $edit->getErrors() === []) {
×
1322
                                        $this->inlineEdit->onSubmit($id, $form->getComponent('inline_edit')->getValues());
×
1323
                                        $this->getPresenterInstance()->payload->_datagrid_inline_edited = $id;
×
1324
                                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1325
                                } else {
1326
                                        $this->getPresenterInstance()->payload->_datagrid_inline_edit_cancel = $id;
×
1327
                                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1328
                                }
1329

1330
                                if ($edit['submit']->isSubmittedBy() && $this->inlineEdit->onCustomRedraw !== []) {
×
1331
                                        $this->inlineEdit->onCustomRedraw('submit');
×
1332
                                } elseif ($edit['cancel']->isSubmittedBy() && $this->inlineEdit->onCustomRedraw !== []) {
×
1333
                                        $this->inlineEdit->onCustomRedraw('cancel');
×
1334
                                } else {
1335
                                        $this->redrawItem($id, $primaryWhereColumn);
×
1336
                                        $this->redrawControl('summary');
×
1337
                                }
1338

1339
                                return;
×
1340
                        }
1341
                }
1342

1343
                /**
1344
                 * Inline add
1345
                 */
1346
                if (
1347
                        isset($form['inline_add'])
1✔
1348
                        && isset($form['inline_add']['submit'])
1✔
1349
                        && isset($form['inline_add']['cancel'])
1✔
1350
                        && $this->inlineAdd !== null
1✔
1351
                ) {
1352
                        $add = $form['inline_add'];
1✔
1353

1354
                        if (
1355
                                !$add instanceof Container
1✔
1356
                                || !$add['submit'] instanceof FormsSubmitButton
1✔
1357
                                || !$add['cancel'] instanceof FormsSubmitButton
1✔
1358
                        ) {
1359
                                throw new UnexpectedValueException();
×
1360
                        }
1361

1362
                        if ($add['submit']->isSubmittedBy() || $add['cancel']->isSubmittedBy()) {
1✔
1363
                                if ($add['submit']->isSubmittedBy() && $add->getErrors() === []) {
×
1364
                                        $this->inlineAdd->onSubmit($form->getComponent('inline_add')->getValues());
×
1365
                                }
1366

1367
                                $this->redrawControl('tbody');
×
1368

1369
                                $this->onRedraw();
×
1370

1371
                                return;
×
1372
                        }
1373
                }
1374

1375
                /**
1376
                 * Filter itself
1377
                 */
1378
                $values = $form->getComponent('filter')->getValues();
1✔
1379

1380
                if (!$values instanceof ArrayHash) {
1✔
1381
                        throw new UnexpectedValueException();
×
1382
                }
1383

1384
                $storedFilters = $this->getStorageData('_grid_filters', []);
1✔
1385
                foreach ($values as $key => $value) {
1✔
1386
                        /**
1387
                         * Storage stuff
1388
                         */
1389
                        $storedValue = $storedFilters[(string) $key] ?? null;
1✔
1390
                        if ($this->rememberState && $storedValue !== $value) {
1✔
1391
                                /**
1392
                                 * Has been filter changed?
1393
                                 */
1394
                                $this->page = 1;
1✔
1395
                                $this->saveStorageData('_grid_page', 1);
1✔
1396
                        }
1397

1398
                        $storedFilters[(string) $key] = $value;
1✔
1399

1400
                        /**
1401
                         * Other stuff
1402
                         */
1403
                        $this->filter[$key] = $value;
1✔
1404
                }
1405

1406
                $this->saveStorageData('_grid_filters', $storedFilters);
1✔
1407

1408
                if ($values->count() > 0) {
1✔
1409
                        $this->saveStorageData('_grid_has_filtered', 1);
1✔
1410
                }
1411

1412
                if ($this->getPresenterInstance()->isAjax()) {
1✔
1413
                        $this->getPresenterInstance()->payload->_datagrid_sort = [];
×
1414

1415
                        foreach ($this->columns as $key => $column) {
×
1416
                                if ($column->isSortable()) {
×
1417
                                        $this->getPresenterInstance()->payload->_datagrid_sort[$key] = $this->link('sort!', [
×
1418
                                                'sort' => $column->getSortNext(),
×
1419
                                        ]);
1420
                                }
1421
                        }
1422
                }
1423

1424
                $this->reload();
1✔
1425
        }
1426

1427
        /**
1428
         * @return static
1429
         */
1430
        public function setOuterFilterRendering(bool $outerFilterRendering = true): self
1431
        {
1432
                $this->outerFilterRendering = $outerFilterRendering;
×
1433

1434
                return $this;
×
1435
        }
1436

1437
        public function hasOuterFilterRendering(): bool
1438
        {
1439
                return $this->outerFilterRendering;
×
1440
        }
1441

1442
        /**
1443
         * @return static
1444
         * @throws InvalidArgumentException
1445
         */
1446
        public function setOuterFilterColumnsCount(int $count): self
1447
        {
1448
                $columnsCounts = [1, 2, 3, 4, 6, 12];
×
1449

1450
                if (!in_array($count, $columnsCounts, true)) {
×
1451
                        throw new InvalidArgumentException(sprintf(
×
1452
                                'Columns count must be one of following values: %s. Value %s given.',
×
1453
                                implode(', ', $columnsCounts),
×
1454
                                $count
1455
                        ));
1456
                }
1457

1458
                $this->outerFilterColumnsCount = $count;
×
1459

1460
                return $this;
×
1461
        }
1462

1463
        public function getOuterFilterColumnsCount(): int
1464
        {
1465
                return $this->outerFilterColumnsCount;
×
1466
        }
1467

1468
        /**
1469
         * @return static
1470
         */
1471
        public function setCollapsibleOuterFilters(bool $collapsibleOuterFilters = true): self
1472
        {
1473
                $this->collapsibleOuterFilters = $collapsibleOuterFilters;
×
1474

1475
                return $this;
×
1476
        }
1477

1478
        public function hasCollapsibleOuterFilters(): bool
1479
        {
1480
                return $this->collapsibleOuterFilters;
×
1481
        }
1482

1483
        /**
1484
         * Try to restore storage stuff
1485
         *
1486
         * @throws DatagridFilterNotFoundException
1487
         */
1488
        public function findStorageValues(): void
1489
        {
1490
                if (!ArraysHelper::testEmpty($this->filter) || ($this->page !== 1) || $this->sort !== []) {
1✔
1491
                        return;
×
1492
                }
1493

1494
                if (!$this->rememberState) {
1✔
1495
                        return;
×
1496
                }
1497

1498
                $page = $this->getStorageData('_grid_page');
1✔
1499

1500
                if ($page !== null) {
1✔
1501
                        $this->page = (int) $page;
×
1502
                }
1503

1504
                $perPage = $this->getStorageData('_grid_perPage');
1✔
1505

1506
                if ($perPage !== null) {
1✔
1507
                        $this->perPage = $perPage;
×
1508
                }
1509

1510
                $sort = $this->getStorageData('_grid_sort');
1✔
1511

1512
                if (is_array($sort) && $sort !== []) {
1✔
1513
                        $this->sort = $sort;
×
1514
                }
1515

1516
                foreach ($this->getStorageData('_grid_filters', []) as $key => $value) {
1✔
1517
                        try {
1518
                                $this->getFilter($key);
×
1519

1520
                                $this->filter[$key] = $value;
×
1521

1522
                        } catch (DatagridException) {
×
1523
                                if ($this->strictStorageFilterValues) {
×
1524
                                        throw new DatagridFilterNotFoundException(
×
1525
                                                sprintf('Storage filter: Filter [%s] not found', $key)
×
1526
                                        );
1527
                                }
1528
                        }
1529
                }
1530

1531
                /**
1532
                 * When column is sorted via custom callback, apply it
1533
                 */
1534
                if ($this->sortCallback === null && $this->sort !== []) {
1✔
1535
                        foreach (array_keys($this->sort) as $key) {
×
1536
                                try {
1537
                                        $column = $this->getColumn((string) $key);
×
1538

1539
                                } catch (DatagridColumnNotFoundException) {
×
1540
                                        $this->deleteStorageData('_grid_sort');
×
1541
                                        $this->sort = [];
×
1542

1543
                                        return;
×
1544
                                }
1545

1546
                                if ($column->isSortable() && is_callable($column->getSortableCallback())) {
×
1547
                                        $this->sortCallback = $column->getSortableCallback();
×
1548
                                }
1549
                        }
1550
                }
1551
        }
1✔
1552

1553
        /********************************************************************************
1554
         *                                    EXPORTS *
1555
         ********************************************************************************/
1556
        public function addExportCallback(
1✔
1557
                string $text,
1558
                callable $callback,
1559
                bool $filtered = false
1560
        ): Export
1561
        {
1562
                return $this->addToExports(new Export($this, $text, $callback, $filtered));
1✔
1563
        }
1564

1565
        public function addExportCsvFiltered(
1566
                string $text,
1567
                string $csvFileName,
1568
                string $outputEncoding = 'utf-8',
1569
                string $delimiter = ';',
1570
                bool $includeBom = false
1571
        ): ExportCsv
1572
        {
1573
                return $this->addExportCsv($text, $csvFileName, $outputEncoding, $delimiter, $includeBom, true);
×
1574
        }
1575

1576
        public function addExportCsv(
1✔
1577
                string $text,
1578
                string $csvFileName,
1579
                string $outputEncoding = 'utf-8',
1580
                string $delimiter = ';',
1581
                bool $includeBom = false,
1582
                bool $filtered = false
1583
        ): ExportCsv
1584
        {
1585
                $exportCsv = new ExportCsv($this, $text, $csvFileName, $filtered, $outputEncoding, $delimiter, $includeBom);
1✔
1586

1587
                $this->addToExports($exportCsv);
1✔
1588

1589
                return $exportCsv;
1✔
1590
        }
1591

1592
        public function resetExportsLinks(): void
1593
        {
1594
                foreach ($this->exports as $id => $export) {
×
1595
                        $link = new Link($this, 'export!', ['id' => $id]);
×
1596

1597
                        $export->setLink($link);
×
1598
                }
1599
        }
1600

1601

1602
        /********************************************************************************
1603
         *                                TOOLBAR BUTTONS *
1604
         ********************************************************************************/
1605

1606
        /**
1607
         * @throws DatagridException
1608
         */
1609
        public function addToolbarButton(
1610
                string $href,
1611
                string $text = '',
1612
                array $params = []
1613
        ): ToolbarButton
1614
        {
1615
                if (isset($this->toolbarButtons[$href])) {
×
1616
                        throw new DatagridException(
×
1617
                                sprintf('There is already toolbar button at key [%s] defined.', $href)
×
1618
                        );
1619
                }
1620

1621
                return $this->toolbarButtons[$href] = new ToolbarButton($this, $href, $text, $params);
×
1622
        }
1623

1624
        /**
1625
         * @throws DatagridException
1626
         */
1627
        public function getToolbarButton(string $key): ToolbarButton
1628
        {
1629
                if (!isset($this->toolbarButtons[$key])) {
×
1630
                        throw new DatagridException(
×
1631
                                sprintf('There is no toolbar button at key [%s] defined.', $key)
×
1632
                        );
1633
                }
1634

1635
                return $this->toolbarButtons[$key];
×
1636
        }
1637

1638
        /**
1639
         * @return static
1640
         */
1641
        public function removeToolbarButton(string $key): self
1642
        {
1643
                unset($this->toolbarButtons[$key]);
×
1644

1645
                return $this;
×
1646
        }
1647

1648
        /********************************************************************************
1649
         *                                 GROUP ACTIONS *
1650
         ********************************************************************************/
1651
        public function addGroupAction(string $title, array $options = []): GroupAction
1652
        {
1653
                return $this->getGroupActionCollection()->addGroupSelectAction($title, $options);
×
1654
        }
1655

1656
        public function addGroupButtonAction(string $title, ?string $class = null): GroupButtonAction
1657
        {
1658
                return $this->getGroupActionCollection()->addGroupButtonAction($title, $class);
×
1659
        }
1660

1661
        public function addGroupSelectAction(string $title, array $options = []): GroupAction
1662
        {
1663
                return $this->getGroupActionCollection()->addGroupSelectAction($title, $options);
×
1664
        }
1665

1666
        public function addGroupMultiSelectAction(string $title, array $options = []): GroupAction
1667
        {
1668
                return $this->getGroupActionCollection()->addGroupMultiSelectAction($title, $options);
×
1669
        }
1670

1671
        public function addGroupTextAction(string $title): GroupAction
1672
        {
1673
                return $this->getGroupActionCollection()->addGroupTextAction($title);
×
1674
        }
1675

1676
        public function addGroupTextareaAction(string $title): GroupAction
1677
        {
1678
                return $this->getGroupActionCollection()->addGroupTextareaAction($title);
×
1679
        }
1680

1681
        public function getGroupActionCollection(): GroupActionCollection
1682
        {
1683
                if ($this->groupActionCollection === null) {
×
1684
                        $this->groupActionCollection = new GroupActionCollection($this);
×
1685
                }
1686

1687
                return $this->groupActionCollection;
×
1688
        }
1689

1690
        public function hasGroupActions(): bool
1691
        {
1692
                return $this->groupActionCollection instanceof GroupActionCollection;
1✔
1693
        }
1694

1695
        public function shouldShowSelectedRowsCount(): bool
1696
        {
1697
                return $this->showSelectedRowsCount;
×
1698
        }
1699

1700
        /**
1701
         * @return static
1702
         */
1703
        public function setShowSelectedRowsCount(bool $show = true): self
1704
        {
1705
                $this->showSelectedRowsCount = $show;
×
1706

1707
                return $this;
×
1708
        }
1709

1710
        /********************************************************************************
1711
         *                                   HANDLERS *
1712
         ********************************************************************************/
1713
        public function handlePage(int $page): void
1714
        {
1715
                $this->page = $page;
×
1716
                $this->saveStorageData('_grid_page', $page);
×
1717

1718
                $this->reload(['table']);
×
1719
        }
1720

1721
        /**
1722
         * @throws DatagridColumnNotFoundException
1723
         */
1724
        public function handleSort(array $sort): void
1725
        {
1726
                if (count($sort) === 0) {
×
1727
                        $sort = $this->defaultSort;
×
1728
                }
1729

1730
                foreach (array_keys($sort) as $key) {
×
1731
                        try {
1732
                                $column = $this->getColumn($key);
×
1733

1734
                        } catch (DatagridColumnNotFoundException) {
×
1735
                                unset($sort[$key]);
×
1736

1737
                                continue;
×
1738
                        }
1739

1740
                        if ($column->sortableResetPagination()) {
×
1741
                                $this->saveStorageData('_grid_page', $this->page = 1);
×
1742
                        }
1743

1744
                        if ($column->getSortableCallback() !== null) {
×
1745
                                $this->sortCallback = $column->getSortableCallback();
×
1746
                        }
1747
                }
1748

1749
                $this->saveStorageData('_grid_has_sorted', 1);
×
1750
                $this->saveStorageData('_grid_sort', $this->sort = $sort);
×
1751

1752
                $this->reloadTheWholeGrid();
×
1753
        }
1754

1755
        public function handleResetFilter(): void
1756
        {
1757
                /**
1758
                 * Storage stuff
1759
                 */
1760
                $this->deleteStorageData('_grid_page');
1✔
1761

1762
                if ($this->defaultFilterUseOnReset) {
1✔
1763
                        $this->deleteStorageData('_grid_has_filtered');
1✔
1764
                }
1765

1766
                if ($this->defaultSortUseOnReset) {
1✔
1767
                        $this->deleteStorageData('_grid_has_sorted');
1✔
1768
                }
1769

1770
                $this->deleteStorageData('_grid_filters');
1✔
1771

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

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

1777
        public function handleResetColumnFilter(string $key): void
1778
        {
NEW
1779
                $storedFilters = $this->getStorageData('_grid_filters', []);
×
NEW
1780
                if (isset($storedFilters[$key])) {
×
NEW
1781
                        unset($storedFilters[$key]);
×
NEW
1782
                        $this->saveStorageData('_grid_filters', $storedFilters);
×
1783
                }
1784

UNCOV
1785
                unset($this->filter[$key]);
×
1786

1787
                $this->reloadTheWholeGrid();
×
1788
        }
1789

1790
        /**
1791
         * @return static
1792
         */
1793
        public function setColumnReset(bool $reset = true): self
1794
        {
1795
                $this->hasColumnReset = $reset;
×
1796

1797
                return $this;
×
1798
        }
1799

1800
        public function hasColumnReset(): bool
1801
        {
1802
                return $this->hasColumnReset;
1✔
1803
        }
1804

1805
        /**
1806
         * @param array<Filter> $filters
1807
         */
1808
        public function sendNonEmptyFiltersInPayload(array $filters): void
1✔
1809
        {
1810
                if (!$this->hasColumnReset()) {
1✔
1811
                        return;
×
1812
                }
1813

1814
                $non_empty_filters = [];
1✔
1815

1816
                foreach ($filters as $filter) {
1✔
1817
                        if ($filter->isValueSet()) {
1✔
1818
                                $non_empty_filters[] = $filter->getKey();
×
1819
                        }
1820
                }
1821

1822
                $this->getPresenterInstance()->payload->non_empty_filters = $non_empty_filters;
1✔
1823
        }
1✔
1824

1825
        public function handleExport(mixed $id): void
1✔
1826
        {
1827
                if (!isset($this->exports[$id])) {
1✔
1828
                        throw new ForbiddenRequestException();
×
1829
                }
1830

1831
                if ($this->columnsExportOrder !== []) {
1✔
1832
                        $this->setColumnsOrder($this->columnsExportOrder);
×
1833
                }
1834

1835
                $export = $this->exports[$id];
1✔
1836

1837
                /**
1838
                 * Invoke possible events
1839
                 */
1840
                $this->onExport($this);
1✔
1841

1842
                if ($export->isFiltered()) {
1✔
1843
                        $sort = $this->sort;
1✔
1844
                        $filter = $this->assembleFilters();
1✔
1845
                } else {
1846
                        $sort = [$this->primaryKey => 'ASC'];
1✔
1847
                        $filter = [];
1✔
1848
                }
1849

1850
                if ($this->dataModel === null) {
1✔
1851
                        throw new DatagridException('You have to set a data source first.');
1✔
1852
                }
1853

1854
                $rows = [];
1✔
1855

1856
                $items = $this->dataModel->filterData(
1✔
1857
                        null,
1✔
1858
                        $this->createSorting($sort, $this->sortCallback),
1✔
1859
                        $filter
1860
                );
1861

1862
                foreach ($items as $item) {
1✔
1863
                        $rows[] = new Row($this, $item, $this->getPrimaryKey());
1✔
1864
                }
1865

1866
                if ($export instanceof ExportCsv) {
1✔
1867
                        $export->invoke($rows);
1✔
1868
                } else {
1869
                        $export->invoke($items);
1✔
1870
                }
1871

1872
                if ($export->isAjax()) {
1✔
1873
                        $this->reload();
×
1874
                }
1875
        }
1✔
1876

1877
        public function handleGetChildren(mixed $parent): void
1878
        {
1879
                if (!is_callable($this->treeViewChildrenCallback)) {
×
1880
                        throw new UnexpectedValueException();
×
1881
                }
1882

1883
                $this->setDataSource(call_user_func($this->treeViewChildrenCallback, $parent));
×
1884

1885
                if ($this->getPresenterInstance()->isAjax()) {
×
1886
                        $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
1887
                        $this->getPresenterInstance()->payload->_datagrid_tree = $parent;
×
1888

1889
                        $this->redrawControl('items');
×
1890

1891
                        $this->onRedraw();
×
1892
                } else {
1893
                        $this->getPresenterInstance()->redirect('this');
×
1894
                }
1895
        }
1896

1897
        public function handleGetItemDetail(mixed $id): void
1898
        {
1899
                $template = $this->getTemplate();
×
1900

1901
                if (!$template instanceof Template) {
×
1902
                        throw new UnexpectedValueException();
×
1903
                }
1904

1905
                $template->add('toggle_detail', $id);
×
1906

1907
                if ($this->itemsDetail === null) {
×
1908
                        throw new UnexpectedValueException();
×
1909
                }
1910

1911
                $this->redrawItem = [$this->itemsDetail->getPrimaryWhereColumn() => $id];
×
1912

1913
                if ($this->getPresenterInstance()->isAjax()) {
×
1914
                        $this->getPresenterInstance()->payload->_datagrid_toggle_detail = $id;
×
1915
                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1916
                        $this->redrawControl('items');
×
1917

1918
                        /**
1919
                         * Only for nette 2.4
1920
                         */
1921
                        if (method_exists($template->getLatte(), 'addProvider')) {
×
1922
                                $this->redrawControl('gridSnippets');
×
1923
                        }
1924

1925
                        $this->onRedraw();
×
1926
                } else {
1927
                        $this->getPresenterInstance()->redirect('this');
×
1928
                }
1929
        }
1930

1931
        public function handleEdit(mixed $id, mixed $key): void
1932
        {
1933
                $column = $this->getColumn($key);
×
1934
                $request = $this->getPresenterInstance()->getRequest();
×
1935

1936
                if (!$request instanceof Request) {
×
1937
                        throw new UnexpectedValueException();
×
1938
                }
1939

1940
                $value = $request->getPost('value');
×
1941

1942
                // Could be null of course
1943
                if ($column->getEditableCallback() === null) {
×
1944
                        throw new UnexpectedValueException();
×
1945
                }
1946

1947
                $newValue = $column->getEditableCallback()($id, $value);
×
1948

1949
                $this->getPresenterInstance()->payload->_datagrid_editable_new_value = $newValue;
×
1950
                $this->getPresenterInstance()->payload->postGet = true;
×
1951
                $this->getPresenterInstance()->payload->url = $this->link('this');
×
1952

1953
                if (!$this->getPresenterInstance()->isControlInvalid(null)) {
×
1954
                        $this->getPresenterInstance()->sendPayload();
×
1955
                }
1956
        }
1957

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

1969
                        /**
1970
                         * manualy reset exports links...
1971
                         */
1972
                        $this->resetExportsLinks();
×
1973
                        $this->redrawControl('exports');
×
1974

1975
                        foreach ($snippets as $snippet) {
×
1976
                                $this->redrawControl($snippet);
×
1977
                        }
1978

1979
                        $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
1980
                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1981

1982
                        $this->onRedraw();
×
1983
                } else {
1984
                        $this->getPresenterInstance()->redirect('this');
1✔
1985
                }
1986
        }
1987

1988
        public function reloadTheWholeGrid(): void
1989
        {
1990
                if ($this->getPresenterInstance()->isAjax()) {
1✔
1991
                        $this->redrawControl('grid');
×
1992

1993
                        $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
1994
                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1995

1996
                        $this->onRedraw();
×
1997
                } else {
1998
                        $this->getPresenterInstance()->redirect('this');
1✔
1999
                }
2000
        }
2001

2002
        public function handleChangeStatus(string $id, string $key, string $value): void
2003
        {
2004
                if (!isset($this->columns[$key])) {
×
2005
                        throw new DatagridException(sprintf('ColumnStatus[%s] does not exist', $key));
×
2006
                }
2007

2008
                if (!$this->columns[$key] instanceof ColumnStatus) {
×
2009
                        throw new UnexpectedValueException();
×
2010
                }
2011

2012
                $this->columns[$key]->onChange($id, $value);
×
2013
        }
2014

2015
        public function redrawItem(string|int $id, mixed $primaryWhereColumn = null): void
2016
        {
2017
                $this->snippetsSet = true;
×
2018

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

2021
                $this->redrawControl('items');
×
2022

2023
                $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
2024

2025
                $this->onRedraw();
×
2026
        }
2027

2028
        public function handleShowAllColumns(): void
2029
        {
2030
                $this->deleteStorageData('_grid_hidden_columns');
×
2031
                $this->saveStorageData('_grid_hidden_columns_manipulated', true);
×
2032

2033
                $this->redrawControl();
×
2034

2035
                $this->onShowAllColumns();
×
2036
                $this->onRedraw();
×
2037
        }
2038

2039
        public function handleShowDefaultColumns(): void
2040
        {
2041
                $this->deleteStorageData('_grid_hidden_columns');
×
2042
                $this->saveStorageData('_grid_hidden_columns_manipulated', false);
×
2043

2044
                $this->redrawControl();
×
2045

2046
                $this->onShowDefaultColumns();
×
2047
                $this->onRedraw();
×
2048
        }
2049

2050
        public function handleShowColumn(string $column): void
2051
        {
2052
                $columns = $this->getStorageData('_grid_hidden_columns');
×
2053

2054
                if ($columns !== [] && $columns !== null) {
×
2055
                        $pos = array_search($column, $columns, true);
×
2056

2057
                        if ($pos !== false) {
×
2058
                                unset($columns[$pos]);
×
2059
                        }
2060
                }
2061

2062
                $this->saveStorageData('_grid_hidden_columns', $columns);
×
2063
                $this->saveStorageData('_grid_hidden_columns_manipulated', true);
×
2064

2065
                $this->redrawControl();
×
2066

2067
                $this->onColumnShow($column);
×
2068
                $this->onRedraw();
×
2069
        }
2070

2071
        public function handleHideColumn(string $column): void
2072
        {
2073
                /**
2074
                 * Store info about hiding a column to storage
2075
                 */
2076
                $columns = $this->getStorageData('_grid_hidden_columns');
×
2077

2078
                if ($columns === [] || $columns === null) {
×
2079
                        $columns = [$column];
×
2080
                } elseif (!in_array($column, $columns, true)) {
×
2081
                        array_push($columns, $column);
×
2082
                }
2083

2084
                $this->saveStorageData('_grid_hidden_columns', $columns);
×
2085
                $this->saveStorageData('_grid_hidden_columns_manipulated', true);
×
2086

2087
                $this->redrawControl();
×
2088

2089
                $this->onColumnHide($column);
×
2090
                $this->onRedraw();
×
2091
        }
2092

2093
        public function handleActionCallback(mixed $__key, mixed $__id): void
2094
        {
2095
                $action = $this->getAction($__key);
×
2096

2097
                if (!($action instanceof ActionCallback)) {
×
2098
                        throw new DatagridException(
×
2099
                                sprintf('Action [%s] does not exist or is not an callback aciton.', $__key)
×
2100
                        );
2101
                }
2102

2103
                $action->onClick($__id);
×
2104
        }
2105

2106

2107
        /********************************************************************************
2108
         *                                  PAGINATION *
2109
         ********************************************************************************/
2110

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

2121
                $this->itemsPerPageList = $itemsPerPageList;
1✔
2122

2123
                if ($includeAll) {
1✔
2124
                        $this->itemsPerPageList[] = 'all';
1✔
2125
                }
2126

2127
                return $this;
1✔
2128
        }
2129

2130
        /**
2131
         * @return static
2132
         */
2133
        public function setDefaultPerPage(int $count): self
2134
        {
2135
                $this->defaultPerPage = $count;
×
2136

2137
                return $this;
×
2138
        }
2139

2140
        /**
2141
         * User may set default "items per page" value, apply it
2142
         */
2143
        public function findDefaultPerPage(): void
2144
        {
2145
                if ($this->perPage !== null) {
×
2146
                        return;
×
2147
                }
2148

2149
                if ($this->defaultPerPage !== null) {
×
2150
                        $this->perPage = $this->defaultPerPage;
×
2151
                }
2152

2153
                $this->saveStorageData('_grid_perPage', $this->perPage);
×
2154
        }
2155

2156
        public function createComponentPaginator(): DatagridPaginator
2157
        {
2158
                $component = new DatagridPaginator(
×
2159
                        $this->getTranslator(),
×
2160
                        static::$iconPrefix,
×
2161
                        static::$btnSecondaryClass
×
2162
                );
2163
                $paginator = $component->getPaginator();
×
2164

2165
                $paginator->setPage($this->page);
×
2166

2167
                if (is_int($this->getPerPage())) {
×
2168
                        $paginator->setItemsPerPage($this->getPerPage());
×
2169
                }
2170

2171
                if ($this->customPaginatorTemplate !== null) {
×
2172
                        $component->setTemplateFile($this->customPaginatorTemplate);
×
2173
                }
2174

2175
                return $component;
×
2176
        }
2177

2178
        public function getPerPage(): int|string
2179
        {
2180
                $itemsPerPageList = array_keys($this->getItemsPerPageList());
1✔
2181

2182
                $perPage = $this->perPage ?? reset($itemsPerPageList);
1✔
2183

2184
                if (($perPage !== 'all' && !in_array((int) $this->perPage, $itemsPerPageList, true))
1✔
2185
                        || ($perPage === 'all' && !in_array($this->perPage, $itemsPerPageList, true))) {
1✔
2186
                        $perPage = reset($itemsPerPageList);
1✔
2187
                }
2188

2189
                return $perPage === 'all'
1✔
2190
                        ? 'all'
1✔
2191
                        : (int) $perPage;
1✔
2192
        }
2193

2194
        /**
2195
         * @return array|array|int[]|array|string[]|\Stringable[]
2196
         */
2197
        public function getItemsPerPageList(): array
2198
        {
2199
                $list = array_flip($this->itemsPerPageList);
1✔
2200

2201
                foreach (array_keys($list) as $key) {
1✔
2202
                        $list[$key] = $key;
1✔
2203
                }
2204

2205
                if (array_key_exists('all', $list)) {
1✔
2206
                        $list['all'] = $this->getTranslator()->translate('contributte_datagrid.all');
1✔
2207
                }
2208

2209
                return $list;
1✔
2210
        }
2211

2212
        /**
2213
         * @return static
2214
         */
2215
        public function setPagination(bool $doPaginate): self
2216
        {
2217
                $this->doPaginate = $doPaginate;
×
2218

2219
                return $this;
×
2220
        }
2221

2222
        public function isPaginated(): bool
2223
        {
2224
                return $this->doPaginate;
1✔
2225
        }
2226

2227
        public function getPaginator(): ?DatagridPaginator
2228
        {
2229
                if ($this->isPaginated() && $this->perPage !== 'all') {
×
2230
                        return $this['paginator'];
×
2231
                }
2232

2233
                return null;
×
2234
        }
2235

2236

2237
        /********************************************************************************
2238
         *                                     I18N *
2239
         ********************************************************************************/
2240

2241
        /**
2242
         * @return static
2243
         */
2244
        public function setTranslator(Translator $translator): self
1✔
2245
        {
2246
                $this->translator = $translator;
1✔
2247

2248
                return $this;
1✔
2249
        }
2250

2251
        public function getTranslator(): Translator
2252
        {
2253
                if ($this->translator === null) {
1✔
2254
                        $this->translator = new SimpleTranslator();
1✔
2255
                }
2256

2257
                return $this->translator;
1✔
2258
        }
2259

2260

2261
        /********************************************************************************
2262
         *                                 COLUMNS ORDER *
2263
         ********************************************************************************/
2264

2265
        /**
2266
         * Set order of datagrid columns
2267
         *
2268
         * @param array|string[] $order
2269
         * @return static
2270
         */
2271
        public function setColumnsOrder(array $order): self
2272
        {
2273
                $new_order = [];
×
2274

2275
                foreach ($order as $key) {
×
2276
                        if (isset($this->columns[$key])) {
×
2277
                                $new_order[$key] = $this->columns[$key];
×
2278
                        }
2279
                }
2280

2281
                if (count($new_order) === count($this->columns)) {
×
2282
                        $this->columns = $new_order;
×
2283
                } else {
2284
                        throw new DatagridException('When changing columns order, you have to specify all columns');
×
2285
                }
2286

2287
                return $this;
×
2288
        }
2289

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

2300
                return $this;
×
2301
        }
2302

2303
        /********************************************************************************
2304
         *                                SESSION & URL *
2305
         ********************************************************************************/
2306
        public function getSessionSectionName(): string
2307
        {
2308
                $presenter = $this->getPresenterInstance();
1✔
2309

2310
                return $presenter->getName() . ':' . $this->getUniqueId();
1✔
2311
        }
2312

2313
        /**
2314
         * @return static
2315
         */
2316
        public function setRememberState(bool $remember = true, bool $rememberHideableColumnsState = false): self
1✔
2317
        {
2318
                $this->rememberState = $remember;
1✔
2319
                $this->rememberHideableColumnsState = $rememberHideableColumnsState;
1✔
2320

2321
                return $this;
1✔
2322
        }
2323

2324
        /**
2325
         * @return static
2326
         */
2327
        public function setRefreshUrl(bool $refresh = true): self
2328
        {
2329
                $this->refreshURL = $refresh;
×
2330

2331
                return $this;
×
2332
        }
2333

2334
        public function getStorageData(string $key, mixed $defaultValue = null): mixed
1✔
2335
        {
2336
                if ($this->shouldRememberState($key)) {
1✔
2337
                        return $this->getStateStorage()->loadState($key) ?? $defaultValue;
1✔
2338
                }
2339

2340
                return $defaultValue;
1✔
2341
        }
2342

2343
        public function saveStorageData(string $key, mixed $value): void
1✔
2344
        {
2345
                if ($this->shouldRememberState($key)) {
1✔
2346
                        $this->getStateStorage()->saveState($key, $value);
1✔
2347
                }
2348
        }
1✔
2349

2350
        public function deleteStorageData(string $key): void
1✔
2351
        {
2352
                $this->getStateStorage()->deleteState($key);
1✔
2353
        }
1✔
2354

2355
        /********************************************************************************
2356
         *                                  ITEM DETAIL *
2357
         ********************************************************************************/
2358

2359
        /**
2360
         * Get items detail parameters
2361
         */
2362
        public function getItemsDetail(): ?ItemDetail
2363
        {
2364
                return $this->itemsDetail;
×
2365
        }
2366

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

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

2378
                if (is_string($detail)) {
×
2379
                        /**
2380
                         * Item detail will be in separate template
2381
                         */
2382
                        $this->itemsDetail->setType('template');
×
2383
                        $this->itemsDetail->setTemplate($detail);
×
2384

2385
                } elseif (is_callable($detail)) {
×
2386
                        /**
2387
                         * Item detail will be rendered via custom callback renderer
2388
                         */
2389
                        $this->itemsDetail->setType('renderer');
×
2390
                        $this->itemsDetail->setRenderer($detail);
×
2391

2392
                } elseif ($detail === true) {
×
2393
                        /**
2394
                         * Item detail will be rendered probably via block #detail
2395
                         */
2396
                        $this->itemsDetail->setType('block');
×
2397

2398
                } else {
2399
                        throw new DatagridException('::setItemsDetail() can be called either with no parameters or with parameter = template path or callable renderer.');
×
2400
                }
2401

2402
                return $this->itemsDetail;
×
2403
        }
2404

2405
        /**
2406
         * @return static
2407
         */
2408
        public function setItemsDetailForm(callable $callableSetContainer): self
2409
        {
2410
                if ($this->itemsDetail instanceof ItemDetail) {
×
2411
                        $this->itemsDetail->setForm(
×
2412
                                new ItemDetailForm($callableSetContainer)
×
2413
                        );
2414

2415
                        return $this;
×
2416
                }
2417

2418
                throw new DatagridException('Please set the ItemDetail first.');
×
2419
        }
2420

2421
        public function getItemDetailForm(): ?Container
2422
        {
2423
                if ($this->itemsDetail instanceof ItemDetail) {
1✔
2424
                        return $this->itemsDetail->getForm();
×
2425
                }
2426

2427
                return null;
1✔
2428
        }
2429

2430
        /********************************************************************************
2431
         *                                ROW PRIVILEGES *
2432
         ********************************************************************************/
2433
        public function allowRowsGroupAction(callable $condition): void
2434
        {
2435
                $this->rowConditions['group_action'] = $condition;
×
2436
        }
2437

2438
        public function allowRowsInlineEdit(callable $condition): void
2439
        {
2440
                $this->rowConditions['inline_edit'] = $condition;
×
2441
        }
2442

2443
        public function allowRowsAction(string $key, callable $condition): void
2444
        {
2445
                $this->rowConditions['action'][$key] = $condition;
×
2446
        }
2447

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

2463
                if (!$this->actions[$multiActionKey] instanceof MultiAction) {
×
2464
                        throw new DatagridException(
×
2465
                                sprintf('Action at key [%s] is not a MultiAction.', $multiActionKey)
×
2466
                        );
2467
                }
2468

2469
                $this->actions[$multiActionKey]->setRowCondition($actionKey, $condition);
×
2470
        }
2471

2472
        public function getRowCondition(string $name, ?string $key = null): bool|callable
2473
        {
2474
                if (!isset($this->rowConditions[$name])) {
×
2475
                        return false;
×
2476
                }
2477

2478
                $condition = $this->rowConditions[$name];
×
2479

2480
                if ($key === null) {
×
2481
                        return $condition;
×
2482
                }
2483

2484
                return $condition[$key] ?? false;
×
2485
        }
2486

2487
        /********************************************************************************
2488
         *                               COLUMN CALLBACK *
2489
         ********************************************************************************/
2490
        public function addColumnCallback(string $key, callable $callback): void
2491
        {
2492
                $this->columnCallbacks[$key] = $callback;
×
2493
        }
2494

2495
        public function getColumnCallback(string $key): ?callable
2496
        {
2497
                return $this->columnCallbacks[$key] ?? null;
×
2498
        }
2499

2500
        /********************************************************************************
2501
         *                                 INLINE EDIT *
2502
         ********************************************************************************/
2503
        public function addInlineEdit(?string $primaryWhereColumn = null): InlineEdit
2504
        {
2505
                $this->inlineEdit = new InlineEdit($this, $primaryWhereColumn ?? $this->primaryKey);
×
2506

2507
                return $this->inlineEdit;
×
2508
        }
2509

2510
        public function getInlineEdit(): ?InlineEdit
2511
        {
2512
                return $this->inlineEdit;
×
2513
        }
2514

2515
        public function handleShowInlineAdd(): void
2516
        {
2517
                if ($this->inlineAdd !== null) {
×
2518
                        $this->inlineAdd->setShouldBeRendered(true);
×
2519
                }
2520

2521
                $presenter = $this->getPresenterInstance();
×
2522

2523
                if ($presenter->isAjax()) {
×
2524
                        $presenter->payload->_datagrid_inline_adding = true;
×
2525
                        $presenter->payload->_datagrid_name = $this->getFullName();
×
2526

2527
                        $this->redrawControl('tbody');
×
2528

2529
                        $this->onRedraw();
×
2530
                }
2531
        }
2532

2533
        public function handleInlineEdit(mixed $id): void
2534
        {
2535
                if ($this->inlineEdit !== null) {
×
2536
                        $this->inlineEdit->setItemId($id);
×
2537

2538
                        $primaryWhereColumn = $this->inlineEdit->getPrimaryWhereColumn();
×
2539

2540
                        $filterContainer = $this['filter'];
×
2541
                        $inlineEditContainer = $filterContainer['inline_edit'];
×
2542

2543
                        if (!$inlineEditContainer instanceof Container) {
×
2544
                                throw new UnexpectedValueException();
×
2545
                        }
2546

2547
                        $inlineEditContainer->addHidden('_id', $id);
×
2548
                        $inlineEditContainer->addHidden('_primary_where_column', $primaryWhereColumn);
×
2549

2550
                        $presenter = $this->getPresenterInstance();
×
2551

2552
                        if ($presenter->isAjax()) {
×
2553
                                $presenter->payload->_datagrid_inline_editing = true;
×
2554
                                $presenter->payload->_datagrid_name = $this->getFullName();
×
2555
                        }
2556

2557
                        $this->redrawItem($id, $primaryWhereColumn);
×
2558
                }
2559
        }
2560

2561
        /********************************************************************************
2562
         *                                  INLINE ADD *
2563
         ********************************************************************************/
2564
        public function addInlineAdd(): InlineAdd
2565
        {
2566
                $this->inlineAdd = new InlineAdd($this);
1✔
2567

2568
                $this->inlineAdd
1✔
2569
                        ->setTitle('contributte_datagrid.add')
1✔
2570
                        ->setIcon('plus');
1✔
2571

2572
                return $this->inlineAdd;
1✔
2573
        }
2574

2575
        public function getInlineAdd(): ?InlineAdd
2576
        {
2577
                return $this->inlineAdd;
×
2578
        }
2579

2580

2581
        /********************************************************************************
2582
         *                               COLUMNS HIDING *
2583
         ********************************************************************************/
2584

2585
        /**
2586
         * Can datagrid hide colums?
2587
         */
2588
        public function canHideColumns(): bool
2589
        {
2590
                return $this->canHideColumns;
1✔
2591
        }
2592

2593
        /**
2594
         * Order Grid to set columns hideable.
2595
         *
2596
         * @return static
2597
         */
2598
        public function setColumnsHideable(bool $columnsHideable = true): self
1✔
2599
        {
2600
                $this->canHideColumns = $columnsHideable;
1✔
2601

2602
                return $this;
1✔
2603
        }
2604

2605
        /********************************************************************************
2606
         *                                COLUMNS SUMMARY *
2607
         ********************************************************************************/
2608
        public function hasColumnsSummary(): bool
2609
        {
2610
                return $this->columnsSummary instanceof ColumnsSummary;
×
2611
        }
2612

2613
        /**
2614
         * @param array|string[] $columns
2615
         */
2616
        public function setColumnsSummary(array $columns, ?callable $rowCallback = null): ColumnsSummary
2617
        {
2618
                if ($this->hasSomeAggregationFunction()) {
×
2619
                        throw new DatagridException('You can use either ColumnsSummary or AggregationFunctions');
×
2620
                }
2621

2622
                $this->columnsSummary = new ColumnsSummary($this, $columns, $rowCallback);
×
2623

2624
                return $this->columnsSummary;
×
2625
        }
2626

2627
        public function getColumnsSummary(): ?ColumnsSummary
2628
        {
2629
                return $this->columnsSummary;
1✔
2630
        }
2631

2632

2633
        /********************************************************************************
2634
         *                                   INTERNAL *
2635
         ********************************************************************************/
2636

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

2648
                return $this->componentFullName;
×
2649
        }
2650

2651
        /**
2652
         * Tell grid filters to by submitted automatically
2653
         *
2654
         * @return static
2655
         */
2656
        public function setAutoSubmit(bool $autoSubmit = true): self
2657
        {
2658
                $this->autoSubmit = $autoSubmit;
×
2659

2660
                return $this;
×
2661
        }
2662

2663
        public function hasAutoSubmit(): bool
2664
        {
2665
                return $this->autoSubmit;
1✔
2666
        }
2667

2668
        public function getFilterSubmitButton(): SubmitButton
2669
        {
2670
                if ($this->hasAutoSubmit()) {
×
2671
                        throw new DatagridException('Datagrid has auto-submit. Turn it off before setting filter submit button.');
×
2672
                }
2673

2674
                if ($this->filterSubmitButton === null) {
×
2675
                        $this->filterSubmitButton = new SubmitButton($this);
×
2676
                }
2677

2678
                return $this->filterSubmitButton;
×
2679
        }
2680

2681

2682
        /********************************************************************************
2683
         *                                   INTERNAL *
2684
         ********************************************************************************/
2685

2686
        /**
2687
         * @internal
2688
         */
2689
        public function getColumnsCount(): int
2690
        {
2691
                $count = count($this->getColumns());
×
2692

2693
                if ($this->actions !== []
×
2694
                        || $this->isSortable()
×
2695
                        || $this->getItemsDetail() !== null
×
2696
                        || $this->getInlineEdit() !== null
×
2697
                        || $this->getInlineAdd() !== null) {
×
2698
                        $count++;
×
2699
                }
2700

2701
                if ($this->hasGroupActions()) {
×
2702
                        $count++;
×
2703
                }
2704

2705
                return $count;
×
2706
        }
2707

2708
        /**
2709
         * @internal
2710
         */
2711
        public function getPrimaryKey(): string
2712
        {
2713
                return $this->primaryKey;
1✔
2714
        }
2715

2716
        /**
2717
         * @return array<Column>
2718
         * @internal
2719
         */
2720
        public function getColumns(): array
2721
        {
2722
                $return = $this->columns;
1✔
2723

2724
                try {
2725
                        $this->getParentComponent();
1✔
2726

2727
                        if (! (bool) $this->getStorageData('_grid_hidden_columns_manipulated', false)) {
1✔
2728
                                $columns_to_hide = [];
1✔
2729

2730
                                foreach ($this->columns as $key => $column) {
1✔
2731
                                        if ($column->getDefaultHide()) {
×
2732
                                                $columns_to_hide[] = $key;
×
2733
                                        }
2734
                                }
2735

2736
                                if ($columns_to_hide !== []) {
1✔
2737
                                        $this->saveStorageData('_grid_hidden_columns', $columns_to_hide);
×
2738
                                        $this->saveStorageData('_grid_hidden_columns_manipulated', true);
×
2739
                                }
2740
                        }
2741

2742
                        $hidden_columns = $this->getStorageData('_grid_hidden_columns', []);
1✔
2743

2744
                        foreach ($hidden_columns ?? [] as $column) {
1✔
2745
                                if (isset($this->columns[$column])) {
×
2746
                                        $this->columnsVisibility[$column] = [
×
2747
                                                'visible' => false,
2748
                                        ];
2749

2750
                                        unset($return[$column]);
×
2751
                                }
2752
                        }
2753
                } catch (DatagridHasToBeAttachedToPresenterComponentException) {
×
2754
                        // No need to worry
2755
                }
2756

2757
                return $return;
1✔
2758
        }
2759

2760
        /**
2761
         * @internal
2762
         */
2763
        public function getColumnsVisibility(): array
2764
        {
2765
                $return = $this->columnsVisibility;
1✔
2766

2767
                foreach (array_keys($this->columnsVisibility) as $key) {
1✔
2768
                        $return[$key]['column'] = $this->columns[$key];
×
2769
                }
2770

2771
                return $return;
1✔
2772
        }
2773

2774
        /**
2775
         * @internal
2776
         */
2777
        public function getParentComponent(): Component
2778
        {
2779
                $parent = parent::getParent();
1✔
2780

2781
                if (!$parent instanceof Component) {
1✔
2782
                        throw new DatagridHasToBeAttachedToPresenterComponentException(
×
2783
                                sprintf(
×
2784
                                        'Datagrid is attached to: "%s", but instance of %s is needed.',
×
2785
                                        ($parent !== null ? $parent::class : 'null'),
×
2786
                                        Component::class
×
2787
                                )
2788
                        );
2789
                }
2790

2791
                return $parent;
1✔
2792
        }
2793

2794
        /**
2795
         * @internal
2796
         * @throws UnexpectedValueException
2797
         */
2798
        public function getSortableParentPath(): string
2799
        {
2800
                if ($this->getParentComponent() instanceof IPresenter) {
×
2801
                        return '';
×
2802
                }
2803

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

2806
                if ($presenter === null) {
×
2807
                        throw new UnexpectedValueException(
×
2808
                                sprintf('%s needs %s', self::class, IPresenter::class)
×
2809
                        );
2810
                }
2811

2812
                return $presenter;
×
2813
        }
2814

2815
        /**
2816
         * Some of datagrid columns may be hidden by default
2817
         *
2818
         * @internal
2819
         * @return static
2820
         */
2821
        public function setSomeColumnDefaultHide(bool $defaultHide): self
2822
        {
2823
                $this->someColumnDefaultHide = $defaultHide;
×
2824

2825
                return $this;
×
2826
        }
2827

2828
        /**
2829
         * Are some of columns hidden bydefault?
2830
         *
2831
         * @internal
2832
         */
2833
        public function hasSomeColumnDefaultHide(): bool
2834
        {
2835
                return $this->someColumnDefaultHide;
×
2836
        }
2837

2838
        /**
2839
         * Simply refresh url
2840
         *
2841
         * @internal
2842
         */
2843
        public function handleRefreshState(): void
2844
        {
2845
                $this->findStorageValues();
×
2846
                $this->findDefaultFilter();
×
2847
                $this->findDefaultSort();
×
2848
                $this->findDefaultPerPage();
×
2849

2850
                $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
2851
                $this->redrawControl('non-existing-snippet');
×
2852
        }
2853

2854
        /**
2855
         * @internal
2856
         */
2857
        public function setCustomPaginatorTemplate(string $templateFile): void
2858
        {
2859
                $this->customPaginatorTemplate = $templateFile;
×
2860
        }
2861

2862
        protected function createSorting(array $sort, ?callable $sortCallback = null): Sorting
1✔
2863
        {
2864
                foreach ($sort as $key => $order) {
1✔
2865
                        unset($sort[$key]);
1✔
2866

2867
                        if ($order !== 'ASC' && $order !== 'DESC') {
1✔
2868
                                continue;
×
2869
                        }
2870

2871
                        try {
2872
                                $column = $this->getColumn($key);
1✔
2873

2874
                        } catch (DatagridColumnNotFoundException) {
1✔
2875
                                continue;
1✔
2876
                        }
2877

2878
                        $sort[$column->getSortingColumn()] = $order;
×
2879
                }
2880

2881
                if ($sortCallback === null && isset($column)) {
1✔
2882
                        $sortCallback = $column->getSortableCallback();
×
2883
                }
2884

2885
                return new Sorting($sort, $sortCallback);
1✔
2886
        }
2887

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

2899
                $this->onColumnAdd($key, $column);
1✔
2900

2901
                $this->columnsVisibility[$key] = ['visible' => true];
1✔
2902

2903
                return $this->columns[$key] = $column;
1✔
2904
        }
2905

2906
        /**
2907
         * Check whether given key already exists in $this->filters
2908
         *
2909
         * @throws DatagridException
2910
         */
2911
        protected function addActionCheck(string $key): void
1✔
2912
        {
2913
                if (isset($this->actions[$key])) {
1✔
2914
                        throw new DatagridException(
1✔
2915
                                sprintf('There is already action at key [%s] defined.', $key)
1✔
2916
                        );
2917
                }
2918
        }
1✔
2919

2920
 /********************************************************************************
2921
  *                                    FILTERS *
2922
  ********************************************************************************/
2923

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

2938
        protected function addToExports(Export $export): Export
1✔
2939
        {
2940
                $id = count($this->exports) > 0 ? count($this->exports) + 1 : 1;
1✔
2941

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

2944
                $export->setLink($link);
1✔
2945

2946
                return $this->exports[$id] = $export;
1✔
2947
        }
2948

2949
        private function getPresenterInstance(): Presenter
2950
        {
2951
                return $this->getPresenter();
1✔
2952
        }
2953

2954
        private function shouldRememberState(string $key): bool
1✔
2955
        {
2956
                return $this->rememberState ||
1✔
2957
                        ($this->rememberHideableColumnsState && in_array($key, self::HIDEABLE_COLUMNS_STORAGE_KEYS, true));
1✔
2958
        }
2959

2960
}
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