TabulatorGrid::configureFromDataObject()   F
last analyzed

Complexity

Conditions 29
Paths > 20000

Size

Total Lines 173
Code Lines 96

Duplication

Lines 0
Ratio 0 %

Importance

Changes 14
Bugs 3 Features 4
Metric Value
eloc 96
c 14
b 3
f 4
dl 0
loc 173
rs 0
cc 29
nc 60010
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace LeKoala\Tabulator;
4
5
use Exception;
6
use RuntimeException;
7
use SilverStripe\i18n\i18n;
8
use SilverStripe\Forms\Form;
9
use InvalidArgumentException;
10
use LeKoala\Tabulator\BulkActions\BulkDeleteAction;
11
use SilverStripe\ORM\SS_List;
12
use SilverStripe\Core\Convert;
13
use SilverStripe\ORM\DataList;
14
use SilverStripe\ORM\ArrayList;
15
use SilverStripe\Core\ClassInfo;
16
use SilverStripe\ORM\DataObject;
17
use SilverStripe\View\ArrayData;
18
use SilverStripe\Forms\FieldList;
19
use SilverStripe\Forms\FormField;
20
use SilverStripe\Control\Director;
21
use SilverStripe\View\Requirements;
22
use SilverStripe\Control\Controller;
23
use SilverStripe\Control\HTTPRequest;
24
use SilverStripe\Control\HTTPResponse;
25
use SilverStripe\ORM\FieldType\DBEnum;
26
use SilverStripe\Control\RequestHandler;
27
use SilverStripe\Core\Injector\Injector;
28
use SilverStripe\Security\SecurityToken;
29
use SilverStripe\ORM\DataObjectInterface;
30
use SilverStripe\ORM\FieldType\DBBoolean;
31
use SilverStripe\Forms\GridField\GridFieldConfig;
32
use SilverStripe\ORM\Filters\PartialMatchFilter;
33
34
/**
35
 * This is a replacement for most GridField usages in SilverStripe
36
 * It can easily work in the frontend too
37
 *
38
 * @link http://www.tabulator.info/
39
 */
40
class TabulatorGrid extends FormField
41
{
42
    const POS_START = 'start';
43
    const POS_END = 'end';
44
45
    const UI_EDIT = "ui_edit";
46
    const UI_DELETE = "ui_delete";
47
    const UI_UNLINK = "ui_unlink";
48
    const UI_VIEW = "ui_view";
49
    const UI_SORT = "ui_sort";
50
51
    const TOOL_ADD_NEW = "add_new";
52
    const TOOL_EXPORT = "export"; // xlsx
53
    const TOOL_EXPORT_CSV = "export_csv";
54
    const TOOL_ADD_EXISTING = "add_existing";
55
56
    // @link http://www.tabulator.info/examples/6.2?#fittodata
57
    const LAYOUT_FIT_DATA = "fitData";
58
    const LAYOUT_FIT_DATA_FILL = "fitDataFill";
59
    const LAYOUT_FIT_DATA_STRETCH = "fitDataStretch";
60
    const LAYOUT_FIT_DATA_TABLE = "fitDataTable";
61
    const LAYOUT_FIT_COLUMNS = "fitColumns";
62
63
    const RESPONSIVE_LAYOUT_HIDE = "hide";
64
    const RESPONSIVE_LAYOUT_COLLAPSE = "collapse";
65
66
    // @link http://www.tabulator.info/docs/6.2/format
67
    const FORMATTER_PLAINTEXT = 'plaintext';
68
    const FORMATTER_TEXTAREA = 'textarea';
69
    const FORMATTER_HTML = 'html';
70
    const FORMATTER_MONEY = 'money';
71
    const FORMATTER_IMAGE = 'image';
72
    const FORMATTER_LINK = 'link';
73
    const FORMATTER_DATETIME = 'datetime';
74
    const FORMATTER_DATETIME_DIFF = 'datetimediff';
75
    const FORMATTER_TICKCROSS = 'tickCross';
76
    const FORMATTER_COLOR = 'color';
77
    const FORMATTER_STAR = 'star';
78
    const FORMATTER_TRAFFIC = 'traffic';
79
    const FORMATTER_PROGRESS = 'progress';
80
    const FORMATTER_LOOKUP = 'lookup';
81
    const FORMATTER_BUTTON_TICK = 'buttonTick';
82
    const FORMATTER_BUTTON_CROSS = 'buttonCross';
83
    const FORMATTER_ROWNUM = 'rownum';
84
    const FORMATTER_HANDLE = 'handle';
85
    // @link http://www.tabulator.info/docs/6.2/format#format-module
86
    const FORMATTER_ROW_SELECTION = 'rowSelection';
87
    const FORMATTER_RESPONSIVE_COLLAPSE = 'responsiveCollapse';
88
89
    // our built in functions
90
    const JS_BOOL_GROUP_HEADER = 'SSTabulator.boolGroupHeader';
91
    const JS_DATA_AJAX_RESPONSE = 'SSTabulator.dataAjaxResponse';
92
    const JS_INIT_CALLBACK = 'SSTabulator.initCallback';
93
    const JS_CONFIG_CALLBACK = 'SSTabulator.configCallback';
94
95
    /**
96
     * @config
97
     */
98
    private static array $allowed_actions = [
99
        'load',
100
        'handleItem',
101
        'handleTool',
102
        'configProvider',
103
        'autocomplete',
104
        'handleBulkAction',
105
    ];
106
107
    private static $url_handlers = [
108
        'item/$ID' => 'handleItem',
109
        'tool/$ID//$OtherID' => 'handleTool',
110
        'bulkAction/$ID' => 'handleBulkAction',
111
    ];
112
113
    private static array $casting = [
114
        'JsonOptions' => 'HTMLFragment',
115
        'ShowTools' => 'HTMLFragment',
116
        'dataAttributesHTML' => 'HTMLFragment',
117
    ];
118
119
    /**
120
     * @config
121
     */
122
    private static bool $load_styles = true;
123
124
    /**
125
     * @config
126
     */
127
    private static string $luxon_version = '3';
128
129
    /**
130
     * @config
131
     */
132
    private static string $last_icon_version = '2';
133
134
    /**
135
     * @config
136
     */
137
    private static bool $use_cdn = false;
138
139
    /**
140
     * @config
141
     */
142
    private static bool $use_v5 = false;
143
144
    /**
145
     * @config
146
     */
147
    private static bool $enable_luxon = false;
148
149
    /**
150
     * @config
151
     */
152
    private static bool $enable_last_icon = false;
153
154
    /**
155
     * @config
156
     */
157
    private static bool $enable_requirements = true;
158
159
    /**
160
     * @config
161
     */
162
    private static bool $enable_js_modules = true;
163
164
    /**
165
     * @link http://www.tabulator.info/docs/6.2/options
166
     * @config
167
     */
168
    private static array $default_options = [
169
        'index' => "ID", // http://tabulator.info/docs/6.2/data#row-index
170
        'layout' => 'fitColumns', // http://www.tabulator.info/docs/6.2/layout#layout
171
        'height' => '100%', // http://www.tabulator.info/docs/6.2/layout#height-fixed
172
        'responsiveLayout' => "hide", // http://www.tabulator.info/docs/6.2/layout#responsive
173
    ];
174
175
    /**
176
     * @link http://tabulator.info/docs/6.2/columns#defaults
177
     * @config
178
     */
179
    private static array $default_column_options = [
180
        'resizable' => false,
181
    ];
182
183
    private static bool $enable_ajax_init = true;
184
185
    /**
186
     * @config
187
     */
188
    private static bool $default_lazy_init = false;
189
190
    /**
191
     * @config
192
     */
193
    private static bool $show_row_delete = false;
194
195
    /**
196
     * Data source.
197
     */
198
    protected ?SS_List $list;
199
200
    /**
201
     * @link http://www.tabulator.info/docs/6.2/columns
202
     */
203
    protected array $columns = [];
204
205
    /**
206
     * @link http://tabulator.info/docs/6.2/columns#defaults
207
     */
208
    protected array $columnDefaults = [];
209
210
    /**
211
     * @link http://www.tabulator.info/docs/6.2/options
212
     */
213
    protected array $options = [];
214
215
    protected bool $autoloadDataList = true;
216
217
    protected bool $rowClickTriggersAction = false;
218
219
    protected int $pageSize = 10;
220
221
    protected string $itemRequestClass = '';
222
223
    protected string $modelClass = '';
224
225
    protected bool $lazyInit = false;
226
227
    protected array $tools = [];
228
229
    /**
230
     * @var AbstractBulkAction[]
231
     */
232
    protected array $bulkActions = [];
233
234
    protected array $listeners = [];
235
236
    protected array $linksOptions = [
237
        'ajaxURL'
238
    ];
239
240
    protected array $dataAttributes = [];
241
242
    protected string $controllerFunction = "";
243
244
    protected string $editUrl = "";
245
246
    protected string $moveUrl = "";
247
248
    protected string $bulkUrl = "";
249
250
    protected bool $globalSearch = false;
251
252
    protected array $wildcardFields = [];
253
254
    protected array $quickFilters = [];
255
256
    protected string $defaultFilter = 'PartialMatch';
257
258
    protected bool $groupLayout = false;
259
260
    protected bool $enableGridManipulation = false;
261
262
    /**
263
     * @param string $fieldName
264
     * @param string|null|bool $title
265
     * @param SS_List $value
266
     */
267
    public function __construct($name, $title = null, $value = null)
268
    {
269
        // Set options and defaults first
270
        $this->options = self::config()->default_options ?? [];
271
        $this->columnDefaults = self::config()->default_column_options ?? [];
272
273
        parent::__construct($name, $title, $value);
274
        $this->setLazyInit(self::config()->default_lazy_init);
275
276
        // We don't want regular setValue for this since it would break with loadFrom logic
277
        if ($value) {
278
            $this->setList($value);
279
        }
280
    }
281
282
    /**
283
     * This helps if some third party code expects the TabulatorGrid to be a GridField
284
     * Only works to a really basic extent
285
     */
286
    public function getConfig(): GridFieldConfig
287
    {
288
        return new GridFieldConfig;
289
    }
290
291
    /**
292
     * This helps if some third party code expects the TabulatorGrid to be a GridField
293
     * Only works to a really basic extent
294
     */
295
    public function setConfig($config)
296
    {
297
        // ignore
298
    }
299
300
    /**
301
     * @return string
302
     */
303
    public function getValueJson()
304
    {
305
        $v = $this->value ?? '';
306
        if (is_array($v)) {
307
            $v = json_encode($v);
308
        }
309
        if (strpos($v, '[') !== 0) {
310
            return '[]';
311
        }
312
        return $v;
313
    }
314
315
    public function saveInto(DataObjectInterface $record)
316
    {
317
        if ($this->enableGridManipulation) {
318
            $value = $this->dataValue();
319
            if (is_array($value)) {
320
                $this->value = json_encode(array_values($value));
321
            }
322
            parent::saveInto($record);
323
        }
324
    }
325
326
    /**
327
     * Temporary link that will be replaced by a real link by processLinks
328
     * TODO: not really happy with this, find a better way
329
     *
330
     * @param string $action
331
     * @return string
332
     */
333
    public function TempLink(string $action, bool $controller = true): string
334
    {
335
        // It's an absolute link
336
        if (strpos($action, '/') === 0 || strpos($action, 'http') === 0) {
337
            return $action;
338
        }
339
        // Already temp
340
        if (strpos($action, ':') !== false) {
341
            return $action;
342
        }
343
        $prefix = $controller ? "controller" : "form";
344
        return "$prefix:$action";
345
    }
346
347
    public function ControllerLink(string $action): string
348
    {
349
        return $this->getForm()->getController()->Link($action);
350
    }
351
352
    public function getCreateLink(): string
353
    {
354
        return Controller::join_links($this->Link('item'), 'new');
355
    }
356
357
    /**
358
     * @param FieldList $fields
359
     * @param string $name
360
     * @return TabulatorGrid|null
361
     */
362
    public static function replaceGridField(FieldList $fields, string $name)
363
    {
364
        /** @var \SilverStripe\Forms\GridField\GridField $gridField */
365
        $gridField = $fields->dataFieldByName($name);
366
        if (!$gridField) {
0 ignored issues
show
introduced by
$gridField is of type SilverStripe\Forms\GridField\GridField, thus it always evaluated to true.
Loading history...
367
            return;
368
        }
369
        if ($gridField instanceof TabulatorGrid) {
0 ignored issues
show
introduced by
$gridField is never a sub-type of LeKoala\Tabulator\TabulatorGrid.
Loading history...
370
            return $gridField;
371
        }
372
        $tabulatorGrid = new TabulatorGrid($name, $gridField->Title(), $gridField->getList());
373
        // In the cms, this is mostly never happening
374
        if ($gridField->getForm()) {
375
            $tabulatorGrid->setForm($gridField->getForm());
376
        }
377
        $tabulatorGrid->configureFromDataObject($gridField->getModelClass());
378
        $tabulatorGrid->setLazyInit(true);
379
        $fields->replaceField($name, $tabulatorGrid);
380
381
        return $tabulatorGrid;
382
    }
383
384
    /**
385
     * A shortcut to convert editable records to view only
386
     * Disables adding new records as well
387
     */
388
    public function setViewOnly(): void
389
    {
390
        $itemUrl = $this->TempLink('item/{ID}', false);
391
        $this->removeButton(self::UI_EDIT);
392
        $this->removeButton(self::UI_DELETE);
393
        $this->removeButton(self::UI_UNLINK);
394
        $this->addButton(self::UI_VIEW, $itemUrl, "visibility", "View");
395
        $this->removeTool(TabulatorAddNewButton::class);
396
    }
397
398
    public function isViewOnly(): bool
399
    {
400
        return !$this->hasButton(self::UI_EDIT);
401
    }
402
403
    public function setManageRelations(): array
404
    {
405
        $this->addToolEnd($AddExistingAutocompleter = new TabulatorAddExistingAutocompleter());
406
407
        $unlinkBtn = $this->makeButton($this->TempLink('item/{ID}/unlink', false), "link_off", _t('TabulatorGrid.Unlink', 'Unlink'));
408
        $unlinkBtn["formatterParams"]["classes"] = 'btn btn-danger';
409
        $unlinkBtn['formatterParams']['ajax'] = true;
410
        $this->addButtonFromArray(self::UI_UNLINK, $unlinkBtn);
411
412
        return [
413
            $AddExistingAutocompleter,
414
            $unlinkBtn
415
        ];
416
    }
417
418
    protected function getTabulatorOptions(DataObject $singl)
419
    {
420
        $opts = [];
421
        if ($singl->hasMethod('tabulatorOptions')) {
422
            $opts = $singl->tabulatorOptions();
423
        }
424
        return $opts;
425
    }
426
427
    public function configureFromDataObject($className = null): void
428
    {
429
        $this->columns = [];
430
431
        if (!$className) {
432
            $className = $this->getModelClass();
433
        }
434
        if (!$className) {
435
            throw new RuntimeException("Could not find the model class");
436
        }
437
        $this->modelClass = $className;
438
439
        /** @var DataObject $singl */
440
        $singl = singleton($className);
441
        $opts = $this->getTabulatorOptions($singl);
442
443
        // Mock some base columns using SilverStripe built-in methods
444
        $columns = [];
445
446
        $summaryFields = $opts['summaryFields'] ?? $singl->summaryFields();
447
        foreach ($summaryFields as $field => $title) {
448
            // Deal with this in load() instead
449
            // if (strpos($field, '.') !== false) {
450
            // $fieldParts = explode(".", $field);
451
452
            // It can be a relation Users.Count or a field Field.Nice
453
            // $classOrField = $fieldParts[0];
454
            // $relationOrMethod = $fieldParts[1];
455
            // }
456
            $title = str_replace(".", " ", $title);
457
            $columns[$field] = [
458
                'field' => $field,
459
                'title' => $title,
460
                'headerSort' => false,
461
            ];
462
463
            $dbObject = $singl->dbObject($field);
464
            if ($dbObject) {
465
                if ($dbObject instanceof DBBoolean) {
466
                    $columns[$field]['formatter'] = "customTickCross";
467
                }
468
            }
469
        }
470
        $searchableFields = $opts['searchableFields'] ?? $singl->searchableFields();
471
        $searchAliases = $opts['searchAliases'] ?? [];
472
        foreach ($searchableFields as $key => $searchOptions) {
473
            $key = $searchAliases[$key] ?? $key;
474
475
            // Allow "nice"
476
            $niceKey = $key . ".Nice";
477
            if (isset($columns[$niceKey])) {
478
                $key =   $niceKey;
479
            }
480
481
            /*
482
            "filter" => "NameOfTheFilter"
483
            "field" => "SilverStripe\Forms\FormField"
484
            "title" => "Title of the field"
485
            */
486
            if (!isset($columns[$key])) {
487
                continue;
488
            }
489
            $columns[$key]['headerFilter'] = true;
490
            $columns[$key]['headerSort'] = true;
491
            // $columns[$key]['headerFilterPlaceholder'] = $searchOptions['title'];
492
            //TODO: implement filter mapping
493
            switch ($searchOptions['filter']) {
494
                default:
495
                    $columns[$key]['headerFilterFunc'] =  "like";
496
                    break;
497
            }
498
499
            // Restrict based on data type
500
            $dbObject = $singl->dbObject($key);
501
            if ($dbObject) {
502
                if ($dbObject instanceof DBBoolean) {
503
                    $columns[$key]['headerFilter'] = 'tickCross';
504
                    $columns[$key]['headerFilterFunc'] =  "=";
505
                    $columns[$key]['headerFilterParams'] =  [
506
                        'tristate' => true
507
                    ];
508
                }
509
                if ($dbObject instanceof DBEnum) {
510
                    $columns[$key]['headerFilter'] = 'list';
511
                    $columns[$key]['headerFilterFunc'] =  "=";
512
                    $columns[$key]['headerFilterParams'] =  [
513
                        'values' => $dbObject->enumValues()
514
                    ];
515
                }
516
            }
517
        }
518
519
        // Allow customizing our columns based on record
520
        if ($singl->hasMethod('tabulatorColumns')) {
521
            $fields = $singl->tabulatorColumns();
522
            if (!is_array($fields)) {
523
                throw new RuntimeException("tabulatorColumns must return an array");
524
            }
525
            foreach ($fields as $key => $columnOptions) {
526
                $baseOptions = $columns[$key] ?? [];
527
                $columns[$key] = array_merge($baseOptions, $columnOptions);
528
            }
529
        }
530
531
        $this->extend('updateConfiguredColumns', $columns);
532
533
        foreach ($columns as $col) {
534
            $this->addColumn($col['field'], $col['title'], $col);
535
        }
536
537
        // Sortable ?
538
        $sortable = $opts['sortable'] ?? $singl->hasField('Sort');
539
        if ($sortable) {
540
            $this->wizardMoveable();
541
        }
542
543
        // Actions
544
        // We use a pseudo link, because maybe we cannot call Link() yet if it's not linked to a form
545
546
        $this->bulkUrl = $this->TempLink("bulkAction/", false);
547
548
        // - Core actions, handled by TabulatorGrid
549
        $itemUrl = $this->TempLink('item/{ID}', false);
550
        if ($singl->canEdit()) {
551
            $this->addEditButton();
552
        } elseif ($singl->canView()) {
553
            $this->addButton(self::UI_VIEW, $itemUrl, "visibility", _t('TabulatorGrid.View', 'View'));
554
        }
555
556
        $showRowDelete = $opts['rowDelete'] ?? self::config()->show_row_delete;
557
        if ($singl->canDelete() && $showRowDelete) {
558
            $deleteBtn = $this->makeButton($this->TempLink('item/{ID}/delete', false), "delete", _t('TabulatorGrid.Delete', 'Delete'));
559
            $deleteBtn["formatterParams"]["classes"] = 'btn btn-danger';
560
            $this->addButtonFromArray(self::UI_DELETE, $deleteBtn);
561
        }
562
563
        // - Tools
564
        $this->tools = [];
565
566
        $addNew = $opts['addNew'] ?? true;
567
        if ($singl->canCreate() && $addNew) {
568
            $this->addTool(self::POS_START, new TabulatorAddNewButton($this), self::TOOL_ADD_NEW);
0 ignored issues
show
Unused Code introduced by
The call to LeKoala\Tabulator\Tabula...ewButton::__construct() has too many arguments starting with $this. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

568
            $this->addTool(self::POS_START, /** @scrutinizer ignore-call */ new TabulatorAddNewButton($this), self::TOOL_ADD_NEW);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
569
        }
570
        $export = $opts['export'] ?? true;
571
        if (class_exists(\LeKoala\ExcelImportExport\ExcelImportExport::class) && $export) {
0 ignored issues
show
Bug introduced by
The type LeKoala\ExcelImportExport\ExcelImportExport was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
572
            $xlsxExportButton = new TabulatorExportButton($this);
0 ignored issues
show
Unused Code introduced by
The call to LeKoala\Tabulator\Tabula...rtButton::__construct() has too many arguments starting with $this. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

572
            $xlsxExportButton = /** @scrutinizer ignore-call */ new TabulatorExportButton($this);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
573
            $this->addTool(self::POS_END, $xlsxExportButton, self::TOOL_EXPORT);
574
            $csvExportButton = new TabulatorExportButton($this);
575
            $csvExportButton->setExportFormat('csv');
576
            $this->addTool(self::POS_END, $csvExportButton, self::TOOL_EXPORT_CSV);
577
        }
578
579
        // - Custom actions are forwarded to the model itself
580
        if ($singl->hasMethod('tabulatorRowActions')) {
581
            $rowActions = $singl->tabulatorRowActions();
582
            if (!is_array($rowActions)) {
583
                throw new RuntimeException("tabulatorRowActions must return an array");
584
            }
585
            foreach ($rowActions as $key => $actionConfig) {
586
                $action = $actionConfig['action'] ?? $key;
587
                $url = $this->TempLink("item/{ID}/customAction/$action", false);
588
                $icon = $actionConfig['icon'] ?? "cog";
589
                $title = $actionConfig['title'] ?? "";
590
591
                $button = $this->makeButton($url, $icon, $title);
592
                if (!empty($actionConfig['ajax'])) {
593
                    $button['formatterParams']['ajax'] = true;
594
                }
595
                $this->addButtonFromArray("ui_customaction_$action", $button);
596
            }
597
        }
598
599
        $this->setRowClickTriggersAction(true);
600
    }
601
602
    public static function requirements(): void
603
    {
604
        $load_styles = self::config()->load_styles;
605
        $luxon_version = self::config()->luxon_version;
606
        $enable_luxon = self::config()->enable_luxon;
607
        $last_icon_version = self::config()->last_icon_version;
608
        $enable_last_icon = self::config()->enable_last_icon;
609
        $enable_js_modules = self::config()->enable_js_modules;
610
611
        $jsOpts = [];
612
        if ($enable_js_modules) {
613
            $jsOpts['type'] = 'module';
614
        }
615
616
        if ($luxon_version && $enable_luxon) {
617
            // Do not load as module or we would get undefined luxon global var
618
            Requirements::javascript("https://cdn.jsdelivr.net/npm/luxon@$luxon_version/build/global/luxon.min.js");
619
        }
620
        if ($last_icon_version && $enable_last_icon) {
621
            Requirements::css("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.css");
622
            // Do not load as module even if asked to ensure load speed
623
            Requirements::javascript("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.js");
624
        }
625
626
        $use_v5 = self::config()->use_v5;
627
628
        Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js', $jsOpts);
629
        if ($load_styles) {
630
            Requirements::css('lekoala/silverstripe-tabulator:client/custom-tabulator.css');
631
            if ($use_v5) {
632
                Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.5.5.min.js', $jsOpts);
633
            } else {
634
                Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.min.js', $jsOpts);
635
            }
636
        } else {
637
            // you must load the css yourself based on your preferences
638
            if ($use_v5) {
639
                Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.5.5.raw.min.js', $jsOpts);
640
            } else {
641
                Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.raw.min.js', $jsOpts);
642
            }
643
        }
644
    }
645
646
    public function setValue($value, $data = null)
647
    {
648
        // Allow set raw json as value
649
        if ($value && is_string($value) && strpos($value, '[') === 0) {
650
            $value = json_decode($value);
651
        }
652
        if ($value instanceof DataList) {
653
            $this->configureFromDataObject($value->dataClass());
654
        }
655
        return parent::setValue($value, $data);
656
    }
657
658
    public function Field($properties = [])
659
    {
660
        if (self::config()->enable_requirements) {
661
            self::requirements();
662
        }
663
664
        // Make sure we can use a standalone version of the field without a form
665
        // Function should match the name
666
        if (!$this->form) {
667
            $this->form = new Form(Controller::curr(), $this->getControllerFunction());
668
        }
669
670
        // Data attributes for our custom behaviour
671
        $this->setDataAttribute("row-click-triggers-action", $this->rowClickTriggersAction);
672
673
        $this->setDataAttribute("listeners", $this->listeners);
674
        if ($this->editUrl) {
675
            $url = $this->processLink($this->editUrl);
676
            $this->setDataAttribute("edit-url", $url);
677
        }
678
        if ($this->moveUrl) {
679
            $url = $this->processLink($this->moveUrl);
680
            $this->setDataAttribute("move-url", $url);
681
        }
682
        if (!empty($this->bulkActions)) {
683
            $url = $this->processLink($this->bulkUrl);
684
            $this->setDataAttribute("bulk-url", $url);
685
        }
686
687
        return parent::Field($properties);
688
    }
689
690
    public function ShowTools(): string
691
    {
692
        if (empty($this->tools)) {
693
            return '';
694
        }
695
        $html = '';
696
        $html .= '<div class="tabulator-tools">';
697
        $html .= '<div class="tabulator-tools-start">';
698
        foreach ($this->tools as $tool) {
699
            if ($tool['position'] != self::POS_START) {
700
                continue;
701
            }
702
            $html .= ($tool['tool'])->forTemplate();
703
        }
704
        $html .= '</div>';
705
        $html .= '<div class="tabulator-tools-end">';
706
        foreach ($this->tools as $tool) {
707
            if ($tool['position'] != self::POS_END) {
708
                continue;
709
            }
710
            $html .= ($tool['tool'])->forTemplate();
711
        }
712
        // Show bulk actions at the end
713
        if (!empty($this->bulkActions)) {
714
            $selectLabel = _t(__CLASS__ . ".BULKSELECT", "Select a bulk action");
715
            $confirmLabel = _t(__CLASS__ . ".BULKCONFIRM", "Go");
716
            $html .= "<select class=\"tabulator-bulk-select\">";
717
            $html .= "<option>" . $selectLabel . "</option>";
718
            foreach ($this->bulkActions as $bulkAction) {
719
                $v = $bulkAction->getName();
720
                $xhr = $bulkAction->getXhr();
721
                $destructive = $bulkAction->getDestructive();
722
                $html .= "<option value=\"$v\" data-xhr=\"$xhr\" data-destructive=\"$destructive\">" . $bulkAction->getLabel() . "</option>";
723
            }
724
            $html .= "</select>";
725
            $html .= "<button class=\"tabulator-bulk-confirm btn\">" . $confirmLabel . "</button>";
726
        }
727
        $html .= '</div>';
728
        $html .= '</div>';
729
        return $html;
730
    }
731
732
    public function JsonOptions(): string
733
    {
734
        $this->processLinks();
735
736
        $data = $this->list ?? [];
737
        if ($this->autoloadDataList && $data instanceof DataList) {
738
            $data = null;
739
        }
740
        $opts = $this->options;
741
        $opts['columnDefaults'] = $this->columnDefaults;
742
743
        if (empty($this->columns)) {
744
            $opts['autoColumns'] = true;
745
        } else {
746
            $opts['columns'] = array_values($this->columns);
747
        }
748
749
        if ($data && is_iterable($data)) {
750
            if ($data instanceof ArrayList) {
751
                $data = $data->toArray();
752
            } else {
753
                if (is_iterable($data) && !is_array($data)) {
754
                    $data = iterator_to_array($data);
755
                }
756
            }
757
            $opts['data'] = $data;
758
        }
759
760
        // i18n
761
        $locale = strtolower(str_replace('_', '-', i18n::get_locale()));
762
        $paginationTranslations = [
763
            "first" => _t("TabulatorPagination.first", "First"),
764
            "first_title" =>  _t("TabulatorPagination.first_title", "First Page"),
765
            "last" =>  _t("TabulatorPagination.last", "Last"),
766
            "last_title" => _t("TabulatorPagination.last_title", "Last Page"),
767
            "prev" => _t("TabulatorPagination.prev", "Previous"),
768
            "prev_title" =>  _t("TabulatorPagination.prev_title", "Previous Page"),
769
            "next" => _t("TabulatorPagination.next", "Next"),
770
            "next_title" =>  _t("TabulatorPagination.next_title", "Next Page"),
771
            "all" =>  _t("TabulatorPagination.all", "All"),
772
        ];
773
        $dataTranslations = [
774
            "loading" => _t("TabulatorData.loading", "Loading"),
775
            "error" => _t("TabulatorData.error", "Error"),
776
        ];
777
        $groupsTranslations = [
778
            "item" => _t("TabulatorGroups.item", "Item"),
779
            "items" => _t("TabulatorGroups.items", "Items"),
780
        ];
781
        $headerFiltersTranslations = [
782
            "default" => _t("TabulatorHeaderFilters.default", "filter column..."),
783
        ];
784
        $bulkActionsTranslations = [
785
            "no_action" => _t("TabulatorBulkActions.no_action", "Please select an action"),
786
            "no_records" => _t("TabulatorBulkActions.no_records", "Please select a record"),
787
            "destructive" => _t("TabulatorBulkActions.destructive", "Confirm destructive action ?"),
788
        ];
789
        $translations = [
790
            'data' => $dataTranslations,
791
            'groups' => $groupsTranslations,
792
            'pagination' => $paginationTranslations,
793
            'headerFilters' => $headerFiltersTranslations,
794
            'bulkActions' => $bulkActionsTranslations,
795
        ];
796
        $opts['locale'] = $locale;
797
        $opts['langs'] = [
798
            $locale => $translations
799
        ];
800
801
        // Apply state
802
        // TODO: finalize persistence on the client side instead of this when using TabID
803
        $state = $this->getState();
804
        if ($state) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $state of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
805
            if (!empty($state['filter'])) {
806
                // @link https://tabulator.info/docs/6.2/filter#initial
807
                // We need to split between global filters and header filters
808
                $allFilters = $state['filter'] ?? [];
809
                $globalFilters = [];
810
                $headerFilters = [];
811
                foreach ($allFilters as $allFilter) {
812
                    if (strpos($allFilter['field'], '__') === 0) {
813
                        $globalFilters[] = $allFilter;
814
                    } else {
815
                        $headerFilters[] = $allFilter;
816
                    }
817
                }
818
                $opts['initialFilter'] = $globalFilters;
819
                $opts['initialHeaderFilter'] = $headerFilters;
820
            }
821
            if (!empty($state['sort'])) {
822
                // @link https://tabulator.info/docs/6.2/sort#initial
823
                $opts['initialSort'] = $state['sort'];
824
            }
825
826
            // Restore state from server
827
            $opts['_state'] = $state;
828
        }
829
830
        if ($this->enableGridManipulation) {
831
            // $opts['renderVertical'] = 'basic';
832
        }
833
834
        // Add our extension initCallback
835
        $opts['_initCallback'] = ['__fn' => self::JS_INIT_CALLBACK];
836
        $opts['_configCallback'] = ['__fn' => self::JS_CONFIG_CALLBACK];
837
838
        unset($opts['height']);
839
        $json = json_encode($opts);
840
841
        // Escape '
842
        $json = str_replace("'", '&#39;', $json);
843
844
        return $json;
845
    }
846
847
    /**
848
     * @param Controller $controller
849
     * @return CompatLayerInterface
850
     */
851
    public function getCompatLayer(Controller $controller = null)
852
    {
853
        if ($controller === null) {
854
            $controller = Controller::curr();
855
        }
856
        if (is_subclass_of($controller, \SilverStripe\Admin\LeftAndMain::class)) {
0 ignored issues
show
Bug introduced by
The type SilverStripe\Admin\LeftAndMain was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
857
            return new SilverstripeAdminCompat();
858
        }
859
        if (is_subclass_of($controller, \LeKoala\Admini\LeftAndMain::class)) {
0 ignored issues
show
Bug introduced by
The type LeKoala\Admini\LeftAndMain was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
860
            return new AdminiCompat();
861
        }
862
    }
863
864
    public function getAttributes()
865
    {
866
        $attrs = parent::getAttributes();
867
        unset($attrs['type']);
868
        unset($attrs['name']);
869
        unset($attrs['value']);
870
        return $attrs;
871
    }
872
873
    public function getOption(string $k)
874
    {
875
        return $this->options[$k] ?? null;
876
    }
877
878
    public function setOption(string $k, $v): self
879
    {
880
        $this->options[$k] = $v;
881
        return $this;
882
    }
883
884
    public function getRowHeight(): int
885
    {
886
        return $this->getOption('rowHeight');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getOption('rowHeight') could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
887
    }
888
889
    /**
890
     * Prevent row height automatic computation
891
     * @link https://tabulator.info/docs/6.2/layout#height-row
892
     */
893
    public function setRowHeight(int $v): self
894
    {
895
        $this->setOption('rowHeight', $v);
896
        return $this;
897
    }
898
899
    public function makeHeadersSticky(): self
900
    {
901
        // note: we could also use the "sticky" attribute on the custom element
902
        $this->addExtraClass("tabulator-sticky");
903
        return $this;
904
    }
905
906
    public function setRemoteSource(string $url, array $extraParams = [], bool $dataResponse = false): self
907
    {
908
        $this->setOption("ajaxURL", $url); //set url for ajax request
909
        $params = array_merge([
910
            'SecurityID' => SecurityToken::getSecurityID()
911
        ], $extraParams);
912
        $this->setOption("ajaxParams", $params);
913
        // Accept response where data is nested under the data key
914
        if ($dataResponse) {
915
            $this->setOption("ajaxResponse", ['__fn' => self::JS_DATA_AJAX_RESPONSE]);
916
        }
917
        return $this;
918
    }
919
920
    /**
921
     * @link http://www.tabulator.info/docs/6.2/page#remote
922
     * @param string $url
923
     * @param array $params
924
     * @param integer $pageSize
925
     * @param integer $initialPage
926
     */
927
    public function setRemotePagination(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1): self
928
    {
929
        $this->setOption("pagination", true); //enable pagination
930
        $this->setOption("paginationMode", 'remote'); //enable remote pagination
931
        $this->setRemoteSource($url, $params);
932
        if (!$pageSize) {
933
            $pageSize = $this->pageSize;
934
        } else {
935
            $this->pageSize = $pageSize;
936
        }
937
        $this->setOption("paginationSize", $pageSize);
938
        $this->setOption("paginationInitialPage", $initialPage);
939
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/6.2/page#counter
940
        return $this;
941
    }
942
943
    public function wizardRemotePagination(int $pageSize = 0, int $initialPage = 1, array $params = []): self
944
    {
945
        $this->setRemotePagination($this->TempLink('load', false), $params, $pageSize, $initialPage);
946
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/6.2/sort#ajax-sort
947
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/6.2/filter#ajax-filter
948
        return $this;
949
    }
950
951
    public function setProgressiveLoad(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0): self
952
    {
953
        $this->setOption("ajaxURL", $url);
954
        if (!empty($params)) {
955
            $this->setOption("ajaxParams", $params);
956
        }
957
        $this->setOption("progressiveLoad", $mode);
958
        if ($scrollMargin > 0) {
959
            $this->setOption("progressiveLoadScrollMargin", $scrollMargin);
960
        }
961
        if (!$pageSize) {
962
            $pageSize = $this->pageSize;
963
        } else {
964
            $this->pageSize = $pageSize;
965
        }
966
        $this->setOption("paginationSize", $pageSize);
967
        $this->setOption("paginationInitialPage", $initialPage);
968
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/6.2/page#counter
969
        return $this;
970
    }
971
972
    public function wizardProgressiveLoad(int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0, array $extraParams = []): self
973
    {
974
        $params = array_merge([
975
            'SecurityID' => SecurityToken::getSecurityID()
976
        ], $extraParams);
977
        $this->setProgressiveLoad($this->TempLink('load', false), $params, $pageSize, $initialPage, $mode, $scrollMargin);
978
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/6.2/sort#ajax-sort
979
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/6.2/filter#ajax-filter
980
        return $this;
981
    }
982
983
    /**
984
     * @link https://tabulator.info/docs/6.2/layout#responsive
985
     * @param boolean $startOpen
986
     * @param string $mode collapse|hide|flexCollapse
987
     * @return self
988
     */
989
    public function wizardResponsiveCollapse(bool $startOpen = false, string $mode = "collapse"): self
990
    {
991
        $this->setOption("responsiveLayout", $mode);
992
        $this->setOption("responsiveLayoutCollapseStartOpen", $startOpen);
993
        if ($mode != "hide") {
994
            $this->columns = array_merge([
995
                'ui_responsive_collapse' => [
996
                    "cssClass" => 'tabulator-cell-btn',
997
                    'formatter' => 'responsiveCollapse',
998
                    'headerSort' => false,
999
                    'width' => 40,
1000
                    'responsive' => 0,
1001
                ]
1002
            ], $this->columns);
1003
        }
1004
        return $this;
1005
    }
1006
1007
    public function wizardDataTree(bool $startExpanded = false, bool $filter = false, bool $sort = false, string $el = null): self
1008
    {
1009
        $this->setOption("dataTree", true);
1010
        $this->setOption("dataTreeStartExpanded", $startExpanded);
1011
        $this->setOption("dataTreeFilter", $filter);
1012
        $this->setOption("dataTreeSort", $sort);
1013
        if ($el) {
1014
            $this->setOption("dataTreeElementColumn", $el);
1015
        }
1016
        return $this;
1017
    }
1018
1019
    /**
1020
     * @param array $actions An array of bulk actions, that can extend the abstract one or use the generic with callbable
1021
     * @return self
1022
     */
1023
    public function wizardSelectable(array $actions = []): self
1024
    {
1025
        $this->columns = array_merge([
1026
            'ui_selectable' => [
1027
                "hozAlign" => 'center',
1028
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector',
1029
                'formatter' => 'rowSelection',
1030
                'titleFormatter' => 'rowSelection',
1031
                'width' => 40,
1032
                'maxWidth' => 40,
1033
                "headerSort" => false,
1034
            ]
1035
        ], $this->columns);
1036
        $this->setBulkActions($actions);
1037
        return $this;
1038
    }
1039
1040
    public function wizardMoveable(string $callback = "SSTabulator.rowMoved", $field = "Sort"): self
1041
    {
1042
        $this->moveUrl = $this->TempLink("item/{ID}/ajaxMove", false);
1043
        $this->setOption("movableRows", true);
1044
        $this->addListener("rowMoved", $callback);
1045
        $this->columns = array_merge([
1046
            'ui_move' => [
1047
                "hozAlign" => 'center',
1048
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector tabulator-ui-sort',
1049
                'rowHandle' => true,
1050
                'formatter' => 'handle',
1051
                'headerSort' => false,
1052
                'frozen' => true,
1053
                'width' => 40,
1054
                'maxWidth' => 40,
1055
            ],
1056
            // We need a hidden sort column
1057
            self::UI_SORT => [
1058
                "field" => $field,
1059
                'visible' => false,
1060
            ],
1061
        ], $this->columns);
1062
        return $this;
1063
    }
1064
1065
    /**
1066
     * @param string $field
1067
     * @param string $toggleElement arrow|header|false (header by default)
1068
     * @param boolean $isBool
1069
     * @return void
1070
     */
1071
    public function wizardGroupBy(string $field, string $toggleElement = 'header', bool $isBool = false)
1072
    {
1073
        $this->setOption("groupBy", $field);
1074
        $this->setOption("groupToggleElement", $toggleElement);
1075
        if ($isBool) {
1076
            $this->setOption("groupHeader", ['_fn' => self::JS_BOOL_GROUP_HEADER]);
1077
        }
1078
    }
1079
1080
    /**
1081
     * @param HTTPRequest $request
1082
     * @return HTTPResponse
1083
     */
1084
    public function handleItem($request)
1085
    {
1086
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1087
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1088
        $requestHandler = $this->getForm()->getController();
1089
        try {
1090
            $record = $this->getRecordFromRequest($request);
1091
        } catch (Exception $e) {
1092
            return $requestHandler->httpError(404, $e->getMessage());
1093
        }
1094
1095
        if (!$record) {
1096
            return $requestHandler->httpError(404, 'That record was not found');
1097
        }
1098
        $handler = $this->getItemRequestHandler($record, $requestHandler);
1099
        return $handler->handleRequest($request);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $handler->handleRequest($request) also could return the type SilverStripe\Control\RequestHandler|array|string which is incompatible with the documented return type SilverStripe\Control\HTTPResponse.
Loading history...
1100
    }
1101
1102
    /**
1103
     * @param HTTPRequest $request
1104
     * @return HTTPResponse
1105
     */
1106
    public function handleTool($request)
1107
    {
1108
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1109
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1110
        $requestHandler = $this->getForm()->getController();
1111
        $tool = $this->getToolFromRequest($request);
1112
        if (!$tool) {
1113
            return $requestHandler->httpError(404, 'That tool was not found');
1114
        }
1115
        return $tool->handleRequest($request);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $tool->handleRequest($request) also could return the type SilverStripe\Control\RequestHandler|array|string which is incompatible with the documented return type SilverStripe\Control\HTTPResponse.
Loading history...
1116
    }
1117
1118
    /**
1119
     * @param HTTPRequest $request
1120
     * @return HTTPResponse
1121
     */
1122
    public function handleBulkAction($request)
1123
    {
1124
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1125
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1126
        $requestHandler = $this->getForm()->getController();
1127
        $bulkAction = $this->getBulkActionFromRequest($request);
1128
        if (!$bulkAction) {
1129
            return $requestHandler->httpError(404, 'That bulk action was not found');
1130
        }
1131
        return $bulkAction->handleRequest($request);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $bulkAction->handleRequest($request) also could return the type SilverStripe\Control\RequestHandler|array|string which is incompatible with the documented return type SilverStripe\Control\HTTPResponse.
Loading history...
1132
    }
1133
1134
    /**
1135
     * @return string name of {@see TabulatorGrid_ItemRequest} subclass
1136
     */
1137
    public function getItemRequestClass(): string
1138
    {
1139
        if ($this->itemRequestClass) {
1140
            return $this->itemRequestClass;
1141
        } elseif (ClassInfo::exists(static::class . '_ItemRequest')) {
1142
            return static::class . '_ItemRequest';
1143
        }
1144
        return TabulatorGrid_ItemRequest::class;
1145
    }
1146
1147
    /**
1148
     * Build a request handler for the given record
1149
     *
1150
     * @param DataObject $record
1151
     * @param RequestHandler $requestHandler
1152
     * @return TabulatorGrid_ItemRequest
1153
     */
1154
    protected function getItemRequestHandler($record, $requestHandler)
1155
    {
1156
        $class = $this->getItemRequestClass();
1157
        $assignedClass = $this->itemRequestClass;
1158
        $this->extend('updateItemRequestClass', $class, $record, $requestHandler, $assignedClass);
1159
        /** @var TabulatorGrid_ItemRequest $handler */
1160
        $handler = Injector::inst()->createWithArgs(
1161
            $class,
1162
            [$this, $record, $requestHandler]
1163
        );
1164
        if ($template = $this->getTemplate()) {
1165
            $handler->setTemplate($template);
1166
        }
1167
        $this->extend('updateItemRequestHandler', $handler);
1168
        return $handler;
1169
    }
1170
1171
    public function getStateKey(string $TabID = null)
1172
    {
1173
        $nested = [];
1174
        $form = $this->getForm();
1175
        $scope = $this->modelClass ? str_replace('_', '\\', $this->modelClass) :  "default";
1176
        if ($form) {
0 ignored issues
show
introduced by
$form is of type SilverStripe\Forms\Form, thus it always evaluated to true.
Loading history...
1177
            $controller = $form->getController();
1178
1179
            // We are in a nested form, track by id since each records needs it own state
1180
            while ($controller instanceof TabulatorGrid_ItemRequest) {
1181
                $record = $controller->getRecord();
1182
                $nested[str_replace('_', '\\', get_class($record))] = $record->ID;
1183
1184
                // Move to parent controller
1185
                $controller = $controller->getController();
1186
            }
1187
1188
            // Scope by top controller class
1189
            $scope = str_replace('_', '\\', get_class($controller));
1190
        }
1191
1192
        $baseKey = 'TabulatorState';
1193
        if ($TabID) {
1194
            $baseKey .= '_' . $TabID;
1195
        }
1196
        $name = $this->getName();
1197
        $key = "$baseKey.$scope.$name";
1198
        foreach ($nested as $k => $v) {
1199
            $key .= "$k.$v";
1200
        }
1201
        return $key;
1202
    }
1203
1204
    /**
1205
     * @param HTTPRequest|null $request
1206
     * @return array{'page': int, 'limit': int, 'sort': array, 'filter': array}
1207
     */
1208
    public function getState(HTTPRequest $request = null)
1209
    {
1210
        if ($request === null) {
1211
            $request = Controller::curr()->getRequest();
1212
        }
1213
        $TabID = $request->requestVar('TabID') ?? null;
1214
        $stateKey = $this->getStateKey($TabID);
1215
        $state = $request->getSession()->get($stateKey);
1216
        return $state ?? [
1217
            'page' => 1,
1218
            'limit' => $this->pageSize,
1219
            'sort' => [],
1220
            'filter' => [],
1221
        ];
1222
    }
1223
1224
    public function setState(HTTPRequest $request, $state)
1225
    {
1226
        $TabID = $request->requestVar('TabID') ?? null;
1227
        $stateKey = $this->getStateKey($TabID);
1228
        $request->getSession()->set($stateKey, $state);
1229
        // If we are in a new controller, we can clear other states
1230
        // Note: this would break tabbed navigation if you try to open multiple tabs, see below for more info
1231
        // @link https://github.com/silverstripe/silverstripe-framework/issues/9556
1232
        $matches = [];
1233
        preg_match_all('/\.(.*?)\./', $stateKey, $matches);
1234
        $scope = $matches[1][0] ?? null;
1235
        if ($scope) {
1236
            self::clearAllStates($scope);
1237
        }
1238
    }
1239
1240
    public function clearState(HTTPRequest $request)
1241
    {
1242
        $TabID = $request->requestVar('TabID') ?? null;
1243
        $stateKey = $this->getStateKey($TabID);
1244
        $request->getSession()->clear($stateKey);
1245
    }
1246
1247
    public static function clearAllStates(string $exceptScope = null, string $TabID = null)
1248
    {
1249
        $request = Controller::curr()->getRequest();
1250
        $baseKey = 'TabulatorState';
1251
        if ($TabID) {
1252
            $baseKey .= '_' . $TabID;
1253
        }
1254
        $allStates = $request->getSession()->get($baseKey);
1255
        if (!$allStates) {
1256
            return;
1257
        }
1258
        foreach ($allStates as $scope => $data) {
1259
            if ($exceptScope && $scope == $exceptScope) {
1260
                continue;
1261
            }
1262
            $request->getSession()->clear("TabulatorState.$scope");
1263
        }
1264
    }
1265
1266
    public function StateValue($key, $field): ?string
1267
    {
1268
        $state = $this->getState();
1269
        $arr = $state[$key] ?? [];
1270
        foreach ($arr as $s) {
1271
            if ($s['field'] === $field) {
1272
                return $s['value'];
1273
            }
1274
        }
1275
        return null;
1276
    }
1277
1278
    /**
1279
     * Provides autocomplete lists
1280
     *
1281
     * @param HTTPRequest $request
1282
     * @return HTTPResponse
1283
     */
1284
    public function autocomplete(HTTPRequest $request)
1285
    {
1286
        if ($this->isDisabled() || $this->isReadonly()) {
1287
            return $this->httpError(403);
1288
        }
1289
        $SecurityID = $request->getVar('SecurityID');
1290
        if (!SecurityToken::inst()->check($SecurityID)) {
1291
            return $this->httpError(404, "Invalid SecurityID");
1292
        }
1293
1294
        $name = $request->getVar("Column");
1295
        $col = $this->getColumn($name);
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type null; however, parameter $key of LeKoala\Tabulator\TabulatorGrid::getColumn() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1295
        $col = $this->getColumn(/** @scrutinizer ignore-type */ $name);
Loading history...
1296
        if (!$col) {
1297
            return $this->httpError(403, "Invalid column");
1298
        }
1299
1300
        // Don't use % term as it prevents use of indexes
1301
        $term = $request->getVar('term') . '%';
1302
        $term = str_replace(' ', '%', $term);
1303
1304
        $parts = explode(".", $name);
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1304
        $parts = explode(".", /** @scrutinizer ignore-type */ $name);
Loading history...
1305
        if (count($parts) > 2) {
1306
            array_pop($parts);
1307
        }
1308
        if (count($parts) == 2) {
1309
            $class = $parts[0];
1310
            $field = $parts[1];
1311
        } elseif (count($parts) == 1) {
1312
            $class = preg_replace("/ID$/", "", $parts[0]);
1313
            $field = 'Title';
1314
        } else {
1315
            return $this->httpError(403, "Invalid field");
1316
        }
1317
1318
        /** @var DataObject $sng */
1319
        $sng = $class::singleton();
1320
        $baseTable = $sng->baseTable();
1321
1322
        $searchField = null;
1323
        $searchCandidates = [
1324
            $field, 'Name', 'Surname', 'Email', 'ID'
1325
        ];
1326
1327
        // Ensure field exists, this is really rudimentary
1328
        $db = $class::config()->db;
1329
        foreach ($searchCandidates as $searchCandidate) {
1330
            if ($searchField) {
1331
                continue;
1332
            }
1333
            if (isset($db[$searchCandidate])) {
1334
                $searchField = $searchCandidate;
1335
            }
1336
        }
1337
        $searchCols = [$searchField];
1338
1339
        // For members, do something better
1340
        if ($baseTable == 'Member') {
1341
            $searchField = ['FirstName', 'Surname'];
1342
            $searchCols = ['FirstName', 'Surname', 'Email'];
1343
        }
1344
1345
        if (!empty($col['editorParams']['customSearchField'])) {
1346
            $searchField = $col['editorParams']['customSearchField'];
1347
        }
1348
        if (!empty($col['editorParams']['customSearchCols'])) {
1349
            $searchCols = $col['editorParams']['customSearchCols'];
1350
        }
1351
1352
        // Note: we need to use the orm, even if it's slower, to make sure any extension is properly applied
1353
        /** @var DataList $list */
1354
        $list = $sng::get();
1355
1356
        // Make sure at least one field is not null...
1357
        $where = [];
1358
        foreach ($searchCols as $searchCol) {
1359
            $where[] = $searchCol . ' IS NOT NULL';
1360
        }
1361
        $list = $list->where($where);
1362
        // ... and matches search term ...
1363
        $where = [];
1364
        foreach ($searchCols as $searchCol) {
1365
            $where[$searchCol . ' LIKE ?'] = $term;
1366
        }
1367
        $list = $list->whereAny($where);
1368
1369
        // ... and any user set requirements
1370
        if (!empty($col['editorParams']['where'])) {
1371
            // Deal with in clause
1372
            $customWhere = [];
1373
            foreach ($col['editorParams']['where'] as $col => $param) {
1374
                // For array, we need a IN statement with a ? for each value
1375
                if (is_array($param)) {
1376
                    $prepValue = [];
1377
                    $params = [];
1378
                    foreach ($param as $paramValue) {
1379
                        $params[] = $paramValue;
1380
                        $prepValue[] = "?";
1381
                    }
1382
                    $customWhere["$col IN (" . implode(',', $prepValue) . ")"] = $params;
1383
                } else {
1384
                    $customWhere["$col = ?"] = $param;
1385
                }
1386
            }
1387
            $list = $list->where($customWhere);
1388
        }
1389
1390
        $results = iterator_to_array($list);
1391
        $data = [];
1392
        foreach ($results as $record) {
1393
            if (is_array($searchField)) {
1394
                $labelParts = [];
1395
                foreach ($searchField as $sf) {
1396
                    $labelParts[] = $record->$sf;
1397
                }
1398
                $label = implode(" ", $labelParts);
1399
            } else {
1400
                $label = $record->$searchField;
1401
            }
1402
            $data[] = [
1403
                'value' => $record->ID,
1404
                'label' => $label,
1405
            ];
1406
        }
1407
1408
        $json = json_encode($data);
1409
        $response = new HTTPResponse($json);
1410
        $response->addHeader('Content-Type', 'application/script');
1411
        return $response;
1412
    }
1413
1414
    /**
1415
     * @link http://www.tabulator.info/docs/6.2/page#remote-response
1416
     * @param HTTPRequest $request
1417
     * @return HTTPResponse
1418
     */
1419
    public function load(HTTPRequest $request)
1420
    {
1421
        if ($this->isDisabled() || $this->isReadonly()) {
1422
            return $this->httpError(403);
1423
        }
1424
        $SecurityID = $request->getVar('SecurityID');
1425
        if (!SecurityToken::inst()->check($SecurityID)) {
1426
            return $this->httpError(404, "Invalid SecurityID");
1427
        }
1428
1429
        $page = (int) $request->getVar('page');
1430
        $limit = (int) $request->getVar('size');
1431
1432
        $sort = $request->getVar('sort');
1433
        $filter = $request->getVar('filter');
1434
1435
        // Persist state to allow the ItemEditForm to display navigation
1436
        $state = [
1437
            'page' => $page,
1438
            'limit' => $limit,
1439
            'sort' => $sort,
1440
            'filter' => $filter,
1441
        ];
1442
        $this->setState($request, $state);
1443
1444
        $offset = ($page - 1) * $limit;
1445
        $data = $this->getManipulatedData($limit, $offset, $sort, $filter);
1446
        $data['state'] = $state;
1447
1448
        $encodedData = json_encode($data);
1449
        if (!$encodedData) {
1450
            throw new Exception(json_last_error_msg());
1451
        }
1452
1453
        $response = new HTTPResponse($encodedData);
1454
        $response->addHeader('Content-Type', 'application/json');
1455
        return $response;
1456
    }
1457
1458
    /**
1459
     * @param HTTPRequest $request
1460
     * @return DataObject|null
1461
     */
1462
    protected function getRecordFromRequest(HTTPRequest $request): ?DataObject
1463
    {
1464
        $id = $request->param('ID');
1465
        /** @var DataObject $record */
1466
        if (is_numeric($id)) {
1467
            /** @var Filterable $dataList */
1468
            $dataList = $this->getList();
1469
            $record = $dataList->byID($id);
1470
1471
            if (!$record) {
1472
                $record = DataObject::get_by_id($this->getModelClass(), $id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type boolean|integer expected by parameter $idOrCache of SilverStripe\ORM\DataObject::get_by_id(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1472
                $record = DataObject::get_by_id($this->getModelClass(), /** @scrutinizer ignore-type */ $id);
Loading history...
1473
                if ($record) {
1474
                    throw new RuntimeException('This record is not accessible from the list');
1475
                }
1476
            }
1477
        } else {
1478
            $record = Injector::inst()->create($this->getModelClass());
1479
        }
1480
        return $record;
1481
    }
1482
1483
    /**
1484
     * @param HTTPRequest $request
1485
     * @return AbstractTabulatorTool|null
1486
     */
1487
    protected function getToolFromRequest(HTTPRequest $request): ?AbstractTabulatorTool
1488
    {
1489
        $toolID = $request->param('ID');
1490
        $tool = $this->getTool($toolID);
1491
        return $tool;
1492
    }
1493
1494
    /**
1495
     * @param HTTPRequest $request
1496
     * @return AbstractBulkAction|null
1497
     */
1498
    protected function getBulkActionFromRequest(HTTPRequest $request): ?AbstractBulkAction
1499
    {
1500
        $toolID = $request->param('ID');
1501
        $tool = $this->getBulkAction($toolID);
1502
        return $tool;
1503
    }
1504
1505
    /**
1506
     * Get the value of a named field  on the given record.
1507
     *
1508
     * Use of this method ensures that any special rules around the data for this gridfield are
1509
     * followed.
1510
     *
1511
     * @param DataObject $record
1512
     * @param string $fieldName
1513
     *
1514
     * @return mixed
1515
     */
1516
    public function getDataFieldValue($record, $fieldName)
1517
    {
1518
        if ($record->hasMethod('relField')) {
1519
            return $record->relField($fieldName);
1520
        }
1521
1522
        if ($record->hasMethod($fieldName)) {
1523
            return $record->$fieldName();
1524
        }
1525
1526
        return $record->$fieldName;
1527
    }
1528
1529
    public function getManipulatedList(): SS_List
1530
    {
1531
        return $this->list;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->list could return the type null which is incompatible with the type-hinted return SilverStripe\ORM\SS_List. Consider adding an additional type-check to rule them out.
Loading history...
1532
    }
1533
1534
    public function getList(): SS_List
1535
    {
1536
        return $this->list;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->list could return the type null which is incompatible with the type-hinted return SilverStripe\ORM\SS_List. Consider adding an additional type-check to rule them out.
Loading history...
1537
    }
1538
1539
    public function setList(SS_List $list): self
1540
    {
1541
        if ($this->autoloadDataList && $list instanceof DataList) {
1542
            $this->wizardRemotePagination();
1543
        }
1544
        $this->list = $list;
1545
        return $this;
1546
    }
1547
1548
    public function hasArrayList(): bool
1549
    {
1550
        return $this->list instanceof ArrayList;
1551
    }
1552
1553
    public function getArrayList(): ArrayList
1554
    {
1555
        if (!$this->list instanceof ArrayList) {
1556
            throw new RuntimeException("Value is not a ArrayList, it is a: " . get_class($this->list));
0 ignored issues
show
Bug introduced by
It seems like $this->list can also be of type null; however, parameter $object of get_class() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1556
            throw new RuntimeException("Value is not a ArrayList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1557
        }
1558
        return $this->list;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->list returns the type null which is incompatible with the type-hinted return SilverStripe\ORM\ArrayList.
Loading history...
1559
    }
1560
1561
    public function hasDataList(): bool
1562
    {
1563
        return $this->list instanceof DataList;
1564
    }
1565
1566
    /**
1567
     * A properly typed on which you can call byID
1568
     * @return ArrayList|DataList
1569
     */
1570
    public function getByIDList()
1571
    {
1572
        return $this->list;
1573
    }
1574
1575
    public function hasByIDList(): bool
1576
    {
1577
        return $this->hasDataList() || $this->hasArrayList();
1578
    }
1579
1580
    public function getDataList(): DataList
1581
    {
1582
        if (!$this->list instanceof DataList) {
1583
            throw new RuntimeException("Value is not a DataList, it is a: " . get_class($this->list));
0 ignored issues
show
Bug introduced by
It seems like $this->list can also be of type null; however, parameter $object of get_class() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1583
            throw new RuntimeException("Value is not a DataList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1584
        }
1585
        return $this->list;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->list returns the type null which is incompatible with the type-hinted return SilverStripe\ORM\DataList.
Loading history...
1586
    }
1587
1588
    public function getManipulatedData(int $limit, int $offset, array $sort = null, array $filter = null): array
1589
    {
1590
        if (!$this->hasDataList()) {
1591
            $data = $this->list->toNestedArray();
0 ignored issues
show
Bug introduced by
The method toNestedArray() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1591
            /** @scrutinizer ignore-call */ 
1592
            $data = $this->list->toNestedArray();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1592
1593
            $lastRow = $this->list->count();
1594
            $lastPage = ceil($lastRow / $limit);
1595
1596
            $result = [
1597
                'last_row' => $lastRow,
1598
                'last_page' => $lastPage,
1599
                'data' => $data,
1600
            ];
1601
1602
            return $result;
1603
        }
1604
1605
        $dataList = $this->getDataList();
1606
1607
        $schema = DataObject::getSchema();
1608
        $dataClass = $dataList->dataClass();
1609
1610
        /** @var DataObject $singleton */
1611
        $singleton = singleton($dataClass);
1612
        $opts = $this->getTabulatorOptions($singleton);
1613
        $resolutionMap = [];
1614
1615
        $sortSql = [];
1616
        if ($sort) {
1617
            foreach ($sort as $sortValues) {
1618
                $cols = array_keys($this->columns);
1619
                $field = $sortValues['field'];
1620
                $sortField = $field;
1621
                $sortClass = $dataClass;
1622
                if (!in_array($field, $cols)) {
1623
                    throw new Exception("Invalid sort field: $field");
1624
                }
1625
                $dir = $sortValues['dir'];
1626
                if (!in_array($dir, ['asc', 'desc'])) {
1627
                    throw new Exception("Invalid sort dir: $dir");
1628
                }
1629
1630
                // Nested sort
1631
                if (str_contains($field, '.')) {
1632
                    $parts = explode(".", $field);
1633
                    $relationName = $parts[0];
1634
1635
                    // Resolve relation only once in case of multiples similar keys
1636
                    if (!isset($resolutionMap[$relationName])) {
1637
                        $resolutionMap[$relationName] = $singleton->relObject($relationName);
1638
                    }
1639
                    // Not matching anything (maybe a formatting .Nice ?)
1640
                    $resolvedObject = $resolutionMap[$relationName] ?? null;
1641
                    if (!$resolvedObject) {
1642
                        continue;
1643
                    }
1644
                    // Maybe it's an helper method like .Nice and it's not sortable in the query
1645
                    if (!($resolvedObject instanceof DataList) && !($resolvedObject instanceof DataObject)) {
1646
                        $field = $parts[0];
0 ignored issues
show
Unused Code introduced by
The assignment to $field is dead and can be removed.
Loading history...
1647
                        continue;
1648
                    }
1649
                    $sortClass = get_class($resolvedObject);
1650
                    $sortField = $parts[1];
1651
                    $tableName = $schema->tableForField($sortClass, $sortField);
1652
                    $baseIDColumn = $schema->sqlColumnForField($dataClass, 'ID');
1653
                    $dataList = $dataList->leftJoin($tableName, "\"{$relationName}\".\"ID\" = {$baseIDColumn}", $relationName);
1654
                }
1655
1656
                // Is it an actual field or an expression ?
1657
                $sortedField = $schema->tableForField($sortClass, $sortField);
1658
                if ($sortedField) {
1659
                    $sortSql[] = $field . ' ' . $dir;
1660
                }
1661
            }
1662
        } else {
1663
            // If we have a sort column
1664
            if (isset($this->columns[self::UI_SORT])) {
1665
                $sortSql[] = $this->columns[self::UI_SORT]['field'] . ' ASC';
1666
            }
1667
        }
1668
        if (!empty($sortSql)) {
1669
            $dataList = $dataList->sort(implode(", ", $sortSql));
1670
        }
1671
1672
        // Filtering is an array of field/type/value arrays
1673
        $filters = [];
1674
        $anyFilters = [];
1675
        $where = [];
1676
        $anyWhere = [];
1677
        if ($filter) {
1678
            $searchAliases = $opts['searchAliases'] ?? [];
1679
            $searchAliases = array_flip($searchAliases);
1680
            foreach ($filter as $filterValues) {
1681
                $cols = array_keys($this->columns);
1682
                $field = $filterValues['field'];
1683
                if (strpos($field, '__') !== 0 && !in_array($field, $cols)) {
1684
                    throw new Exception("Invalid filter field: $field");
1685
                }
1686
                // If .Nice was used
1687
                $field = str_replace('.Nice', '', $field);
1688
1689
                $field = $searchAliases[$field] ?? $field;
1690
                $value = $filterValues['value'];
1691
                $type = $filterValues['type'];
1692
1693
                // Some types of fields need custom sql expressions (eg uuids)
1694
                $fieldInstance = $singleton->dbObject($field);
1695
                if ($fieldInstance && $fieldInstance->hasMethod('filterExpression')) {
1696
                    $where[] = $fieldInstance->filterExpression($type, $value);
1697
                    continue;
1698
                }
1699
1700
                $rawValue = $value;
1701
1702
                // Strict value
1703
                if ($value === "true") {
1704
                    $value = true;
1705
                } elseif ($value === "false") {
1706
                    $value = false;
1707
                }
1708
1709
                switch ($type) {
1710
                    case "=":
1711
                        if ($field === "__wildcard") {
1712
                            // It's a wildcard search
1713
                            $anyFilters = $this->createWildcardFilters($rawValue);
1714
                        } elseif ($field === "__quickfilter") {
1715
                            // It's a quickfilter search
1716
                            $this->createQuickFilter($rawValue, $dataList);
1717
                        } else {
1718
                            $filters["$field"] = $value;
1719
                        }
1720
                        break;
1721
                    case "!=":
1722
                        $filters["$field:not"] = $value;
1723
                        break;
1724
                    case "like":
1725
                        $filters["$field:PartialMatch:nocase"] = $value;
1726
                        break;
1727
                    case "keywords":
1728
                        $filters["$field:PartialMatch:nocase"] = str_replace(" ", "%", $value);
1729
                        break;
1730
                    case "starts":
1731
                        $filters["$field:StartsWith:nocase"] = $value;
1732
                        break;
1733
                    case "ends":
1734
                        $filters["$field:EndsWith:nocase"] = $value;
1735
                        break;
1736
                    case "<":
1737
                        $filters["$field:LessThan:nocase"] = $value;
1738
                        break;
1739
                    case "<=":
1740
                        $filters["$field:LessThanOrEqual:nocase"] = $value;
1741
                        break;
1742
                    case ">":
1743
                        $filters["$field:GreaterThan:nocase"] = $value;
1744
                        break;
1745
                    case ">=":
1746
                        $filters["$field:GreaterThanOrEqual:nocase"] = $value;
1747
                        break;
1748
                    case "in":
1749
                        $filters["$field"] = $value;
1750
                        break;
1751
                    case "regex":
1752
                        $dataList = $dataList->filters('REGEXP ' . Convert::raw2sql($value));
0 ignored issues
show
Bug introduced by
Are you sure SilverStripe\Core\Convert::raw2sql($value) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1752
                        $dataList = $dataList->filters('REGEXP ' . /** @scrutinizer ignore-type */ Convert::raw2sql($value));
Loading history...
1753
                        break;
1754
                    default:
1755
                        throw new Exception("Invalid filter type: $type");
1756
                }
1757
            }
1758
        }
1759
        if (!empty($filters)) {
1760
            $dataList = $dataList->filter($filters);
1761
        }
1762
        if (!empty($anyFilters)) {
1763
            $dataList = $dataList->filterAny($anyFilters);
1764
        }
1765
        if (!empty($where)) {
1766
            $dataList = $dataList->where(implode(' AND ', $where));
1767
        }
1768
        if (!empty($anyWhere)) {
1769
            $dataList = $dataList->where(implode(' OR ', $anyWhere));
1770
        }
1771
1772
        $lastRow = $dataList->count();
1773
        $lastPage = ceil($lastRow / $limit);
1774
1775
        $data = [];
1776
        /** @var DataObject $record */
1777
        foreach ($dataList->limit($limit, $offset) as $record) {
1778
            if ($record->hasMethod('canView') && !$record->canView()) {
1779
                continue;
1780
            }
1781
1782
            $item = [
1783
                'ID' => $record->ID,
1784
            ];
1785
1786
            // Add row class
1787
            if ($record->hasMethod('TabulatorRowClass')) {
1788
                $item['_class'] = $record->TabulatorRowClass();
1789
            } elseif ($record->hasMethod('getRowClass')) {
1790
                $item['_class'] = $record->getRowClass();
1791
            }
1792
            // Add row color
1793
            if ($record->hasMethod('TabulatorRowColor')) {
1794
                $item['_color'] = $record->TabulatorRowColor();
1795
            }
1796
1797
            $nested = [];
1798
            foreach ($this->columns as $col) {
1799
                // UI field are skipped
1800
                if (empty($col['field'])) {
1801
                    continue;
1802
                }
1803
1804
                $field = $col['field'];
1805
1806
                // Explode relations or formatters
1807
                if (strpos($field, '.') !== false) {
1808
                    $parts = explode('.', $field);
1809
                    $classOrField = $parts[0];
1810
                    $relationOrMethod = $parts[1];
1811
                    // For relations, like Users.count
1812
                    if ($singleton->getRelationClass($classOrField)) {
1813
                        $nested[$classOrField][] = $relationOrMethod;
1814
                        continue;
1815
                    } else {
1816
                        // For fields, like SomeValue.Nice
1817
                        $dbObject = $record->dbObject($classOrField);
1818
                        if ($dbObject) {
1819
                            $item[$classOrField] = [
1820
                                $relationOrMethod => $dbObject->$relationOrMethod()
1821
                            ];
1822
                            continue;
1823
                        }
1824
                    }
1825
                }
1826
1827
                // Do not override already set fields
1828
                if (!isset($item[$field])) {
1829
                    $getField = 'get' . ucfirst($field);
1830
1831
                    if ($record->hasMethod($getField)) {
1832
                        // Prioritize getXyz method
1833
                        $item[$field] = $record->$getField();
1834
                    } elseif ($record->hasMethod($field)) {
1835
                        // Regular xyz method method
1836
                        $item[$field] = $record->$field();
1837
                    } else {
1838
                        // Field
1839
                        $item[$field] = $record->getField($field);
1840
                    }
1841
                }
1842
            }
1843
            // Fill in nested data, like Users.count
1844
            foreach ($nested as $nestedClass => $nestedColumns) {
1845
                /** @var DataObject $relObject */
1846
                $relObject = $record->relObject($nestedClass);
1847
                $nestedData = [];
1848
                foreach ($nestedColumns as $nestedColumn) {
1849
                    $nestedData[$nestedColumn] = $this->getDataFieldValue($relObject, $nestedColumn);
1850
                }
1851
                $item[$nestedClass] = $nestedData;
1852
            }
1853
            $data[] = $item;
1854
        }
1855
1856
        $result = [
1857
            'last_row' => $lastRow,
1858
            'last_page' => $lastPage,
1859
            'data' => $data,
1860
        ];
1861
1862
        if (Director::isDev()) {
1863
            $result['sql'] = $dataList->sql();
1864
        }
1865
1866
        return $result;
1867
    }
1868
1869
    public function QuickFiltersList()
1870
    {
1871
        $current = $this->StateValue('filter', '__quickfilter');
1872
        $list = new ArrayList();
1873
        foreach ($this->quickFilters as $k => $v) {
1874
            $list->push([
1875
                'Value' => $k,
1876
                'Label' => $v['label'],
1877
                'Selected' => $k == $current
1878
            ]);
1879
        }
1880
        return $list;
1881
    }
1882
1883
    protected function createQuickFilter($filter, &$list)
1884
    {
1885
        $qf = $this->quickFilters[$filter] ?? null;
1886
        if (!$qf) {
1887
            return;
1888
        }
1889
1890
        $callback = $qf['callback'] ?? null;
1891
        if (!$callback) {
1892
            return;
1893
        }
1894
1895
        $callback($list);
1896
    }
1897
1898
    protected function createWildcardFilters(string $value)
1899
    {
1900
        $wildcardFields = $this->wildcardFields;
1901
1902
        // Create from model
1903
        if (empty($wildcardFields)) {
1904
            /** @var DataObject $singl */
1905
            $singl = singleton($this->modelClass);
1906
            $searchableFields = $singl->searchableFields();
1907
1908
            foreach ($searchableFields as $k => $v) {
1909
                $general = $v['general'] ?? true;
1910
                if (!$general) {
1911
                    continue;
1912
                }
1913
                $wildcardFields[] = $k;
1914
            }
1915
        }
1916
1917
        // Queries can have the format s:... or e:... or =:.... or %:....
1918
        $filter = $this->defaultFilter;
1919
        if (strpos($value, ':') === 1) {
1920
            $parts = explode(":", $value);
1921
            $shortcut = array_shift($parts);
1922
            $value = implode(":", $parts);
1923
            switch ($shortcut) {
1924
                case 's':
1925
                    $filter = 'StartsWith';
1926
                    break;
1927
                case 'e':
1928
                    $filter = 'EndsWith';
1929
                    break;
1930
                case '=':
1931
                    $filter = 'ExactMatch';
1932
                    break;
1933
                case '%':
1934
                    $filter = 'PartialMatch';
1935
                    break;
1936
            }
1937
        }
1938
1939
        // Process value
1940
        $baseValue = $value;
1941
        $value = str_replace(" ", "%", $value);
1942
        $value = str_replace(['.', '_', '-'], ' ', $value);
1943
1944
        // Create filters
1945
        $anyWhere = [];
1946
        foreach ($wildcardFields as $f) {
1947
            if (!$value) {
1948
                continue;
1949
            }
1950
            $key = $f . ":" . $filter;
1951
            $anyWhere[$key] = $value;
1952
1953
            // also look on unfiltered data
1954
            if ($value != $baseValue) {
1955
                $anyWhere[$key] = $baseValue;
1956
            }
1957
        }
1958
1959
        return $anyWhere;
1960
    }
1961
1962
    public function getModelClass(): ?string
1963
    {
1964
        if ($this->modelClass) {
1965
            return $this->modelClass;
1966
        }
1967
        if ($this->list && $this->list instanceof DataList) {
1968
            return $this->list->dataClass();
1969
        }
1970
        return null;
1971
    }
1972
1973
    public function setModelClass(string $modelClass): self
1974
    {
1975
        $this->modelClass = $modelClass;
1976
        return $this;
1977
    }
1978
1979
1980
    public function getDataAttribute(string $k)
1981
    {
1982
        if (isset($this->dataAttributes[$k])) {
1983
            return $this->dataAttributes[$k];
1984
        }
1985
        return $this->getAttribute("data-$k");
1986
    }
1987
1988
    public function setDataAttribute(string $k, $v): self
1989
    {
1990
        $this->dataAttributes[$k] = $v;
1991
        return $this;
1992
    }
1993
1994
    public function dataAttributesHTML(): string
1995
    {
1996
        $parts = [];
1997
        foreach ($this->dataAttributes as $k => $v) {
1998
            if (!$v) {
1999
                continue;
2000
            }
2001
            if (is_array($v)) {
2002
                $v = json_encode($v);
2003
            }
2004
            $parts[] = "data-$k='$v'";
2005
        }
2006
        return implode(" ", $parts);
2007
    }
2008
2009
    protected function processLink(string $url): string
2010
    {
2011
        // It's not necessary to process
2012
        if ($url == '#') {
2013
            return $url;
2014
        }
2015
        // It's a temporary link on the form
2016
        if (strpos($url, 'form:') === 0) {
2017
            if (!$this->form) {
2018
                $controller = Controller::curr();
2019
                if ($controller->hasMethod('getForm')) {
2020
                    $form = $controller->getForm();
2021
                    $this->form = $form;
2022
                } else {
2023
                    return $url;
2024
                }
2025
            }
2026
            return $this->Link(preg_replace('/^form:/', '', $url));
2027
        }
2028
        // It's a temporary link on the controller
2029
        if (strpos($url, 'controller:') === 0) {
2030
            return $this->ControllerLink(preg_replace('/^controller:/', '', $url));
2031
        }
2032
        // It's a custom protocol (mailto: etc)
2033
        if (strpos($url, ':') !== false) {
2034
            return $url;
2035
        }
2036
        return $url;
2037
    }
2038
2039
    protected function processLinks(): void
2040
    {
2041
        // Process editor and formatter links
2042
        foreach ($this->columns as $name => $params) {
2043
            if (!empty($params['formatterParams']['url'])) {
2044
                $url = $this->processLink($params['formatterParams']['url']);
2045
                $this->columns[$name]['formatterParams']['url'] = $url;
2046
            }
2047
            if (!empty($params['editorParams']['url'])) {
2048
                $url = $this->processLink($params['editorParams']['url']);
2049
                $this->columns[$name]['editorParams']['url'] = $url;
2050
            }
2051
            // Set valuesURL automatically if not already set
2052
            if (!empty($params['editorParams']['autocomplete'])) {
2053
                if (empty($params['editorParams']['valuesURL'])) {
2054
                    $params = [
2055
                        'Column' => $name,
2056
                        'SecurityID' => SecurityToken::getSecurityID(),
2057
                    ];
2058
                    $url = $this->Link('autocomplete') . '?' . http_build_query($params);
2059
                    $this->columns[$name]['editorParams']['valuesURL'] = $url;
2060
                    $this->columns[$name]['editorParams']['filterRemote'] = true;
2061
                }
2062
            }
2063
        }
2064
2065
        // Other links
2066
        $url = $this->getOption('ajaxURL');
2067
        if ($url) {
2068
            $this->setOption('ajaxURL', $this->processLink($url));
2069
        }
2070
    }
2071
2072
    /**
2073
     * @link https://github.com/lekoala/formidable-elements/blob/master/src/classes/tabulator/Format/formatters/button.js
2074
     */
2075
    public function makeButton(string $urlOrAction, string $icon, string $title): array
2076
    {
2077
        $opts = [
2078
            "responsive" => 0,
2079
            "cssClass" => 'tabulator-cell-btn',
2080
            "tooltip" => $title,
2081
            "formatter" => "button",
2082
            "formatterParams" => [
2083
                "icon" => $icon,
2084
                "title" => $title,
2085
                "url" => $this->TempLink($urlOrAction), // On the controller by default
2086
            ],
2087
            "cellClick" => ["__fn" => "SSTabulator.buttonHandler"],
2088
            // We need to force its size otherwise Tabulator will assign too much space
2089
            "width" => 36 + strlen($title) * 12,
2090
            "hozAlign" => "center",
2091
            "headerSort" => false,
2092
        ];
2093
        return $opts;
2094
    }
2095
2096
    public function addButtonFromArray(string $action, array $opts = [], string $before = null): self
2097
    {
2098
        // Insert before given column
2099
        if ($before) {
2100
            $this->addColumnBefore("action_$action", $opts, $before);
2101
        } else {
2102
            $this->columns["action_$action"] = $opts;
2103
        }
2104
        return $this;
2105
    }
2106
2107
    /**
2108
     * @param string $action Action name
2109
     * @param string $url Parameters between {} will be interpolated by row values.
2110
     * @param string $icon
2111
     * @param string $title
2112
     * @param string|null $before
2113
     * @return self
2114
     */
2115
    public function addButton(string $action, string $url, string $icon, string $title, string $before = null): self
2116
    {
2117
        $opts = $this->makeButton($url, $icon, $title);
2118
        $this->addButtonFromArray($action, $opts, $before);
2119
        return $this;
2120
    }
2121
2122
    public function addEditButton()
2123
    {
2124
        $itemUrl = $this->TempLink('item/{ID}', false);
2125
        $this->addButton(self::UI_EDIT, $itemUrl, "edit", _t('TabulatorGrid.Edit', 'Edit'));
2126
        $this->editUrl = $this->TempLink("item/{ID}/ajaxEdit", false);
2127
    }
2128
2129
    public function moveButton(string $action, $pos = self::POS_END): self
2130
    {
2131
        $keep = null;
2132
        foreach ($this->columns as $k => $v) {
2133
            if ($k == "action_$action") {
2134
                $keep = $this->columns[$k];
2135
                unset($this->columns[$k]);
2136
            }
2137
        }
2138
        if ($keep) {
2139
            if ($pos == self::POS_END) {
2140
                $this->columns["action_$action"] = $keep;
2141
            }
2142
            if ($pos == self::POS_START) {
2143
                $this->columns = ["action_$action" => $keep] + $this->columns;
2144
            }
2145
        }
2146
        return $this;
2147
    }
2148
2149
    public function shiftButton(string $action, string $url, string $icon, string $title): self
2150
    {
2151
        // Find first action
2152
        foreach ($this->columns as $name => $options) {
2153
            if (strpos($name, 'action_') === 0) {
2154
                return $this->addButton($action, $url, $icon, $title, $name);
2155
            }
2156
        }
2157
        return $this->addButton($action, $url, $icon, $title);
2158
    }
2159
2160
    public function getActions(): array
2161
    {
2162
        $cols = [];
2163
        foreach ($this->columns as $name => $options) {
2164
            if (strpos($name, 'action_') === 0) {
2165
                $cols[$name] = $options;
2166
            }
2167
        }
2168
        return $cols;
2169
    }
2170
2171
    public function getUiColumns(): array
2172
    {
2173
        $cols = [];
2174
        foreach ($this->columns as $name => $options) {
2175
            if (strpos($name, 'ui_') === 0) {
2176
                $cols[$name] = $options;
2177
            }
2178
        }
2179
        return $cols;
2180
    }
2181
2182
    public function getSystemColumns(): array
2183
    {
2184
        return array_merge($this->getActions(), $this->getUiColumns());
2185
    }
2186
2187
    public function removeButton(string $action): self
2188
    {
2189
        if ($this->hasButton($action)) {
2190
            unset($this->columns["action_$action"]);
2191
        }
2192
        return $this;
2193
    }
2194
2195
    public function hasButton(string $action): bool
2196
    {
2197
        return isset($this->columns["action_$action"]);
2198
    }
2199
2200
    /**
2201
     * @link http://www.tabulator.info/docs/6.2/columns#definition
2202
     * @param string $field (Required) this is the key for this column in the data array
2203
     * @param string $title (Required) This is the title that will be displayed in the header for this column
2204
     * @param array $opts Other options to merge in
2205
     * @return $this
2206
     */
2207
    public function addColumn(string $field, string $title = null, array $opts = []): self
2208
    {
2209
        if ($title === null) {
2210
            $title = $field;
2211
        }
2212
2213
        $baseOpts = [
2214
            "field" => $field,
2215
            "title" => $title,
2216
        ];
2217
2218
        if (!empty($opts)) {
2219
            $baseOpts = array_merge($baseOpts, $opts);
2220
        }
2221
2222
        $this->columns[$field] = $baseOpts;
2223
        return $this;
2224
    }
2225
2226
    /**
2227
     * @link http://www.tabulator.info/docs/6.2/columns#definition
2228
     * @param array $opts Other options to merge in
2229
     * @param ?string $before
2230
     * @return $this
2231
     */
2232
    public function addColumnFromArray(array $opts = [], $before = null)
2233
    {
2234
        if (empty($opts['field']) || !isset($opts['title'])) {
2235
            throw new Exception("Missing field or title key");
2236
        }
2237
        $field = $opts['field'];
2238
2239
        if ($before) {
2240
            $this->addColumnBefore($field, $opts, $before);
2241
        } else {
2242
            $this->columns[$field] = $opts;
2243
        }
2244
2245
        return $this;
2246
    }
2247
2248
    protected function addColumnBefore($field, $opts, $before)
2249
    {
2250
        if (array_key_exists($before, $this->columns)) {
2251
            $new = [];
2252
            foreach ($this->columns as $k => $value) {
2253
                if ($k === $before) {
2254
                    $new[$field] = $opts;
2255
                }
2256
                $new[$k] = $value;
2257
            }
2258
            $this->columns = $new;
2259
        }
2260
    }
2261
2262
    public function makeColumnEditable(string $field, string $editor = "input", array $params = [])
2263
    {
2264
        $col = $this->getColumn($field);
2265
        if (!$col) {
2266
            throw new InvalidArgumentException("$field is not a valid column");
2267
        }
2268
2269
        switch ($editor) {
2270
            case 'date':
2271
                $editor = "input";
2272
                $params = [
2273
                    'mask' => "9999-99-99",
2274
                    'maskAutoFill' => 'true',
2275
                ];
2276
                break;
2277
            case 'datetime':
2278
                $editor = "input";
2279
                $params = [
2280
                    'mask' => "9999-99-99 99:99:99",
2281
                    'maskAutoFill' => 'true',
2282
                ];
2283
                break;
2284
        }
2285
2286
        if (empty($col['cssClass'])) {
2287
            $col['cssClass'] = 'no-change-track';
2288
        } else {
2289
            $col['cssClass'] .= ' no-change-track';
2290
        }
2291
2292
        $col['editor'] = $editor;
2293
        $col['editorParams'] = $params;
2294
        if ($editor == "list") {
2295
            if (!empty($params['autocomplete'])) {
2296
                $col['headerFilter'] = "input"; // force input
2297
            } else {
2298
                $col['headerFilterParams'] = $params; // editor is used as base filter editor
2299
            }
2300
        }
2301
2302
2303
        $this->setColumn($field, $col);
2304
    }
2305
2306
    /**
2307
     * Get column details
2308
2309
     * @param string $key
2310
     */
2311
    public function getColumn(string $key): ?array
2312
    {
2313
        if (isset($this->columns[$key])) {
2314
            return $this->columns[$key];
2315
        }
2316
        return null;
2317
    }
2318
2319
    /**
2320
     * Set column details
2321
     *
2322
     * @param string $key
2323
     * @param array $col
2324
     */
2325
    public function setColumn(string $key, array $col): self
2326
    {
2327
        $this->columns[$key] = $col;
2328
        return $this;
2329
    }
2330
2331
    /**
2332
     * Update column details
2333
     *
2334
     * @param string $key
2335
     * @param array $col
2336
     */
2337
    public function updateColumn(string $key, array $col): self
2338
    {
2339
        $data = $this->getColumn($key);
2340
        if ($data) {
2341
            $this->setColumn($key, array_merge($data, $col));
2342
        }
2343
        return $this;
2344
    }
2345
2346
    /**
2347
     * Remove a column
2348
     *
2349
     * @param string $key
2350
     */
2351
    public function removeColumn(string $key): void
2352
    {
2353
        unset($this->columns[$key]);
2354
    }
2355
2356
    /**
2357
     * Remove a column
2358
     *
2359
     * @param array $keys
2360
     */
2361
    public function removeColumns(array $keys): void
2362
    {
2363
        foreach ($keys as $key) {
2364
            $this->removeColumn($key);
2365
        }
2366
    }
2367
2368
    /**
2369
     * Get the value of columns
2370
     */
2371
    public function getColumns(): array
2372
    {
2373
        return $this->columns;
2374
    }
2375
2376
    /**
2377
     * Set the value of columns
2378
     */
2379
    public function setColumns(array $columns): self
2380
    {
2381
        $this->columns = $columns;
2382
        return $this;
2383
    }
2384
2385
    public function clearColumns(bool $keepSystem = true): void
2386
    {
2387
        $sysNames = array_keys($this->getSystemColumns());
2388
        foreach ($this->columns as $k => $v) {
2389
            if ($keepSystem && in_array($k, $sysNames)) {
2390
                continue;
2391
            }
2392
            $this->removeColumn($k);
2393
        }
2394
    }
2395
2396
    /**
2397
     * This should be the rough equivalent to GridFieldDataColumns::getDisplayFields
2398
     */
2399
    public function getDisplayFields(): array
2400
    {
2401
        $fields = [];
2402
        foreach ($this->columns as $col) {
2403
            if (empty($col['field'])) {
2404
                continue;
2405
            }
2406
            $fields[$col['field']] = $col['title'];
2407
        }
2408
        return $fields;
2409
    }
2410
2411
    /**
2412
     * This should be the rough equivalent to GridFieldDataColumns::setDisplayFields
2413
     */
2414
    public function setDisplayFields(array $arr): void
2415
    {
2416
        $currentCols = $this->columns;
2417
        $this->clearColumns();
2418
        $actions = array_keys($this->getActions());
2419
        $before = $actions[0] ?? null;
2420
        foreach ($arr as $k => $v) {
2421
            if (!$k || !$v) {
2422
                continue;
2423
            }
2424
            $currentCol = $currentCols[$k] ?? [
2425
                'headerSort' => false,
2426
            ];
2427
            $this->addColumnFromArray(array_merge($currentCol, [
2428
                'field' => $k,
2429
                'title' => $v,
2430
            ]), $before);
2431
        }
2432
    }
2433
2434
    /**
2435
     * Convenience method that get/set fields
2436
     */
2437
    public function addDisplayFields(array $arr): void
2438
    {
2439
        $fields = $this->getDisplayFields();
2440
        $fields = array_merge($fields, $arr);
2441
        $this->setDisplayFields($fields);
2442
    }
2443
2444
    /**
2445
     * @param string|AbstractTabulatorTool $tool Pass name or class
2446
     * @return AbstractTabulatorTool|null
2447
     */
2448
    public function getTool($tool): ?AbstractTabulatorTool
2449
    {
2450
        if (is_object($tool)) {
2451
            $tool = get_class($tool);
2452
        }
2453
        if (!is_string($tool)) {
0 ignored issues
show
introduced by
The condition is_string($tool) is always true.
Loading history...
2454
            throw new InvalidArgumentException('Tool must be an object or a class name');
2455
        }
2456
        foreach ($this->tools as $t) {
2457
            if ($t['name'] === $tool) {
2458
                return $t['tool'];
2459
            }
2460
            if ($t['tool'] instanceof $tool) {
2461
                return $t['tool'];
2462
            }
2463
        }
2464
        return null;
2465
    }
2466
2467
    /**
2468
     * @param string $pos start|end
2469
     * @param AbstractTabulatorTool $tool
2470
     * @param string $name
2471
     * @return self
2472
     */
2473
    public function addTool(string $pos, AbstractTabulatorTool $tool, string $name = ''): self
2474
    {
2475
        $tool->setTabulatorGrid($this);
2476
        if ($tool->getName() && !$name) {
2477
            $name = $tool->getName();
2478
        }
2479
        $tool->setName($name);
2480
2481
        $this->tools[] = [
2482
            'position' => $pos,
2483
            'tool' => $tool,
2484
            'name' => $name,
2485
        ];
2486
        return $this;
2487
    }
2488
2489
    public function addToolStart(AbstractTabulatorTool $tool, string $name = ''): self
2490
    {
2491
        return $this->addTool(self::POS_START, $tool, $name);
2492
    }
2493
2494
    public function addToolEnd(AbstractTabulatorTool $tool, string $name = ''): self
2495
    {
2496
        return $this->addTool(self::POS_END, $tool, $name);
2497
    }
2498
2499
    public function removeTool($toolName): self
2500
    {
2501
        if (is_object($toolName)) {
2502
            $toolName = get_class($toolName);
2503
        }
2504
        if (!is_string($toolName)) {
2505
            throw new InvalidArgumentException('Tool must be an object or a class name');
2506
        }
2507
        foreach ($this->tools as $idx => $tool) {
2508
            if ($tool['name'] === $toolName) {
2509
                unset($this->tools[$idx]);
2510
            }
2511
            if (class_exists($toolName) && $tool['tool'] instanceof $toolName) {
2512
                unset($this->tools[$idx]);
2513
            }
2514
        }
2515
        return $this;
2516
    }
2517
2518
    /**
2519
     * @param string|AbstractBulkAction $bulkAction Pass name or class
2520
     * @return AbstractBulkAction|null
2521
     */
2522
    public function getBulkAction($bulkAction): ?AbstractBulkAction
2523
    {
2524
        if (is_object($bulkAction)) {
2525
            $bulkAction = get_class($bulkAction);
2526
        }
2527
        if (!is_string($bulkAction)) {
0 ignored issues
show
introduced by
The condition is_string($bulkAction) is always true.
Loading history...
2528
            throw new InvalidArgumentException('BulkAction must be an object or a class name');
2529
        }
2530
        foreach ($this->bulkActions as $ba) {
2531
            if ($ba->getName() == $bulkAction) {
2532
                return $ba;
2533
            }
2534
            if ($ba instanceof $bulkAction) {
2535
                return $ba;
2536
            }
2537
        }
2538
        return null;
2539
    }
2540
2541
    public function getBulkActions(): array
2542
    {
2543
        return $this->bulkActions;
2544
    }
2545
2546
    /**
2547
     * @param AbstractBulkAction[] $bulkActions
2548
     * @return self
2549
     */
2550
    public function setBulkActions(array $bulkActions): self
2551
    {
2552
        foreach ($bulkActions as $bulkAction) {
2553
            $bulkAction->setTabulatorGrid($this);
2554
        }
2555
        $this->bulkActions = $bulkActions;
2556
        return $this;
2557
    }
2558
2559
    /**
2560
     * If you didn't before, you probably want to call wizardSelectable
2561
     * to get the actual selection checkbox too
2562
     *
2563
     * @param AbstractBulkAction $handler
2564
     * @return self
2565
     */
2566
    public function addBulkAction(AbstractBulkAction $handler): self
2567
    {
2568
        $handler->setTabulatorGrid($this);
2569
2570
        $this->bulkActions[] = $handler;
2571
        return $this;
2572
    }
2573
2574
    public function removeBulkAction($bulkAction): self
2575
    {
2576
        if (is_object($bulkAction)) {
2577
            $bulkAction = get_class($bulkAction);
2578
        }
2579
        if (!is_string($bulkAction)) {
2580
            throw new InvalidArgumentException('Bulk action must be an object or a class name');
2581
        }
2582
        foreach ($this->bulkActions as $idx => $ba) {
2583
            if ($ba->getName() == $bulkAction) {
2584
                unset($this->bulkAction[$idx]);
0 ignored issues
show
Bug Best Practice introduced by
The property bulkAction does not exist on LeKoala\Tabulator\TabulatorGrid. Since you implemented __get, consider adding a @property annotation.
Loading history...
2585
            }
2586
            if ($ba instanceof $bulkAction) {
2587
                unset($this->bulkAction[$idx]);
2588
            }
2589
        }
2590
        return $this;
2591
    }
2592
2593
    public function getColumnDefault(string $opt)
2594
    {
2595
        return $this->columnDefaults[$opt] ?? null;
2596
    }
2597
2598
    public function setColumnDefault(string $opt, $value)
2599
    {
2600
        $this->columnDefaults[$opt] = $value;
2601
    }
2602
2603
    public function getColumnDefaults(): array
2604
    {
2605
        return $this->columnDefaults;
2606
    }
2607
2608
    public function setColumnDefaults(array $columnDefaults): self
2609
    {
2610
        $this->columnDefaults = $columnDefaults;
2611
        return $this;
2612
    }
2613
2614
    public function getListeners(): array
2615
    {
2616
        return $this->listeners;
2617
    }
2618
2619
    public function setListeners(array $listeners): self
2620
    {
2621
        $this->listeners = $listeners;
2622
        return $this;
2623
    }
2624
2625
    public function addListener(string $event, string $functionName): self
2626
    {
2627
        $this->listeners[$event] = $functionName;
2628
        return $this;
2629
    }
2630
2631
    public function removeListener(string $event): self
2632
    {
2633
        if (isset($this->listeners[$event])) {
2634
            unset($this->listeners[$event]);
2635
        }
2636
        return $this;
2637
    }
2638
2639
    public function getLinksOptions(): array
2640
    {
2641
        return $this->linksOptions;
2642
    }
2643
2644
    public function setLinksOptions(array $linksOptions): self
2645
    {
2646
        $this->linksOptions = $linksOptions;
2647
        return $this;
2648
    }
2649
2650
    public function registerLinkOption(string $linksOption): self
2651
    {
2652
        $this->linksOptions[] = $linksOption;
2653
        return $this;
2654
    }
2655
2656
    public function unregisterLinkOption(string $linksOption): self
2657
    {
2658
        $this->linksOptions = array_diff($this->linksOptions, [$linksOption]);
2659
        return $this;
2660
    }
2661
2662
    /**
2663
     * Get the value of pageSize
2664
     */
2665
    public function getPageSize(): int
2666
    {
2667
        return $this->pageSize;
2668
    }
2669
2670
    /**
2671
     * Set the value of pageSize
2672
     *
2673
     * @param int $pageSize
2674
     */
2675
    public function setPageSize(int $pageSize): self
2676
    {
2677
        $this->pageSize = $pageSize;
2678
        return $this;
2679
    }
2680
2681
    /**
2682
     * Get the value of autoloadDataList
2683
     */
2684
    public function getAutoloadDataList(): bool
2685
    {
2686
        return $this->autoloadDataList;
2687
    }
2688
2689
    /**
2690
     * Set the value of autoloadDataList
2691
     *
2692
     * @param bool $autoloadDataList
2693
     */
2694
    public function setAutoloadDataList(bool $autoloadDataList): self
2695
    {
2696
        $this->autoloadDataList = $autoloadDataList;
2697
        return $this;
2698
    }
2699
2700
    /**
2701
     * Set the value of itemRequestClass
2702
     */
2703
    public function setItemRequestClass(string $itemRequestClass): self
2704
    {
2705
        $this->itemRequestClass = $itemRequestClass;
2706
        return $this;
2707
    }
2708
2709
    /**
2710
     * Get the value of lazyInit
2711
     */
2712
    public function getLazyInit(): bool
2713
    {
2714
        return $this->lazyInit;
2715
    }
2716
2717
    /**
2718
     * Set the value of lazyInit
2719
     */
2720
    public function setLazyInit(bool $lazyInit): self
2721
    {
2722
        $this->lazyInit = $lazyInit;
2723
        return $this;
2724
    }
2725
2726
    /**
2727
     * Get the value of rowClickTriggersAction
2728
     */
2729
    public function getRowClickTriggersAction(): bool
2730
    {
2731
        return $this->rowClickTriggersAction;
2732
    }
2733
2734
    /**
2735
     * Set the value of rowClickTriggersAction
2736
     */
2737
    public function setRowClickTriggersAction(bool $rowClickTriggersAction): self
2738
    {
2739
        $this->rowClickTriggersAction = $rowClickTriggersAction;
2740
        return $this;
2741
    }
2742
2743
    /**
2744
     * Get the value of controllerFunction
2745
     */
2746
    public function getControllerFunction(): string
2747
    {
2748
        if (!$this->controllerFunction) {
2749
            return $this->getName() ?? "TabulatorGrid";
2750
        }
2751
        return $this->controllerFunction;
2752
    }
2753
2754
    /**
2755
     * Set the value of controllerFunction
2756
     */
2757
    public function setControllerFunction(string $controllerFunction): self
2758
    {
2759
        $this->controllerFunction = $controllerFunction;
2760
        return $this;
2761
    }
2762
2763
    /**
2764
     * Get the value of editUrl
2765
     */
2766
    public function getEditUrl(): string
2767
    {
2768
        return $this->editUrl;
2769
    }
2770
2771
    /**
2772
     * Set the value of editUrl
2773
     */
2774
    public function setEditUrl(string $editUrl): self
2775
    {
2776
        $this->editUrl = $editUrl;
2777
        return $this;
2778
    }
2779
2780
    /**
2781
     * Get the value of moveUrl
2782
     */
2783
    public function getMoveUrl(): string
2784
    {
2785
        return $this->moveUrl;
2786
    }
2787
2788
    /**
2789
     * Set the value of moveUrl
2790
     */
2791
    public function setMoveUrl(string $moveUrl): self
2792
    {
2793
        $this->moveUrl = $moveUrl;
2794
        return $this;
2795
    }
2796
2797
    /**
2798
     * Get the value of bulkUrl
2799
     */
2800
    public function getBulkUrl(): string
2801
    {
2802
        return $this->bulkUrl;
2803
    }
2804
2805
    /**
2806
     * Set the value of bulkUrl
2807
     */
2808
    public function setBulkUrl(string $bulkUrl): self
2809
    {
2810
        $this->bulkUrl = $bulkUrl;
2811
        return $this;
2812
    }
2813
2814
    /**
2815
     * Get the value of globalSearch
2816
     */
2817
    public function getGlobalSearch(): bool
2818
    {
2819
        return $this->globalSearch;
2820
    }
2821
2822
    /**
2823
     * Set the value of globalSearch
2824
     *
2825
     * @param bool $globalSearch
2826
     */
2827
    public function setGlobalSearch($globalSearch): self
2828
    {
2829
        $this->globalSearch = $globalSearch;
2830
        return $this;
2831
    }
2832
2833
    /**
2834
     * Get the value of wildcardFields
2835
     */
2836
    public function getWildcardFields(): array
2837
    {
2838
        return $this->wildcardFields;
2839
    }
2840
2841
    /**
2842
     * Set the value of wildcardFields
2843
     *
2844
     * @param array $wildcardFields
2845
     */
2846
    public function setWildcardFields($wildcardFields): self
2847
    {
2848
        $this->wildcardFields = $wildcardFields;
2849
        return $this;
2850
    }
2851
2852
    /**
2853
     * Get the value of quickFilters
2854
     */
2855
    public function getQuickFilters(): array
2856
    {
2857
        return $this->quickFilters;
2858
    }
2859
2860
    /**
2861
     * Pass an array with as a key, the name of the filter
2862
     * and as a value, an array with two keys: label and callback
2863
     *
2864
     * For example:
2865
     * 'myquickfilter' => [
2866
     *   'label' => 'My Quick Filter',
2867
     *   'callback' => function (&$list) {
2868
     *     ...
2869
     *   }
2870
     * ]
2871
     *
2872
     * @param array $quickFilters
2873
     */
2874
    public function setQuickFilters($quickFilters): self
2875
    {
2876
        $this->quickFilters = $quickFilters;
2877
        return $this;
2878
    }
2879
2880
    /**
2881
     * Get the value of groupLayout
2882
     */
2883
    public function getGroupLayout(): bool
2884
    {
2885
        return $this->groupLayout;
2886
    }
2887
2888
    /**
2889
     * Set the value of groupLayout
2890
     *
2891
     * @param bool $groupLayout
2892
     */
2893
    public function setGroupLayout($groupLayout): self
2894
    {
2895
        $this->groupLayout = $groupLayout;
2896
        return $this;
2897
    }
2898
2899
    /**
2900
     * Get the value of enableGridManipulation
2901
     */
2902
    public function getEnableGridManipulation(): bool
2903
    {
2904
        return $this->enableGridManipulation;
2905
    }
2906
2907
    /**
2908
     * Set the value of enableGridManipulation
2909
     *
2910
     * @param bool $enableGridManipulation
2911
     */
2912
    public function setEnableGridManipulation($enableGridManipulation): self
2913
    {
2914
        $this->enableGridManipulation = $enableGridManipulation;
2915
        return $this;
2916
    }
2917
2918
    /**
2919
     * Get the value of defaultFilter
2920
     */
2921
    public function getDefaultFilter(): string
2922
    {
2923
        return $this->defaultFilter;
2924
    }
2925
2926
    /**
2927
     * Set the value of defaultFilter
2928
     *
2929
     * @param string $defaultFilter
2930
     */
2931
    public function setDefaultFilter($defaultFilter): self
2932
    {
2933
        $this->defaultFilter = $defaultFilter;
2934
        return $this;
2935
    }
2936
}
2937