Passed
Push — master ( ed64cb...98de9f )
by Thomas
02:50
created

TabulatorGrid::removeColumns()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
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_EDIT = "ui_edit";
46
    const UI_DELETE = "ui_delete";
47
    const UI_VIEW = "ui_view";
48
    const UI_SORT = "ui_sort";
49
50
    const TOOL_ADD_NEW = "add_new";
51
    const TOOL_EXPORT = "export";
52
    const TOOL_ADD_EXISTING = "add_existing";
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//$OtherID' => '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->addEditButton();
521
        } elseif ($singl->canView()) {
522
            $this->addButton(self::UI_VIEW, $itemUrl, "visibility", _t('TabulatorGrid.View', 'View'));
523
        }
524
525
        $showRowDelete = $opts['rowDelete'] ?? self::config()->show_row_delete;
526
        if ($singl->canDelete() && $showRowDelete) {
527
            $deleteBtn = $this->makeButton($this->TempLink('item/{ID}/delete', false), "delete", _t('TabulatorGrid.Delete', 'Delete'));
528
            $deleteBtn["formatterParams"]["classes"] = 'btn btn-danger';
529
            $this->addButtonFromArray(self::UI_DELETE, $deleteBtn);
530
        }
531
532
        // - Tools
533
        $this->tools = [];
534
535
        $addNew = $opts['addNew'] ?? true;
536
        if ($singl->canCreate() && $addNew) {
537
            $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

537
            $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...
538
        }
539
        $export = $opts['export'] ?? true;
540
        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...
541
            $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

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

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

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

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

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

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