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

contributte / datagrid / 22611613038

03 Mar 2026 06:43AM UTC coverage: 49.598% (+0.9%) from 48.715%
22611613038

Pull #1254

github

web-flow
Merge b489833b7 into 043d16e66
Pull Request #1254: Fix: ColumnsSummary shows incorrect totals after inline edit

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

79 existing lines in 5 files now uncovered.

1419 of 2861 relevant lines covered (49.6%)

0.5 hits per line

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

39.64
/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\Attributes\Persistent;
49
use Nette\Application\ForbiddenRequestException;
50
use Nette\Application\IPresenter;
51
use Nette\Application\Request;
52
use Nette\Application\UI\Component;
53
use Nette\Application\UI\Control;
54
use Nette\Application\UI\Form;
55
use Nette\Application\UI\Link;
56
use Nette\Application\UI\Presenter;
57
use Nette\Bridges\ApplicationLatte\Template;
58
use Nette\ComponentModel\IContainer;
59
use Nette\Forms\Container;
60
use Nette\Forms\Control as FormControl;
61
use Nette\Forms\Controls\SubmitButton as FormsSubmitButton;
62
use Nette\Forms\Form as NetteForm;
63
use Nette\Http\SessionSection;
64
use Nette\Localization\Translator;
65
use Nette\Utils\ArrayHash;
66
use UnexpectedValueException;
67

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

82
        use TDatagridAggregationFunction;
83

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

89
        public static string $iconPrefix = 'fas fa-';
90

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

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

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

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

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

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

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

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

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

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

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

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

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

137
        #[Persistent]
138
        public int $page = 1;
139

140
        #[Persistent]
141
        public string|int|null $perPage = null;
142

143
        #[Persistent]
144
        public array $sort = [];
145

146
        public array $defaultSort = [];
147

148
        public array $defaultFilter = [];
149

150
        public bool $defaultFilterUseOnReset = true;
151

152
        public bool $defaultSortUseOnReset = true;
153

154
        #[Persistent]
155
        public array $filter = [];
156

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

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

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

165
        protected ?int $defaultPerPage = null;
166

167
        protected ?string $templateFile = null;
168

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

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

175
        protected ?GroupActionCollection $groupActionCollection = null;
176

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

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

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

186
        protected ?DataModel $dataModel = null;
187

188
        protected string $primaryKey = 'id';
189

190
        protected bool $doPaginate = true;
191

192
        protected bool $csvExport = true;
193

194
        protected bool $csvExportFiltered = true;
195

196
        protected bool $sortable = false;
197

198
        protected bool $multiSort = false;
199

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

202
        protected ?string $originalTemplate = null;
203

204
        protected array $redrawItem = [];
205

206
        protected ?Translator $translator = null;
207

208
        protected bool $forceFilterActive = false;
209

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

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

216
        protected ?string $treeViewHasChildrenColumn = null;
217

218
        protected bool $outerFilterRendering = false;
219

220
        protected int $outerFilterColumnsCount = 2;
221

222
        protected bool $collapsibleOuterFilters = true;
223

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

227
        protected bool $rememberState = true;
228

229
        protected bool $rememberHideableColumnsState = true;
230

231
        protected bool $refreshURL = true;
232

233
        protected SessionSection $gridSession;
234

235
        protected ?ItemDetail $itemsDetail = null;
236

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

242
        protected array $columnCallbacks = [];
243

244
        protected bool $canHideColumns = false;
245

246
        protected array $columnsVisibility = [];
247

248
        protected ?InlineEdit $inlineEdit = null;
249

250
        protected ?InlineAdd $inlineAdd = null;
251

252
        protected bool $snippetsSet = false;
253

254
        protected bool $someColumnDefaultHide = false;
255

256
        protected ?ColumnsSummary $columnsSummary = null;
257

258
        protected bool $autoSubmit = true;
259

260
        protected ?SubmitButton $filterSubmitButton = null;
261

262
        protected bool $hasColumnReset = true;
263

264
        protected bool $showSelectedRowsCount = true;
265

266
        protected ?IStateStorage $stateStorage = null;
267

268
        private ?string $customPaginatorTemplate = null;
269

270
        private ?string $componentFullName = null;
271

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

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

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

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

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

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

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

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

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

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

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

336
                return $this;
×
337
        }
338

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

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

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

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

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

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

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

NEW
373
                $items = $this->redrawItem !== [] && !($this->columnsSummary instanceof ColumnsSummary)
×
NEW
374
                        ? $this->dataModel->filterRow($this->redrawItem)
×
NEW
375
                        : $this->dataModel->filterData(
×
NEW
376
                                $this->getPaginator(),
×
NEW
377
                                $this->createSorting($this->sort, $this->sortCallback),
×
NEW
378
                                $this->assembleFilters()
×
379
                        );
380

381
                $hasGroupActionOnRows = false;
×
382

383
                foreach ($items as $item) {
×
NEW
384
                        $row = new Row($this, $item, $this->getPrimaryKey());
×
385

386
                        /**
387
                         * When iterating all items (for ColumnsSummary), skip rows that are not the redraw target.
388
                         * The Row object is still created above so ColumnsSummary::add() is called for every row.
389
                         */
NEW
390
                        if ($this->redrawItem !== []) {
×
NEW
391
                                $redrawKey = array_key_first($this->redrawItem);
×
392

NEW
393
                                if ((string) $row->getValue($redrawKey) !== (string) $this->redrawItem[$redrawKey]) {
×
NEW
394
                                        continue;
×
395
                                }
396

NEW
397
                                $this->getPresenterInstance()->payload->_datagrid_redrawItem_class = $row->getControlClass();
×
NEW
398
                                $this->getPresenterInstance()->payload->_datagrid_redrawItem_id = $row->getId();
×
399
                        }
400

NEW
401
                        $rows[] = $row;
×
402

403
                        if (!$hasGroupActionOnRows && $row->hasGroupAction()) {
×
404
                                $hasGroupActionOnRows = true;
×
405
                        }
406

407
                        if ($this->rowCallback !== null) {
×
408
                                ($this->rowCallback)($item, $row->getControl());
×
409
                        }
410
                }
411

412
                if ($hasGroupActionOnRows) {
×
413
                        $hasGroupActionOnRows = $this->hasGroupActions();
×
414
                }
415

416
                if ($this->isTreeView()) {
×
417
                        $template->treeViewHasChildrenColumn = $this->treeViewHasChildrenColumn;
×
418
                }
419

420
                $template->rows = $rows;
×
421

422
                $template->columns = $this->getColumns();
×
423
                $template->actions = $this->actions;
×
424
                $template->exports = $this->exports;
×
425
                $template->filters = $this->filters;
×
426
                $template->toolbarButtons = $this->toolbarButtons;
×
427
                $template->aggregationFunctions = $this->getAggregationFunctions();
×
428
                $template->multipleAggregationFunction = $this->getMultipleAggregationFunction();
×
429

430
                $template->filter_active = $this->isFilterActive();
×
431
                $template->originalTemplate = $this->getOriginalTemplateFile();
×
432
                $template->iconPrefix = static::$iconPrefix;
×
433
                $template->btnSecondaryClass = static::$btnSecondaryClass;
×
434
                $template->itemsDetail = $this->itemsDetail;
×
435
                $template->columnsVisibility = $this->getColumnsVisibility();
×
436
                $template->columnsSummary = $this->columnsSummary;
×
437

438
                $template->inlineEdit = $this->inlineEdit;
×
439
                $template->inlineAdd = $this->inlineAdd;
×
440

441
                $template->hasGroupActions = $this->hasGroupActions();
×
442
                $template->hasGroupActionOnRows = $hasGroupActionOnRows;
×
443

444
                /**
445
                 * Walkaround for Latte (does not know $form in snippet in {form} etc)
446
                 */
447
                $template->filter = $this['filter'];
×
448

449
                /**
450
                 * Set template file and render it
451
                 */
452
                $template->setFile($this->getTemplateFile());
×
453
                $template->render();
×
454
        }
455

456

457
        /********************************************************************************
458
         *                                 ROW CALLBACK *
459
         ********************************************************************************/
460

461
        /**
462
         * Each row can be modified with user defined callback
463
         *
464
         * @return static
465
         */
466
        public function setRowCallback(callable $callback): self
467
        {
468
                $this->rowCallback = $callback;
×
469

470
                return $this;
×
471
        }
472

473

474
        /********************************************************************************
475
         *                                 DATA SOURCE *
476
         ********************************************************************************/
477

478
        /**
479
         * @return static
480
         */
481
        public function setPrimaryKey(string $primaryKey): self
482
        {
483
                if ($this->dataModel instanceof DataModel) {
×
484
                        throw new DatagridException('Please set datagrid primary key before setting datasource.');
×
485
                }
486

487
                $this->primaryKey = $primaryKey;
×
488

489
                return $this;
×
490
        }
491

492
        /**
493
         * @return static
494
         * @throws InvalidArgumentException
495
         */
496
        public function setDataSource(mixed $source): self
1✔
497
        {
498
                $this->dataModel = new DataModel($source, $this->primaryKey);
1✔
499

500
                $this->dataModel->onBeforeFilter[] = [$this, 'beforeDataModelFilter'];
1✔
501
                $this->dataModel->onAfterFilter[] = [$this, 'afterDataModelFilter'];
1✔
502
                $this->dataModel->onAfterPaginated[] = [$this, 'afterDataModelPaginated'];
1✔
503

504
                return $this;
1✔
505
        }
506

507
        public function getDataSource(): IDataSource|array|null
508
        {
509
                return isset($this->dataModel)
×
510
                        ? $this->dataModel->getDataSource()
×
511
                        : null;
×
512
        }
513

514

515
        /********************************************************************************
516
         *                                  TEMPLATING *
517
         ********************************************************************************/
518

519
        /**
520
         * @return static
521
         */
522
        public function setTemplateFile(string $templateFile): self
523
        {
524
                $this->templateFile = $templateFile;
×
525

526
                return $this;
×
527
        }
528

529
        public function getTemplateFile(): string
530
        {
531
                return $this->templateFile ?? $this->getOriginalTemplateFile();
×
532
        }
533

534
        public function getOriginalTemplateFile(): string
535
        {
536
                return __DIR__ . '/templates/datagrid.latte';
×
537
        }
538

539
        /********************************************************************************
540
         *                                   SORTING *
541
         ********************************************************************************/
542

543
        /**
544
         * @return static
545
         */
546
        public function setDefaultSort(string|array $sort, bool $useOnReset = true): self
547
        {
548
                $sort = is_string($sort)
×
549
                        ? [$sort => 'ASC']
×
550
                        : $sort;
×
551

552
                $this->defaultSort = $sort;
×
553
                $this->defaultSortUseOnReset = $useOnReset;
×
554

555
                return $this;
×
556
        }
557

558
        /**
559
         * Return default sort for column, if specified
560
         */
561
        public function getColumnDefaultSort(string $columnKey): ?string
562
        {
563
                if (isset($this->defaultSort[$columnKey])) {
×
564
                        return $this->defaultSort[$columnKey];
×
565
                }
566

567
                return null;
×
568
        }
569

570
        /**
571
         * User may set default sorting, apply it
572
         */
573
        public function findDefaultSort(): void
574
        {
575
                if ($this->sort !== []) {
1✔
576
                        return;
×
577
                }
578

579
                if ((bool) $this->getStorageData('_grid_has_sorted')) {
1✔
580
                        return;
×
581
                }
582

583
                if ($this->defaultSort !== []) {
1✔
584
                        $this->sort = $this->defaultSort;
×
585
                }
586

587
                $this->saveStorageData('_grid_sort', $this->sort);
1✔
588
        }
1✔
589

590
        /**
591
         * @return static
592
         * @throws DatagridException
593
         */
594
        public function setSortable(bool $sortable = true): self
595
        {
596
                if ($this->getItemsDetail() !== null) {
×
597
                        throw new DatagridException('You can not use both sortable datagrid and items detail.');
×
598
                }
599

600
                $this->sortable = $sortable;
×
601

602
                return $this;
×
603
        }
604

605
        public function isSortable(): bool
606
        {
607
                return $this->sortable;
×
608
        }
609

610
        /**
611
         * @return static
612
         */
613
        public function setMultiSortEnabled(bool $multiSort = true): self
614
        {
615
                $this->multiSort = $multiSort;
×
616

617
                return $this;
×
618
        }
619

620
        public function isMultiSortEnabled(): bool
621
        {
622
                return $this->multiSort;
×
623
        }
624

625
        /**
626
         * @return static
627
         */
628
        public function setSortableHandler(string $handler = 'sort!'): self
629
        {
630
                $this->sortableHandler = $handler;
×
631

632
                return $this;
×
633
        }
634

635
        public function getSortableHandler(): string
636
        {
637
                return $this->sortableHandler;
×
638
        }
639

640
        public function getSortNext(Column $column): array
641
        {
642
                $sort = $column->getSortNext();
×
643

644
                if ($this->isMultiSortEnabled()) {
×
645
                        $sort = array_merge($this->sort, $sort);
×
646
                }
647

648
                return array_filter($sort);
×
649
        }
650

651
        /********************************************************************************
652
         *                                  TREE VIEW *
653
         ********************************************************************************/
654
        public function isTreeView(): bool
655
        {
656
                return $this->treeViewChildrenCallback !== null;
×
657
        }
658

659
        /**
660
         * @return static
661
         */
662
        public function setTreeView(
663
                callable $getChildrenCallback,
664
                string|callable $treeViewHasChildrenColumn = 'has_children'
665
        ): self
666
        {
667
                if (is_callable($treeViewHasChildrenColumn)) {
×
668
                        $this->treeViewHasChildrenCallback = $treeViewHasChildrenColumn;
×
669
                        $treeViewHasChildrenColumn = null;
×
670
                }
671

672
                $this->treeViewChildrenCallback = $getChildrenCallback;
×
673
                $this->treeViewHasChildrenColumn = $treeViewHasChildrenColumn;
×
674

675
                /**
676
                 * Torn off pagination
677
                 */
678
                $this->setPagination(false);
×
679

680
                /**
681
                 * Set tree view template file
682
                 */
683
                if ($this->templateFile === null) {
×
684
                        $this->setTemplateFile(__DIR__ . '/templates/datagrid_tree.latte');
×
685
                }
686

687
                return $this;
×
688
        }
689

690
        public function hasTreeViewChildrenCallback(): bool
691
        {
692
                return is_callable($this->treeViewHasChildrenCallback);
×
693
        }
694

695
        public function treeViewChildrenCallback(mixed $item): bool
696
        {
697
                if ($this->treeViewHasChildrenCallback === null) {
×
698
                        throw new UnexpectedValueException();
×
699
                }
700

701
                return (bool) call_user_func($this->treeViewHasChildrenCallback, $item);
×
702
        }
703

704
        /********************************************************************************
705
         *                                    COLUMNS *
706
         ********************************************************************************/
707
        public function addColumnText(
708
                string $key,
1✔
709
                string $name,
1✔
710
                ?string $column = null
1✔
711
        ): ColumnText
712
        {
713
                $column ??= $key;
1✔
714

715
                $columnText = new ColumnText($this, $key, $column, $name);
1✔
716
                $this->addColumn($key, $columnText);
1✔
717

718
                return $columnText;
1✔
719
        }
720

721
        public function addColumnLink(
722
                string $key,
1✔
723
                string $name,
1✔
724
                ?string $href = null,
1✔
725
                ?string $column = null,
726
                ?array $params = null
727
        ): ColumnLink
728
        {
729
                $column ??= $key;
1✔
730
                $href ??= $key;
1✔
731

732
                if ($params === null) {
1✔
733
                        $params = [$this->primaryKey];
1✔
734
                }
735

736
                $columnLink = new ColumnLink($this, $key, $column, $name, $href, $params);
1✔
737
                $this->addColumn($key, $columnLink);
1✔
738

739
                return $columnLink;
1✔
740
        }
741

742
        public function addColumnNumber(
743
                string $key,
1✔
744
                string $name,
1✔
745
                ?string $column = null
1✔
746
        ): ColumnNumber
747
        {
748
                $column ??= $key;
1✔
749

750
                $columnNumber = new ColumnNumber($this, $key, $column, $name);
1✔
751
                $this->addColumn($key, $columnNumber);
1✔
752

753
                return $columnNumber;
1✔
754
        }
755

756
        public function addColumnDateTime(
757
                string $key,
1✔
758
                string $name,
1✔
759
                ?string $column = null
1✔
760
        ): ColumnDateTime
761
        {
762
                $column ??= $key;
1✔
763

764
                $columnDateTime = new ColumnDateTime($this, $key, $column, $name);
1✔
765
                $this->addColumn($key, $columnDateTime);
1✔
766

767
                return $columnDateTime;
1✔
768
        }
769

770
        public function addColumnStatus(
771
                string $key,
1✔
772
                string $name,
1✔
773
                ?string $column = null
1✔
774
        ): ColumnStatus
775
        {
776
                $column ??= $key;
1✔
777

778
                $columnStatus = new ColumnStatus($this, $key, $column, $name);
1✔
779
                $this->addColumn($key, $columnStatus);
1✔
780

781
                return $columnStatus;
1✔
782
        }
783

784
        /**
785
         * @throws DatagridColumnNotFoundException
786
         */
787
        public function getColumn(string $key): Column
1✔
788
        {
789
                if (!isset($this->columns[$key])) {
1✔
790
                        throw new DatagridColumnNotFoundException(
1✔
791
                                sprintf('There is no column at key [%s] defined.', $key)
1✔
792
                        );
793
                }
794

795
                return $this->columns[$key];
1✔
796
        }
797

798
        /**
799
         * @return static
800
         */
801
        public function removeColumn(string $key): self
1✔
802
        {
803
                unset($this->columnsVisibility[$key], $this->columns[$key]);
1✔
804

805
                return $this;
1✔
806
        }
807

808
        /********************************************************************************
809
         *                                    ACTIONS *
810
         ********************************************************************************/
811
        public function addAction(
812
                string $key,
1✔
813
                string $name,
1✔
814
                ?string $href = null,
1✔
815
                ?array $params = null
816
        ): Action
817
        {
818
                $this->addActionCheck($key);
1✔
819

820
                $href ??= $key;
1✔
821

822
                if ($params === null) {
1✔
823
                        $params = [$this->primaryKey];
1✔
824
                }
825

826
                return $this->actions[$key] = new Action($this, $key, $href, $name, $params);
1✔
827
        }
828

829
        public function addActionCallback(
830
                string $key,
831
                string $name,
832
                ?callable $callback = null
833
        ): ActionCallback
834
        {
835
                $this->addActionCheck($key);
×
836

837
                $params = ['__id' => $this->primaryKey];
×
838

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

841
                if ($callback !== null) {
×
842
                        $action->onClick[] = $callback;
×
843
                }
844

845
                return $action;
×
846
        }
847

848
        public function addMultiAction(string $key, string $name): MultiAction
849
        {
850
                $this->addActionCheck($key);
×
851

852
                $action = new MultiAction($this, $key, $name);
×
853

854
                $this->actions[$key] = $action;
×
855

856
                return $action;
×
857
        }
858

859
        /**
860
         * @throws DatagridException
861
         */
862
        public function getAction(string $key): Action|MultiAction
863
        {
864
                if (!isset($this->actions[$key])) {
×
865
                        throw new DatagridException(sprintf('There is no action at key [%s] defined.', $key));
×
866
                }
867

868
                return $this->actions[$key];
×
869
        }
870

871
        /**
872
         * @return static
873
         */
874
        public function removeAction(string $key): self
875
        {
876
                unset($this->actions[$key]);
×
877

878
                return $this;
×
879
        }
880

881
        public function addFilterText(
882
                string $key,
1✔
883
                string $name,
1✔
884
                array|string|null $columns = null
1✔
885
        ): FilterText
886
        {
887
                $columns = $columns === null ? [$key] : (is_string($columns) ? [$columns] : $columns);
1✔
888

889
                $this->addFilterCheck($key);
1✔
890

891
                return $this->filters[$key] = new FilterText($this, $key, $name, $columns);
1✔
892
        }
893

894
        public function addFilterSelect(
895
                string $key,
896
                string $name,
897
                array $options,
898
                ?string $column = null
899
        ): FilterSelect
900
        {
901
                $column ??= $key;
×
902

903
                $this->addFilterCheck($key);
×
904

905
                return $this->filters[$key] = new FilterSelect($this, $key, $name, $options, $column);
×
906
        }
907

908
        public function addFilterMultiSelect(
909
                string $key,
910
                string $name,
911
                array $options,
912
                ?string $column = null
913
        ): FilterMultiSelect
914
        {
915
                $column ??= $key;
×
916

917
                $this->addFilterCheck($key);
×
918

919
                return $this->filters[$key] = new FilterMultiSelect($this, $key, $name, $options, $column);
×
920
        }
921

922
        public function addFilterDate(string $key, string $name, ?string $column = null): FilterDate
923
        {
924
                $column ??= $key;
×
925

926
                $this->addFilterCheck($key);
×
927

928
                return $this->filters[$key] = new FilterDate($this, $key, $name, $column);
×
929
        }
930

931
        public function addFilterRange(
932
                string $key,
933
                string $name,
934
                ?string $column = null,
935
                string $nameSecond = '-'
936
        ): FilterRange
937
        {
938
                $column ??= $key;
×
939

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

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

945
        /**
946
         * @throws DatagridException
947
         */
948
        public function addFilterDateRange(
949
                string $key,
950
                string $name,
951
                ?string $column = null,
952
                string $nameSecond = '-'
953
        ): FilterDateRange
954
        {
955
                $column ??= $key;
×
956

957
                $this->addFilterCheck($key);
×
958

959
                return $this->filters[$key] = new FilterDateRange($this, $key, $name, $column, $nameSecond);
×
960
        }
961

962
        /**
963
         * Fill array of Filter\Filter[] with values from $this->filter persistent parameter
964
         * Fill array of Column\Column[] with values from $this->sort persistent parameter
965
         *
966
         * @return array<Filter>
967
         */
968
        public function assembleFilters(): array
969
        {
970
                foreach ($this->filter as $key => $value) {
1✔
971
                        if (!isset($this->filters[$key])) {
×
972
                                $this->deleteStorageData($key);
×
973

974
                                continue;
×
975
                        }
976

977
                        if (is_iterable($value)) {
×
978
                                if (!ArraysHelper::testEmpty($value)) {
×
979
                                        $this->filters[$key]->setValue($value);
×
980
                                }
981
                        } else {
982
                                if ($value !== '' && $value !== null) {
×
983
                                        $this->filters[$key]->setValue($value);
×
984
                                }
985
                        }
986
                }
987

988
                foreach ($this->columns as $key => $column) {
1✔
989
                        if (isset($this->sort[$key])) {
×
990
                                $column->setSort($this->sort[$key]);
×
991
                        }
992
                }
993

994
                $this->onFiltersAssembled($this->filters);
1✔
995

996
                return $this->filters;
1✔
997
        }
998

999
        /**
1000
         * @return static
1001
         */
1002
        public function removeFilter(string $key): self
1003
        {
1004
                unset($this->filters[$key]);
×
1005

1006
                return $this;
×
1007
        }
1008

1009
        public function getFilter(string $key): Filter
1✔
1010
        {
1011
                if (!isset($this->filters[$key])) {
1✔
1012
                        throw new DatagridException(sprintf('Filter [%s] is not defined', $key));
×
1013
                }
1014

1015
                return $this->filters[$key];
1✔
1016
        }
1017

1018
        /**
1019
         * @deprecated Use setStrictStorageFilterValues() instead
1020
         * @return static
1021
         */
1022
        public function setStrictSessionFilterValues(bool $strictStorageFilterValues = true): self
1023
        {
1024
                return $this->setStrictStorageFilterValues($strictStorageFilterValues);
×
1025
        }
1026

1027
        /**
1028
         * @return static
1029
         */
1030
        public function setStrictStorageFilterValues(bool $strictStorageFilterValues = true): self
1031
        {
1032
                $this->strictStorageFilterValues = $strictStorageFilterValues;
×
1033

1034
                return $this;
×
1035
        }
1036

1037
        /********************************************************************************
1038
         *                                  FILTERING *
1039
         ********************************************************************************/
1040
        public function isFilterActive(): bool
1041
        {
1042
                $is_filter = ArraysHelper::testTruthy($this->filter);
×
1043

1044
                return $is_filter || $this->forceFilterActive;
×
1045
        }
1046

1047
        public function isFilterDefault(): bool
1048
        {
1049
                return $this->filter === $this->defaultFilter;
1✔
1050
        }
1051

1052
        /**
1053
         * Tell that filter is active from whatever reasons
1054
         *
1055
         * @return static
1056
         */
1057
        public function setFilterActive(): self
1058
        {
1059
                $this->forceFilterActive = true;
×
1060

1061
                return $this;
×
1062
        }
1063

1064
        /**
1065
         * Set filter values (force - overwrite user data)
1066
         *
1067
         * @return static
1068
         */
1069
        public function setFilter(array $filter): self
1✔
1070
        {
1071
                $this->filter = $filter;
1✔
1072

1073
                $this->saveStorageData('_grid_has_filtered', 1);
1✔
1074

1075
                return $this;
1✔
1076
        }
1077

1078
        /**
1079
         * If we want to sent some initial filter
1080
         *
1081
         * @return static
1082
         * @throws DatagridException
1083
         */
1084
        public function setDefaultFilter(array $defaultFilter, bool $useOnReset = true): self
1✔
1085
        {
1086
                foreach ($defaultFilter as $key => $value) {
1✔
1087
                        $filter = $this->getFilter($key);
1✔
1088

1089
                        if ($filter instanceof FilterMultiSelect && !is_array($value)) {
1✔
1090
                                throw new DatagridException(
×
1091
                                        sprintf('Default value of filter [%s] - MultiSelect has to be an array', $key)
×
1092
                                );
1093
                        }
1094

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

1102
                                $temp = $value;
×
1103
                                unset($temp['from'], $temp['to']);
×
1104

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

1116
                $this->defaultFilter = $defaultFilter;
1✔
1117
                $this->defaultFilterUseOnReset = $useOnReset;
1✔
1118

1119
                return $this;
1✔
1120
        }
1121

1122
        public function findDefaultFilter(): void
1123
        {
1124
                if ($this->filter !== []) {
1✔
1125
                        return;
×
1126
                }
1127

1128
                if ((bool) $this->getStorageData('_grid_has_filtered')) {
1✔
1129
                        return;
×
1130
                }
1131

1132
                if ($this->defaultFilter !== []) {
1✔
1133
                        $this->filter = $this->defaultFilter;
×
1134
                }
1135

1136
                $storedFilters = $this->getStorageData('_grid_filters', []);
1✔
1137
                foreach ($this->filter as $key => $value) {
1✔
1138
                        $storedFilters[(string) $key] = $value;
×
1139
                }
1140

1141
                $this->saveStorageData('_grid_filters', $storedFilters);
1✔
1142
        }
1✔
1143

1144
        public function createComponentFilter(): Form
1145
        {
1146
                $form = new Form($this, 'filter');
1✔
1147

1148
                $form->setMethod(static::$formMethod);
1✔
1149

1150
                $form->setTranslator($this->getTranslator());
1✔
1151

1152
                /**
1153
                 * InlineEdit part
1154
                 */
1155
                $inline_edit_container = $form->addContainer('inline_edit');
1✔
1156

1157
                if ($this->inlineEdit instanceof InlineEdit) {
1✔
1158
                        $inline_edit_container->addSubmit('submit', 'contributte_datagrid.save')
×
1159
                                ->setValidationScope([$inline_edit_container]);
×
1160
                        $inline_edit_container->addSubmit('cancel', 'contributte_datagrid.cancel')
×
1161
                                ->setValidationScope(null);
×
1162

1163
                        $this->inlineEdit->onControlAdd($inline_edit_container);
×
1164
                        $this->inlineEdit->onControlAfterAdd($inline_edit_container);
×
1165
                }
1166

1167
                /**
1168
                 * InlineAdd part
1169
                 */
1170
                $inlineAddContainer = $form->addContainer('inline_add');
1✔
1171

1172
                if ($this->inlineAdd instanceof InlineAdd) {
1✔
1173
                        $inlineAddContainer->addSubmit('submit', 'contributte_datagrid.save')
1✔
1174
                                ->setValidationScope([$inlineAddContainer]);
1✔
1175
                        $inlineAddContainer->addSubmit('cancel', 'contributte_datagrid.cancel')
1✔
1176
                                ->setValidationScope(null)
1✔
1177
                                ->setHtmlAttribute('data-datagrid-cancel-inline-add', true);
1✔
1178

1179
                        $this->inlineAdd->onControlAdd($inlineAddContainer);
1✔
1180
                        $this->inlineAdd->onControlAfterAdd($inlineAddContainer);
1✔
1181
                }
1182

1183
                /**
1184
                 * ItemDetail form part
1185
                 */
1186
                $itemsDetailForm = $this->getItemDetailForm();
1✔
1187

1188
                if ($itemsDetailForm instanceof Container) {
1✔
1189
                        $form['items_detail_form'] = $itemsDetailForm;
×
1190
                }
1191

1192
                /**
1193
                 * Filter part
1194
                 */
1195
                $filterContainer = $form->addContainer('filter');
1✔
1196

1197
                foreach ($this->filters as $filter) {
1✔
1198
                        $filter->addToFormContainer($filterContainer);
1✔
1199
                }
1200

1201
                if (!$this->hasAutoSubmit()) {
1✔
1202
                        $filterContainer['submit'] = $this->getFilterSubmitButton();
×
1203
                }
1204

1205
                /**
1206
                 * Group action part
1207
                 */
1208
                $groupActionContainer = $form->addContainer('group_action');
1✔
1209

1210
                if ($this->hasGroupActions()) {
1✔
1211
                        $this->getGroupActionCollection()->addToFormContainer($groupActionContainer);
×
1212
                }
1213

1214
                if ($form->isSubmitted() === false) {
1✔
1215
                        $this->setFilterContainerDefaults($filterContainer, $this->filter);
1✔
1216
                }
1217

1218
                /**
1219
                 * Per page part
1220
                 */
1221
                if ($this->isPaginated()) {
1✔
1222
                        $select = $form->addSelect('perPage', '', $this->getItemsPerPageList())
1✔
1223
                                ->setTranslator(null);
1✔
1224

1225
                        if ($form->isSubmitted() === false) {
1✔
1226
                                $select->setValue($this->getPerPage());
1✔
1227
                        }
1228

1229
                        $form->addSubmit('perPage_submit', 'contributte_datagrid.per_page_submit')
1✔
1230
                                ->setValidationScope([$select]);
1✔
1231
                }
1232

1233
                $form->onSubmit[] = function (NetteForm $form): void {
1✔
1234
                        $this->filterSucceeded($form);
1235
                };
1236

1237
                return $form;
1✔
1238
        }
1239

1240
        public function setFilterContainerDefaults(Container $container, array $values, ?string $parentKey = null): void
1✔
1241
        {
1242
                foreach ($container->getComponents() as $key => $control) {
1✔
1243
                        if (!isset($values[$key])) {
1✔
1244
                                continue;
1✔
1245
                        }
1246

1247
                        if ($control instanceof Container) {
1✔
1248
                                $this->setFilterContainerDefaults($control, (array) $values[$key], (string) $key);
×
1249

1250
                                continue;
×
1251
                        }
1252

1253
                        $value = $values[$key];
1✔
1254

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

1258
                                if ($filter instanceof IFilterDate) {
×
1259
                                        $value = $value->format($filter->getPhpFormat());
×
1260
                                }
1261
                        }
1262

1263
                        try {
1264
                                if (!$control instanceof FormControl) {
1✔
1265
                                        throw new UnexpectedValueException();
×
1266
                                }
1267

1268
                                $control->setValue($value);
1✔
1269

1270
                        } catch (InvalidArgumentException $e) {
×
1271
                                if ($this->strictStorageFilterValues) {
×
1272
                                        throw $e;
×
1273
                                }
1274
                        }
1275
                }
1276
        }
1✔
1277

1278
        /**
1279
         * Set $this->filter values after filter form submitted
1280
         */
1281
        public function filterSucceeded(NetteForm $form): void
1✔
1282
        {
1283
                if ($this->snippetsSet) {
1✔
1284
                        return;
×
1285
                }
1286

1287
                if ($this->getPresenterInstance()->isAjax()) {
1✔
1288
                        if (isset($form['group_action']['submit']) && $form['group_action']['submit']->isSubmittedBy()) {
×
1289
                                return;
×
1290
                        }
1291
                }
1292

1293
                /**
1294
                 * Per page
1295
                 */
1296
                if ($form->getComponent('perPage', false) !== null) {
1✔
1297
                        $perPage = $form->getComponent('perPage')->getValue();
1✔
1298
                        $this->saveStorageData('_grid_perPage', $perPage);
1✔
1299
                        $this->perPage = $perPage;
1✔
1300
                }
1301

1302
                /**
1303
                 * Inline edit
1304
                 */
1305
                if (
1306
                        isset($form['inline_edit'])
1✔
1307
                        && isset($form['inline_edit']['submit'])
1✔
1308
                        && isset($form['inline_edit']['cancel'])
1✔
1309
                        && $this->inlineEdit !== null
1✔
1310
                ) {
1311
                        $edit = $form['inline_edit'];
×
1312

1313
                        if (
1314
                                !$edit instanceof Container
×
1315
                                || !$edit['submit'] instanceof FormsSubmitButton
×
1316
                                || !$edit['cancel'] instanceof FormsSubmitButton
×
1317
                        ) {
1318
                                throw new UnexpectedValueException();
×
1319
                        }
1320

1321
                        if ($edit['submit']->isSubmittedBy() || $edit['cancel']->isSubmittedBy()) {
×
1322
                                /** @var string $id */
1323
                                $id = $form->getHttpData(Form::DataLine, 'inline_edit[_id]');
×
1324
                                $primaryWhereColumn = $form->getHttpData(Form::DataLine, 'inline_edit[_primary_where_column]');
×
1325

1326
                                if ($edit->getComponent('submit')->isSubmittedBy() && $edit->getErrors() === []) {
×
1327
                                        $this->inlineEdit->onSubmit($id, $form->getComponent('inline_edit')->getValues());
×
1328
                                        $this->getPresenterInstance()->payload->_datagrid_inline_edited = $id;
×
1329
                                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1330
                                } else {
1331
                                        $this->getPresenterInstance()->payload->_datagrid_inline_edit_cancel = $id;
×
1332
                                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1333
                                }
1334

1335
                                if ($edit['submit']->isSubmittedBy() && $this->inlineEdit->onCustomRedraw !== []) {
×
1336
                                        $this->inlineEdit->onCustomRedraw('submit');
×
1337
                                } elseif ($edit['cancel']->isSubmittedBy() && $this->inlineEdit->onCustomRedraw !== []) {
×
1338
                                        $this->inlineEdit->onCustomRedraw('cancel');
×
1339
                                } else {
1340
                                        $this->redrawItem($id, $primaryWhereColumn);
×
1341
                                        $this->redrawControl('summary');
×
1342
                                }
1343

1344
                                return;
×
1345
                        }
1346
                }
1347

1348
                /**
1349
                 * Inline add
1350
                 */
1351
                if (
1352
                        isset($form['inline_add'])
1✔
1353
                        && isset($form['inline_add']['submit'])
1✔
1354
                        && isset($form['inline_add']['cancel'])
1✔
1355
                        && $this->inlineAdd !== null
1✔
1356
                ) {
1357
                        $add = $form['inline_add'];
1✔
1358

1359
                        if (
1360
                                !$add instanceof Container
1✔
1361
                                || !$add['submit'] instanceof FormsSubmitButton
1✔
1362
                                || !$add['cancel'] instanceof FormsSubmitButton
1✔
1363
                        ) {
1364
                                throw new UnexpectedValueException();
×
1365
                        }
1366

1367
                        if ($add['submit']->isSubmittedBy() || $add['cancel']->isSubmittedBy()) {
1✔
1368
                                if ($add['submit']->isSubmittedBy() && $add->getErrors() === []) {
×
1369
                                        $this->inlineAdd->onSubmit($form->getComponent('inline_add')->getValues());
×
1370
                                }
1371

1372
                                $this->redrawControl('tbody');
×
1373

1374
                                $this->onRedraw();
×
1375

1376
                                return;
×
1377
                        }
1378
                }
1379

1380
                /**
1381
                 * Filter itself
1382
                 */
1383
                $values = $form->getComponent('filter')->getValues();
1✔
1384

1385
                if (!$values instanceof ArrayHash) {
1✔
1386
                        throw new UnexpectedValueException();
×
1387
                }
1388

1389
                $storedFilters = $this->getStorageData('_grid_filters', []);
1✔
1390
                $hasNonEmptyFilter = false;
1✔
1391

1392
                foreach ($values as $key => $value) {
1✔
1393
                        /**
1394
                         * Check if value is empty
1395
                         */
1396
                        $isEmpty = is_iterable($value) ? ArraysHelper::testEmpty($value) : $value === '' || $value === null;
1✔
1397

1398
                        /**
1399
                         * Storage stuff
1400
                         */
1401
                        $storedValue = $storedFilters[(string) $key] ?? null;
1✔
1402
                        if ($this->rememberState && $storedValue !== $value) {
1✔
1403
                                /**
1404
                                 * Has been filter changed?
1405
                                 */
1406
                                $this->page = 1;
1✔
1407
                                $this->saveStorageData('_grid_page', 1);
1✔
1408
                        }
1409

1410
                        $storedFilters[(string) $key] = $value;
1✔
1411

1412
                        /**
1413
                         * Only add non-empty filters to the persistent parameter
1414
                         */
1415
                        if (!$isEmpty) {
1✔
1416
                                $this->filter[$key] = $value;
1✔
1417
                                $hasNonEmptyFilter = true;
1✔
1418
                        } else {
1419
                                unset($this->filter[$key]);
1✔
1420
                        }
1421
                }
1422

1423
                $this->saveStorageData('_grid_filters', $storedFilters);
1✔
1424

1425
                if ($hasNonEmptyFilter) {
1✔
1426
                        $this->saveStorageData('_grid_has_filtered', 1);
1✔
1427
                }
1428

1429
                if ($this->getPresenterInstance()->isAjax()) {
1✔
1430
                        $this->getPresenterInstance()->payload->_datagrid_sort = [];
×
1431

1432
                        foreach ($this->columns as $key => $column) {
×
1433
                                if ($column->isSortable()) {
×
1434
                                        $this->getPresenterInstance()->payload->_datagrid_sort[$key] = $this->link('sort!', [
×
1435
                                                'sort' => $column->getSortNext(),
×
1436
                                        ]);
1437
                                }
1438
                        }
1439
                }
1440

1441
                $this->reload();
1✔
1442
        }
1443

1444
        /**
1445
         * @return static
1446
         */
1447
        public function setOuterFilterRendering(bool $outerFilterRendering = true): self
1448
        {
1449
                $this->outerFilterRendering = $outerFilterRendering;
×
1450

1451
                return $this;
×
1452
        }
1453

1454
        public function hasOuterFilterRendering(): bool
1455
        {
1456
                return $this->outerFilterRendering;
×
1457
        }
1458

1459
        /**
1460
         * @return static
1461
         * @throws InvalidArgumentException
1462
         */
1463
        public function setOuterFilterColumnsCount(int $count): self
1464
        {
1465
                $columnsCounts = [1, 2, 3, 4, 6, 12];
×
1466

1467
                if (!in_array($count, $columnsCounts, true)) {
×
1468
                        throw new InvalidArgumentException(sprintf(
×
1469
                                'Columns count must be one of following values: %s. Value %s given.',
×
1470
                                implode(', ', $columnsCounts),
×
1471
                                $count
1472
                        ));
1473
                }
1474

1475
                $this->outerFilterColumnsCount = $count;
×
1476

1477
                return $this;
×
1478
        }
1479

1480
        public function getOuterFilterColumnsCount(): int
1481
        {
1482
                return $this->outerFilterColumnsCount;
×
1483
        }
1484

1485
        /**
1486
         * @return static
1487
         */
1488
        public function setCollapsibleOuterFilters(bool $collapsibleOuterFilters = true): self
1489
        {
1490
                $this->collapsibleOuterFilters = $collapsibleOuterFilters;
×
1491

1492
                return $this;
×
1493
        }
1494

1495
        public function hasCollapsibleOuterFilters(): bool
1496
        {
1497
                return $this->collapsibleOuterFilters;
×
1498
        }
1499

1500
        /**
1501
         * Try to restore storage stuff
1502
         *
1503
         * @throws DatagridFilterNotFoundException
1504
         */
1505
        public function findStorageValues(): void
1506
        {
1507
                if (!ArraysHelper::testEmpty($this->filter) || ($this->page !== 1) || $this->sort !== []) {
1✔
1508
                        return;
×
1509
                }
1510

1511
                if (!$this->rememberState) {
1✔
1512
                        return;
×
1513
                }
1514

1515
                $page = $this->getStorageData('_grid_page');
1✔
1516

1517
                if ($page !== null) {
1✔
1518
                        $this->page = (int) $page;
×
1519
                }
1520

1521
                $perPage = $this->getStorageData('_grid_perPage');
1✔
1522

1523
                if ($perPage !== null) {
1✔
1524
                        $this->perPage = $perPage;
×
1525
                }
1526

1527
                $sort = $this->getStorageData('_grid_sort');
1✔
1528

1529
                if (is_array($sort) && $sort !== []) {
1✔
1530
                        $this->sort = $sort;
×
1531
                }
1532

1533
                foreach ($this->getStorageData('_grid_filters', []) as $key => $value) {
1✔
1534
                        try {
1535
                                $this->getFilter($key);
×
1536

1537
                                $this->filter[$key] = $value;
×
1538

1539
                        } catch (DatagridException) {
×
1540
                                if ($this->strictStorageFilterValues) {
×
1541
                                        throw new DatagridFilterNotFoundException(
×
1542
                                                sprintf('Storage filter: Filter [%s] not found', $key)
×
1543
                                        );
1544
                                }
1545
                        }
1546
                }
1547

1548
                /**
1549
                 * When column is sorted via custom callback, apply it
1550
                 */
1551
                if ($this->sortCallback === null && $this->sort !== []) {
1✔
1552
                        foreach (array_keys($this->sort) as $key) {
×
1553
                                try {
1554
                                        $column = $this->getColumn((string) $key);
×
1555

1556
                                } catch (DatagridColumnNotFoundException) {
×
1557
                                        $this->deleteStorageData('_grid_sort');
×
1558
                                        $this->sort = [];
×
1559

1560
                                        return;
×
1561
                                }
1562

1563
                                if ($column->isSortable() && is_callable($column->getSortableCallback())) {
×
1564
                                        $this->sortCallback = $column->getSortableCallback();
×
1565
                                }
1566
                        }
1567
                }
1568
        }
1✔
1569

1570
        /********************************************************************************
1571
         *                                    EXPORTS *
1572
         ********************************************************************************/
1573
        public function addExportCallback(
1574
                string $text,
1✔
1575
                callable $callback,
1✔
1576
                bool $filtered = false
1✔
1577
        ): Export
1578
        {
1579
                return $this->addToExports(new Export($this, $text, $callback, $filtered));
1✔
1580
        }
1581

1582
        public function addExportCsvFiltered(
1583
                string $text,
1584
                string $csvFileName,
1585
                string $outputEncoding = 'utf-8',
1586
                string $delimiter = ';',
1587
                bool $includeBom = false
1588
        ): ExportCsv
1589
        {
1590
                return $this->addExportCsv($text, $csvFileName, $outputEncoding, $delimiter, $includeBom, true);
×
1591
        }
1592

1593
        public function addExportCsv(
1594
                string $text,
1✔
1595
                string $csvFileName,
1✔
1596
                string $outputEncoding = 'utf-8',
1✔
1597
                string $delimiter = ';',
1598
                bool $includeBom = false,
1599
                bool $filtered = false
1600
        ): ExportCsv
1601
        {
1602
                $exportCsv = new ExportCsv($this, $text, $csvFileName, $filtered, $outputEncoding, $delimiter, $includeBom);
1✔
1603

1604
                $this->addToExports($exportCsv);
1✔
1605

1606
                return $exportCsv;
1✔
1607
        }
1608

1609
        public function resetExportsLinks(): void
1610
        {
1611
                foreach ($this->exports as $id => $export) {
×
1612
                        $link = new Link($this, 'export!', ['id' => $id]);
×
1613

1614
                        $export->setLink($link);
×
1615
                }
1616
        }
1617

1618

1619
        /********************************************************************************
1620
         *                                TOOLBAR BUTTONS *
1621
         ********************************************************************************/
1622

1623
        /**
1624
         * @throws DatagridException
1625
         */
1626
        public function addToolbarButton(
1627
                string $href,
1628
                string $text = '',
1629
                array $params = []
1630
        ): ToolbarButton
1631
        {
1632
                if (isset($this->toolbarButtons[$href])) {
×
1633
                        throw new DatagridException(
×
1634
                                sprintf('There is already toolbar button at key [%s] defined.', $href)
×
1635
                        );
1636
                }
1637

1638
                return $this->toolbarButtons[$href] = new ToolbarButton($this, $href, $text, $params);
×
1639
        }
1640

1641
        /**
1642
         * @throws DatagridException
1643
         */
1644
        public function getToolbarButton(string $key): ToolbarButton
1645
        {
1646
                if (!isset($this->toolbarButtons[$key])) {
×
1647
                        throw new DatagridException(
×
1648
                                sprintf('There is no toolbar button at key [%s] defined.', $key)
×
1649
                        );
1650
                }
1651

1652
                return $this->toolbarButtons[$key];
×
1653
        }
1654

1655
        /**
1656
         * @return static
1657
         */
1658
        public function removeToolbarButton(string $key): self
1659
        {
1660
                unset($this->toolbarButtons[$key]);
×
1661

1662
                return $this;
×
1663
        }
1664

1665
        /********************************************************************************
1666
         *                                 GROUP ACTIONS *
1667
         ********************************************************************************/
1668
        public function addGroupAction(string $title, array $options = []): GroupAction
1669
        {
1670
                return $this->getGroupActionCollection()->addGroupSelectAction($title, $options);
×
1671
        }
1672

1673
        public function addGroupButtonAction(string $title, ?string $class = null): GroupButtonAction
1674
        {
1675
                return $this->getGroupActionCollection()->addGroupButtonAction($title, $class);
×
1676
        }
1677

1678
        public function addGroupSelectAction(string $title, array $options = []): GroupAction
1679
        {
1680
                return $this->getGroupActionCollection()->addGroupSelectAction($title, $options);
×
1681
        }
1682

1683
        public function addGroupMultiSelectAction(string $title, array $options = []): GroupAction
1684
        {
1685
                return $this->getGroupActionCollection()->addGroupMultiSelectAction($title, $options);
×
1686
        }
1687

1688
        public function addGroupTextAction(string $title): GroupAction
1689
        {
1690
                return $this->getGroupActionCollection()->addGroupTextAction($title);
×
1691
        }
1692

1693
        public function addGroupTextareaAction(string $title): GroupAction
1694
        {
1695
                return $this->getGroupActionCollection()->addGroupTextareaAction($title);
×
1696
        }
1697

1698
        public function getGroupActionCollection(): GroupActionCollection
1699
        {
1700
                if ($this->groupActionCollection === null) {
×
1701
                        $this->groupActionCollection = new GroupActionCollection($this);
×
1702
                }
1703

1704
                return $this->groupActionCollection;
×
1705
        }
1706

1707
        public function hasGroupActions(): bool
1708
        {
1709
                return $this->groupActionCollection instanceof GroupActionCollection;
1✔
1710
        }
1711

1712
        public function shouldShowSelectedRowsCount(): bool
1713
        {
1714
                return $this->showSelectedRowsCount;
×
1715
        }
1716

1717
        /**
1718
         * @return static
1719
         */
1720
        public function setShowSelectedRowsCount(bool $show = true): self
1721
        {
1722
                $this->showSelectedRowsCount = $show;
×
1723

1724
                return $this;
×
1725
        }
1726

1727
        /********************************************************************************
1728
         *                                   HANDLERS *
1729
         ********************************************************************************/
1730
        public function handlePage(int $page): void
1731
        {
1732
                $this->page = $page;
×
1733
                $this->saveStorageData('_grid_page', $page);
×
1734

1735
                $this->reload(['table']);
×
1736
        }
1737

1738
        /**
1739
         * @throws DatagridColumnNotFoundException
1740
         */
1741
        public function handleSort(array $sort): void
1742
        {
1743
                if (count($sort) === 0) {
×
1744
                        $sort = $this->defaultSort;
×
1745
                }
1746

1747
                foreach (array_keys($sort) as $key) {
×
1748
                        try {
1749
                                $column = $this->getColumn($key);
×
1750

1751
                        } catch (DatagridColumnNotFoundException) {
×
1752
                                unset($sort[$key]);
×
1753

1754
                                continue;
×
1755
                        }
1756

1757
                        if ($column->sortableResetPagination()) {
×
1758
                                $this->saveStorageData('_grid_page', $this->page = 1);
×
1759
                        }
1760

1761
                        if ($column->getSortableCallback() !== null) {
×
1762
                                $this->sortCallback = $column->getSortableCallback();
×
1763
                        }
1764
                }
1765

1766
                $this->saveStorageData('_grid_has_sorted', 1);
×
1767
                $this->saveStorageData('_grid_sort', $this->sort = $sort);
×
1768

1769
                $this->reloadTheWholeGrid();
×
1770
        }
1771

1772
        public function handleResetFilter(): void
1773
        {
1774
                /**
1775
                 * Storage stuff
1776
                 */
1777
                $this->deleteStorageData('_grid_page');
1✔
1778

1779
                if ($this->defaultFilterUseOnReset) {
1✔
1780
                        $this->deleteStorageData('_grid_has_filtered');
1✔
1781
                }
1782

1783
                if ($this->defaultSortUseOnReset) {
1✔
1784
                        $this->deleteStorageData('_grid_has_sorted');
1✔
1785
                }
1786

1787
                $this->deleteStorageData('_grid_filters');
1✔
1788

1789
                $this->filter = [];
1✔
1790

1791
                $this->reloadTheWholeGrid();
1✔
1792
        }
1793

1794
        public function handleResetColumnFilter(string $key): void
1795
        {
1796
                $storedFilters = $this->getStorageData('_grid_filters', []);
×
1797
                if (isset($storedFilters[$key])) {
×
1798
                        unset($storedFilters[$key]);
×
1799
                        $this->saveStorageData('_grid_filters', $storedFilters);
×
1800
                }
1801

1802
                unset($this->filter[$key]);
×
1803

1804
                $this->reloadTheWholeGrid();
×
1805
        }
1806

1807
        /**
1808
         * @return static
1809
         */
1810
        public function setColumnReset(bool $reset = true): self
1811
        {
1812
                $this->hasColumnReset = $reset;
×
1813

1814
                return $this;
×
1815
        }
1816

1817
        public function hasColumnReset(): bool
1818
        {
1819
                return $this->hasColumnReset;
1✔
1820
        }
1821

1822
        /**
1823
         * @param array<Filter> $filters
1824
         */
1825
        public function sendNonEmptyFiltersInPayload(array $filters): void
1✔
1826
        {
1827
                if (!$this->hasColumnReset()) {
1✔
1828
                        return;
×
1829
                }
1830

1831
                $non_empty_filters = [];
1✔
1832

1833
                foreach ($filters as $filter) {
1✔
1834
                        if ($filter->isValueSet()) {
1✔
1835
                                $non_empty_filters[] = $filter->getKey();
×
1836
                        }
1837
                }
1838

1839
                $this->getPresenterInstance()->payload->non_empty_filters = $non_empty_filters;
1✔
1840
        }
1✔
1841

1842
        public function handleExport(mixed $id): void
1✔
1843
        {
1844
                if (!isset($this->exports[$id])) {
1✔
1845
                        throw new ForbiddenRequestException();
×
1846
                }
1847

1848
                if ($this->columnsExportOrder !== []) {
1✔
1849
                        $this->setColumnsOrder($this->columnsExportOrder);
×
1850
                }
1851

1852
                $export = $this->exports[$id];
1✔
1853

1854
                /**
1855
                 * Invoke possible events
1856
                 */
1857
                $this->onExport($this);
1✔
1858

1859
                if ($export->isFiltered()) {
1✔
1860
                        $sort = $this->sort;
1✔
1861
                        $filter = $this->assembleFilters();
1✔
1862
                } else {
1863
                        $sort = [$this->primaryKey => 'ASC'];
1✔
1864
                        $filter = [];
1✔
1865
                }
1866

1867
                if ($this->dataModel === null) {
1✔
1868
                        throw new DatagridException('You have to set a data source first.');
1✔
1869
                }
1870

1871
                $rows = [];
1✔
1872

1873
                $items = $this->dataModel->filterData(
1✔
1874
                        null,
1✔
1875
                        $this->createSorting($sort, $this->sortCallback),
1✔
1876
                        $filter
1877
                );
1878

1879
                foreach ($items as $item) {
1✔
1880
                        $rows[] = new Row($this, $item, $this->getPrimaryKey());
1✔
1881
                }
1882

1883
                if ($export instanceof ExportCsv) {
1✔
1884
                        $export->invoke($rows);
1✔
1885
                } else {
1886
                        $export->invoke($items);
1✔
1887
                }
1888

1889
                if ($export->isAjax()) {
1✔
1890
                        $this->reload();
×
1891
                }
1892
        }
1✔
1893

1894
        public function handleGetChildren(mixed $parent): void
1895
        {
1896
                if (!is_callable($this->treeViewChildrenCallback)) {
×
1897
                        throw new UnexpectedValueException();
×
1898
                }
1899

1900
                $this->setDataSource(call_user_func($this->treeViewChildrenCallback, $parent));
×
1901

1902
                if ($this->getPresenterInstance()->isAjax()) {
×
1903
                        $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
1904
                        $this->getPresenterInstance()->payload->_datagrid_tree = $parent;
×
1905

1906
                        $this->redrawControl('items');
×
1907

1908
                        $this->onRedraw();
×
1909
                } else {
1910
                        $this->getPresenterInstance()->redirect('this');
×
1911
                }
1912
        }
1913

1914
        public function handleGetItemDetail(mixed $id): void
1915
        {
1916
                $template = $this->getTemplate();
×
1917

1918
                if (!$template instanceof Template) {
×
1919
                        throw new UnexpectedValueException();
×
1920
                }
1921

1922
                $template->toggle_detail = $id;
×
1923

1924
                if ($this->itemsDetail === null) {
×
1925
                        throw new UnexpectedValueException();
×
1926
                }
1927

1928
                $this->redrawItem = [$this->itemsDetail->getPrimaryWhereColumn() => $id];
×
1929

1930
                if ($this->getPresenterInstance()->isAjax()) {
×
1931
                        $this->getPresenterInstance()->payload->_datagrid_toggle_detail = $id;
×
1932
                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1933
                        $this->redrawControl('items');
×
1934

1935
                        /**
1936
                         * Only for nette 2.4
1937
                         */
1938
                        if (method_exists($template->getLatte(), 'addProvider')) {
×
1939
                                $this->redrawControl('gridSnippets');
×
1940
                        }
1941

1942
                        $this->onRedraw();
×
1943
                } else {
1944
                        $this->getPresenterInstance()->redirect('this');
×
1945
                }
1946
        }
1947

1948
        public function handleEdit(mixed $id, mixed $key): void
1949
        {
1950
                $column = $this->getColumn($key);
×
1951
                $request = $this->getPresenterInstance()->getRequest();
×
1952

1953
                if (!$request instanceof Request) {
×
1954
                        throw new UnexpectedValueException();
×
1955
                }
1956

1957
                $value = $request->getPost('value');
×
1958

1959
                // Could be null of course
1960
                if ($column->getEditableCallback() === null) {
×
1961
                        throw new UnexpectedValueException();
×
1962
                }
1963

1964
                $newValue = $column->getEditableCallback()($id, $value);
×
1965

1966
                $this->getPresenterInstance()->payload->_datagrid_editable_new_value = $newValue;
×
1967
                $this->getPresenterInstance()->payload->postGet = true;
×
1968
                $this->getPresenterInstance()->payload->url = $this->link('this');
×
1969

1970
                if (!$this->getPresenterInstance()->isControlInvalid(null)) {
×
1971
                        $this->getPresenterInstance()->sendPayload();
×
1972
                }
1973
        }
1974

1975
        /**
1976
         * @param array|string[] $snippets
1977
         */
1978
        public function reload(array $snippets = []): void
1✔
1979
        {
1980
                if ($this->getPresenterInstance()->isAjax()) {
1✔
1981
                        $this->redrawControl('tbody');
×
1982
                        $this->redrawControl('pagination');
×
1983
                        $this->redrawControl('summary');
×
1984
                        $this->redrawControl('thead-group-action');
×
1985

1986
                        /**
1987
                         * manualy reset exports links...
1988
                         */
1989
                        $this->resetExportsLinks();
×
1990
                        $this->redrawControl('exports');
×
1991

1992
                        foreach ($snippets as $snippet) {
×
1993
                                $this->redrawControl($snippet);
×
1994
                        }
1995

1996
                        $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
1997
                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
1998

1999
                        $this->onRedraw();
×
2000
                } else {
2001
                        $this->getPresenterInstance()->redirect('this');
1✔
2002
                }
2003
        }
2004

2005
        public function reloadTheWholeGrid(): void
2006
        {
2007
                if ($this->getPresenterInstance()->isAjax()) {
1✔
2008
                        $this->redrawControl('grid');
×
2009

2010
                        $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
2011
                        $this->getPresenterInstance()->payload->_datagrid_name = $this->getFullName();
×
2012

2013
                        $this->onRedraw();
×
2014
                } else {
2015
                        $this->getPresenterInstance()->redirect('this');
1✔
2016
                }
2017
        }
2018

2019
        public function handleChangeStatus(string $id, string $key, string $value): void
2020
        {
2021
                if (!isset($this->columns[$key])) {
×
2022
                        throw new DatagridException(sprintf('ColumnStatus[%s] does not exist', $key));
×
2023
                }
2024

2025
                if (!$this->columns[$key] instanceof ColumnStatus) {
×
2026
                        throw new UnexpectedValueException();
×
2027
                }
2028

2029
                $this->columns[$key]->onChange($id, $value);
×
2030
        }
2031

2032
        public function redrawItem(string|int $id, mixed $primaryWhereColumn = null): void
2033
        {
2034
                $this->snippetsSet = true;
×
2035

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

2038
                $this->redrawControl('items');
×
2039

2040
                $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
2041

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

2045
        public function handleShowAllColumns(): void
2046
        {
2047
                $this->deleteStorageData('_grid_hidden_columns');
×
2048
                $this->saveStorageData('_grid_hidden_columns_manipulated', true);
×
2049

2050
                $this->redrawControl();
×
2051

2052
                $this->onShowAllColumns();
×
2053
                $this->onRedraw();
×
2054
        }
2055

2056
        public function handleShowDefaultColumns(): void
2057
        {
2058
                $this->deleteStorageData('_grid_hidden_columns');
×
2059
                $this->saveStorageData('_grid_hidden_columns_manipulated', false);
×
2060

2061
                $this->redrawControl();
×
2062

2063
                $this->onShowDefaultColumns();
×
2064
                $this->onRedraw();
×
2065
        }
2066

2067
        public function handleShowColumn(string $column): void
2068
        {
2069
                $columns = $this->getStorageData('_grid_hidden_columns');
×
2070

2071
                if ($columns !== [] && $columns !== null) {
×
2072
                        $pos = array_search($column, $columns, true);
×
2073

2074
                        if ($pos !== false) {
×
2075
                                unset($columns[$pos]);
×
2076
                        }
2077
                }
2078

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

2082
                $this->redrawControl();
×
2083

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

2088
        public function handleHideColumn(string $column): void
2089
        {
2090
                /**
2091
                 * Store info about hiding a column to storage
2092
                 */
2093
                $columns = $this->getStorageData('_grid_hidden_columns');
×
2094

2095
                if ($columns === [] || $columns === null) {
×
2096
                        $columns = [$column];
×
2097
                } elseif (!in_array($column, $columns, true)) {
×
2098
                        array_push($columns, $column);
×
2099
                }
2100

2101
                $this->saveStorageData('_grid_hidden_columns', $columns);
×
2102
                $this->saveStorageData('_grid_hidden_columns_manipulated', true);
×
2103

2104
                $this->redrawControl();
×
2105

2106
                $this->onColumnHide($column);
×
2107
                $this->onRedraw();
×
2108
        }
2109

2110
        public function handleActionCallback(mixed $__key, mixed $__id): void
2111
        {
2112
                $action = $this->getAction($__key);
×
2113

2114
                if (!($action instanceof ActionCallback)) {
×
2115
                        throw new DatagridException(
×
2116
                                sprintf('Action [%s] does not exist or is not an callback aciton.', $__key)
×
2117
                        );
2118
                }
2119

2120
                $action->onClick($__id);
×
2121
        }
2122

2123

2124
        /********************************************************************************
2125
         *                                  PAGINATION *
2126
         ********************************************************************************/
2127

2128
        /**
2129
         * @param array|array|int[]|array|string[] $itemsPerPageList
2130
         * @return static
2131
         */
2132
        public function setItemsPerPageList(array $itemsPerPageList, bool $includeAll = true): self
1✔
2133
        {
2134
                if ($itemsPerPageList === []) {
1✔
2135
                        throw new InvalidArgumentException('$itemsPerPageList can not be an empty array');
×
2136
                }
2137

2138
                $this->itemsPerPageList = $itemsPerPageList;
1✔
2139

2140
                if ($includeAll) {
1✔
2141
                        $this->itemsPerPageList[] = 'all';
1✔
2142
                }
2143

2144
                return $this;
1✔
2145
        }
2146

2147
        /**
2148
         * @return static
2149
         */
2150
        public function setDefaultPerPage(int $count): self
2151
        {
2152
                $this->defaultPerPage = $count;
×
2153

2154
                return $this;
×
2155
        }
2156

2157
        /**
2158
         * User may set default "items per page" value, apply it
2159
         */
2160
        public function findDefaultPerPage(): void
2161
        {
2162
                if ($this->perPage !== null) {
×
2163
                        return;
×
2164
                }
2165

2166
                if ($this->defaultPerPage !== null) {
×
2167
                        $this->perPage = $this->defaultPerPage;
×
2168
                }
2169

2170
                $this->saveStorageData('_grid_perPage', $this->perPage);
×
2171
        }
2172

2173
        public function createComponentPaginator(): DatagridPaginator
2174
        {
2175
                $component = new DatagridPaginator(
×
2176
                        $this->getTranslator(),
×
2177
                        static::$iconPrefix,
×
2178
                        static::$btnSecondaryClass
×
2179
                );
2180
                $paginator = $component->getPaginator();
×
2181

2182
                $paginator->setPage($this->page);
×
2183

2184
                if (is_int($this->getPerPage())) {
×
2185
                        $paginator->setItemsPerPage($this->getPerPage());
×
2186
                }
2187

2188
                if ($this->customPaginatorTemplate !== null) {
×
2189
                        $component->setTemplateFile($this->customPaginatorTemplate);
×
2190
                }
2191

2192
                return $component;
×
2193
        }
2194

2195
        public function getPerPage(): int|string
2196
        {
2197
                $itemsPerPageList = array_keys($this->getItemsPerPageList());
1✔
2198

2199
                $perPage = $this->perPage ?? reset($itemsPerPageList);
1✔
2200

2201
                if (($perPage !== 'all' && !in_array((int) $this->perPage, $itemsPerPageList, true))
1✔
2202
                        || ($perPage === 'all' && !in_array($this->perPage, $itemsPerPageList, true))) {
1✔
2203
                        $perPage = reset($itemsPerPageList);
1✔
2204
                }
2205

2206
                return $perPage === 'all'
1✔
2207
                        ? 'all'
1✔
2208
                        : (int) $perPage;
1✔
2209
        }
2210

2211
        /**
2212
         * @return array|array|int[]|array|string[]|\Stringable[]
2213
         */
2214
        public function getItemsPerPageList(): array
2215
        {
2216
                $list = array_flip($this->itemsPerPageList);
1✔
2217

2218
                foreach (array_keys($list) as $key) {
1✔
2219
                        $list[$key] = $key;
1✔
2220
                }
2221

2222
                if (array_key_exists('all', $list)) {
1✔
2223
                        $list['all'] = $this->getTranslator()->translate('contributte_datagrid.all');
1✔
2224
                }
2225

2226
                return $list;
1✔
2227
        }
2228

2229
        /**
2230
         * @return static
2231
         */
2232
        public function setPagination(bool $doPaginate): self
2233
        {
2234
                $this->doPaginate = $doPaginate;
×
2235

2236
                return $this;
×
2237
        }
2238

2239
        public function isPaginated(): bool
2240
        {
2241
                return $this->doPaginate;
1✔
2242
        }
2243

2244
        public function getPaginator(): ?DatagridPaginator
2245
        {
2246
                if ($this->isPaginated() && $this->perPage !== 'all') {
×
2247
                        return $this['paginator'];
×
2248
                }
2249

2250
                return null;
×
2251
        }
2252

2253

2254
        /********************************************************************************
2255
         *                                     I18N *
2256
         ********************************************************************************/
2257

2258
        /**
2259
         * @return static
2260
         */
2261
        public function setTranslator(Translator $translator): self
1✔
2262
        {
2263
                $this->translator = $translator;
1✔
2264

2265
                return $this;
1✔
2266
        }
2267

2268
        public function getTranslator(): Translator
2269
        {
2270
                if ($this->translator === null) {
1✔
2271
                        $this->translator = new SimpleTranslator();
1✔
2272
                }
2273

2274
                return $this->translator;
1✔
2275
        }
2276

2277

2278
        /********************************************************************************
2279
         *                                 COLUMNS ORDER *
2280
         ********************************************************************************/
2281

2282
        /**
2283
         * Set order of datagrid columns
2284
         *
2285
         * @param array|string[] $order
2286
         * @return static
2287
         */
2288
        public function setColumnsOrder(array $order): self
2289
        {
2290
                $new_order = [];
×
2291

2292
                foreach ($order as $key) {
×
2293
                        if (isset($this->columns[$key])) {
×
2294
                                $new_order[$key] = $this->columns[$key];
×
2295
                        }
2296
                }
2297

2298
                if (count($new_order) === count($this->columns)) {
×
2299
                        $this->columns = $new_order;
×
2300
                } else {
2301
                        throw new DatagridException('When changing columns order, you have to specify all columns');
×
2302
                }
2303

2304
                return $this;
×
2305
        }
2306

2307
        /**
2308
         * Columns order may be different for export and normal grid
2309
         *
2310
         * @param array|string[] $order
2311
         * @return static
2312
         */
2313
        public function setColumnsExportOrder(array $order): self
2314
        {
2315
                $this->columnsExportOrder = $order;
×
2316

2317
                return $this;
×
2318
        }
2319

2320
        /********************************************************************************
2321
         *                                SESSION & URL *
2322
         ********************************************************************************/
2323
        public function getSessionSectionName(): string
2324
        {
2325
                $presenter = $this->getPresenterInstance();
1✔
2326

2327
                return $presenter->getName() . ':' . $this->getUniqueId();
1✔
2328
        }
2329

2330
        /**
2331
         * @return static
2332
         */
2333
        public function setRememberState(bool $remember = true, bool $rememberHideableColumnsState = false): self
1✔
2334
        {
2335
                $this->rememberState = $remember;
1✔
2336
                $this->rememberHideableColumnsState = $rememberHideableColumnsState;
1✔
2337

2338
                return $this;
1✔
2339
        }
2340

2341
        /**
2342
         * @return static
2343
         */
2344
        public function setRefreshUrl(bool $refresh = true): self
2345
        {
2346
                $this->refreshURL = $refresh;
×
2347

2348
                return $this;
×
2349
        }
2350

2351
        public function getStorageData(string $key, mixed $defaultValue = null): mixed
1✔
2352
        {
2353
                if ($this->shouldRememberState($key)) {
1✔
2354
                        return $this->getStateStorage()->loadState($key) ?? $defaultValue;
1✔
2355
                }
2356

2357
                return $defaultValue;
1✔
2358
        }
2359

2360
        public function saveStorageData(string $key, mixed $value): void
1✔
2361
        {
2362
                if ($this->shouldRememberState($key)) {
1✔
2363
                        $this->getStateStorage()->saveState($key, $value);
1✔
2364
                }
2365
        }
1✔
2366

2367
        public function deleteStorageData(string $key): void
1✔
2368
        {
2369
                $this->getStateStorage()->deleteState($key);
1✔
2370
        }
1✔
2371

2372
        /********************************************************************************
2373
         *                                  ITEM DETAIL *
2374
         ********************************************************************************/
2375

2376
        /**
2377
         * Get items detail parameters
2378
         */
2379
        public function getItemsDetail(): ?ItemDetail
2380
        {
2381
                return $this->itemsDetail;
×
2382
        }
2383

2384
        /**
2385
         * @param mixed $detail callable|string|bool
2386
         */
2387
        public function setItemsDetail(mixed $detail = true, ?string $primaryWhereColumn = null): ItemDetail
2388
        {
2389
                if ($this->isSortable()) {
×
2390
                        throw new DatagridException('You can not use both sortable datagrid and items detail.');
×
2391
                }
2392

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

2395
                if (is_string($detail)) {
×
2396
                        /**
2397
                         * Item detail will be in separate template
2398
                         */
2399
                        $this->itemsDetail->setType('template');
×
2400
                        $this->itemsDetail->setTemplate($detail);
×
2401

2402
                } elseif (is_callable($detail)) {
×
2403
                        /**
2404
                         * Item detail will be rendered via custom callback renderer
2405
                         */
2406
                        $this->itemsDetail->setType('renderer');
×
2407
                        $this->itemsDetail->setRenderer($detail);
×
2408

2409
                } elseif ($detail === true) {
×
2410
                        /**
2411
                         * Item detail will be rendered probably via block #detail
2412
                         */
2413
                        $this->itemsDetail->setType('block');
×
2414

2415
                } else {
2416
                        throw new DatagridException('::setItemsDetail() can be called either with no parameters or with parameter = template path or callable renderer.');
×
2417
                }
2418

2419
                return $this->itemsDetail;
×
2420
        }
2421

2422
        /**
2423
         * @return static
2424
         */
2425
        public function setItemsDetailForm(callable $callableSetContainer): self
2426
        {
2427
                if ($this->itemsDetail instanceof ItemDetail) {
×
2428
                        $this->itemsDetail->setForm(
×
2429
                                new ItemDetailForm($callableSetContainer)
×
2430
                        );
2431

2432
                        return $this;
×
2433
                }
2434

2435
                throw new DatagridException('Please set the ItemDetail first.');
×
2436
        }
2437

2438
        public function getItemDetailForm(): ?Container
2439
        {
2440
                if ($this->itemsDetail instanceof ItemDetail) {
1✔
2441
                        return $this->itemsDetail->getForm();
×
2442
                }
2443

2444
                return null;
1✔
2445
        }
2446

2447
        /********************************************************************************
2448
         *                                ROW PRIVILEGES *
2449
         ********************************************************************************/
2450
        public function allowRowsGroupAction(callable $condition): void
2451
        {
2452
                $this->rowConditions['group_action'] = $condition;
×
2453
        }
2454

2455
        public function allowRowsInlineEdit(callable $condition): void
2456
        {
2457
                $this->rowConditions['inline_edit'] = $condition;
×
2458
        }
2459

2460
        public function allowRowsAction(string $key, callable $condition): void
2461
        {
2462
                $this->rowConditions['action'][$key] = $condition;
×
2463
        }
2464

2465
        /**
2466
         * @throws DatagridException
2467
         */
2468
        public function allowRowsMultiAction(
2469
                string $multiActionKey,
2470
                string $actionKey,
2471
                callable $condition
2472
        ): void
2473
        {
2474
                if (!isset($this->actions[$multiActionKey])) {
×
2475
                        throw new DatagridException(
×
2476
                                sprintf('There is no action at key [%s] defined.', $multiActionKey)
×
2477
                        );
2478
                }
2479

2480
                if (!$this->actions[$multiActionKey] instanceof MultiAction) {
×
2481
                        throw new DatagridException(
×
2482
                                sprintf('Action at key [%s] is not a MultiAction.', $multiActionKey)
×
2483
                        );
2484
                }
2485

2486
                $this->actions[$multiActionKey]->setRowCondition($actionKey, $condition);
×
2487
        }
2488

2489
        public function getRowCondition(string $name, ?string $key = null): bool|callable
2490
        {
2491
                if (!isset($this->rowConditions[$name])) {
×
2492
                        return false;
×
2493
                }
2494

2495
                $condition = $this->rowConditions[$name];
×
2496

2497
                if ($key === null) {
×
2498
                        return $condition;
×
2499
                }
2500

2501
                return $condition[$key] ?? false;
×
2502
        }
2503

2504
        /********************************************************************************
2505
         *                               COLUMN CALLBACK *
2506
         ********************************************************************************/
2507
        public function addColumnCallback(string $key, callable $callback): void
2508
        {
2509
                $this->columnCallbacks[$key] = $callback;
×
2510
        }
2511

2512
        public function getColumnCallback(string $key): ?callable
2513
        {
2514
                return $this->columnCallbacks[$key] ?? null;
×
2515
        }
2516

2517
        /********************************************************************************
2518
         *                                 INLINE EDIT *
2519
         ********************************************************************************/
2520
        public function addInlineEdit(?string $primaryWhereColumn = null): InlineEdit
2521
        {
2522
                $this->inlineEdit = new InlineEdit($this, $primaryWhereColumn ?? $this->primaryKey);
×
2523

2524
                return $this->inlineEdit;
×
2525
        }
2526

2527
        public function getInlineEdit(): ?InlineEdit
2528
        {
2529
                return $this->inlineEdit;
×
2530
        }
2531

2532
        public function handleShowInlineAdd(): void
2533
        {
2534
                if ($this->inlineAdd !== null) {
×
2535
                        $this->inlineAdd->setShouldBeRendered(true);
×
2536
                }
2537

2538
                $presenter = $this->getPresenterInstance();
×
2539

2540
                if ($presenter->isAjax()) {
×
2541
                        $presenter->payload->_datagrid_inline_adding = true;
×
2542
                        $presenter->payload->_datagrid_name = $this->getFullName();
×
2543

2544
                        $this->redrawControl('tbody');
×
2545

2546
                        $this->onRedraw();
×
2547
                }
2548
        }
2549

2550
        public function handleInlineEdit(mixed $id): void
2551
        {
2552
                if ($this->inlineEdit !== null) {
×
2553
                        $this->inlineEdit->setItemId($id);
×
2554

2555
                        $primaryWhereColumn = $this->inlineEdit->getPrimaryWhereColumn();
×
2556

2557
                        $filterContainer = $this['filter'];
×
2558
                        $inlineEditContainer = $filterContainer['inline_edit'];
×
2559

2560
                        if (!$inlineEditContainer instanceof Container) {
×
2561
                                throw new UnexpectedValueException();
×
2562
                        }
2563

2564
                        $inlineEditContainer->addHidden('_id', $id);
×
2565
                        $inlineEditContainer->addHidden('_primary_where_column', $primaryWhereColumn);
×
2566

2567
                        $presenter = $this->getPresenterInstance();
×
2568

2569
                        if ($presenter->isAjax()) {
×
2570
                                $presenter->payload->_datagrid_inline_editing = true;
×
2571
                                $presenter->payload->_datagrid_name = $this->getFullName();
×
2572
                        }
2573

2574
                        $this->redrawItem($id, $primaryWhereColumn);
×
2575
                }
2576
        }
2577

2578
        /********************************************************************************
2579
         *                                  INLINE ADD *
2580
         ********************************************************************************/
2581
        public function addInlineAdd(): InlineAdd
2582
        {
2583
                $this->inlineAdd = new InlineAdd($this);
1✔
2584

2585
                $this->inlineAdd
1✔
2586
                        ->setTitle('contributte_datagrid.add')
1✔
2587
                        ->setIcon('plus');
1✔
2588

2589
                return $this->inlineAdd;
1✔
2590
        }
2591

2592
        public function getInlineAdd(): ?InlineAdd
2593
        {
2594
                return $this->inlineAdd;
×
2595
        }
2596

2597

2598
        /********************************************************************************
2599
         *                               COLUMNS HIDING *
2600
         ********************************************************************************/
2601

2602
        /**
2603
         * Can datagrid hide colums?
2604
         */
2605
        public function canHideColumns(): bool
2606
        {
2607
                return $this->canHideColumns;
1✔
2608
        }
2609

2610
        /**
2611
         * Order Grid to set columns hideable.
2612
         *
2613
         * @return static
2614
         */
2615
        public function setColumnsHideable(bool $columnsHideable = true): self
1✔
2616
        {
2617
                $this->canHideColumns = $columnsHideable;
1✔
2618

2619
                return $this;
1✔
2620
        }
2621

2622
        /********************************************************************************
2623
         *                                COLUMNS SUMMARY *
2624
         ********************************************************************************/
2625
        public function hasColumnsSummary(): bool
2626
        {
2627
                return $this->columnsSummary instanceof ColumnsSummary;
1✔
2628
        }
2629

2630
        /**
2631
         * @param array|string[] $columns
2632
         */
2633
        public function setColumnsSummary(array $columns, ?callable $rowCallback = null): ColumnsSummary
1✔
2634
        {
2635
                if ($this->hasSomeAggregationFunction()) {
1✔
2636
                        throw new DatagridException('You can use either ColumnsSummary or AggregationFunctions');
×
2637
                }
2638

2639
                $this->columnsSummary = new ColumnsSummary($this, $columns, $rowCallback);
1✔
2640

2641
                return $this->columnsSummary;
1✔
2642
        }
2643

2644
        public function getColumnsSummary(): ?ColumnsSummary
2645
        {
2646
                return $this->columnsSummary;
1✔
2647
        }
2648

2649

2650
        /********************************************************************************
2651
         *                                   INTERNAL *
2652
         ********************************************************************************/
2653

2654
        /**
2655
         * Gets component's full name in component tree
2656
         *
2657
         * @throws DatagridHasToBeAttachedToPresenterComponentException
2658
         */
2659
        public function getFullName(): string
2660
        {
2661
                if ($this->componentFullName === null) {
×
2662
                        throw new DatagridHasToBeAttachedToPresenterComponentException('Datagrid needs to be attached to presenter in order to get its full name.');
×
2663
                }
2664

2665
                return $this->componentFullName;
×
2666
        }
2667

2668
        /**
2669
         * Tell grid filters to by submitted automatically
2670
         *
2671
         * @return static
2672
         */
2673
        public function setAutoSubmit(bool $autoSubmit = true): self
2674
        {
2675
                $this->autoSubmit = $autoSubmit;
×
2676

2677
                return $this;
×
2678
        }
2679

2680
        public function hasAutoSubmit(): bool
2681
        {
2682
                return $this->autoSubmit;
1✔
2683
        }
2684

2685
        public function getFilterSubmitButton(): SubmitButton
2686
        {
2687
                if ($this->hasAutoSubmit()) {
×
2688
                        throw new DatagridException('Datagrid has auto-submit. Turn it off before setting filter submit button.');
×
2689
                }
2690

2691
                if ($this->filterSubmitButton === null) {
×
2692
                        $this->filterSubmitButton = new SubmitButton($this);
×
2693
                }
2694

2695
                return $this->filterSubmitButton;
×
2696
        }
2697

2698

2699
        /********************************************************************************
2700
         *                                   INTERNAL *
2701
         ********************************************************************************/
2702

2703
        /**
2704
         * @internal
2705
         */
2706
        public function getColumnsCount(): int
2707
        {
2708
                $count = count($this->getColumns());
×
2709

2710
                if ($this->actions !== []
×
2711
                        || $this->isSortable()
×
2712
                        || $this->getItemsDetail() !== null
×
2713
                        || $this->getInlineEdit() !== null
×
2714
                        || $this->getInlineAdd() !== null) {
×
2715
                        $count++;
×
2716
                }
2717

2718
                if ($this->hasGroupActions()) {
×
2719
                        $count++;
×
2720
                }
2721

2722
                return $count;
×
2723
        }
2724

2725
        /**
2726
         * @internal
2727
         */
2728
        public function getPrimaryKey(): string
2729
        {
2730
                return $this->primaryKey;
1✔
2731
        }
2732

2733
        /**
2734
         * @return array<Column>
2735
         * @internal
2736
         */
2737
        public function getColumns(): array
2738
        {
2739
                $return = $this->columns;
1✔
2740

2741
                try {
2742
                        $this->getParentComponent();
1✔
2743

2744
                        if (! (bool) $this->getStorageData('_grid_hidden_columns_manipulated', false)) {
1✔
2745
                                $columns_to_hide = [];
1✔
2746

2747
                                foreach ($this->columns as $key => $column) {
1✔
2748
                                        if ($column->getDefaultHide()) {
×
2749
                                                $columns_to_hide[] = $key;
×
2750
                                        }
2751
                                }
2752

2753
                                if ($columns_to_hide !== []) {
1✔
2754
                                        $this->saveStorageData('_grid_hidden_columns', $columns_to_hide);
×
2755
                                        $this->saveStorageData('_grid_hidden_columns_manipulated', true);
×
2756
                                }
2757
                        }
2758

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

2761
                        foreach ($hidden_columns ?? [] as $column) {
1✔
2762
                                if (isset($this->columns[$column])) {
×
2763
                                        $this->columnsVisibility[$column] = [
×
2764
                                                'visible' => false,
2765
                                        ];
2766

2767
                                        unset($return[$column]);
×
2768
                                }
2769
                        }
2770
                } catch (DatagridHasToBeAttachedToPresenterComponentException) {
×
2771
                        // No need to worry
2772
                }
2773

2774
                return $return;
1✔
2775
        }
2776

2777
        /**
2778
         * @internal
2779
         */
2780
        public function getColumnsVisibility(): array
2781
        {
2782
                $return = $this->columnsVisibility;
1✔
2783

2784
                foreach (array_keys($this->columnsVisibility) as $key) {
1✔
2785
                        $return[$key]['column'] = $this->columns[$key];
×
2786
                }
2787

2788
                return $return;
1✔
2789
        }
2790

2791
        /**
2792
         * @internal
2793
         */
2794
        public function getParentComponent(): Component
2795
        {
2796
                $parent = parent::getParent();
1✔
2797

2798
                if (!$parent instanceof Component) {
1✔
2799
                        throw new DatagridHasToBeAttachedToPresenterComponentException(
×
2800
                                sprintf(
×
2801
                                        'Datagrid is attached to: "%s", but instance of %s is needed.',
×
2802
                                        ($parent !== null ? $parent::class : 'null'),
×
2803
                                        Component::class
×
2804
                                )
2805
                        );
2806
                }
2807

2808
                return $parent;
1✔
2809
        }
2810

2811
        /**
2812
         * @internal
2813
         * @throws UnexpectedValueException
2814
         */
2815
        public function getSortableParentPath(): string
2816
        {
2817
                if ($this->getParentComponent() instanceof IPresenter) {
×
2818
                        return '';
×
2819
                }
2820

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

2823
                if ($presenter === null) {
×
2824
                        throw new UnexpectedValueException(
×
2825
                                sprintf('%s needs %s', self::class, IPresenter::class)
×
2826
                        );
2827
                }
2828

2829
                return $presenter;
×
2830
        }
2831

2832
        /**
2833
         * Some of datagrid columns may be hidden by default
2834
         *
2835
         * @internal
2836
         * @return static
2837
         */
2838
        public function setSomeColumnDefaultHide(bool $defaultHide): self
2839
        {
2840
                $this->someColumnDefaultHide = $defaultHide;
×
2841

2842
                return $this;
×
2843
        }
2844

2845
        /**
2846
         * Are some of columns hidden bydefault?
2847
         *
2848
         * @internal
2849
         */
2850
        public function hasSomeColumnDefaultHide(): bool
2851
        {
2852
                return $this->someColumnDefaultHide;
×
2853
        }
2854

2855
        /**
2856
         * Simply refresh url
2857
         *
2858
         * @internal
2859
         */
2860
        public function handleRefreshState(): void
2861
        {
2862
                $this->findStorageValues();
×
2863
                $this->findDefaultFilter();
×
2864
                $this->findDefaultSort();
×
2865
                $this->findDefaultPerPage();
×
2866

2867
                $this->getPresenterInstance()->payload->_datagrid_url = $this->refreshURL;
×
2868
                $this->redrawControl('non-existing-snippet');
×
2869
        }
2870

2871
        /**
2872
         * @internal
2873
         */
2874
        public function setCustomPaginatorTemplate(string $templateFile): void
2875
        {
2876
                $this->customPaginatorTemplate = $templateFile;
×
2877
        }
2878

2879
        protected function createSorting(array $sort, ?callable $sortCallback = null): Sorting
1✔
2880
        {
2881
                foreach ($sort as $key => $order) {
1✔
2882
                        unset($sort[$key]);
1✔
2883

2884
                        if ($order !== 'ASC' && $order !== 'DESC') {
1✔
2885
                                continue;
×
2886
                        }
2887

2888
                        try {
2889
                                $column = $this->getColumn($key);
1✔
2890

2891
                        } catch (DatagridColumnNotFoundException) {
1✔
2892
                                continue;
1✔
2893
                        }
2894

2895
                        $sort[$column->getSortingColumn()] = $order;
×
2896
                }
2897

2898
                if ($sortCallback === null && isset($column)) {
1✔
2899
                        $sortCallback = $column->getSortableCallback();
×
2900
                }
2901

2902
                return new Sorting($sort, $sortCallback);
1✔
2903
        }
2904

2905
        /**
2906
         * @throws DatagridException
2907
         */
2908
        protected function addColumn(string $key, Column $column): Column
1✔
2909
        {
2910
                if (isset($this->columns[$key])) {
1✔
2911
                        throw new DatagridException(
×
2912
                                sprintf('There is already column at key [%s] defined.', $key)
×
2913
                        );
2914
                }
2915

2916
                $this->onColumnAdd($key, $column);
1✔
2917

2918
                $this->columnsVisibility[$key] = ['visible' => true];
1✔
2919

2920
                return $this->columns[$key] = $column;
1✔
2921
        }
2922

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

2937
 /********************************************************************************
2938
  *                                    FILTERS *
2939
  ********************************************************************************/
2940

2941
        /**
2942
         * Check whether given key already exists in $this->filters
2943
         *
2944
         * @throws DatagridException
2945
         */
2946
        protected function addFilterCheck(string $key): void
1✔
2947
        {
2948
                if (isset($this->filters[$key])) {
1✔
2949
                        throw new DatagridException(
×
2950
                                sprintf('There is already action at key [%s] defined.', $key)
×
2951
                        );
2952
                }
2953
        }
1✔
2954

2955
        protected function addToExports(Export $export): Export
1✔
2956
        {
2957
                $id = count($this->exports) > 0 ? count($this->exports) + 1 : 1;
1✔
2958

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

2961
                $export->setLink($link);
1✔
2962

2963
                return $this->exports[$id] = $export;
1✔
2964
        }
2965

2966
        private function getPresenterInstance(): Presenter
2967
        {
2968
                return $this->getPresenter();
1✔
2969
        }
2970

2971
        private function shouldRememberState(string $key): bool
1✔
2972
        {
2973
                return $this->rememberState ||
1✔
2974
                        ($this->rememberHideableColumnsState && in_array($key, self::HIDEABLE_COLUMNS_STORAGE_KEYS, true));
1✔
2975
        }
2976

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