Passed
Push — master ( a4042e...97363c )
by Thomas
02:59
created

TabulatorGrid::getTabulatorOptions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

538
            $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...
539
        }
540
        $export = $opts['export'] ?? true;
541
        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...
542
            $this->addTool(self::POS_END, new TabulatorExportButton($this), self::TOOL_EXPORT);
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

542
            $this->addTool(self::POS_END, /** @scrutinizer ignore-call */ new TabulatorExportButton($this), self::TOOL_EXPORT);

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...
543
        }
544
545
        // - Custom actions are forwarded to the model itself
546
        if ($singl->hasMethod('tabulatorRowActions')) {
547
            $rowActions = $singl->tabulatorRowActions();
548
            if (!is_array($rowActions)) {
549
                throw new RuntimeException("tabulatorRowActions must return an array");
550
            }
551
            foreach ($rowActions as $key => $actionConfig) {
552
                $action = $actionConfig['action'] ?? $key;
553
                $url = $this->TempLink("item/{ID}/customAction/$action", false);
554
                $icon = $actionConfig['icon'] ?? "cog";
555
                $title = $actionConfig['title'] ?? "";
556
557
                $button = $this->makeButton($url, $icon, $title);
558
                if (!empty($actionConfig['ajax'])) {
559
                    $button['formatterParams']['ajax'] = true;
560
                }
561
                $this->addButtonFromArray("ui_customaction_$action", $button);
562
            }
563
        }
564
565
        $this->setRowClickTriggersAction(true);
566
    }
567
568
    public static function requirements(): void
569
    {
570
        $load_styles = self::config()->load_styles;
571
        $luxon_version = self::config()->luxon_version;
572
        $enable_luxon = self::config()->enable_luxon;
573
        $last_icon_version = self::config()->last_icon_version;
574
        $enable_last_icon = self::config()->enable_last_icon;
575
        $enable_js_modules = self::config()->enable_js_modules;
576
577
        $jsOpts = [];
578
        if ($enable_js_modules) {
579
            $jsOpts['type'] = 'module';
580
        }
581
582
        if ($luxon_version && $enable_luxon) {
583
            // Do not load as module or we would get undefined luxon global var
584
            Requirements::javascript("https://cdn.jsdelivr.net/npm/luxon@$luxon_version/build/global/luxon.min.js");
585
        }
586
        if ($last_icon_version && $enable_last_icon) {
587
            Requirements::css("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.css");
588
            // Do not load as module even if asked to ensure load speed
589
            Requirements::javascript("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.js");
590
        }
591
592
        Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js', $jsOpts);
593
        if ($load_styles) {
594
            Requirements::css('lekoala/silverstripe-tabulator:client/custom-tabulator.css');
595
            Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.min.js', $jsOpts);
596
        } else {
597
            // you must load th css yourself based on your preferences
598
            Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.raw.min.js', $jsOpts);
599
        }
600
    }
601
602
    public function setValue($value, $data = null)
603
    {
604
        // Allow set raw json as value
605
        if ($value && is_string($value) && strpos($value, '[') === 0) {
606
            $value = json_decode($value);
607
        }
608
        if ($value instanceof DataList) {
609
            $this->configureFromDataObject($value->dataClass());
610
        }
611
        return parent::setValue($value, $data);
612
    }
613
614
    public function Field($properties = [])
615
    {
616
        if (self::config()->enable_requirements) {
617
            self::requirements();
618
        }
619
620
        // Make sure we can use a standalone version of the field without a form
621
        // Function should match the name
622
        if (!$this->form) {
623
            $this->form = new Form(Controller::curr(), $this->getControllerFunction());
624
        }
625
626
        // Data attributes for our custom behaviour
627
        $this->setDataAttribute("row-click-triggers-action", $this->rowClickTriggersAction);
628
629
        $this->setDataAttribute("listeners", $this->listeners);
630
        if ($this->editUrl) {
631
            $url = $this->processLink($this->editUrl);
632
            $this->setDataAttribute("edit-url", $url);
633
        }
634
        if ($this->moveUrl) {
635
            $url = $this->processLink($this->moveUrl);
636
            $this->setDataAttribute("move-url", $url);
637
        }
638
        if (!empty($this->bulkActions)) {
639
            $url = $this->processLink($this->bulkUrl);
640
            $this->setDataAttribute("bulk-url", $url);
641
        }
642
643
        return parent::Field($properties);
644
    }
645
646
    public function ShowTools(): string
647
    {
648
        if (empty($this->tools)) {
649
            return '';
650
        }
651
        $html = '';
652
        $html .= '<div class="tabulator-tools">';
653
        $html .= '<div class="tabulator-tools-start">';
654
        foreach ($this->tools as $tool) {
655
            if ($tool['position'] != self::POS_START) {
656
                continue;
657
            }
658
            $html .= ($tool['tool'])->forTemplate();
659
        }
660
        $html .= '</div>';
661
        $html .= '<div class="tabulator-tools-end">';
662
        foreach ($this->tools as $tool) {
663
            if ($tool['position'] != self::POS_END) {
664
                continue;
665
            }
666
            $html .= ($tool['tool'])->forTemplate();
667
        }
668
        // Show bulk actions at the end
669
        if (!empty($this->bulkActions)) {
670
            $selectLabel = _t(__CLASS__ . ".BULKSELECT", "Select a bulk action");
671
            $confirmLabel = _t(__CLASS__ . ".BULKCONFIRM", "Go");
672
            $html .= "<select class=\"tabulator-bulk-select\">";
673
            $html .= "<option>" . $selectLabel . "</option>";
674
            foreach ($this->bulkActions as $bulkAction) {
675
                $v = $bulkAction->getName();
676
                $xhr = $bulkAction->getXhr();
677
                $destructive = $bulkAction->getDestructive();
678
                $html .= "<option value=\"$v\" data-xhr=\"$xhr\" data-destructive=\"$destructive\">" . $bulkAction->getLabel() . "</option>";
679
            }
680
            $html .= "</select>";
681
            $html .= "<button class=\"tabulator-bulk-confirm btn\">" . $confirmLabel . "</button>";
682
        }
683
        $html .= '</div>';
684
        $html .= '</div>';
685
        return $html;
686
    }
687
688
    public function JsonOptions(): string
689
    {
690
        $this->processLinks();
691
692
        $data = $this->list ?? [];
693
        if ($this->autoloadDataList && $data instanceof DataList) {
694
            $data = null;
695
        }
696
        $opts = $this->options;
697
        $opts['columnDefaults'] = $this->columnDefaults;
698
699
        if (empty($this->columns)) {
700
            $opts['autoColumns'] = true;
701
        } else {
702
            $opts['columns'] = array_values($this->columns);
703
        }
704
705
        if ($data && is_iterable($data)) {
706
            if ($data instanceof ArrayList) {
707
                $data = $data->toArray();
708
            } else {
709
                if (is_iterable($data) && !is_array($data)) {
710
                    $data = iterator_to_array($data);
711
                }
712
            }
713
            $opts['data'] = $data;
714
        }
715
716
        // i18n
717
        $locale = strtolower(str_replace('_', '-', i18n::get_locale()));
718
        $paginationTranslations = [
719
            "first" => _t("TabulatorPagination.first", "First"),
720
            "first_title" =>  _t("TabulatorPagination.first_title", "First Page"),
721
            "last" =>  _t("TabulatorPagination.last", "Last"),
722
            "last_title" => _t("TabulatorPagination.last_title", "Last Page"),
723
            "prev" => _t("TabulatorPagination.prev", "Previous"),
724
            "prev_title" =>  _t("TabulatorPagination.prev_title", "Previous Page"),
725
            "next" => _t("TabulatorPagination.next", "Next"),
726
            "next_title" =>  _t("TabulatorPagination.next_title", "Next Page"),
727
            "all" =>  _t("TabulatorPagination.all", "All"),
728
        ];
729
        $dataTranslations = [
730
            "loading" => _t("TabulatorData.loading", "Loading"),
731
            "error" => _t("TabulatorData.error", "Error"),
732
        ];
733
        $groupsTranslations = [
734
            "item" => _t("TabulatorGroups.item", "Item"),
735
            "items" => _t("TabulatorGroups.items", "Items"),
736
        ];
737
        $headerFiltersTranslations = [
738
            "default" => _t("TabulatorHeaderFilters.default", "filter column..."),
739
        ];
740
        $bulkActionsTranslations = [
741
            "no_action" => _t("TabulatorBulkActions.no_action", "Please select an action"),
742
            "no_records" => _t("TabulatorBulkActions.no_records", "Please select a record"),
743
            "destructive" => _t("TabulatorBulkActions.destructive", "Confirm destructive action ?"),
744
        ];
745
        $translations = [
746
            'data' => $dataTranslations,
747
            'groups' => $groupsTranslations,
748
            'pagination' => $paginationTranslations,
749
            'headerFilters' => $headerFiltersTranslations,
750
            'bulkActions' => $bulkActionsTranslations,
751
        ];
752
        $opts['locale'] = $locale;
753
        $opts['langs'] = [
754
            $locale => $translations
755
        ];
756
757
        // Apply state
758
        // TODO: finalize persistence on the client side instead of this when using TabID
759
        $state = $this->getState();
760
        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...
761
            if (!empty($state['filter'])) {
762
                // @link https://tabulator.info/docs/5.5/filter#initial
763
                // We need to split between global filters and header filters
764
                $allFilters = $state['filter'] ?? [];
765
                $globalFilters = [];
766
                $headerFilters = [];
767
                foreach ($allFilters as $allFilter) {
768
                    if (strpos($allFilter['field'], '__') === 0) {
769
                        $globalFilters[] = $allFilter;
770
                    } else {
771
                        $headerFilters[] = $allFilter;
772
                    }
773
                }
774
                $opts['initialFilter'] = $globalFilters;
775
                $opts['initialHeaderFilter'] = $headerFilters;
776
            }
777
            if (!empty($state['sort'])) {
778
                // @link https://tabulator.info/docs/5.5/sort#initial
779
                $opts['initialSort'] = $state['sort'];
780
            }
781
782
            // Restore state from server
783
            $opts['_state'] = $state;
784
        }
785
786
        if ($this->enableGridManipulation) {
787
            // $opts['renderVertical'] = 'basic';
788
        }
789
790
        // Add our extension initCallback
791
        $opts['_initCallback'] = ['__fn' => self::JS_INIT_CALLBACK];
792
        $opts['_configCallback'] = ['__fn' => self::JS_CONFIG_CALLBACK];
793
794
        unset($opts['height']);
795
        $json = json_encode($opts);
796
797
        // Escape '
798
        $json = str_replace("'", '&#39;', $json);
799
800
        return $json;
801
    }
802
803
    /**
804
     * @param Controller $controller
805
     * @return CompatLayerInterface
806
     */
807
    public function getCompatLayer(Controller $controller = null)
808
    {
809
        if ($controller === null) {
810
            $controller = Controller::curr();
811
        }
812
        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...
813
            return new SilverstripeAdminCompat();
814
        }
815
        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...
816
            return new AdminiCompat();
817
        }
818
    }
819
820
    public function getAttributes()
821
    {
822
        $attrs = parent::getAttributes();
823
        unset($attrs['type']);
824
        unset($attrs['name']);
825
        unset($attrs['value']);
826
        return $attrs;
827
    }
828
829
    public function getOption(string $k)
830
    {
831
        return $this->options[$k] ?? null;
832
    }
833
834
    public function setOption(string $k, $v): self
835
    {
836
        $this->options[$k] = $v;
837
        return $this;
838
    }
839
840
    public function getRowHeight(): int
841
    {
842
        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...
843
    }
844
845
    /**
846
     * Prevent row height automatic computation
847
     * @link https://tabulator.info/docs/5.5/layout#height-row
848
     */
849
    public function setRowHeight(int $v): self
850
    {
851
        $this->setOption('rowHeight', $v);
852
        return $this;
853
    }
854
855
    public function makeHeadersSticky(): self
856
    {
857
        // note: we could also use the "sticky" attribute on the custom element
858
        $this->addExtraClass("tabulator-sticky");
859
        return $this;
860
    }
861
862
    public function setRemoteSource(string $url, array $extraParams = [], bool $dataResponse = false): self
863
    {
864
        $this->setOption("ajaxURL", $url); //set url for ajax request
865
        $params = array_merge([
866
            'SecurityID' => SecurityToken::getSecurityID()
867
        ], $extraParams);
868
        $this->setOption("ajaxParams", $params);
869
        // Accept response where data is nested under the data key
870
        if ($dataResponse) {
871
            $this->setOption("ajaxResponse", ['__fn' => self::JS_DATA_AJAX_RESPONSE]);
872
        }
873
        return $this;
874
    }
875
876
    /**
877
     * @link http://www.tabulator.info/docs/5.5/page#remote
878
     * @param string $url
879
     * @param array $params
880
     * @param integer $pageSize
881
     * @param integer $initialPage
882
     */
883
    public function setRemotePagination(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1): self
884
    {
885
        $this->setOption("pagination", true); //enable pagination
886
        $this->setOption("paginationMode", 'remote'); //enable remote pagination
887
        $this->setRemoteSource($url, $params);
888
        if (!$pageSize) {
889
            $pageSize = $this->pageSize;
890
        } else {
891
            $this->pageSize = $pageSize;
892
        }
893
        $this->setOption("paginationSize", $pageSize);
894
        $this->setOption("paginationInitialPage", $initialPage);
895
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/5.5/page#counter
896
        return $this;
897
    }
898
899
    public function wizardRemotePagination(int $pageSize = 0, int $initialPage = 1, array $params = []): self
900
    {
901
        $this->setRemotePagination($this->TempLink('load', false), $params, $pageSize, $initialPage);
902
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/5.5/sort#ajax-sort
903
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/5.5/filter#ajax-filter
904
        return $this;
905
    }
906
907
    public function setProgressiveLoad(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0): self
908
    {
909
        $this->setOption("ajaxURL", $url);
910
        if (!empty($params)) {
911
            $this->setOption("ajaxParams", $params);
912
        }
913
        $this->setOption("progressiveLoad", $mode);
914
        if ($scrollMargin > 0) {
915
            $this->setOption("progressiveLoadScrollMargin", $scrollMargin);
916
        }
917
        if (!$pageSize) {
918
            $pageSize = $this->pageSize;
919
        } else {
920
            $this->pageSize = $pageSize;
921
        }
922
        $this->setOption("paginationSize", $pageSize);
923
        $this->setOption("paginationInitialPage", $initialPage);
924
        $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/5.5/page#counter
925
        return $this;
926
    }
927
928
    public function wizardProgressiveLoad(int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0, array $extraParams = []): self
929
    {
930
        $params = array_merge([
931
            'SecurityID' => SecurityToken::getSecurityID()
932
        ], $extraParams);
933
        $this->setProgressiveLoad($this->TempLink('load', false), $params, $pageSize, $initialPage, $mode, $scrollMargin);
934
        $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/5.5/sort#ajax-sort
935
        $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/5.5/filter#ajax-filter
936
        return $this;
937
    }
938
939
    /**
940
     * @link https://tabulator.info/docs/5.5/layout#responsive
941
     * @param boolean $startOpen
942
     * @param string $mode collapse|hide|flexCollapse
943
     * @return self
944
     */
945
    public function wizardResponsiveCollapse(bool $startOpen = false, string $mode = "collapse"): self
946
    {
947
        $this->setOption("responsiveLayout", $mode);
948
        $this->setOption("responsiveLayoutCollapseStartOpen", $startOpen);
949
        if ($mode != "hide") {
950
            $this->columns = array_merge([
951
                'ui_responsive_collapse' => [
952
                    "cssClass" => 'tabulator-cell-btn',
953
                    'formatter' => 'responsiveCollapse',
954
                    'headerSort' => false,
955
                    'width' => 40,
956
                ]
957
            ], $this->columns);
958
        }
959
        return $this;
960
    }
961
962
    public function wizardDataTree(bool $startExpanded = false, bool $filter = false, bool $sort = false, string $el = null): self
963
    {
964
        $this->setOption("dataTree", true);
965
        $this->setOption("dataTreeStartExpanded", $startExpanded);
966
        $this->setOption("dataTreeFilter", $filter);
967
        $this->setOption("dataTreeSort", $sort);
968
        if ($el) {
969
            $this->setOption("dataTreeElementColumn", $el);
970
        }
971
        return $this;
972
    }
973
974
    /**
975
     * @param array $actions An array of bulk actions, that can extend the abstract one or use the generic with callbable
976
     * @return self
977
     */
978
    public function wizardSelectable(array $actions = []): self
979
    {
980
        $this->columns = array_merge([
981
            'ui_selectable' => [
982
                "hozAlign" => 'center',
983
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector',
984
                'formatter' => 'rowSelection',
985
                'titleFormatter' => 'rowSelection',
986
                'width' => 40,
987
                'maxWidth' => 40,
988
                "headerSort" => false,
989
            ]
990
        ], $this->columns);
991
        $this->setBulkActions($actions);
992
        return $this;
993
    }
994
995
    public function wizardMoveable(string $callback = "SSTabulator.rowMoved", $field = "Sort"): self
996
    {
997
        $this->moveUrl = $this->TempLink("item/{ID}/ajaxMove", false);
998
        $this->setOption("movableRows", true);
999
        $this->addListener("rowMoved", $callback);
1000
        $this->columns = array_merge([
1001
            'ui_move' => [
1002
                "hozAlign" => 'center',
1003
                "cssClass" => 'tabulator-cell-btn tabulator-cell-selector tabulator-ui-sort',
1004
                'rowHandle' => true,
1005
                'formatter' => 'handle',
1006
                'headerSort' => false,
1007
                'frozen' => true,
1008
                'width' => 40,
1009
                'maxWidth' => 40,
1010
            ],
1011
            // We need a hidden sort column
1012
            self::UI_SORT => [
1013
                "field" => $field,
1014
                'visible' => false,
1015
            ],
1016
        ], $this->columns);
1017
        return $this;
1018
    }
1019
1020
    /**
1021
     * @param string $field
1022
     * @param string $toggleElement arrow|header|false (header by default)
1023
     * @param boolean $isBool
1024
     * @return void
1025
     */
1026
    public function wizardGroupBy(string $field, string $toggleElement = 'header', bool $isBool = false)
1027
    {
1028
        $this->setOption("groupBy", $field);
1029
        $this->setOption("groupToggleElement", $toggleElement);
1030
        if ($isBool) {
1031
            $this->setOption("groupHeader", ['_fn' => self::JS_BOOL_GROUP_HEADER]);
1032
        }
1033
    }
1034
1035
    /**
1036
     * @param HTTPRequest $request
1037
     * @return HTTPResponse
1038
     */
1039
    public function handleItem($request)
1040
    {
1041
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1042
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1043
        $requestHandler = $this->getForm()->getController();
1044
        $record = $this->getRecordFromRequest($request);
1045
        if (!$record) {
1046
            return $requestHandler->httpError(404, 'That record was not found');
1047
        }
1048
        $handler = $this->getItemRequestHandler($record, $requestHandler);
1049
        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...
1050
    }
1051
1052
    /**
1053
     * @param HTTPRequest $request
1054
     * @return HTTPResponse
1055
     */
1056
    public function handleTool($request)
1057
    {
1058
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1059
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1060
        $requestHandler = $this->getForm()->getController();
1061
        $tool = $this->getToolFromRequest($request);
1062
        if (!$tool) {
1063
            return $requestHandler->httpError(404, 'That tool was not found');
1064
        }
1065
        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...
1066
    }
1067
1068
    /**
1069
     * @param HTTPRequest $request
1070
     * @return HTTPResponse
1071
     */
1072
    public function handleBulkAction($request)
1073
    {
1074
        // Our getController could either give us a true Controller, if this is the top-level GridField.
1075
        // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...)
1076
        $requestHandler = $this->getForm()->getController();
1077
        $bulkAction = $this->getBulkActionFromRequest($request);
1078
        if (!$bulkAction) {
1079
            return $requestHandler->httpError(404, 'That bulk action was not found');
1080
        }
1081
        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...
1082
    }
1083
1084
    /**
1085
     * @return string name of {@see TabulatorGrid_ItemRequest} subclass
1086
     */
1087
    public function getItemRequestClass(): string
1088
    {
1089
        if ($this->itemRequestClass) {
1090
            return $this->itemRequestClass;
1091
        } elseif (ClassInfo::exists(static::class . '_ItemRequest')) {
1092
            return static::class . '_ItemRequest';
1093
        }
1094
        return TabulatorGrid_ItemRequest::class;
1095
    }
1096
1097
    /**
1098
     * Build a request handler for the given record
1099
     *
1100
     * @param DataObject $record
1101
     * @param RequestHandler $requestHandler
1102
     * @return TabulatorGrid_ItemRequest
1103
     */
1104
    protected function getItemRequestHandler($record, $requestHandler)
1105
    {
1106
        $class = $this->getItemRequestClass();
1107
        $assignedClass = $this->itemRequestClass;
1108
        $this->extend('updateItemRequestClass', $class, $record, $requestHandler, $assignedClass);
1109
        /** @var TabulatorGrid_ItemRequest $handler */
1110
        $handler = Injector::inst()->createWithArgs(
1111
            $class,
1112
            [$this, $record, $requestHandler]
1113
        );
1114
        if ($template = $this->getTemplate()) {
1115
            $handler->setTemplate($template);
1116
        }
1117
        $this->extend('updateItemRequestHandler', $handler);
1118
        return $handler;
1119
    }
1120
1121
    public function getStateKey(string $TabID = null)
1122
    {
1123
        $nested = [];
1124
        $form = $this->getForm();
1125
        $scope = $this->modelClass ? str_replace('_', '\\', $this->modelClass) :  "default";
1126
        if ($form) {
0 ignored issues
show
introduced by
$form is of type SilverStripe\Forms\Form, thus it always evaluated to true.
Loading history...
1127
            $controller = $form->getController();
1128
1129
            // We are in a nested form, track by id since each records needs it own state
1130
            while ($controller instanceof TabulatorGrid_ItemRequest) {
1131
                $record = $controller->getRecord();
1132
                $nested[str_replace('_', '\\', get_class($record))] = $record->ID;
1133
1134
                // Move to parent controller
1135
                $controller = $controller->getController();
1136
            }
1137
1138
            // Scope by top controller class
1139
            $scope = str_replace('_', '\\', get_class($controller));
1140
        }
1141
1142
        $baseKey = 'TabulatorState';
1143
        if ($TabID) {
1144
            $baseKey .= '_' . $TabID;
1145
        }
1146
        $name = $this->getName();
1147
        $key = "$baseKey.$scope.$name";
1148
        foreach ($nested as $k => $v) {
1149
            $key .= "$k.$v";
1150
        }
1151
        return $key;
1152
    }
1153
1154
    /**
1155
     * @param HTTPRequest|null $request
1156
     * @return array{'page': int, 'limit': int, 'sort': array, 'filter': array}
1157
     */
1158
    public function getState(HTTPRequest $request = null)
1159
    {
1160
        if ($request === null) {
1161
            $request = Controller::curr()->getRequest();
1162
        }
1163
        $TabID = $request->requestVar('TabID') ?? null;
1164
        $stateKey = $this->getStateKey($TabID);
1165
        $state = $request->getSession()->get($stateKey);
1166
        return $state ?? [
1167
            'page' => 1,
1168
            'limit' => $this->pageSize,
1169
            'sort' => [],
1170
            'filter' => [],
1171
        ];
1172
    }
1173
1174
    public function setState(HTTPRequest $request, $state)
1175
    {
1176
        $TabID = $request->requestVar('TabID') ?? null;
1177
        $stateKey = $this->getStateKey($TabID);
1178
        $request->getSession()->set($stateKey, $state);
1179
        // If we are in a new controller, we can clear other states
1180
        // Note: this would break tabbed navigation if you try to open multiple tabs, see below for more info
1181
        // @link https://github.com/silverstripe/silverstripe-framework/issues/9556
1182
        $matches = [];
1183
        preg_match_all('/\.(.*?)\./', $stateKey, $matches);
1184
        $scope = $matches[1][0] ?? null;
1185
        if ($scope) {
1186
            self::clearAllStates($scope);
1187
        }
1188
    }
1189
1190
    public function clearState(HTTPRequest $request)
1191
    {
1192
        $TabID = $request->requestVar('TabID') ?? null;
1193
        $stateKey = $this->getStateKey($TabID);
1194
        $request->getSession()->clear($stateKey);
1195
    }
1196
1197
    public static function clearAllStates(string $exceptScope = null, string $TabID = null)
1198
    {
1199
        $request = Controller::curr()->getRequest();
1200
        $baseKey = 'TabulatorState';
1201
        if ($TabID) {
1202
            $baseKey .= '_' . $TabID;
1203
        }
1204
        $allStates = $request->getSession()->get($baseKey);
1205
        if (!$allStates) {
1206
            return;
1207
        }
1208
        foreach ($allStates as $scope => $data) {
1209
            if ($exceptScope && $scope == $exceptScope) {
1210
                continue;
1211
            }
1212
            $request->getSession()->clear("TabulatorState.$scope");
1213
        }
1214
    }
1215
1216
    public function StateValue($key, $field): ?string
1217
    {
1218
        $state = $this->getState();
1219
        $arr = $state[$key] ?? [];
1220
        foreach ($arr as $s) {
1221
            if ($s['field'] === $field) {
1222
                return $s['value'];
1223
            }
1224
        }
1225
        return null;
1226
    }
1227
1228
    /**
1229
     * Provides autocomplete lists
1230
     *
1231
     * @param HTTPRequest $request
1232
     * @return HTTPResponse
1233
     */
1234
    public function autocomplete(HTTPRequest $request)
1235
    {
1236
        if ($this->isDisabled() || $this->isReadonly()) {
1237
            return $this->httpError(403);
1238
        }
1239
        $SecurityID = $request->getVar('SecurityID');
1240
        if (!SecurityToken::inst()->check($SecurityID)) {
1241
            return $this->httpError(404, "Invalid SecurityID");
1242
        }
1243
1244
        $name = $request->getVar("Column");
1245
        $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

1245
        $col = $this->getColumn(/** @scrutinizer ignore-type */ $name);
Loading history...
1246
        if (!$col) {
1247
            return $this->httpError(403, "Invalid column");
1248
        }
1249
1250
        // Don't use % term as it prevents use of indexes
1251
        $term = $request->getVar('term') . '%';
1252
        $term = str_replace(' ', '%', $term);
1253
1254
        $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

1254
        $parts = explode(".", /** @scrutinizer ignore-type */ $name);
Loading history...
1255
        if (count($parts) > 2) {
1256
            array_pop($parts);
1257
        }
1258
        if (count($parts) == 2) {
1259
            $class = $parts[0];
1260
            $field = $parts[1];
1261
        } elseif (count($parts) == 1) {
1262
            $class = preg_replace("/ID$/", "", $parts[0]);
1263
            $field = 'Title';
1264
        } else {
1265
            return $this->httpError(403, "Invalid field");
1266
        }
1267
1268
        /** @var DataObject $sng */
1269
        $sng = $class::singleton();
1270
        $baseTable = $sng->baseTable();
1271
1272
        $searchField = null;
1273
        $searchCandidates = [
1274
            $field, 'Name', 'Surname', 'Email', 'ID'
1275
        ];
1276
1277
        // Ensure field exists, this is really rudimentary
1278
        $db = $class::config()->db;
1279
        foreach ($searchCandidates as $searchCandidate) {
1280
            if ($searchField) {
1281
                continue;
1282
            }
1283
            if (isset($db[$searchCandidate])) {
1284
                $searchField = $searchCandidate;
1285
            }
1286
        }
1287
        $searchCols = [$searchField];
1288
1289
        // For members, do something better
1290
        if ($baseTable == 'Member') {
1291
            $searchField = ['FirstName', 'Surname'];
1292
            $searchCols = ['FirstName', 'Surname', 'Email'];
1293
        }
1294
1295
        if (!empty($col['editorParams']['customSearchField'])) {
1296
            $searchField = $col['editorParams']['customSearchField'];
1297
        }
1298
        if (!empty($col['editorParams']['customSearchCols'])) {
1299
            $searchCols = $col['editorParams']['customSearchCols'];
1300
        }
1301
1302
        // Note: we need to use the orm, even if it's slower, to make sure any extension is properly applied
1303
        /** @var DataList $list */
1304
        $list = $sng::get();
1305
1306
        // Make sure at least one field is not null...
1307
        $where = [];
1308
        foreach ($searchCols as $searchCol) {
1309
            $where[] = $searchCol . ' IS NOT NULL';
1310
        }
1311
        $list = $list->where($where);
1312
        // ... and matches search term ...
1313
        $where = [];
1314
        foreach ($searchCols as $searchCol) {
1315
            $where[$searchCol . ' LIKE ?'] = $term;
1316
        }
1317
        $list = $list->whereAny($where);
1318
1319
        // ... and any user set requirements
1320
        if (!empty($col['editorParams']['where'])) {
1321
            // Deal with in clause
1322
            $customWhere = [];
1323
            foreach ($col['editorParams']['where'] as $col => $param) {
1324
                // For array, we need a IN statement with a ? for each value
1325
                if (is_array($param)) {
1326
                    $prepValue = [];
1327
                    $params = [];
1328
                    foreach ($param as $paramValue) {
1329
                        $params[] = $paramValue;
1330
                        $prepValue[] = "?";
1331
                    }
1332
                    $customWhere["$col IN (" . implode(',', $prepValue) . ")"] = $params;
1333
                } else {
1334
                    $customWhere["$col = ?"] = $param;
1335
                }
1336
            }
1337
            $list = $list->where($customWhere);
1338
        }
1339
1340
        $results = iterator_to_array($list);
1341
        $data = [];
1342
        foreach ($results as $record) {
1343
            if (is_array($searchField)) {
1344
                $labelParts = [];
1345
                foreach ($searchField as $sf) {
1346
                    $labelParts[] = $record->$sf;
1347
                }
1348
                $label = implode(" ", $labelParts);
1349
            } else {
1350
                $label = $record->$searchField;
1351
            }
1352
            $data[] = [
1353
                'value' => $record->ID,
1354
                'label' => $label,
1355
            ];
1356
        }
1357
1358
        $json = json_encode($data);
1359
        $response = new HTTPResponse($json);
1360
        $response->addHeader('Content-Type', 'application/script');
1361
        return $response;
1362
    }
1363
1364
    /**
1365
     * @link http://www.tabulator.info/docs/5.5/page#remote-response
1366
     * @param HTTPRequest $request
1367
     * @return HTTPResponse
1368
     */
1369
    public function load(HTTPRequest $request)
1370
    {
1371
        if ($this->isDisabled() || $this->isReadonly()) {
1372
            return $this->httpError(403);
1373
        }
1374
        $SecurityID = $request->getVar('SecurityID');
1375
        if (!SecurityToken::inst()->check($SecurityID)) {
1376
            return $this->httpError(404, "Invalid SecurityID");
1377
        }
1378
1379
        $page = (int) $request->getVar('page');
1380
        $limit = (int) $request->getVar('size');
1381
1382
        $sort = $request->getVar('sort');
1383
        $filter = $request->getVar('filter');
1384
1385
        // Persist state to allow the ItemEditForm to display navigation
1386
        $state = [
1387
            'page' => $page,
1388
            'limit' => $limit,
1389
            'sort' => $sort,
1390
            'filter' => $filter,
1391
        ];
1392
        $this->setState($request, $state);
1393
1394
        $offset = ($page - 1) * $limit;
1395
        $data = $this->getManipulatedData($limit, $offset, $sort, $filter);
1396
        $data['state'] = $state;
1397
1398
        $encodedData = json_encode($data);
1399
        if (!$encodedData) {
1400
            throw new Exception(json_last_error_msg());
1401
        }
1402
1403
        $response = new HTTPResponse($encodedData);
1404
        $response->addHeader('Content-Type', 'application/json');
1405
        return $response;
1406
    }
1407
1408
    /**
1409
     * @param HTTPRequest $request
1410
     * @return DataObject|null
1411
     */
1412
    protected function getRecordFromRequest(HTTPRequest $request): ?DataObject
1413
    {
1414
        /** @var DataObject $record */
1415
        if (is_numeric($request->param('ID'))) {
1416
            /** @var Filterable $dataList */
1417
            $dataList = $this->getList();
1418
            $record = $dataList->byID($request->param('ID'));
1419
        } else {
1420
            $record = Injector::inst()->create($this->getModelClass());
1421
        }
1422
        return $record;
1423
    }
1424
1425
    /**
1426
     * @param HTTPRequest $request
1427
     * @return AbstractTabulatorTool|null
1428
     */
1429
    protected function getToolFromRequest(HTTPRequest $request): ?AbstractTabulatorTool
1430
    {
1431
        $toolID = $request->param('ID');
1432
        $tool = $this->getTool($toolID);
1433
        return $tool;
1434
    }
1435
1436
    /**
1437
     * @param HTTPRequest $request
1438
     * @return AbstractBulkAction|null
1439
     */
1440
    protected function getBulkActionFromRequest(HTTPRequest $request): ?AbstractBulkAction
1441
    {
1442
        $toolID = $request->param('ID');
1443
        $tool = $this->getBulkAction($toolID);
1444
        return $tool;
1445
    }
1446
1447
    /**
1448
     * Get the value of a named field  on the given record.
1449
     *
1450
     * Use of this method ensures that any special rules around the data for this gridfield are
1451
     * followed.
1452
     *
1453
     * @param DataObject $record
1454
     * @param string $fieldName
1455
     *
1456
     * @return mixed
1457
     */
1458
    public function getDataFieldValue($record, $fieldName)
1459
    {
1460
        if ($record->hasMethod('relField')) {
1461
            return $record->relField($fieldName);
1462
        }
1463
1464
        if ($record->hasMethod($fieldName)) {
1465
            return $record->$fieldName();
1466
        }
1467
1468
        return $record->$fieldName;
1469
    }
1470
1471
    public function getManipulatedList(): SS_List
1472
    {
1473
        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...
1474
    }
1475
1476
    public function getList(): SS_List
1477
    {
1478
        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...
1479
    }
1480
1481
    public function setList(SS_List $list): self
1482
    {
1483
        if ($this->autoloadDataList && $list instanceof DataList) {
1484
            $this->wizardRemotePagination();
1485
        }
1486
        $this->list = $list;
1487
        return $this;
1488
    }
1489
1490
    public function hasArrayList(): bool
1491
    {
1492
        return $this->list instanceof ArrayList;
1493
    }
1494
1495
    public function getArrayList(): ArrayList
1496
    {
1497
        if (!$this->list instanceof ArrayList) {
1498
            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

1498
            throw new RuntimeException("Value is not a ArrayList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1499
        }
1500
        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...
1501
    }
1502
1503
    public function hasDataList(): bool
1504
    {
1505
        return $this->list instanceof DataList;
1506
    }
1507
1508
    /**
1509
     * A properly typed on which you can call byID
1510
     * @return ArrayList|DataList
1511
     */
1512
    public function getByIDList()
1513
    {
1514
        return $this->list;
1515
    }
1516
1517
    public function hasByIDList(): bool
1518
    {
1519
        return $this->hasDataList() || $this->hasArrayList();
1520
    }
1521
1522
    public function getDataList(): DataList
1523
    {
1524
        if (!$this->list instanceof DataList) {
1525
            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

1525
            throw new RuntimeException("Value is not a DataList, it is a: " . get_class(/** @scrutinizer ignore-type */ $this->list));
Loading history...
1526
        }
1527
        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...
1528
    }
1529
1530
    public function getManipulatedData(int $limit, int $offset, array $sort = null, array $filter = null): array
1531
    {
1532
        if (!$this->hasDataList()) {
1533
            $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

1533
            /** @scrutinizer ignore-call */ 
1534
            $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...
1534
1535
            $lastRow = $this->list->count();
1536
            $lastPage = ceil($lastRow / $limit);
1537
1538
            $result = [
1539
                'last_row' => $lastRow,
1540
                'last_page' => $lastPage,
1541
                'data' => $data,
1542
            ];
1543
1544
            return $result;
1545
        }
1546
1547
        $dataList = $this->getDataList();
1548
1549
        $schema = DataObject::getSchema();
1550
        $dataClass = $dataList->dataClass();
1551
1552
        /** @var DataObject $singleton */
1553
        $singleton = singleton($dataClass);
1554
        $opts = $this->getTabulatorOptions($singleton);
1555
        $resolutionMap = [];
1556
1557
        $sortSql = [];
1558
        if ($sort) {
1559
            foreach ($sort as $sortValues) {
1560
                $cols = array_keys($this->columns);
1561
                $field = $sortValues['field'];
1562
                if (!in_array($field, $cols)) {
1563
                    throw new Exception("Invalid sort field: $field");
1564
                }
1565
                $dir = $sortValues['dir'];
1566
                if (!in_array($dir, ['asc', 'desc'])) {
1567
                    throw new Exception("Invalid sort dir: $dir");
1568
                }
1569
1570
                // Nested sort
1571
                if (strpos($field, '.') !== false) {
1572
                    $parts = explode(".", $field);
1573
1574
                    // Resolve relation only once in case of multiples similar keys
1575
                    if (!isset($resolutionMap[$parts[0]])) {
1576
                        $resolutionMap[$parts[0]] = $singleton->relObject($parts[0]);
1577
                    }
1578
                    // Not matching anything (maybe a formatting .Nice ?)
1579
                    if (!$resolutionMap[$parts[0]] || !($resolutionMap[$parts[0]] instanceof DataList)) {
1580
                        $field = $parts[0];
0 ignored issues
show
Unused Code introduced by
The assignment to $field is dead and can be removed.
Loading history...
1581
                        continue;
1582
                    }
1583
                    $relatedObject = get_class($resolutionMap[$parts[0]]);
1584
                    $tableName = $schema->tableForField($relatedObject, $parts[1]);
1585
                    $baseIDColumn = $schema->sqlColumnForField($dataClass, 'ID');
1586
                    $tableAlias = $parts[0];
1587
                    $dataList = $dataList->leftJoin($tableName, "\"{$tableAlias}\".\"ID\" = {$baseIDColumn}", $tableAlias);
1588
                }
1589
1590
                $sortSql[] = $field . ' ' . $dir;
1591
            }
1592
        } else {
1593
            // If we have a sort column
1594
            if (isset($this->columns[self::UI_SORT])) {
1595
                $sortSql[] = $this->columns[self::UI_SORT]['field'] . ' ASC';
1596
            }
1597
        }
1598
        if (!empty($sortSql)) {
1599
            $dataList = $dataList->sort(implode(", ", $sortSql));
1600
        }
1601
1602
        // Filtering is an array of field/type/value arrays
1603
        $where = [];
1604
        $anyWhere = [];
1605
        if ($filter) {
1606
            $searchAliases = $opts['searchAliases'] ?? [];
1607
            $searchAliases = array_flip($searchAliases);
1608
            foreach ($filter as $filterValues) {
1609
                $cols = array_keys($this->columns);
1610
                $field = $filterValues['field'];
1611
                if (strpos($field, '__') !== 0 && !in_array($field, $cols)) {
1612
                    throw new Exception("Invalid filter field: $field");
1613
                }
1614
                $field = $searchAliases[$field] ?? $field;
1615
                $value = $filterValues['value'];
1616
                $type = $filterValues['type'];
1617
1618
                $rawValue = $value;
1619
1620
                // Strict value
1621
                if ($value === "true") {
1622
                    $value = true;
1623
                } elseif ($value === "false") {
1624
                    $value = false;
1625
                }
1626
1627
                switch ($type) {
1628
                    case "=":
1629
                        if ($field === "__wildcard") {
1630
                            // It's a wildcard search
1631
                            $anyWhere = $this->createWildcardFilters($rawValue);
1632
                        } elseif ($field === "__quickfilter") {
1633
                            // It's a quickfilter search
1634
                            $this->createQuickFilter($rawValue, $dataList);
1635
                        } else {
1636
                            $where["$field"] = $value;
1637
                        }
1638
                        break;
1639
                    case "!=":
1640
                        $where["$field:not"] = $value;
1641
                        break;
1642
                    case "like":
1643
                        $where["$field:PartialMatch:nocase"] = $value;
1644
                        break;
1645
                    case "keywords":
1646
                        $where["$field:PartialMatch:nocase"] = str_replace(" ", "%", $value);
1647
                        break;
1648
                    case "starts":
1649
                        $where["$field:StartsWith:nocase"] = $value;
1650
                        break;
1651
                    case "ends":
1652
                        $where["$field:EndsWith:nocase"] = $value;
1653
                        break;
1654
                    case "<":
1655
                        $where["$field:LessThan:nocase"] = $value;
1656
                        break;
1657
                    case "<=":
1658
                        $where["$field:LessThanOrEqual:nocase"] = $value;
1659
                        break;
1660
                    case ">":
1661
                        $where["$field:GreaterThan:nocase"] = $value;
1662
                        break;
1663
                    case ">=":
1664
                        $where["$field:GreaterThanOrEqual:nocase"] = $value;
1665
                        break;
1666
                    case "in":
1667
                        $where["$field"] = $value;
1668
                        break;
1669
                    case "regex":
1670
                        $dataList = $dataList->where('REGEXP ' . Convert::raw2sql($value));
1671
                        break;
1672
                    default:
1673
                        throw new Exception("Invalid sort dir: $dir");
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $dir does not seem to be defined for all execution paths leading up to this point.
Loading history...
1674
                }
1675
            }
1676
        }
1677
        if (!empty($where)) {
1678
            $dataList = $dataList->filter($where);
1679
        }
1680
        if (!empty($anyWhere)) {
1681
            $dataList = $dataList->filterAny($anyWhere);
1682
        }
1683
1684
        $lastRow = $dataList->count();
1685
        $lastPage = ceil($lastRow / $limit);
1686
1687
        $data = [];
1688
        /** @var DataObject $record */
1689
        foreach ($dataList->limit($limit, $offset) as $record) {
1690
            if ($record->hasMethod('canView') && !$record->canView()) {
1691
                continue;
1692
            }
1693
1694
            $item = [
1695
                'ID' => $record->ID,
1696
            ];
1697
1698
            // Add row class
1699
            if ($record->hasMethod('TabulatorRowClass')) {
1700
                $item['_class'] = $record->TabulatorRowClass();
1701
            } elseif ($record->hasMethod('getRowClass')) {
1702
                $item['_class'] = $record->getRowClass();
1703
            }
1704
            // Add row color
1705
            if ($record->hasMethod('TabulatorRowColor')) {
1706
                $item['_color'] = $record->TabulatorRowColor();
1707
            }
1708
1709
            $nested = [];
1710
            foreach ($this->columns as $col) {
1711
                // UI field are skipped
1712
                if (empty($col['field'])) {
1713
                    continue;
1714
                }
1715
1716
                $field = $col['field'];
1717
1718
                // Explode relations or formatters
1719
                if (strpos($field, '.') !== false) {
1720
                    $parts = explode('.', $field);
1721
                    $classOrField = $parts[0];
1722
                    $relationOrMethod = $parts[1];
1723
                    // For relations, like Users.count
1724
                    if ($singleton->getRelationClass($classOrField)) {
1725
                        $nested[$classOrField][] = $relationOrMethod;
1726
                        continue;
1727
                    } else {
1728
                        // For fields, like SomeValue.Nice
1729
                        $dbObject = $record->dbObject($classOrField);
1730
                        if ($dbObject) {
1731
                            $item[$classOrField] = [
1732
                                $relationOrMethod => $dbObject->$relationOrMethod()
1733
                            ];
1734
                            continue;
1735
                        }
1736
                    }
1737
                }
1738
1739
                // Do not override already set fields
1740
                if (!isset($item[$field])) {
1741
                    $getField = 'get' . ucfirst($field);
1742
1743
                    if ($record->hasMethod($getField)) {
1744
                        // Prioritize getXyz method
1745
                        $item[$field] = $record->$getField();
1746
                    } elseif ($record->hasMethod($field)) {
1747
                        // Regular xyz method method
1748
                        $item[$field] = $record->$field();
1749
                    } else {
1750
                        // Field
1751
                        $item[$field] = $record->getField($field);
1752
                    }
1753
                }
1754
            }
1755
            // Fill in nested data, like Users.count
1756
            foreach ($nested as $nestedClass => $nestedColumns) {
1757
                /** @var DataObject $relObject */
1758
                $relObject = $record->relObject($nestedClass);
1759
                $nestedData = [];
1760
                foreach ($nestedColumns as $nestedColumn) {
1761
                    $nestedData[$nestedColumn] = $this->getDataFieldValue($relObject, $nestedColumn);
1762
                }
1763
                $item[$nestedClass] = $nestedData;
1764
            }
1765
            $data[] = $item;
1766
        }
1767
1768
        $result = [
1769
            'last_row' => $lastRow,
1770
            'last_page' => $lastPage,
1771
            'data' => $data,
1772
        ];
1773
1774
        if (Director::isDev()) {
1775
            $result['sql'] = $dataList->sql();
1776
        }
1777
1778
        return $result;
1779
    }
1780
1781
    public function QuickFiltersList()
1782
    {
1783
        $current = $this->StateValue('filter', '__quickfilter');
1784
        $list = new ArrayList();
1785
        foreach ($this->quickFilters as $k => $v) {
1786
            $list->push([
1787
                'Value' => $k,
1788
                'Label' => $v['label'],
1789
                'Selected' => $k == $current
1790
            ]);
1791
        }
1792
        return $list;
1793
    }
1794
1795
    protected function createQuickFilter($filter, &$list)
1796
    {
1797
        $qf = $this->quickFilters[$filter] ?? null;
1798
        if (!$qf) {
1799
            return;
1800
        }
1801
1802
        $callback = $qf['callback'] ?? null;
1803
        if (!$callback) {
1804
            return;
1805
        }
1806
1807
        $callback($list);
1808
    }
1809
1810
    protected function createWildcardFilters(string $value)
1811
    {
1812
        $wildcardFields = $this->wildcardFields;
1813
1814
        // Create from model
1815
        if (empty($wildcardFields)) {
1816
            /** @var DataObject $singl */
1817
            $singl = singleton($this->modelClass);
1818
            $searchableFields = $singl->searchableFields();
1819
1820
            foreach ($searchableFields as $k => $v) {
1821
                $general = $v['general'] ?? true;
1822
                if (!$general) {
1823
                    continue;
1824
                }
1825
                $wildcardFields[] = $k;
1826
            }
1827
        }
1828
1829
        // Queries can have the format s:... or e:... or =:.... or %:....
1830
        $filter = $this->defaultFilter;
1831
        if (strpos($value, ':') === 1) {
1832
            $parts = explode(":", $value);
1833
            $shortcut = array_shift($parts);
1834
            $value = implode(":", $parts);
1835
            switch ($shortcut) {
1836
                case 's':
1837
                    $filter = 'StartsWith';
1838
                    break;
1839
                case 'e':
1840
                    $filter = 'EndsWith';
1841
                    break;
1842
                case '=':
1843
                    $filter = 'ExactMatch';
1844
                    break;
1845
                case '%':
1846
                    $filter = 'PartialMatch';
1847
                    break;
1848
            }
1849
        }
1850
1851
        // Process value
1852
        $baseValue = $value;
1853
        $value = str_replace(" ", "%", $value);
1854
        $value = str_replace(['.', '_', '-'], ' ', $value);
1855
1856
        // Create filters
1857
        $anyWhere = [];
1858
        foreach ($wildcardFields as $f) {
1859
            if (!$value) {
1860
                continue;
1861
            }
1862
            $key = $f . ":" . $filter;
1863
            $anyWhere[$key] = $value;
1864
1865
            // also look on unfiltered data
1866
            if ($value != $baseValue) {
1867
                $anyWhere[$key] = $baseValue;
1868
            }
1869
        }
1870
1871
        return $anyWhere;
1872
    }
1873
1874
    public function getModelClass(): ?string
1875
    {
1876
        if ($this->modelClass) {
1877
            return $this->modelClass;
1878
        }
1879
        if ($this->list && $this->list instanceof DataList) {
1880
            return $this->list->dataClass();
1881
        }
1882
        return null;
1883
    }
1884
1885
    public function setModelClass(string $modelClass): self
1886
    {
1887
        $this->modelClass = $modelClass;
1888
        return $this;
1889
    }
1890
1891
1892
    public function getDataAttribute(string $k)
1893
    {
1894
        if (isset($this->dataAttributes[$k])) {
1895
            return $this->dataAttributes[$k];
1896
        }
1897
        return $this->getAttribute("data-$k");
1898
    }
1899
1900
    public function setDataAttribute(string $k, $v): self
1901
    {
1902
        $this->dataAttributes[$k] = $v;
1903
        return $this;
1904
    }
1905
1906
    public function dataAttributesHTML(): string
1907
    {
1908
        $parts = [];
1909
        foreach ($this->dataAttributes as $k => $v) {
1910
            if (!$v) {
1911
                continue;
1912
            }
1913
            if (is_array($v)) {
1914
                $v = json_encode($v);
1915
            }
1916
            $parts[] = "data-$k='$v'";
1917
        }
1918
        return implode(" ", $parts);
1919
    }
1920
1921
    protected function processLink(string $url): string
1922
    {
1923
        // It's not necessary to process
1924
        if ($url == '#') {
1925
            return $url;
1926
        }
1927
        // It's a temporary link on the form
1928
        if (strpos($url, 'form:') === 0) {
1929
            return $this->Link(preg_replace('/^form:/', '', $url));
1930
        }
1931
        // It's a temporary link on the controller
1932
        if (strpos($url, 'controller:') === 0) {
1933
            return $this->ControllerLink(preg_replace('/^controller:/', '', $url));
1934
        }
1935
        // It's a custom protocol (mailto: etc)
1936
        if (strpos($url, ':') !== false) {
1937
            return $url;
1938
        }
1939
        return $url;
1940
    }
1941
1942
    protected function processLinks(): void
1943
    {
1944
        // Process editor and formatter links
1945
        foreach ($this->columns as $name => $params) {
1946
            if (!empty($params['formatterParams']['url'])) {
1947
                $url = $this->processLink($params['formatterParams']['url']);
1948
                $this->columns[$name]['formatterParams']['url'] = $url;
1949
            }
1950
            if (!empty($params['editorParams']['url'])) {
1951
                $url = $this->processLink($params['editorParams']['url']);
1952
                $this->columns[$name]['editorParams']['url'] = $url;
1953
            }
1954
            // Set valuesURL automatically if not already set
1955
            if (!empty($params['editorParams']['autocomplete'])) {
1956
                if (empty($params['editorParams']['valuesURL'])) {
1957
                    $params = [
1958
                        'Column' => $name,
1959
                        'SecurityID' => SecurityToken::getSecurityID(),
1960
                    ];
1961
                    $url = $this->Link('autocomplete') . '?' . http_build_query($params);
1962
                    $this->columns[$name]['editorParams']['valuesURL'] = $url;
1963
                    $this->columns[$name]['editorParams']['filterRemote'] = true;
1964
                }
1965
            }
1966
        }
1967
1968
        // Other links
1969
        $url = $this->getOption('ajaxURL');
1970
        if ($url) {
1971
            $this->setOption('ajaxURL', $this->processLink($url));
1972
        }
1973
    }
1974
1975
    /**
1976
     * @link https://github.com/lekoala/formidable-elements/blob/master/src/classes/tabulator/Format/formatters/button.js
1977
     */
1978
    public function makeButton(string $urlOrAction, string $icon, string $title): array
1979
    {
1980
        $opts = [
1981
            "responsive" => 0,
1982
            "cssClass" => 'tabulator-cell-btn',
1983
            "tooltip" => $title,
1984
            "formatter" => "button",
1985
            "formatterParams" => [
1986
                "icon" => $icon,
1987
                "title" => $title,
1988
                "url" => $this->TempLink($urlOrAction), // On the controller by default
1989
            ],
1990
            "cellClick" => ["__fn" => "SSTabulator.buttonHandler"],
1991
            // We need to force its size otherwise Tabulator will assign too much space
1992
            "width" => 36 + strlen($title) * 12,
1993
            "hozAlign" => "center",
1994
            "headerSort" => false,
1995
        ];
1996
        return $opts;
1997
    }
1998
1999
    public function addButtonFromArray(string $action, array $opts = [], string $before = null): self
2000
    {
2001
        // Insert before given column
2002
        if ($before) {
2003
            $this->addColumnBefore("action_$action", $opts, $before);
2004
        } else {
2005
            $this->columns["action_$action"] = $opts;
2006
        }
2007
        return $this;
2008
    }
2009
2010
    /**
2011
     * @param string $action Action name
2012
     * @param string $url Parameters between {} will be interpolated by row values.
2013
     * @param string $icon
2014
     * @param string $title
2015
     * @param string|null $before
2016
     * @return self
2017
     */
2018
    public function addButton(string $action, string $url, string $icon, string $title, string $before = null): self
2019
    {
2020
        $opts = $this->makeButton($url, $icon, $title);
2021
        $this->addButtonFromArray($action, $opts, $before);
2022
        return $this;
2023
    }
2024
2025
    public function shiftButton(string $action, string $url, string $icon, string $title): self
2026
    {
2027
        // Find first action
2028
        foreach ($this->columns as $name => $options) {
2029
            if (strpos($name, 'action_') === 0) {
2030
                return $this->addButton($action, $url, $icon, $title, $name);
2031
            }
2032
        }
2033
        return $this->addButton($action, $url, $icon, $title);
2034
    }
2035
2036
    public function getActions(): array
2037
    {
2038
        $cols = [];
2039
        foreach ($this->columns as $name => $options) {
2040
            if (strpos($name, 'action_') === 0) {
2041
                $cols[$name] = $options;
2042
            }
2043
        }
2044
        return $cols;
2045
    }
2046
2047
    public function getUiColumns(): array
2048
    {
2049
        $cols = [];
2050
        foreach ($this->columns as $name => $options) {
2051
            if (strpos($name, 'ui_') === 0) {
2052
                $cols[$name] = $options;
2053
            }
2054
        }
2055
        return $cols;
2056
    }
2057
2058
    public function getSystemColumns(): array
2059
    {
2060
        return array_merge($this->getActions(), $this->getUiColumns());
2061
    }
2062
2063
    public function removeButton(string $action): self
2064
    {
2065
        if ($this->hasButton($action)) {
2066
            unset($this->columns["action_$action"]);
2067
        }
2068
        return $this;
2069
    }
2070
2071
    public function hasButton(string $action): bool
2072
    {
2073
        return isset($this->columns["action_$action"]);
2074
    }
2075
2076
    /**
2077
     * @link http://www.tabulator.info/docs/5.5/columns#definition
2078
     * @param string $field (Required) this is the key for this column in the data array
2079
     * @param string $title (Required) This is the title that will be displayed in the header for this column
2080
     * @param array $opts Other options to merge in
2081
     * @return $this
2082
     */
2083
    public function addColumn(string $field, string $title = null, array $opts = []): self
2084
    {
2085
        if ($title === null) {
2086
            $title = $field;
2087
        }
2088
2089
        $baseOpts = [
2090
            "field" => $field,
2091
            "title" => $title,
2092
        ];
2093
2094
        if (!empty($opts)) {
2095
            $baseOpts = array_merge($baseOpts, $opts);
2096
        }
2097
2098
        $this->columns[$field] = $baseOpts;
2099
        return $this;
2100
    }
2101
2102
    /**
2103
     * @link http://www.tabulator.info/docs/5.5/columns#definition
2104
     * @param array $opts Other options to merge in
2105
     * @param ?string $before
2106
     * @return $this
2107
     */
2108
    public function addColumnFromArray(array $opts = [], $before = null)
2109
    {
2110
        if (empty($opts['field']) || !isset($opts['title'])) {
2111
            throw new Exception("Missing field or title key");
2112
        }
2113
        $field = $opts['field'];
2114
2115
        if ($before) {
2116
            $this->addColumnBefore($field, $opts, $before);
2117
        } else {
2118
            $this->columns[$field] = $opts;
2119
        }
2120
2121
        return $this;
2122
    }
2123
2124
    protected function addColumnBefore($field, $opts, $before)
2125
    {
2126
        if (array_key_exists($before, $this->columns)) {
2127
            $new = [];
2128
            foreach ($this->columns as $k => $value) {
2129
                if ($k === $before) {
2130
                    $new[$field] = $opts;
2131
                }
2132
                $new[$k] = $value;
2133
            }
2134
            $this->columns = $new;
2135
        }
2136
    }
2137
2138
    public function makeColumnEditable(string $field, string $editor = "input", array $params = [])
2139
    {
2140
        $col = $this->getColumn($field);
2141
        if (!$col) {
2142
            throw new InvalidArgumentException("$field is not a valid column");
2143
        }
2144
2145
        switch ($editor) {
2146
            case 'date':
2147
                $editor = "input";
2148
                $params = [
2149
                    'mask' => "9999-99-99",
2150
                    'maskAutoFill' => 'true',
2151
                ];
2152
                break;
2153
            case 'datetime':
2154
                $editor = "input";
2155
                $params = [
2156
                    'mask' => "9999-99-99 99:99:99",
2157
                    'maskAutoFill' => 'true',
2158
                ];
2159
                break;
2160
        }
2161
2162
        if (empty($col['cssClass'])) {
2163
            $col['cssClass'] = 'no-change-track';
2164
        } else {
2165
            $col['cssClass'] .= ' no-change-track';
2166
        }
2167
2168
        $col['editor'] = $editor;
2169
        $col['editorParams'] = $params;
2170
        if ($editor == "list") {
2171
            if (!empty($params['autocomplete'])) {
2172
                $col['headerFilter'] = "input"; // force input
2173
            } else {
2174
                $col['headerFilterParams'] = $params; // editor is used as base filter editor
2175
            }
2176
        }
2177
2178
2179
        $this->setColumn($field, $col);
2180
    }
2181
2182
    /**
2183
     * Get column details
2184
2185
     * @param string $key
2186
     */
2187
    public function getColumn(string $key): ?array
2188
    {
2189
        if (isset($this->columns[$key])) {
2190
            return $this->columns[$key];
2191
        }
2192
        return null;
2193
    }
2194
2195
    /**
2196
     * Set column details
2197
     *
2198
     * @param string $key
2199
     * @param array $col
2200
     */
2201
    public function setColumn(string $key, array $col): self
2202
    {
2203
        $this->columns[$key] = $col;
2204
        return $this;
2205
    }
2206
2207
    /**
2208
     * Update column details
2209
     *
2210
     * @param string $key
2211
     * @param array $col
2212
     */
2213
    public function updateColumn(string $key, array $col): self
2214
    {
2215
        $data = $this->getColumn($key);
2216
        if ($data) {
2217
            $this->setColumn($key, array_merge($data, $col));
2218
        }
2219
        return $this;
2220
    }
2221
2222
    /**
2223
     * Remove a column
2224
     *
2225
     * @param string $key
2226
     */
2227
    public function removeColumn(string $key): void
2228
    {
2229
        unset($this->columns[$key]);
2230
    }
2231
2232
2233
    /**
2234
     * Get the value of columns
2235
     */
2236
    public function getColumns(): array
2237
    {
2238
        return $this->columns;
2239
    }
2240
2241
    /**
2242
     * Set the value of columns
2243
     */
2244
    public function setColumns(array $columns): self
2245
    {
2246
        $this->columns = $columns;
2247
        return $this;
2248
    }
2249
2250
    public function clearColumns(bool $keepSystem = true): void
2251
    {
2252
        $sysNames = array_keys($this->getSystemColumns());
2253
        foreach ($this->columns as $k => $v) {
2254
            if ($keepSystem && in_array($k, $sysNames)) {
2255
                continue;
2256
            }
2257
            $this->removeColumn($k);
2258
        }
2259
    }
2260
2261
    /**
2262
     * This should be the rough equivalent to GridFieldDataColumns::getDisplayFields
2263
     */
2264
    public function getDisplayFields(): array
2265
    {
2266
        $fields = [];
2267
        foreach ($this->columns as $col) {
2268
            if (empty($col['field'])) {
2269
                continue;
2270
            }
2271
            $fields[$col['field']] = $col['title'];
2272
        }
2273
        return $fields;
2274
    }
2275
2276
    /**
2277
     * This should be the rough equivalent to GridFieldDataColumns::setDisplayFields
2278
     */
2279
    public function setDisplayFields(array $arr): void
2280
    {
2281
        $this->clearColumns();
2282
        $actions = array_keys($this->getActions());
2283
        $before = $actions[0] ?? null;
2284
        foreach ($arr as $k => $v) {
2285
            $this->addColumnFromArray([
2286
                'field' => $k,
2287
                'title' => $v,
2288
            ], $before);
2289
        }
2290
    }
2291
2292
    /**
2293
     * Convenience method that get/set fields
2294
     */
2295
    public function addDisplayFields(array $arr): void
2296
    {
2297
        $fields = $this->getDisplayFields();
2298
        $fields = array_merge($fields, $arr);
2299
        $this->setDisplayFields($fields);
2300
    }
2301
2302
    /**
2303
     * @param string|AbstractTabulatorTool $tool Pass name or class
2304
     * @return AbstractTabulatorTool|null
2305
     */
2306
    public function getTool($tool): ?AbstractTabulatorTool
2307
    {
2308
        if (is_object($tool)) {
2309
            $tool = get_class($tool);
2310
        }
2311
        if (!is_string($tool)) {
0 ignored issues
show
introduced by
The condition is_string($tool) is always true.
Loading history...
2312
            throw new InvalidArgumentException('Tool must be an object or a class name');
2313
        }
2314
        foreach ($this->tools as $t) {
2315
            if ($t['name'] === $tool) {
2316
                return $t['tool'];
2317
            }
2318
            if ($t['tool'] instanceof $tool) {
2319
                return $t['tool'];
2320
            }
2321
        }
2322
        return null;
2323
    }
2324
2325
    /**
2326
     * @param string $pos start|end
2327
     * @param AbstractTabulatorTool $tool
2328
     * @param string $name
2329
     * @return self
2330
     */
2331
    public function addTool(string $pos, AbstractTabulatorTool $tool, string $name = ''): self
2332
    {
2333
        $tool->setTabulatorGrid($this);
2334
        if ($tool->getName() && !$name) {
2335
            $name = $tool->getName();
2336
        }
2337
        $tool->setName($name);
2338
2339
        $this->tools[] = [
2340
            'position' => $pos,
2341
            'tool' => $tool,
2342
            'name' => $name,
2343
        ];
2344
        return $this;
2345
    }
2346
2347
    public function addToolStart(AbstractTabulatorTool $tool, string $name = ''): self
2348
    {
2349
        return $this->addTool(self::POS_START, $tool, $name);
2350
    }
2351
2352
    public function addToolEnd(AbstractTabulatorTool $tool, string $name = ''): self
2353
    {
2354
        return $this->addTool(self::POS_END, $tool, $name);
2355
    }
2356
2357
    public function removeTool($toolName): self
2358
    {
2359
        if (is_object($toolName)) {
2360
            $toolName = get_class($toolName);
2361
        }
2362
        if (!is_string($toolName)) {
2363
            throw new InvalidArgumentException('Tool must be an object or a class name');
2364
        }
2365
        foreach ($this->tools as $idx => $tool) {
2366
            if ($tool['name'] === $toolName) {
2367
                unset($this->tools[$idx]);
2368
            }
2369
            if (class_exists($toolName) && $tool['tool'] instanceof $toolName) {
2370
                unset($this->tools[$idx]);
2371
            }
2372
        }
2373
        return $this;
2374
    }
2375
2376
    /**
2377
     * @param string|AbstractBulkAction $bulkAction Pass name or class
2378
     * @return AbstractBulkAction|null
2379
     */
2380
    public function getBulkAction($bulkAction): ?AbstractBulkAction
2381
    {
2382
        if (is_object($bulkAction)) {
2383
            $bulkAction = get_class($bulkAction);
2384
        }
2385
        if (!is_string($bulkAction)) {
0 ignored issues
show
introduced by
The condition is_string($bulkAction) is always true.
Loading history...
2386
            throw new InvalidArgumentException('BulkAction must be an object or a class name');
2387
        }
2388
        foreach ($this->bulkActions as $ba) {
2389
            if ($ba->getName() == $bulkAction) {
2390
                return $ba;
2391
            }
2392
            if ($ba instanceof $bulkAction) {
2393
                return $ba;
2394
            }
2395
        }
2396
        return null;
2397
    }
2398
2399
    public function getBulkActions(): array
2400
    {
2401
        return $this->bulkActions;
2402
    }
2403
2404
    /**
2405
     * @param AbstractBulkAction[] $bulkActions
2406
     * @return self
2407
     */
2408
    public function setBulkActions(array $bulkActions): self
2409
    {
2410
        foreach ($bulkActions as $bulkAction) {
2411
            $bulkAction->setTabulatorGrid($this);
2412
        }
2413
        $this->bulkActions = $bulkActions;
2414
        return $this;
2415
    }
2416
2417
    /**
2418
     * If you didn't before, you probably want to call wizardSelectable
2419
     * to get the actual selection checkbox too
2420
     *
2421
     * @param AbstractBulkAction $handler
2422
     * @return self
2423
     */
2424
    public function addBulkAction(AbstractBulkAction $handler): self
2425
    {
2426
        $handler->setTabulatorGrid($this);
2427
2428
        $this->bulkActions[] = $handler;
2429
        return $this;
2430
    }
2431
2432
    public function removeBulkAction($bulkAction): self
2433
    {
2434
        if (is_object($bulkAction)) {
2435
            $bulkAction = get_class($bulkAction);
2436
        }
2437
        if (!is_string($bulkAction)) {
2438
            throw new InvalidArgumentException('Bulk action must be an object or a class name');
2439
        }
2440
        foreach ($this->bulkActions as $idx => $ba) {
2441
            if ($ba->getName() == $bulkAction) {
2442
                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...
2443
            }
2444
            if ($ba instanceof $bulkAction) {
2445
                unset($this->bulkAction[$idx]);
2446
            }
2447
        }
2448
        return $this;
2449
    }
2450
2451
    public function getColumnDefault(string $opt)
2452
    {
2453
        return $this->columnDefaults[$opt] ?? null;
2454
    }
2455
2456
    public function setColumnDefault(string $opt, $value)
2457
    {
2458
        $this->columnDefaults[$opt] = $value;
2459
    }
2460
2461
    public function getColumnDefaults(): array
2462
    {
2463
        return $this->columnDefaults;
2464
    }
2465
2466
    public function setColumnDefaults(array $columnDefaults): self
2467
    {
2468
        $this->columnDefaults = $columnDefaults;
2469
        return $this;
2470
    }
2471
2472
    public function getListeners(): array
2473
    {
2474
        return $this->listeners;
2475
    }
2476
2477
    public function setListeners(array $listeners): self
2478
    {
2479
        $this->listeners = $listeners;
2480
        return $this;
2481
    }
2482
2483
    public function addListener(string $event, string $functionName): self
2484
    {
2485
        $this->listeners[$event] = $functionName;
2486
        return $this;
2487
    }
2488
2489
    public function removeListener(string $event): self
2490
    {
2491
        if (isset($this->listeners[$event])) {
2492
            unset($this->listeners[$event]);
2493
        }
2494
        return $this;
2495
    }
2496
2497
    public function getLinksOptions(): array
2498
    {
2499
        return $this->linksOptions;
2500
    }
2501
2502
    public function setLinksOptions(array $linksOptions): self
2503
    {
2504
        $this->linksOptions = $linksOptions;
2505
        return $this;
2506
    }
2507
2508
    public function registerLinkOption(string $linksOption): self
2509
    {
2510
        $this->linksOptions[] = $linksOption;
2511
        return $this;
2512
    }
2513
2514
    public function unregisterLinkOption(string $linksOption): self
2515
    {
2516
        $this->linksOptions = array_diff($this->linksOptions, [$linksOption]);
2517
        return $this;
2518
    }
2519
2520
    /**
2521
     * Get the value of pageSize
2522
     */
2523
    public function getPageSize(): int
2524
    {
2525
        return $this->pageSize;
2526
    }
2527
2528
    /**
2529
     * Set the value of pageSize
2530
     *
2531
     * @param int $pageSize
2532
     */
2533
    public function setPageSize(int $pageSize): self
2534
    {
2535
        $this->pageSize = $pageSize;
2536
        return $this;
2537
    }
2538
2539
    /**
2540
     * Get the value of autoloadDataList
2541
     */
2542
    public function getAutoloadDataList(): bool
2543
    {
2544
        return $this->autoloadDataList;
2545
    }
2546
2547
    /**
2548
     * Set the value of autoloadDataList
2549
     *
2550
     * @param bool $autoloadDataList
2551
     */
2552
    public function setAutoloadDataList(bool $autoloadDataList): self
2553
    {
2554
        $this->autoloadDataList = $autoloadDataList;
2555
        return $this;
2556
    }
2557
2558
    /**
2559
     * Set the value of itemRequestClass
2560
     */
2561
    public function setItemRequestClass(string $itemRequestClass): self
2562
    {
2563
        $this->itemRequestClass = $itemRequestClass;
2564
        return $this;
2565
    }
2566
2567
    /**
2568
     * Get the value of lazyInit
2569
     */
2570
    public function getLazyInit(): bool
2571
    {
2572
        return $this->lazyInit;
2573
    }
2574
2575
    /**
2576
     * Set the value of lazyInit
2577
     */
2578
    public function setLazyInit(bool $lazyInit): self
2579
    {
2580
        $this->lazyInit = $lazyInit;
2581
        return $this;
2582
    }
2583
2584
    /**
2585
     * Get the value of rowClickTriggersAction
2586
     */
2587
    public function getRowClickTriggersAction(): bool
2588
    {
2589
        return $this->rowClickTriggersAction;
2590
    }
2591
2592
    /**
2593
     * Set the value of rowClickTriggersAction
2594
     */
2595
    public function setRowClickTriggersAction(bool $rowClickTriggersAction): self
2596
    {
2597
        $this->rowClickTriggersAction = $rowClickTriggersAction;
2598
        return $this;
2599
    }
2600
2601
    /**
2602
     * Get the value of controllerFunction
2603
     */
2604
    public function getControllerFunction(): string
2605
    {
2606
        if (!$this->controllerFunction) {
2607
            return $this->getName() ?? "TabulatorGrid";
2608
        }
2609
        return $this->controllerFunction;
2610
    }
2611
2612
    /**
2613
     * Set the value of controllerFunction
2614
     */
2615
    public function setControllerFunction(string $controllerFunction): self
2616
    {
2617
        $this->controllerFunction = $controllerFunction;
2618
        return $this;
2619
    }
2620
2621
    /**
2622
     * Get the value of editUrl
2623
     */
2624
    public function getEditUrl(): string
2625
    {
2626
        return $this->editUrl;
2627
    }
2628
2629
    /**
2630
     * Set the value of editUrl
2631
     */
2632
    public function setEditUrl(string $editUrl): self
2633
    {
2634
        $this->editUrl = $editUrl;
2635
        return $this;
2636
    }
2637
2638
    /**
2639
     * Get the value of moveUrl
2640
     */
2641
    public function getMoveUrl(): string
2642
    {
2643
        return $this->moveUrl;
2644
    }
2645
2646
    /**
2647
     * Set the value of moveUrl
2648
     */
2649
    public function setMoveUrl(string $moveUrl): self
2650
    {
2651
        $this->moveUrl = $moveUrl;
2652
        return $this;
2653
    }
2654
2655
    /**
2656
     * Get the value of bulkUrl
2657
     */
2658
    public function getBulkUrl(): string
2659
    {
2660
        return $this->bulkUrl;
2661
    }
2662
2663
    /**
2664
     * Set the value of bulkUrl
2665
     */
2666
    public function setBulkUrl(string $bulkUrl): self
2667
    {
2668
        $this->bulkUrl = $bulkUrl;
2669
        return $this;
2670
    }
2671
2672
    /**
2673
     * Get the value of globalSearch
2674
     */
2675
    public function getGlobalSearch(): bool
2676
    {
2677
        return $this->globalSearch;
2678
    }
2679
2680
    /**
2681
     * Set the value of globalSearch
2682
     *
2683
     * @param bool $globalSearch
2684
     */
2685
    public function setGlobalSearch($globalSearch): self
2686
    {
2687
        $this->globalSearch = $globalSearch;
2688
        return $this;
2689
    }
2690
2691
    /**
2692
     * Get the value of wildcardFields
2693
     */
2694
    public function getWildcardFields(): array
2695
    {
2696
        return $this->wildcardFields;
2697
    }
2698
2699
    /**
2700
     * Set the value of wildcardFields
2701
     *
2702
     * @param array $wildcardFields
2703
     */
2704
    public function setWildcardFields($wildcardFields): self
2705
    {
2706
        $this->wildcardFields = $wildcardFields;
2707
        return $this;
2708
    }
2709
2710
    /**
2711
     * Get the value of quickFilters
2712
     */
2713
    public function getQuickFilters(): array
2714
    {
2715
        return $this->quickFilters;
2716
    }
2717
2718
    /**
2719
     * Pass an array with as a key, the name of the filter
2720
     * and as a value, an array with two keys: label and callback
2721
     *
2722
     * For example:
2723
     * 'myquickfilter' => [
2724
     *   'label' => 'My Quick Filter',
2725
     *   'callback' => function (&$list) {
2726
     *     ...
2727
     *   }
2728
     * ]
2729
     *
2730
     * @param array $quickFilters
2731
     */
2732
    public function setQuickFilters($quickFilters): self
2733
    {
2734
        $this->quickFilters = $quickFilters;
2735
        return $this;
2736
    }
2737
2738
    /**
2739
     * Get the value of groupLayout
2740
     */
2741
    public function getGroupLayout(): bool
2742
    {
2743
        return $this->groupLayout;
2744
    }
2745
2746
    /**
2747
     * Set the value of groupLayout
2748
     *
2749
     * @param bool $groupLayout
2750
     */
2751
    public function setGroupLayout($groupLayout): self
2752
    {
2753
        $this->groupLayout = $groupLayout;
2754
        return $this;
2755
    }
2756
2757
    /**
2758
     * Get the value of enableGridManipulation
2759
     */
2760
    public function getEnableGridManipulation(): bool
2761
    {
2762
        return $this->enableGridManipulation;
2763
    }
2764
2765
    /**
2766
     * Set the value of enableGridManipulation
2767
     *
2768
     * @param bool $enableGridManipulation
2769
     */
2770
    public function setEnableGridManipulation($enableGridManipulation): self
2771
    {
2772
        $this->enableGridManipulation = $enableGridManipulation;
2773
        return $this;
2774
    }
2775
2776
    /**
2777
     * Get the value of defaultFilter
2778
     */
2779
    public function getDefaultFilter(): string
2780
    {
2781
        return $this->defaultFilter;
2782
    }
2783
2784
    /**
2785
     * Set the value of defaultFilter
2786
     *
2787
     * @param string $defaultFilter
2788
     */
2789
    public function setDefaultFilter($defaultFilter): self
2790
    {
2791
        $this->defaultFilter = $defaultFilter;
2792
        return $this;
2793
    }
2794
}
2795