1 | <?php |
||||||
2 | |||||||
3 | namespace LeKoala\Tabulator; |
||||||
4 | |||||||
5 | use Exception; |
||||||
6 | use RuntimeException; |
||||||
7 | use SilverStripe\i18n\i18n; |
||||||
8 | use SilverStripe\Forms\Form; |
||||||
9 | use InvalidArgumentException; |
||||||
10 | use LeKoala\Tabulator\BulkActions\BulkDeleteAction; |
||||||
11 | use SilverStripe\ORM\SS_List; |
||||||
12 | use SilverStripe\Core\Convert; |
||||||
13 | use SilverStripe\ORM\DataList; |
||||||
14 | use SilverStripe\ORM\ArrayList; |
||||||
15 | use SilverStripe\Core\ClassInfo; |
||||||
16 | use SilverStripe\ORM\DataObject; |
||||||
17 | use SilverStripe\View\ArrayData; |
||||||
18 | use SilverStripe\Forms\FieldList; |
||||||
19 | use SilverStripe\Forms\FormField; |
||||||
20 | use SilverStripe\Control\Director; |
||||||
21 | use SilverStripe\View\Requirements; |
||||||
22 | use SilverStripe\Control\Controller; |
||||||
23 | use SilverStripe\Control\HTTPRequest; |
||||||
24 | use SilverStripe\Control\HTTPResponse; |
||||||
25 | use SilverStripe\ORM\FieldType\DBEnum; |
||||||
26 | use SilverStripe\Control\RequestHandler; |
||||||
27 | use SilverStripe\Core\Injector\Injector; |
||||||
28 | use SilverStripe\Security\SecurityToken; |
||||||
29 | use SilverStripe\ORM\DataObjectInterface; |
||||||
30 | use SilverStripe\ORM\FieldType\DBBoolean; |
||||||
31 | use SilverStripe\Forms\GridField\GridFieldConfig; |
||||||
32 | use SilverStripe\ORM\Filters\PartialMatchFilter; |
||||||
33 | |||||||
34 | /** |
||||||
35 | * This is a replacement for most GridField usages in SilverStripe |
||||||
36 | * It can easily work in the frontend too |
||||||
37 | * |
||||||
38 | * @link http://www.tabulator.info/ |
||||||
39 | */ |
||||||
40 | class TabulatorGrid extends FormField |
||||||
41 | { |
||||||
42 | const POS_START = 'start'; |
||||||
43 | const POS_END = 'end'; |
||||||
44 | |||||||
45 | const UI_EDIT = "ui_edit"; |
||||||
46 | const UI_DELETE = "ui_delete"; |
||||||
47 | const UI_UNLINK = "ui_unlink"; |
||||||
48 | const UI_VIEW = "ui_view"; |
||||||
49 | const UI_SORT = "ui_sort"; |
||||||
50 | |||||||
51 | const TOOL_ADD_NEW = "add_new"; |
||||||
52 | const TOOL_EXPORT = "export"; // xlsx |
||||||
53 | const TOOL_EXPORT_CSV = "export_csv"; |
||||||
54 | const TOOL_ADD_EXISTING = "add_existing"; |
||||||
55 | |||||||
56 | // @link http://www.tabulator.info/examples/6.2?#fittodata |
||||||
57 | const LAYOUT_FIT_DATA = "fitData"; |
||||||
58 | const LAYOUT_FIT_DATA_FILL = "fitDataFill"; |
||||||
59 | const LAYOUT_FIT_DATA_STRETCH = "fitDataStretch"; |
||||||
60 | const LAYOUT_FIT_DATA_TABLE = "fitDataTable"; |
||||||
61 | const LAYOUT_FIT_COLUMNS = "fitColumns"; |
||||||
62 | |||||||
63 | const RESPONSIVE_LAYOUT_HIDE = "hide"; |
||||||
64 | const RESPONSIVE_LAYOUT_COLLAPSE = "collapse"; |
||||||
65 | |||||||
66 | // @link http://www.tabulator.info/docs/6.2/format |
||||||
67 | const FORMATTER_PLAINTEXT = 'plaintext'; |
||||||
68 | const FORMATTER_TEXTAREA = 'textarea'; |
||||||
69 | const FORMATTER_HTML = 'html'; |
||||||
70 | const FORMATTER_MONEY = 'money'; |
||||||
71 | const FORMATTER_IMAGE = 'image'; |
||||||
72 | const FORMATTER_LINK = 'link'; |
||||||
73 | const FORMATTER_DATETIME = 'datetime'; |
||||||
74 | const FORMATTER_DATETIME_DIFF = 'datetimediff'; |
||||||
75 | const FORMATTER_TICKCROSS = 'tickCross'; |
||||||
76 | const FORMATTER_COLOR = 'color'; |
||||||
77 | const FORMATTER_STAR = 'star'; |
||||||
78 | const FORMATTER_TRAFFIC = 'traffic'; |
||||||
79 | const FORMATTER_PROGRESS = 'progress'; |
||||||
80 | const FORMATTER_LOOKUP = 'lookup'; |
||||||
81 | const FORMATTER_BUTTON_TICK = 'buttonTick'; |
||||||
82 | const FORMATTER_BUTTON_CROSS = 'buttonCross'; |
||||||
83 | const FORMATTER_ROWNUM = 'rownum'; |
||||||
84 | const FORMATTER_HANDLE = 'handle'; |
||||||
85 | // @link http://www.tabulator.info/docs/6.2/format#format-module |
||||||
86 | const FORMATTER_ROW_SELECTION = 'rowSelection'; |
||||||
87 | const FORMATTER_RESPONSIVE_COLLAPSE = 'responsiveCollapse'; |
||||||
88 | |||||||
89 | // our built in functions |
||||||
90 | const JS_BOOL_GROUP_HEADER = 'SSTabulator.boolGroupHeader'; |
||||||
91 | const JS_DATA_AJAX_RESPONSE = 'SSTabulator.dataAjaxResponse'; |
||||||
92 | const JS_INIT_CALLBACK = 'SSTabulator.initCallback'; |
||||||
93 | const JS_CONFIG_CALLBACK = 'SSTabulator.configCallback'; |
||||||
94 | |||||||
95 | /** |
||||||
96 | * @config |
||||||
97 | */ |
||||||
98 | private static array $allowed_actions = [ |
||||||
99 | 'load', |
||||||
100 | 'handleItem', |
||||||
101 | 'handleTool', |
||||||
102 | 'configProvider', |
||||||
103 | 'autocomplete', |
||||||
104 | 'handleBulkAction', |
||||||
105 | ]; |
||||||
106 | |||||||
107 | private static $url_handlers = [ |
||||||
108 | 'item/$ID' => 'handleItem', |
||||||
109 | 'tool/$ID//$OtherID' => 'handleTool', |
||||||
110 | 'bulkAction/$ID' => 'handleBulkAction', |
||||||
111 | ]; |
||||||
112 | |||||||
113 | private static array $casting = [ |
||||||
114 | 'JsonOptions' => 'HTMLFragment', |
||||||
115 | 'ShowTools' => 'HTMLFragment', |
||||||
116 | 'dataAttributesHTML' => 'HTMLFragment', |
||||||
117 | ]; |
||||||
118 | |||||||
119 | /** |
||||||
120 | * @config |
||||||
121 | */ |
||||||
122 | private static bool $load_styles = true; |
||||||
123 | |||||||
124 | /** |
||||||
125 | * @config |
||||||
126 | */ |
||||||
127 | private static string $luxon_version = '3'; |
||||||
128 | |||||||
129 | /** |
||||||
130 | * @config |
||||||
131 | */ |
||||||
132 | private static string $last_icon_version = '2'; |
||||||
133 | |||||||
134 | /** |
||||||
135 | * @config |
||||||
136 | */ |
||||||
137 | private static bool $use_cdn = false; |
||||||
138 | |||||||
139 | /** |
||||||
140 | * @config |
||||||
141 | */ |
||||||
142 | private static bool $use_v5 = false; |
||||||
143 | |||||||
144 | /** |
||||||
145 | * @config |
||||||
146 | */ |
||||||
147 | private static bool $enable_luxon = false; |
||||||
148 | |||||||
149 | /** |
||||||
150 | * @config |
||||||
151 | */ |
||||||
152 | private static bool $enable_last_icon = false; |
||||||
153 | |||||||
154 | /** |
||||||
155 | * @config |
||||||
156 | */ |
||||||
157 | private static bool $enable_requirements = true; |
||||||
158 | |||||||
159 | /** |
||||||
160 | * @config |
||||||
161 | */ |
||||||
162 | private static bool $enable_js_modules = true; |
||||||
163 | |||||||
164 | /** |
||||||
165 | * @link http://www.tabulator.info/docs/6.2/options |
||||||
166 | * @config |
||||||
167 | */ |
||||||
168 | private static array $default_options = [ |
||||||
169 | 'index' => "ID", // http://tabulator.info/docs/6.2/data#row-index |
||||||
170 | 'layout' => 'fitColumns', // http://www.tabulator.info/docs/6.2/layout#layout |
||||||
171 | 'height' => '100%', // http://www.tabulator.info/docs/6.2/layout#height-fixed |
||||||
172 | 'responsiveLayout' => "hide", // http://www.tabulator.info/docs/6.2/layout#responsive |
||||||
173 | ]; |
||||||
174 | |||||||
175 | /** |
||||||
176 | * @link http://tabulator.info/docs/6.2/columns#defaults |
||||||
177 | * @config |
||||||
178 | */ |
||||||
179 | private static array $default_column_options = [ |
||||||
180 | 'resizable' => false, |
||||||
181 | ]; |
||||||
182 | |||||||
183 | private static bool $enable_ajax_init = true; |
||||||
184 | |||||||
185 | /** |
||||||
186 | * @config |
||||||
187 | */ |
||||||
188 | private static bool $default_lazy_init = false; |
||||||
189 | |||||||
190 | /** |
||||||
191 | * @config |
||||||
192 | */ |
||||||
193 | private static bool $show_row_delete = false; |
||||||
194 | |||||||
195 | /** |
||||||
196 | * Data source. |
||||||
197 | */ |
||||||
198 | protected ?SS_List $list; |
||||||
199 | |||||||
200 | /** |
||||||
201 | * @link http://www.tabulator.info/docs/6.2/columns |
||||||
202 | */ |
||||||
203 | protected array $columns = []; |
||||||
204 | |||||||
205 | /** |
||||||
206 | * @link http://tabulator.info/docs/6.2/columns#defaults |
||||||
207 | */ |
||||||
208 | protected array $columnDefaults = []; |
||||||
209 | |||||||
210 | /** |
||||||
211 | * @link http://www.tabulator.info/docs/6.2/options |
||||||
212 | */ |
||||||
213 | protected array $options = []; |
||||||
214 | |||||||
215 | protected bool $autoloadDataList = true; |
||||||
216 | |||||||
217 | protected bool $rowClickTriggersAction = false; |
||||||
218 | |||||||
219 | protected int $pageSize = 10; |
||||||
220 | |||||||
221 | protected string $itemRequestClass = ''; |
||||||
222 | |||||||
223 | protected string $modelClass = ''; |
||||||
224 | |||||||
225 | protected bool $lazyInit = false; |
||||||
226 | |||||||
227 | protected array $tools = []; |
||||||
228 | |||||||
229 | /** |
||||||
230 | * @var AbstractBulkAction[] |
||||||
231 | */ |
||||||
232 | protected array $bulkActions = []; |
||||||
233 | |||||||
234 | protected array $listeners = []; |
||||||
235 | |||||||
236 | protected array $linksOptions = [ |
||||||
237 | 'ajaxURL' |
||||||
238 | ]; |
||||||
239 | |||||||
240 | protected array $dataAttributes = []; |
||||||
241 | |||||||
242 | protected string $controllerFunction = ""; |
||||||
243 | |||||||
244 | protected string $editUrl = ""; |
||||||
245 | |||||||
246 | protected string $moveUrl = ""; |
||||||
247 | |||||||
248 | protected string $bulkUrl = ""; |
||||||
249 | |||||||
250 | protected bool $globalSearch = false; |
||||||
251 | |||||||
252 | protected array $wildcardFields = []; |
||||||
253 | |||||||
254 | protected array $quickFilters = []; |
||||||
255 | |||||||
256 | protected string $defaultFilter = 'PartialMatch'; |
||||||
257 | |||||||
258 | protected bool $groupLayout = false; |
||||||
259 | |||||||
260 | protected bool $enableGridManipulation = false; |
||||||
261 | |||||||
262 | /** |
||||||
263 | * @param string $fieldName |
||||||
264 | * @param string|null|bool $title |
||||||
265 | * @param SS_List $value |
||||||
266 | */ |
||||||
267 | public function __construct($name, $title = null, $value = null) |
||||||
268 | { |
||||||
269 | // Set options and defaults first |
||||||
270 | $this->options = self::config()->default_options ?? []; |
||||||
271 | $this->columnDefaults = self::config()->default_column_options ?? []; |
||||||
272 | |||||||
273 | parent::__construct($name, $title, $value); |
||||||
274 | $this->setLazyInit(self::config()->default_lazy_init); |
||||||
275 | |||||||
276 | // We don't want regular setValue for this since it would break with loadFrom logic |
||||||
277 | if ($value) { |
||||||
278 | $this->setList($value); |
||||||
279 | } |
||||||
280 | } |
||||||
281 | |||||||
282 | /** |
||||||
283 | * This helps if some third party code expects the TabulatorGrid to be a GridField |
||||||
284 | * Only works to a really basic extent |
||||||
285 | */ |
||||||
286 | public function getConfig(): GridFieldConfig |
||||||
287 | { |
||||||
288 | return new GridFieldConfig; |
||||||
289 | } |
||||||
290 | |||||||
291 | /** |
||||||
292 | * This helps if some third party code expects the TabulatorGrid to be a GridField |
||||||
293 | * Only works to a really basic extent |
||||||
294 | */ |
||||||
295 | public function setConfig($config) |
||||||
296 | { |
||||||
297 | // ignore |
||||||
298 | } |
||||||
299 | |||||||
300 | /** |
||||||
301 | * @return string |
||||||
302 | */ |
||||||
303 | public function getValueJson() |
||||||
304 | { |
||||||
305 | $v = $this->value ?? ''; |
||||||
306 | if (is_array($v)) { |
||||||
307 | $v = json_encode($v); |
||||||
308 | } |
||||||
309 | if (strpos($v, '[') !== 0) { |
||||||
310 | return '[]'; |
||||||
311 | } |
||||||
312 | return $v; |
||||||
313 | } |
||||||
314 | |||||||
315 | public function saveInto(DataObjectInterface $record) |
||||||
316 | { |
||||||
317 | if ($this->enableGridManipulation) { |
||||||
318 | $value = $this->dataValue(); |
||||||
319 | if (is_array($value)) { |
||||||
320 | $this->value = json_encode(array_values($value)); |
||||||
321 | } |
||||||
322 | parent::saveInto($record); |
||||||
323 | } |
||||||
324 | } |
||||||
325 | |||||||
326 | /** |
||||||
327 | * Temporary link that will be replaced by a real link by processLinks |
||||||
328 | * TODO: not really happy with this, find a better way |
||||||
329 | * |
||||||
330 | * @param string $action |
||||||
331 | * @return string |
||||||
332 | */ |
||||||
333 | public function TempLink(string $action, bool $controller = true): string |
||||||
334 | { |
||||||
335 | // It's an absolute link |
||||||
336 | if (strpos($action, '/') === 0 || strpos($action, 'http') === 0) { |
||||||
337 | return $action; |
||||||
338 | } |
||||||
339 | // Already temp |
||||||
340 | if (strpos($action, ':') !== false) { |
||||||
341 | return $action; |
||||||
342 | } |
||||||
343 | $prefix = $controller ? "controller" : "form"; |
||||||
344 | return "$prefix:$action"; |
||||||
345 | } |
||||||
346 | |||||||
347 | public function ControllerLink(string $action): string |
||||||
348 | { |
||||||
349 | return $this->getForm()->getController()->Link($action); |
||||||
350 | } |
||||||
351 | |||||||
352 | public function getCreateLink(): string |
||||||
353 | { |
||||||
354 | return Controller::join_links($this->Link('item'), 'new'); |
||||||
355 | } |
||||||
356 | |||||||
357 | /** |
||||||
358 | * @param FieldList $fields |
||||||
359 | * @param string $name |
||||||
360 | * @return TabulatorGrid|null |
||||||
361 | */ |
||||||
362 | public static function replaceGridField(FieldList $fields, string $name) |
||||||
363 | { |
||||||
364 | /** @var \SilverStripe\Forms\GridField\GridField $gridField */ |
||||||
365 | $gridField = $fields->dataFieldByName($name); |
||||||
366 | if (!$gridField) { |
||||||
0 ignored issues
–
show
introduced
by
![]() |
|||||||
367 | return; |
||||||
368 | } |
||||||
369 | if ($gridField instanceof TabulatorGrid) { |
||||||
0 ignored issues
–
show
|
|||||||
370 | return $gridField; |
||||||
371 | } |
||||||
372 | $tabulatorGrid = new TabulatorGrid($name, $gridField->Title(), $gridField->getList()); |
||||||
373 | // In the cms, this is mostly never happening |
||||||
374 | if ($gridField->getForm()) { |
||||||
375 | $tabulatorGrid->setForm($gridField->getForm()); |
||||||
376 | } |
||||||
377 | $tabulatorGrid->configureFromDataObject($gridField->getModelClass()); |
||||||
378 | $tabulatorGrid->setLazyInit(true); |
||||||
379 | $fields->replaceField($name, $tabulatorGrid); |
||||||
380 | |||||||
381 | return $tabulatorGrid; |
||||||
382 | } |
||||||
383 | |||||||
384 | /** |
||||||
385 | * A shortcut to convert editable records to view only |
||||||
386 | * Disables adding new records as well |
||||||
387 | */ |
||||||
388 | public function setViewOnly(): void |
||||||
389 | { |
||||||
390 | $itemUrl = $this->TempLink('item/{ID}', false); |
||||||
391 | $this->removeButton(self::UI_EDIT); |
||||||
392 | $this->removeButton(self::UI_DELETE); |
||||||
393 | $this->removeButton(self::UI_UNLINK); |
||||||
394 | $this->addButton(self::UI_VIEW, $itemUrl, "visibility", "View"); |
||||||
395 | $this->removeTool(TabulatorAddNewButton::class); |
||||||
396 | } |
||||||
397 | |||||||
398 | public function isViewOnly(): bool |
||||||
399 | { |
||||||
400 | return !$this->hasButton(self::UI_EDIT); |
||||||
401 | } |
||||||
402 | |||||||
403 | public function setManageRelations(): array |
||||||
404 | { |
||||||
405 | $this->addToolEnd($AddExistingAutocompleter = new TabulatorAddExistingAutocompleter()); |
||||||
406 | |||||||
407 | $unlinkBtn = $this->makeButton($this->TempLink('item/{ID}/unlink', false), "link_off", _t('TabulatorGrid.Unlink', 'Unlink')); |
||||||
408 | $unlinkBtn["formatterParams"]["classes"] = 'btn btn-danger'; |
||||||
409 | $unlinkBtn['formatterParams']['ajax'] = true; |
||||||
410 | $this->addButtonFromArray(self::UI_UNLINK, $unlinkBtn); |
||||||
411 | |||||||
412 | return [ |
||||||
413 | $AddExistingAutocompleter, |
||||||
414 | $unlinkBtn |
||||||
415 | ]; |
||||||
416 | } |
||||||
417 | |||||||
418 | protected function getTabulatorOptions(DataObject $singl) |
||||||
419 | { |
||||||
420 | $opts = []; |
||||||
421 | if ($singl->hasMethod('tabulatorOptions')) { |
||||||
422 | $opts = $singl->tabulatorOptions(); |
||||||
423 | } |
||||||
424 | return $opts; |
||||||
425 | } |
||||||
426 | |||||||
427 | public function configureFromDataObject($className = null): void |
||||||
428 | { |
||||||
429 | $this->columns = []; |
||||||
430 | |||||||
431 | if (!$className) { |
||||||
432 | $className = $this->getModelClass(); |
||||||
433 | } |
||||||
434 | if (!$className) { |
||||||
435 | throw new RuntimeException("Could not find the model class"); |
||||||
436 | } |
||||||
437 | $this->modelClass = $className; |
||||||
438 | |||||||
439 | /** @var DataObject $singl */ |
||||||
440 | $singl = singleton($className); |
||||||
441 | $opts = $this->getTabulatorOptions($singl); |
||||||
442 | |||||||
443 | // Mock some base columns using SilverStripe built-in methods |
||||||
444 | $columns = []; |
||||||
445 | |||||||
446 | $summaryFields = $opts['summaryFields'] ?? $singl->summaryFields(); |
||||||
447 | foreach ($summaryFields as $field => $title) { |
||||||
448 | // Deal with this in load() instead |
||||||
449 | // if (strpos($field, '.') !== false) { |
||||||
450 | // $fieldParts = explode(".", $field); |
||||||
451 | |||||||
452 | // It can be a relation Users.Count or a field Field.Nice |
||||||
453 | // $classOrField = $fieldParts[0]; |
||||||
454 | // $relationOrMethod = $fieldParts[1]; |
||||||
455 | // } |
||||||
456 | $title = str_replace(".", " ", $title); |
||||||
457 | $columns[$field] = [ |
||||||
458 | 'field' => $field, |
||||||
459 | 'title' => $title, |
||||||
460 | 'headerSort' => false, |
||||||
461 | ]; |
||||||
462 | |||||||
463 | $dbObject = $singl->dbObject($field); |
||||||
464 | if ($dbObject) { |
||||||
465 | if ($dbObject instanceof DBBoolean) { |
||||||
466 | $columns[$field]['formatter'] = "customTickCross"; |
||||||
467 | } |
||||||
468 | } |
||||||
469 | } |
||||||
470 | $searchableFields = $opts['searchableFields'] ?? $singl->searchableFields(); |
||||||
471 | $searchAliases = $opts['searchAliases'] ?? []; |
||||||
472 | foreach ($searchableFields as $key => $searchOptions) { |
||||||
473 | $key = $searchAliases[$key] ?? $key; |
||||||
474 | |||||||
475 | // Allow "nice" |
||||||
476 | $niceKey = $key . ".Nice"; |
||||||
477 | if (isset($columns[$niceKey])) { |
||||||
478 | $key = $niceKey; |
||||||
479 | } |
||||||
480 | |||||||
481 | /* |
||||||
482 | "filter" => "NameOfTheFilter" |
||||||
483 | "field" => "SilverStripe\Forms\FormField" |
||||||
484 | "title" => "Title of the field" |
||||||
485 | */ |
||||||
486 | if (!isset($columns[$key])) { |
||||||
487 | continue; |
||||||
488 | } |
||||||
489 | $columns[$key]['headerFilter'] = true; |
||||||
490 | $columns[$key]['headerSort'] = true; |
||||||
491 | // $columns[$key]['headerFilterPlaceholder'] = $searchOptions['title']; |
||||||
492 | //TODO: implement filter mapping |
||||||
493 | switch ($searchOptions['filter']) { |
||||||
494 | default: |
||||||
495 | $columns[$key]['headerFilterFunc'] = "like"; |
||||||
496 | break; |
||||||
497 | } |
||||||
498 | |||||||
499 | // Restrict based on data type |
||||||
500 | $dbObject = $singl->dbObject($key); |
||||||
501 | if ($dbObject) { |
||||||
502 | if ($dbObject instanceof DBBoolean) { |
||||||
503 | $columns[$key]['headerFilter'] = 'tickCross'; |
||||||
504 | $columns[$key]['headerFilterFunc'] = "="; |
||||||
505 | $columns[$key]['headerFilterParams'] = [ |
||||||
506 | 'tristate' => true |
||||||
507 | ]; |
||||||
508 | } |
||||||
509 | if ($dbObject instanceof DBEnum) { |
||||||
510 | $columns[$key]['headerFilter'] = 'list'; |
||||||
511 | $columns[$key]['headerFilterFunc'] = "="; |
||||||
512 | $columns[$key]['headerFilterParams'] = [ |
||||||
513 | 'values' => $dbObject->enumValues() |
||||||
514 | ]; |
||||||
515 | } |
||||||
516 | } |
||||||
517 | } |
||||||
518 | |||||||
519 | // Allow customizing our columns based on record |
||||||
520 | if ($singl->hasMethod('tabulatorColumns')) { |
||||||
521 | $fields = $singl->tabulatorColumns(); |
||||||
522 | if (!is_array($fields)) { |
||||||
523 | throw new RuntimeException("tabulatorColumns must return an array"); |
||||||
524 | } |
||||||
525 | foreach ($fields as $key => $columnOptions) { |
||||||
526 | $baseOptions = $columns[$key] ?? []; |
||||||
527 | $columns[$key] = array_merge($baseOptions, $columnOptions); |
||||||
528 | } |
||||||
529 | } |
||||||
530 | |||||||
531 | $this->extend('updateConfiguredColumns', $columns); |
||||||
532 | |||||||
533 | foreach ($columns as $col) { |
||||||
534 | $this->addColumn($col['field'], $col['title'], $col); |
||||||
535 | } |
||||||
536 | |||||||
537 | // Sortable ? |
||||||
538 | $sortable = $opts['sortable'] ?? $singl->hasField('Sort'); |
||||||
539 | if ($sortable) { |
||||||
540 | $this->wizardMoveable(); |
||||||
541 | } |
||||||
542 | |||||||
543 | // Actions |
||||||
544 | // We use a pseudo link, because maybe we cannot call Link() yet if it's not linked to a form |
||||||
545 | |||||||
546 | $this->bulkUrl = $this->TempLink("bulkAction/", false); |
||||||
547 | |||||||
548 | // - Core actions, handled by TabulatorGrid |
||||||
549 | $itemUrl = $this->TempLink('item/{ID}', false); |
||||||
550 | if ($singl->canEdit()) { |
||||||
551 | $this->addEditButton(); |
||||||
552 | } elseif ($singl->canView()) { |
||||||
553 | $this->addButton(self::UI_VIEW, $itemUrl, "visibility", _t('TabulatorGrid.View', 'View')); |
||||||
554 | } |
||||||
555 | |||||||
556 | $showRowDelete = $opts['rowDelete'] ?? self::config()->show_row_delete; |
||||||
557 | if ($singl->canDelete() && $showRowDelete) { |
||||||
558 | $deleteBtn = $this->makeButton($this->TempLink('item/{ID}/delete', false), "delete", _t('TabulatorGrid.Delete', 'Delete')); |
||||||
559 | $deleteBtn["formatterParams"]["classes"] = 'btn btn-danger'; |
||||||
560 | $this->addButtonFromArray(self::UI_DELETE, $deleteBtn); |
||||||
561 | } |
||||||
562 | |||||||
563 | // - Tools |
||||||
564 | $this->tools = []; |
||||||
565 | |||||||
566 | $addNew = $opts['addNew'] ?? true; |
||||||
567 | if ($singl->canCreate() && $addNew) { |
||||||
568 | $this->addTool(self::POS_START, new TabulatorAddNewButton($this), self::TOOL_ADD_NEW); |
||||||
0 ignored issues
–
show
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
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. ![]() |
|||||||
569 | } |
||||||
570 | $export = $opts['export'] ?? true; |
||||||
571 | if (class_exists(\LeKoala\ExcelImportExport\ExcelImportExport::class) && $export) { |
||||||
0 ignored issues
–
show
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. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||||||
572 | $xlsxExportButton = new TabulatorExportButton($this); |
||||||
0 ignored issues
–
show
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
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. ![]() |
|||||||
573 | $this->addTool(self::POS_END, $xlsxExportButton, self::TOOL_EXPORT); |
||||||
574 | $csvExportButton = new TabulatorExportButton($this); |
||||||
575 | $csvExportButton->setExportFormat('csv'); |
||||||
576 | $this->addTool(self::POS_END, $csvExportButton, self::TOOL_EXPORT_CSV); |
||||||
577 | } |
||||||
578 | |||||||
579 | // - Custom actions are forwarded to the model itself |
||||||
580 | if ($singl->hasMethod('tabulatorRowActions')) { |
||||||
581 | $rowActions = $singl->tabulatorRowActions(); |
||||||
582 | if (!is_array($rowActions)) { |
||||||
583 | throw new RuntimeException("tabulatorRowActions must return an array"); |
||||||
584 | } |
||||||
585 | foreach ($rowActions as $key => $actionConfig) { |
||||||
586 | $action = $actionConfig['action'] ?? $key; |
||||||
587 | $url = $this->TempLink("item/{ID}/customAction/$action", false); |
||||||
588 | $icon = $actionConfig['icon'] ?? "cog"; |
||||||
589 | $title = $actionConfig['title'] ?? ""; |
||||||
590 | |||||||
591 | $button = $this->makeButton($url, $icon, $title); |
||||||
592 | if (!empty($actionConfig['ajax'])) { |
||||||
593 | $button['formatterParams']['ajax'] = true; |
||||||
594 | } |
||||||
595 | $this->addButtonFromArray("ui_customaction_$action", $button); |
||||||
596 | } |
||||||
597 | } |
||||||
598 | |||||||
599 | $this->setRowClickTriggersAction(true); |
||||||
600 | } |
||||||
601 | |||||||
602 | public static function requirements(): void |
||||||
603 | { |
||||||
604 | $load_styles = self::config()->load_styles; |
||||||
605 | $luxon_version = self::config()->luxon_version; |
||||||
606 | $enable_luxon = self::config()->enable_luxon; |
||||||
607 | $last_icon_version = self::config()->last_icon_version; |
||||||
608 | $enable_last_icon = self::config()->enable_last_icon; |
||||||
609 | $enable_js_modules = self::config()->enable_js_modules; |
||||||
610 | |||||||
611 | $jsOpts = []; |
||||||
612 | if ($enable_js_modules) { |
||||||
613 | $jsOpts['type'] = 'module'; |
||||||
614 | } |
||||||
615 | |||||||
616 | if ($luxon_version && $enable_luxon) { |
||||||
617 | // Do not load as module or we would get undefined luxon global var |
||||||
618 | Requirements::javascript("https://cdn.jsdelivr.net/npm/luxon@$luxon_version/build/global/luxon.min.js"); |
||||||
619 | } |
||||||
620 | if ($last_icon_version && $enable_last_icon) { |
||||||
621 | Requirements::css("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.css"); |
||||||
622 | // Do not load as module even if asked to ensure load speed |
||||||
623 | Requirements::javascript("https://cdn.jsdelivr.net/npm/last-icon@$last_icon_version/last-icon.min.js"); |
||||||
624 | } |
||||||
625 | |||||||
626 | $use_v5 = self::config()->use_v5; |
||||||
627 | |||||||
628 | Requirements::javascript('lekoala/silverstripe-tabulator:client/TabulatorField.js', $jsOpts); |
||||||
629 | if ($load_styles) { |
||||||
630 | Requirements::css('lekoala/silverstripe-tabulator:client/custom-tabulator.css'); |
||||||
631 | if ($use_v5) { |
||||||
632 | Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.5.5.min.js', $jsOpts); |
||||||
633 | } else { |
||||||
634 | Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.min.js', $jsOpts); |
||||||
635 | } |
||||||
636 | } else { |
||||||
637 | // you must load the css yourself based on your preferences |
||||||
638 | if ($use_v5) { |
||||||
639 | Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.5.5.raw.min.js', $jsOpts); |
||||||
640 | } else { |
||||||
641 | Requirements::javascript('lekoala/silverstripe-tabulator:client/tabulator-grid.raw.min.js', $jsOpts); |
||||||
642 | } |
||||||
643 | } |
||||||
644 | } |
||||||
645 | |||||||
646 | public function setValue($value, $data = null) |
||||||
647 | { |
||||||
648 | // Allow set raw json as value |
||||||
649 | if ($value && is_string($value) && strpos($value, '[') === 0) { |
||||||
650 | $value = json_decode($value); |
||||||
651 | } |
||||||
652 | if ($value instanceof DataList) { |
||||||
653 | $this->configureFromDataObject($value->dataClass()); |
||||||
654 | } |
||||||
655 | return parent::setValue($value, $data); |
||||||
656 | } |
||||||
657 | |||||||
658 | public function Field($properties = []) |
||||||
659 | { |
||||||
660 | if (self::config()->enable_requirements) { |
||||||
661 | self::requirements(); |
||||||
662 | } |
||||||
663 | |||||||
664 | // Make sure we can use a standalone version of the field without a form |
||||||
665 | // Function should match the name |
||||||
666 | if (!$this->form) { |
||||||
667 | $this->form = new Form(Controller::curr(), $this->getControllerFunction()); |
||||||
668 | } |
||||||
669 | |||||||
670 | // Data attributes for our custom behaviour |
||||||
671 | $this->setDataAttribute("row-click-triggers-action", $this->rowClickTriggersAction); |
||||||
672 | |||||||
673 | $this->setDataAttribute("listeners", $this->listeners); |
||||||
674 | if ($this->editUrl) { |
||||||
675 | $url = $this->processLink($this->editUrl); |
||||||
676 | $this->setDataAttribute("edit-url", $url); |
||||||
677 | } |
||||||
678 | if ($this->moveUrl) { |
||||||
679 | $url = $this->processLink($this->moveUrl); |
||||||
680 | $this->setDataAttribute("move-url", $url); |
||||||
681 | } |
||||||
682 | if (!empty($this->bulkActions)) { |
||||||
683 | $url = $this->processLink($this->bulkUrl); |
||||||
684 | $this->setDataAttribute("bulk-url", $url); |
||||||
685 | } |
||||||
686 | |||||||
687 | return parent::Field($properties); |
||||||
688 | } |
||||||
689 | |||||||
690 | public function ShowTools(): string |
||||||
691 | { |
||||||
692 | if (empty($this->tools)) { |
||||||
693 | return ''; |
||||||
694 | } |
||||||
695 | $html = ''; |
||||||
696 | $html .= '<div class="tabulator-tools">'; |
||||||
697 | $html .= '<div class="tabulator-tools-start">'; |
||||||
698 | foreach ($this->tools as $tool) { |
||||||
699 | if ($tool['position'] != self::POS_START) { |
||||||
700 | continue; |
||||||
701 | } |
||||||
702 | $html .= ($tool['tool'])->forTemplate(); |
||||||
703 | } |
||||||
704 | $html .= '</div>'; |
||||||
705 | $html .= '<div class="tabulator-tools-end">'; |
||||||
706 | foreach ($this->tools as $tool) { |
||||||
707 | if ($tool['position'] != self::POS_END) { |
||||||
708 | continue; |
||||||
709 | } |
||||||
710 | $html .= ($tool['tool'])->forTemplate(); |
||||||
711 | } |
||||||
712 | // Show bulk actions at the end |
||||||
713 | if (!empty($this->bulkActions)) { |
||||||
714 | $selectLabel = _t(__CLASS__ . ".BULKSELECT", "Select a bulk action"); |
||||||
715 | $confirmLabel = _t(__CLASS__ . ".BULKCONFIRM", "Go"); |
||||||
716 | $html .= "<select class=\"tabulator-bulk-select\">"; |
||||||
717 | $html .= "<option>" . $selectLabel . "</option>"; |
||||||
718 | foreach ($this->bulkActions as $bulkAction) { |
||||||
719 | $v = $bulkAction->getName(); |
||||||
720 | $xhr = $bulkAction->getXhr(); |
||||||
721 | $destructive = $bulkAction->getDestructive(); |
||||||
722 | $html .= "<option value=\"$v\" data-xhr=\"$xhr\" data-destructive=\"$destructive\">" . $bulkAction->getLabel() . "</option>"; |
||||||
723 | } |
||||||
724 | $html .= "</select>"; |
||||||
725 | $html .= "<button class=\"tabulator-bulk-confirm btn\">" . $confirmLabel . "</button>"; |
||||||
726 | } |
||||||
727 | $html .= '</div>'; |
||||||
728 | $html .= '</div>'; |
||||||
729 | return $html; |
||||||
730 | } |
||||||
731 | |||||||
732 | public function JsonOptions(): string |
||||||
733 | { |
||||||
734 | $this->processLinks(); |
||||||
735 | |||||||
736 | $data = $this->list ?? []; |
||||||
737 | if ($this->autoloadDataList && $data instanceof DataList) { |
||||||
738 | $data = null; |
||||||
739 | } |
||||||
740 | $opts = $this->options; |
||||||
741 | $opts['columnDefaults'] = $this->columnDefaults; |
||||||
742 | |||||||
743 | if (empty($this->columns)) { |
||||||
744 | $opts['autoColumns'] = true; |
||||||
745 | } else { |
||||||
746 | $opts['columns'] = array_values($this->columns); |
||||||
747 | } |
||||||
748 | |||||||
749 | if ($data && is_iterable($data)) { |
||||||
750 | if ($data instanceof ArrayList) { |
||||||
751 | $data = $data->toArray(); |
||||||
752 | } else { |
||||||
753 | if (is_iterable($data) && !is_array($data)) { |
||||||
754 | $data = iterator_to_array($data); |
||||||
755 | } |
||||||
756 | } |
||||||
757 | $opts['data'] = $data; |
||||||
758 | } |
||||||
759 | |||||||
760 | // i18n |
||||||
761 | $locale = strtolower(str_replace('_', '-', i18n::get_locale())); |
||||||
762 | $paginationTranslations = [ |
||||||
763 | "first" => _t("TabulatorPagination.first", "First"), |
||||||
764 | "first_title" => _t("TabulatorPagination.first_title", "First Page"), |
||||||
765 | "last" => _t("TabulatorPagination.last", "Last"), |
||||||
766 | "last_title" => _t("TabulatorPagination.last_title", "Last Page"), |
||||||
767 | "prev" => _t("TabulatorPagination.prev", "Previous"), |
||||||
768 | "prev_title" => _t("TabulatorPagination.prev_title", "Previous Page"), |
||||||
769 | "next" => _t("TabulatorPagination.next", "Next"), |
||||||
770 | "next_title" => _t("TabulatorPagination.next_title", "Next Page"), |
||||||
771 | "all" => _t("TabulatorPagination.all", "All"), |
||||||
772 | ]; |
||||||
773 | $dataTranslations = [ |
||||||
774 | "loading" => _t("TabulatorData.loading", "Loading"), |
||||||
775 | "error" => _t("TabulatorData.error", "Error"), |
||||||
776 | ]; |
||||||
777 | $groupsTranslations = [ |
||||||
778 | "item" => _t("TabulatorGroups.item", "Item"), |
||||||
779 | "items" => _t("TabulatorGroups.items", "Items"), |
||||||
780 | ]; |
||||||
781 | $headerFiltersTranslations = [ |
||||||
782 | "default" => _t("TabulatorHeaderFilters.default", "filter column..."), |
||||||
783 | ]; |
||||||
784 | $bulkActionsTranslations = [ |
||||||
785 | "no_action" => _t("TabulatorBulkActions.no_action", "Please select an action"), |
||||||
786 | "no_records" => _t("TabulatorBulkActions.no_records", "Please select a record"), |
||||||
787 | "destructive" => _t("TabulatorBulkActions.destructive", "Confirm destructive action ?"), |
||||||
788 | ]; |
||||||
789 | $translations = [ |
||||||
790 | 'data' => $dataTranslations, |
||||||
791 | 'groups' => $groupsTranslations, |
||||||
792 | 'pagination' => $paginationTranslations, |
||||||
793 | 'headerFilters' => $headerFiltersTranslations, |
||||||
794 | 'bulkActions' => $bulkActionsTranslations, |
||||||
795 | ]; |
||||||
796 | $opts['locale'] = $locale; |
||||||
797 | $opts['langs'] = [ |
||||||
798 | $locale => $translations |
||||||
799 | ]; |
||||||
800 | |||||||
801 | // Apply state |
||||||
802 | // TODO: finalize persistence on the client side instead of this when using TabID |
||||||
803 | $state = $this->getState(); |
||||||
804 | if ($state) { |
||||||
0 ignored issues
–
show
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 ![]() |
|||||||
805 | if (!empty($state['filter'])) { |
||||||
806 | // @link https://tabulator.info/docs/6.2/filter#initial |
||||||
807 | // We need to split between global filters and header filters |
||||||
808 | $allFilters = $state['filter'] ?? []; |
||||||
809 | $globalFilters = []; |
||||||
810 | $headerFilters = []; |
||||||
811 | foreach ($allFilters as $allFilter) { |
||||||
812 | if (strpos($allFilter['field'], '__') === 0) { |
||||||
813 | $globalFilters[] = $allFilter; |
||||||
814 | } else { |
||||||
815 | $headerFilters[] = $allFilter; |
||||||
816 | } |
||||||
817 | } |
||||||
818 | $opts['initialFilter'] = $globalFilters; |
||||||
819 | $opts['initialHeaderFilter'] = $headerFilters; |
||||||
820 | } |
||||||
821 | if (!empty($state['sort'])) { |
||||||
822 | // @link https://tabulator.info/docs/6.2/sort#initial |
||||||
823 | $opts['initialSort'] = $state['sort']; |
||||||
824 | } |
||||||
825 | |||||||
826 | // Restore state from server |
||||||
827 | $opts['_state'] = $state; |
||||||
828 | } |
||||||
829 | |||||||
830 | if ($this->enableGridManipulation) { |
||||||
831 | // $opts['renderVertical'] = 'basic'; |
||||||
832 | } |
||||||
833 | |||||||
834 | // Add our extension initCallback |
||||||
835 | $opts['_initCallback'] = ['__fn' => self::JS_INIT_CALLBACK]; |
||||||
836 | $opts['_configCallback'] = ['__fn' => self::JS_CONFIG_CALLBACK]; |
||||||
837 | |||||||
838 | unset($opts['height']); |
||||||
839 | $json = json_encode($opts); |
||||||
840 | |||||||
841 | // Escape ' |
||||||
842 | $json = str_replace("'", ''', $json); |
||||||
843 | |||||||
844 | return $json; |
||||||
845 | } |
||||||
846 | |||||||
847 | /** |
||||||
848 | * @param Controller $controller |
||||||
849 | * @return CompatLayerInterface |
||||||
850 | */ |
||||||
851 | public function getCompatLayer(Controller $controller = null) |
||||||
852 | { |
||||||
853 | if ($controller === null) { |
||||||
854 | $controller = Controller::curr(); |
||||||
855 | } |
||||||
856 | if (is_subclass_of($controller, \SilverStripe\Admin\LeftAndMain::class)) { |
||||||
0 ignored issues
–
show
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. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||||||
857 | return new SilverstripeAdminCompat(); |
||||||
858 | } |
||||||
859 | if (is_subclass_of($controller, \LeKoala\Admini\LeftAndMain::class)) { |
||||||
0 ignored issues
–
show
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. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||||||
860 | return new AdminiCompat(); |
||||||
861 | } |
||||||
862 | } |
||||||
863 | |||||||
864 | public function getAttributes() |
||||||
865 | { |
||||||
866 | $attrs = parent::getAttributes(); |
||||||
867 | unset($attrs['type']); |
||||||
868 | unset($attrs['name']); |
||||||
869 | unset($attrs['value']); |
||||||
870 | return $attrs; |
||||||
871 | } |
||||||
872 | |||||||
873 | public function getOption(string $k) |
||||||
874 | { |
||||||
875 | return $this->options[$k] ?? null; |
||||||
876 | } |
||||||
877 | |||||||
878 | public function setOption(string $k, $v): self |
||||||
879 | { |
||||||
880 | $this->options[$k] = $v; |
||||||
881 | return $this; |
||||||
882 | } |
||||||
883 | |||||||
884 | public function getRowHeight(): int |
||||||
885 | { |
||||||
886 | return $this->getOption('rowHeight'); |
||||||
0 ignored issues
–
show
|
|||||||
887 | } |
||||||
888 | |||||||
889 | /** |
||||||
890 | * Prevent row height automatic computation |
||||||
891 | * @link https://tabulator.info/docs/6.2/layout#height-row |
||||||
892 | */ |
||||||
893 | public function setRowHeight(int $v): self |
||||||
894 | { |
||||||
895 | $this->setOption('rowHeight', $v); |
||||||
896 | return $this; |
||||||
897 | } |
||||||
898 | |||||||
899 | public function makeHeadersSticky(): self |
||||||
900 | { |
||||||
901 | // note: we could also use the "sticky" attribute on the custom element |
||||||
902 | $this->addExtraClass("tabulator-sticky"); |
||||||
903 | return $this; |
||||||
904 | } |
||||||
905 | |||||||
906 | public function setRemoteSource(string $url, array $extraParams = [], bool $dataResponse = false): self |
||||||
907 | { |
||||||
908 | $this->setOption("ajaxURL", $url); //set url for ajax request |
||||||
909 | $params = array_merge([ |
||||||
910 | 'SecurityID' => SecurityToken::getSecurityID() |
||||||
911 | ], $extraParams); |
||||||
912 | $this->setOption("ajaxParams", $params); |
||||||
913 | // Accept response where data is nested under the data key |
||||||
914 | if ($dataResponse) { |
||||||
915 | $this->setOption("ajaxResponse", ['__fn' => self::JS_DATA_AJAX_RESPONSE]); |
||||||
916 | } |
||||||
917 | return $this; |
||||||
918 | } |
||||||
919 | |||||||
920 | /** |
||||||
921 | * @link http://www.tabulator.info/docs/6.2/page#remote |
||||||
922 | * @param string $url |
||||||
923 | * @param array $params |
||||||
924 | * @param integer $pageSize |
||||||
925 | * @param integer $initialPage |
||||||
926 | */ |
||||||
927 | public function setRemotePagination(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1): self |
||||||
928 | { |
||||||
929 | $this->setOption("pagination", true); //enable pagination |
||||||
930 | $this->setOption("paginationMode", 'remote'); //enable remote pagination |
||||||
931 | $this->setRemoteSource($url, $params); |
||||||
932 | if (!$pageSize) { |
||||||
933 | $pageSize = $this->pageSize; |
||||||
934 | } else { |
||||||
935 | $this->pageSize = $pageSize; |
||||||
936 | } |
||||||
937 | $this->setOption("paginationSize", $pageSize); |
||||||
938 | $this->setOption("paginationInitialPage", $initialPage); |
||||||
939 | $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/6.2/page#counter |
||||||
940 | return $this; |
||||||
941 | } |
||||||
942 | |||||||
943 | public function wizardRemotePagination(int $pageSize = 0, int $initialPage = 1, array $params = []): self |
||||||
944 | { |
||||||
945 | $this->setRemotePagination($this->TempLink('load', false), $params, $pageSize, $initialPage); |
||||||
946 | $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/6.2/sort#ajax-sort |
||||||
947 | $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/6.2/filter#ajax-filter |
||||||
948 | return $this; |
||||||
949 | } |
||||||
950 | |||||||
951 | public function setProgressiveLoad(string $url, array $params = [], int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0): self |
||||||
952 | { |
||||||
953 | $this->setOption("ajaxURL", $url); |
||||||
954 | if (!empty($params)) { |
||||||
955 | $this->setOption("ajaxParams", $params); |
||||||
956 | } |
||||||
957 | $this->setOption("progressiveLoad", $mode); |
||||||
958 | if ($scrollMargin > 0) { |
||||||
959 | $this->setOption("progressiveLoadScrollMargin", $scrollMargin); |
||||||
960 | } |
||||||
961 | if (!$pageSize) { |
||||||
962 | $pageSize = $this->pageSize; |
||||||
963 | } else { |
||||||
964 | $this->pageSize = $pageSize; |
||||||
965 | } |
||||||
966 | $this->setOption("paginationSize", $pageSize); |
||||||
967 | $this->setOption("paginationInitialPage", $initialPage); |
||||||
968 | $this->setOption("paginationCounter", 'rows'); // http://www.tabulator.info/docs/6.2/page#counter |
||||||
969 | return $this; |
||||||
970 | } |
||||||
971 | |||||||
972 | public function wizardProgressiveLoad(int $pageSize = 0, int $initialPage = 1, string $mode = 'scroll', int $scrollMargin = 0, array $extraParams = []): self |
||||||
973 | { |
||||||
974 | $params = array_merge([ |
||||||
975 | 'SecurityID' => SecurityToken::getSecurityID() |
||||||
976 | ], $extraParams); |
||||||
977 | $this->setProgressiveLoad($this->TempLink('load', false), $params, $pageSize, $initialPage, $mode, $scrollMargin); |
||||||
978 | $this->setOption("sortMode", "remote"); // http://www.tabulator.info/docs/6.2/sort#ajax-sort |
||||||
979 | $this->setOption("filterMode", "remote"); // http://www.tabulator.info/docs/6.2/filter#ajax-filter |
||||||
980 | return $this; |
||||||
981 | } |
||||||
982 | |||||||
983 | /** |
||||||
984 | * @link https://tabulator.info/docs/6.2/layout#responsive |
||||||
985 | * @param boolean $startOpen |
||||||
986 | * @param string $mode collapse|hide|flexCollapse |
||||||
987 | * @return self |
||||||
988 | */ |
||||||
989 | public function wizardResponsiveCollapse(bool $startOpen = false, string $mode = "collapse"): self |
||||||
990 | { |
||||||
991 | $this->setOption("responsiveLayout", $mode); |
||||||
992 | $this->setOption("responsiveLayoutCollapseStartOpen", $startOpen); |
||||||
993 | if ($mode != "hide") { |
||||||
994 | $this->columns = array_merge([ |
||||||
995 | 'ui_responsive_collapse' => [ |
||||||
996 | "cssClass" => 'tabulator-cell-btn', |
||||||
997 | 'formatter' => 'responsiveCollapse', |
||||||
998 | 'headerSort' => false, |
||||||
999 | 'width' => 40, |
||||||
1000 | 'responsive' => 0, |
||||||
1001 | ] |
||||||
1002 | ], $this->columns); |
||||||
1003 | } |
||||||
1004 | return $this; |
||||||
1005 | } |
||||||
1006 | |||||||
1007 | public function wizardDataTree(bool $startExpanded = false, bool $filter = false, bool $sort = false, string $el = null): self |
||||||
1008 | { |
||||||
1009 | $this->setOption("dataTree", true); |
||||||
1010 | $this->setOption("dataTreeStartExpanded", $startExpanded); |
||||||
1011 | $this->setOption("dataTreeFilter", $filter); |
||||||
1012 | $this->setOption("dataTreeSort", $sort); |
||||||
1013 | if ($el) { |
||||||
1014 | $this->setOption("dataTreeElementColumn", $el); |
||||||
1015 | } |
||||||
1016 | return $this; |
||||||
1017 | } |
||||||
1018 | |||||||
1019 | /** |
||||||
1020 | * @param array $actions An array of bulk actions, that can extend the abstract one or use the generic with callbable |
||||||
1021 | * @return self |
||||||
1022 | */ |
||||||
1023 | public function wizardSelectable(array $actions = []): self |
||||||
1024 | { |
||||||
1025 | $this->columns = array_merge([ |
||||||
1026 | 'ui_selectable' => [ |
||||||
1027 | "hozAlign" => 'center', |
||||||
1028 | "cssClass" => 'tabulator-cell-btn tabulator-cell-selector', |
||||||
1029 | 'formatter' => 'rowSelection', |
||||||
1030 | 'titleFormatter' => 'rowSelection', |
||||||
1031 | 'width' => 40, |
||||||
1032 | 'maxWidth' => 40, |
||||||
1033 | "headerSort" => false, |
||||||
1034 | ] |
||||||
1035 | ], $this->columns); |
||||||
1036 | $this->setBulkActions($actions); |
||||||
1037 | return $this; |
||||||
1038 | } |
||||||
1039 | |||||||
1040 | public function wizardMoveable(string $callback = "SSTabulator.rowMoved", $field = "Sort"): self |
||||||
1041 | { |
||||||
1042 | $this->moveUrl = $this->TempLink("item/{ID}/ajaxMove", false); |
||||||
1043 | $this->setOption("movableRows", true); |
||||||
1044 | $this->addListener("rowMoved", $callback); |
||||||
1045 | $this->columns = array_merge([ |
||||||
1046 | 'ui_move' => [ |
||||||
1047 | "hozAlign" => 'center', |
||||||
1048 | "cssClass" => 'tabulator-cell-btn tabulator-cell-selector tabulator-ui-sort', |
||||||
1049 | 'rowHandle' => true, |
||||||
1050 | 'formatter' => 'handle', |
||||||
1051 | 'headerSort' => false, |
||||||
1052 | 'frozen' => true, |
||||||
1053 | 'width' => 40, |
||||||
1054 | 'maxWidth' => 40, |
||||||
1055 | ], |
||||||
1056 | // We need a hidden sort column |
||||||
1057 | self::UI_SORT => [ |
||||||
1058 | "field" => $field, |
||||||
1059 | 'visible' => false, |
||||||
1060 | ], |
||||||
1061 | ], $this->columns); |
||||||
1062 | return $this; |
||||||
1063 | } |
||||||
1064 | |||||||
1065 | /** |
||||||
1066 | * @param string $field |
||||||
1067 | * @param string $toggleElement arrow|header|false (header by default) |
||||||
1068 | * @param boolean $isBool |
||||||
1069 | * @return void |
||||||
1070 | */ |
||||||
1071 | public function wizardGroupBy(string $field, string $toggleElement = 'header', bool $isBool = false) |
||||||
1072 | { |
||||||
1073 | $this->setOption("groupBy", $field); |
||||||
1074 | $this->setOption("groupToggleElement", $toggleElement); |
||||||
1075 | if ($isBool) { |
||||||
1076 | $this->setOption("groupHeader", ['_fn' => self::JS_BOOL_GROUP_HEADER]); |
||||||
1077 | } |
||||||
1078 | } |
||||||
1079 | |||||||
1080 | /** |
||||||
1081 | * @param HTTPRequest $request |
||||||
1082 | * @return HTTPResponse |
||||||
1083 | */ |
||||||
1084 | public function handleItem($request) |
||||||
1085 | { |
||||||
1086 | // Our getController could either give us a true Controller, if this is the top-level GridField. |
||||||
1087 | // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...) |
||||||
1088 | $requestHandler = $this->getForm()->getController(); |
||||||
1089 | try { |
||||||
1090 | $record = $this->getRecordFromRequest($request); |
||||||
1091 | } catch (Exception $e) { |
||||||
1092 | return $requestHandler->httpError(404, $e->getMessage()); |
||||||
1093 | } |
||||||
1094 | |||||||
1095 | if (!$record) { |
||||||
1096 | return $requestHandler->httpError(404, 'That record was not found'); |
||||||
1097 | } |
||||||
1098 | $handler = $this->getItemRequestHandler($record, $requestHandler); |
||||||
1099 | return $handler->handleRequest($request); |
||||||
0 ignored issues
–
show
|
|||||||
1100 | } |
||||||
1101 | |||||||
1102 | /** |
||||||
1103 | * @param HTTPRequest $request |
||||||
1104 | * @return HTTPResponse |
||||||
1105 | */ |
||||||
1106 | public function handleTool($request) |
||||||
1107 | { |
||||||
1108 | // Our getController could either give us a true Controller, if this is the top-level GridField. |
||||||
1109 | // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...) |
||||||
1110 | $requestHandler = $this->getForm()->getController(); |
||||||
1111 | $tool = $this->getToolFromRequest($request); |
||||||
1112 | if (!$tool) { |
||||||
1113 | return $requestHandler->httpError(404, 'That tool was not found'); |
||||||
1114 | } |
||||||
1115 | return $tool->handleRequest($request); |
||||||
0 ignored issues
–
show
|
|||||||
1116 | } |
||||||
1117 | |||||||
1118 | /** |
||||||
1119 | * @param HTTPRequest $request |
||||||
1120 | * @return HTTPResponse |
||||||
1121 | */ |
||||||
1122 | public function handleBulkAction($request) |
||||||
1123 | { |
||||||
1124 | // Our getController could either give us a true Controller, if this is the top-level GridField. |
||||||
1125 | // It could also give us a RequestHandler in the form of (GridFieldDetailForm_ItemRequest, TabulatorGrid...) |
||||||
1126 | $requestHandler = $this->getForm()->getController(); |
||||||
1127 | $bulkAction = $this->getBulkActionFromRequest($request); |
||||||
1128 | if (!$bulkAction) { |
||||||
1129 | return $requestHandler->httpError(404, 'That bulk action was not found'); |
||||||
1130 | } |
||||||
1131 | return $bulkAction->handleRequest($request); |
||||||
0 ignored issues
–
show
|
|||||||
1132 | } |
||||||
1133 | |||||||
1134 | /** |
||||||
1135 | * @return string name of {@see TabulatorGrid_ItemRequest} subclass |
||||||
1136 | */ |
||||||
1137 | public function getItemRequestClass(): string |
||||||
1138 | { |
||||||
1139 | if ($this->itemRequestClass) { |
||||||
1140 | return $this->itemRequestClass; |
||||||
1141 | } elseif (ClassInfo::exists(static::class . '_ItemRequest')) { |
||||||
1142 | return static::class . '_ItemRequest'; |
||||||
1143 | } |
||||||
1144 | return TabulatorGrid_ItemRequest::class; |
||||||
1145 | } |
||||||
1146 | |||||||
1147 | /** |
||||||
1148 | * Build a request handler for the given record |
||||||
1149 | * |
||||||
1150 | * @param DataObject $record |
||||||
1151 | * @param RequestHandler $requestHandler |
||||||
1152 | * @return TabulatorGrid_ItemRequest |
||||||
1153 | */ |
||||||
1154 | protected function getItemRequestHandler($record, $requestHandler) |
||||||
1155 | { |
||||||
1156 | $class = $this->getItemRequestClass(); |
||||||
1157 | $assignedClass = $this->itemRequestClass; |
||||||
1158 | $this->extend('updateItemRequestClass', $class, $record, $requestHandler, $assignedClass); |
||||||
1159 | /** @var TabulatorGrid_ItemRequest $handler */ |
||||||
1160 | $handler = Injector::inst()->createWithArgs( |
||||||
1161 | $class, |
||||||
1162 | [$this, $record, $requestHandler] |
||||||
1163 | ); |
||||||
1164 | if ($template = $this->getTemplate()) { |
||||||
1165 | $handler->setTemplate($template); |
||||||
1166 | } |
||||||
1167 | $this->extend('updateItemRequestHandler', $handler); |
||||||
1168 | return $handler; |
||||||
1169 | } |
||||||
1170 | |||||||
1171 | public function getStateKey(string $TabID = null) |
||||||
1172 | { |
||||||
1173 | $nested = []; |
||||||
1174 | $form = $this->getForm(); |
||||||
1175 | $scope = $this->modelClass ? str_replace('_', '\\', $this->modelClass) : "default"; |
||||||
1176 | if ($form) { |
||||||
0 ignored issues
–
show
|
|||||||
1177 | $controller = $form->getController(); |
||||||
1178 | |||||||
1179 | // We are in a nested form, track by id since each records needs it own state |
||||||
1180 | while ($controller instanceof TabulatorGrid_ItemRequest) { |
||||||
1181 | $record = $controller->getRecord(); |
||||||
1182 | $nested[str_replace('_', '\\', get_class($record))] = $record->ID; |
||||||
1183 | |||||||
1184 | // Move to parent controller |
||||||
1185 | $controller = $controller->getController(); |
||||||
1186 | } |
||||||
1187 | |||||||
1188 | // Scope by top controller class |
||||||
1189 | $scope = str_replace('_', '\\', get_class($controller)); |
||||||
1190 | } |
||||||
1191 | |||||||
1192 | $baseKey = 'TabulatorState'; |
||||||
1193 | if ($TabID) { |
||||||
1194 | $baseKey .= '_' . $TabID; |
||||||
1195 | } |
||||||
1196 | $name = $this->getName(); |
||||||
1197 | $key = "$baseKey.$scope.$name"; |
||||||
1198 | foreach ($nested as $k => $v) { |
||||||
1199 | $key .= "$k.$v"; |
||||||
1200 | } |
||||||
1201 | return $key; |
||||||
1202 | } |
||||||
1203 | |||||||
1204 | /** |
||||||
1205 | * @param HTTPRequest|null $request |
||||||
1206 | * @return array{'page': int, 'limit': int, 'sort': array, 'filter': array} |
||||||
1207 | */ |
||||||
1208 | public function getState(HTTPRequest $request = null) |
||||||
1209 | { |
||||||
1210 | if ($request === null) { |
||||||
1211 | $request = Controller::curr()->getRequest(); |
||||||
1212 | } |
||||||
1213 | $TabID = $request->requestVar('TabID') ?? null; |
||||||
1214 | $stateKey = $this->getStateKey($TabID); |
||||||
1215 | $state = $request->getSession()->get($stateKey); |
||||||
1216 | return $state ?? [ |
||||||
1217 | 'page' => 1, |
||||||
1218 | 'limit' => $this->pageSize, |
||||||
1219 | 'sort' => [], |
||||||
1220 | 'filter' => [], |
||||||
1221 | ]; |
||||||
1222 | } |
||||||
1223 | |||||||
1224 | public function setState(HTTPRequest $request, $state) |
||||||
1225 | { |
||||||
1226 | $TabID = $request->requestVar('TabID') ?? null; |
||||||
1227 | $stateKey = $this->getStateKey($TabID); |
||||||
1228 | $request->getSession()->set($stateKey, $state); |
||||||
1229 | // If we are in a new controller, we can clear other states |
||||||
1230 | // Note: this would break tabbed navigation if you try to open multiple tabs, see below for more info |
||||||
1231 | // @link https://github.com/silverstripe/silverstripe-framework/issues/9556 |
||||||
1232 | $matches = []; |
||||||
1233 | preg_match_all('/\.(.*?)\./', $stateKey, $matches); |
||||||
1234 | $scope = $matches[1][0] ?? null; |
||||||
1235 | if ($scope) { |
||||||
1236 | self::clearAllStates($scope); |
||||||
1237 | } |
||||||
1238 | } |
||||||
1239 | |||||||
1240 | public function clearState(HTTPRequest $request) |
||||||
1241 | { |
||||||
1242 | $TabID = $request->requestVar('TabID') ?? null; |
||||||
1243 | $stateKey = $this->getStateKey($TabID); |
||||||
1244 | $request->getSession()->clear($stateKey); |
||||||
1245 | } |
||||||
1246 | |||||||
1247 | public static function clearAllStates(string $exceptScope = null, string $TabID = null) |
||||||
1248 | { |
||||||
1249 | $request = Controller::curr()->getRequest(); |
||||||
1250 | $baseKey = 'TabulatorState'; |
||||||
1251 | if ($TabID) { |
||||||
1252 | $baseKey .= '_' . $TabID; |
||||||
1253 | } |
||||||
1254 | $allStates = $request->getSession()->get($baseKey); |
||||||
1255 | if (!$allStates) { |
||||||
1256 | return; |
||||||
1257 | } |
||||||
1258 | foreach ($allStates as $scope => $data) { |
||||||
1259 | if ($exceptScope && $scope == $exceptScope) { |
||||||
1260 | continue; |
||||||
1261 | } |
||||||
1262 | $request->getSession()->clear("TabulatorState.$scope"); |
||||||
1263 | } |
||||||
1264 | } |
||||||
1265 | |||||||
1266 | public function StateValue($key, $field): ?string |
||||||
1267 | { |
||||||
1268 | $state = $this->getState(); |
||||||
1269 | $arr = $state[$key] ?? []; |
||||||
1270 | foreach ($arr as $s) { |
||||||
1271 | if ($s['field'] === $field) { |
||||||
1272 | return $s['value']; |
||||||
1273 | } |
||||||
1274 | } |
||||||
1275 | return null; |
||||||
1276 | } |
||||||
1277 | |||||||
1278 | /** |
||||||
1279 | * Provides autocomplete lists |
||||||
1280 | * |
||||||
1281 | * @param HTTPRequest $request |
||||||
1282 | * @return HTTPResponse |
||||||
1283 | */ |
||||||
1284 | public function autocomplete(HTTPRequest $request) |
||||||
1285 | { |
||||||
1286 | if ($this->isDisabled() || $this->isReadonly()) { |
||||||
1287 | return $this->httpError(403); |
||||||
1288 | } |
||||||
1289 | $SecurityID = $request->getVar('SecurityID'); |
||||||
1290 | if (!SecurityToken::inst()->check($SecurityID)) { |
||||||
1291 | return $this->httpError(404, "Invalid SecurityID"); |
||||||
1292 | } |
||||||
1293 | |||||||
1294 | $name = $request->getVar("Column"); |
||||||
1295 | $col = $this->getColumn($name); |
||||||
0 ignored issues
–
show
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
![]() |
|||||||
1296 | if (!$col) { |
||||||
1297 | return $this->httpError(403, "Invalid column"); |
||||||
1298 | } |
||||||
1299 | |||||||
1300 | // Don't use % term as it prevents use of indexes |
||||||
1301 | $term = $request->getVar('term') . '%'; |
||||||
1302 | $term = str_replace(' ', '%', $term); |
||||||
1303 | |||||||
1304 | $parts = explode(".", $name); |
||||||
0 ignored issues
–
show
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
![]() |
|||||||
1305 | if (count($parts) > 2) { |
||||||
1306 | array_pop($parts); |
||||||
1307 | } |
||||||
1308 | if (count($parts) == 2) { |
||||||
1309 | $class = $parts[0]; |
||||||
1310 | $field = $parts[1]; |
||||||
1311 | } elseif (count($parts) == 1) { |
||||||
1312 | $class = preg_replace("/ID$/", "", $parts[0]); |
||||||
1313 | $field = 'Title'; |
||||||
1314 | } else { |
||||||
1315 | return $this->httpError(403, "Invalid field"); |
||||||
1316 | } |
||||||
1317 | |||||||
1318 | /** @var DataObject $sng */ |
||||||
1319 | $sng = $class::singleton(); |
||||||
1320 | $baseTable = $sng->baseTable(); |
||||||
1321 | |||||||
1322 | $searchField = null; |
||||||
1323 | $searchCandidates = [ |
||||||
1324 | $field, 'Name', 'Surname', 'Email', 'ID' |
||||||
1325 | ]; |
||||||
1326 | |||||||
1327 | // Ensure field exists, this is really rudimentary |
||||||
1328 | $db = $class::config()->db; |
||||||
1329 | foreach ($searchCandidates as $searchCandidate) { |
||||||
1330 | if ($searchField) { |
||||||
1331 | continue; |
||||||
1332 | } |
||||||
1333 | if (isset($db[$searchCandidate])) { |
||||||
1334 | $searchField = $searchCandidate; |
||||||
1335 | } |
||||||
1336 | } |
||||||
1337 | $searchCols = [$searchField]; |
||||||
1338 | |||||||
1339 | // For members, do something better |
||||||
1340 | if ($baseTable == 'Member') { |
||||||
1341 | $searchField = ['FirstName', 'Surname']; |
||||||
1342 | $searchCols = ['FirstName', 'Surname', 'Email']; |
||||||
1343 | } |
||||||
1344 | |||||||
1345 | if (!empty($col['editorParams']['customSearchField'])) { |
||||||
1346 | $searchField = $col['editorParams']['customSearchField']; |
||||||
1347 | } |
||||||
1348 | if (!empty($col['editorParams']['customSearchCols'])) { |
||||||
1349 | $searchCols = $col['editorParams']['customSearchCols']; |
||||||
1350 | } |
||||||
1351 | |||||||
1352 | // Note: we need to use the orm, even if it's slower, to make sure any extension is properly applied |
||||||
1353 | /** @var DataList $list */ |
||||||
1354 | $list = $sng::get(); |
||||||
1355 | |||||||
1356 | // Make sure at least one field is not null... |
||||||
1357 | $where = []; |
||||||
1358 | foreach ($searchCols as $searchCol) { |
||||||
1359 | $where[] = $searchCol . ' IS NOT NULL'; |
||||||
1360 | } |
||||||
1361 | $list = $list->where($where); |
||||||
1362 | // ... and matches search term ... |
||||||
1363 | $where = []; |
||||||
1364 | foreach ($searchCols as $searchCol) { |
||||||
1365 | $where[$searchCol . ' LIKE ?'] = $term; |
||||||
1366 | } |
||||||
1367 | $list = $list->whereAny($where); |
||||||
1368 | |||||||
1369 | // ... and any user set requirements |
||||||
1370 | if (!empty($col['editorParams']['where'])) { |
||||||
1371 | // Deal with in clause |
||||||
1372 | $customWhere = []; |
||||||
1373 | foreach ($col['editorParams']['where'] as $col => $param) { |
||||||
1374 | // For array, we need a IN statement with a ? for each value |
||||||
1375 | if (is_array($param)) { |
||||||
1376 | $prepValue = []; |
||||||
1377 | $params = []; |
||||||
1378 | foreach ($param as $paramValue) { |
||||||
1379 | $params[] = $paramValue; |
||||||
1380 | $prepValue[] = "?"; |
||||||
1381 | } |
||||||
1382 | $customWhere["$col IN (" . implode(',', $prepValue) . ")"] = $params; |
||||||
1383 | } else { |
||||||
1384 | $customWhere["$col = ?"] = $param; |
||||||
1385 | } |
||||||
1386 | } |
||||||
1387 | $list = $list->where($customWhere); |
||||||
1388 | } |
||||||
1389 | |||||||
1390 | $results = iterator_to_array($list); |
||||||
1391 | $data = []; |
||||||
1392 | foreach ($results as $record) { |
||||||
1393 | if (is_array($searchField)) { |
||||||
1394 | $labelParts = []; |
||||||
1395 | foreach ($searchField as $sf) { |
||||||
1396 | $labelParts[] = $record->$sf; |
||||||
1397 | } |
||||||
1398 | $label = implode(" ", $labelParts); |
||||||
1399 | } else { |
||||||
1400 | $label = $record->$searchField; |
||||||
1401 | } |
||||||
1402 | $data[] = [ |
||||||
1403 | 'value' => $record->ID, |
||||||
1404 | 'label' => $label, |
||||||
1405 | ]; |
||||||
1406 | } |
||||||
1407 | |||||||
1408 | $json = json_encode($data); |
||||||
1409 | $response = new HTTPResponse($json); |
||||||
1410 | $response->addHeader('Content-Type', 'application/script'); |
||||||
1411 | return $response; |
||||||
1412 | } |
||||||
1413 | |||||||
1414 | /** |
||||||
1415 | * @link http://www.tabulator.info/docs/6.2/page#remote-response |
||||||
1416 | * @param HTTPRequest $request |
||||||
1417 | * @return HTTPResponse |
||||||
1418 | */ |
||||||
1419 | public function load(HTTPRequest $request) |
||||||
1420 | { |
||||||
1421 | if ($this->isDisabled() || $this->isReadonly()) { |
||||||
1422 | return $this->httpError(403); |
||||||
1423 | } |
||||||
1424 | $SecurityID = $request->getVar('SecurityID'); |
||||||
1425 | if (!SecurityToken::inst()->check($SecurityID)) { |
||||||
1426 | return $this->httpError(404, "Invalid SecurityID"); |
||||||
1427 | } |
||||||
1428 | |||||||
1429 | $page = (int) $request->getVar('page'); |
||||||
1430 | $limit = (int) $request->getVar('size'); |
||||||
1431 | |||||||
1432 | $sort = $request->getVar('sort'); |
||||||
1433 | $filter = $request->getVar('filter'); |
||||||
1434 | |||||||
1435 | // Persist state to allow the ItemEditForm to display navigation |
||||||
1436 | $state = [ |
||||||
1437 | 'page' => $page, |
||||||
1438 | 'limit' => $limit, |
||||||
1439 | 'sort' => $sort, |
||||||
1440 | 'filter' => $filter, |
||||||
1441 | ]; |
||||||
1442 | $this->setState($request, $state); |
||||||
1443 | |||||||
1444 | $offset = ($page - 1) * $limit; |
||||||
1445 | $data = $this->getManipulatedData($limit, $offset, $sort, $filter); |
||||||
1446 | $data['state'] = $state; |
||||||
1447 | |||||||
1448 | $encodedData = json_encode($data); |
||||||
1449 | if (!$encodedData) { |
||||||
1450 | throw new Exception(json_last_error_msg()); |
||||||
1451 | } |
||||||
1452 | |||||||
1453 | $response = new HTTPResponse($encodedData); |
||||||
1454 | $response->addHeader('Content-Type', 'application/json'); |
||||||
1455 | return $response; |
||||||
1456 | } |
||||||
1457 | |||||||
1458 | /** |
||||||
1459 | * @param HTTPRequest $request |
||||||
1460 | * @return DataObject|null |
||||||
1461 | */ |
||||||
1462 | protected function getRecordFromRequest(HTTPRequest $request): ?DataObject |
||||||
1463 | { |
||||||
1464 | $id = $request->param('ID'); |
||||||
1465 | /** @var DataObject $record */ |
||||||
1466 | if (is_numeric($id)) { |
||||||
1467 | /** @var Filterable $dataList */ |
||||||
1468 | $dataList = $this->getList(); |
||||||
1469 | $record = $dataList->byID($id); |
||||||
1470 | |||||||
1471 | if (!$record) { |
||||||
1472 | $record = DataObject::get_by_id($this->getModelClass(), $id); |
||||||
0 ignored issues
–
show
$id of type string is incompatible with the type boolean|integer expected by parameter $idOrCache of SilverStripe\ORM\DataObject::get_by_id() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
1473 | if ($record) { |
||||||
1474 | throw new RuntimeException('This record is not accessible from the list'); |
||||||
1475 | } |
||||||
1476 | } |
||||||
1477 | } else { |
||||||
1478 | $record = Injector::inst()->create($this->getModelClass()); |
||||||
1479 | } |
||||||
1480 | return $record; |
||||||
1481 | } |
||||||
1482 | |||||||
1483 | /** |
||||||
1484 | * @param HTTPRequest $request |
||||||
1485 | * @return AbstractTabulatorTool|null |
||||||
1486 | */ |
||||||
1487 | protected function getToolFromRequest(HTTPRequest $request): ?AbstractTabulatorTool |
||||||
1488 | { |
||||||
1489 | $toolID = $request->param('ID'); |
||||||
1490 | $tool = $this->getTool($toolID); |
||||||
1491 | return $tool; |
||||||
1492 | } |
||||||
1493 | |||||||
1494 | /** |
||||||
1495 | * @param HTTPRequest $request |
||||||
1496 | * @return AbstractBulkAction|null |
||||||
1497 | */ |
||||||
1498 | protected function getBulkActionFromRequest(HTTPRequest $request): ?AbstractBulkAction |
||||||
1499 | { |
||||||
1500 | $toolID = $request->param('ID'); |
||||||
1501 | $tool = $this->getBulkAction($toolID); |
||||||
1502 | return $tool; |
||||||
1503 | } |
||||||
1504 | |||||||
1505 | /** |
||||||
1506 | * Get the value of a named field on the given record. |
||||||
1507 | * |
||||||
1508 | * Use of this method ensures that any special rules around the data for this gridfield are |
||||||
1509 | * followed. |
||||||
1510 | * |
||||||
1511 | * @param DataObject $record |
||||||
1512 | * @param string $fieldName |
||||||
1513 | * |
||||||
1514 | * @return mixed |
||||||
1515 | */ |
||||||
1516 | public function getDataFieldValue($record, $fieldName) |
||||||
1517 | { |
||||||
1518 | if ($record->hasMethod('relField')) { |
||||||
1519 | return $record->relField($fieldName); |
||||||
1520 | } |
||||||
1521 | |||||||
1522 | if ($record->hasMethod($fieldName)) { |
||||||
1523 | return $record->$fieldName(); |
||||||
1524 | } |
||||||
1525 | |||||||
1526 | return $record->$fieldName; |
||||||
1527 | } |
||||||
1528 | |||||||
1529 | public function getManipulatedList(): SS_List |
||||||
1530 | { |
||||||
1531 | return $this->list; |
||||||
0 ignored issues
–
show
|
|||||||
1532 | } |
||||||
1533 | |||||||
1534 | public function getList(): SS_List |
||||||
1535 | { |
||||||
1536 | return $this->list; |
||||||
0 ignored issues
–
show
|
|||||||
1537 | } |
||||||
1538 | |||||||
1539 | public function setList(SS_List $list): self |
||||||
1540 | { |
||||||
1541 | if ($this->autoloadDataList && $list instanceof DataList) { |
||||||
1542 | $this->wizardRemotePagination(); |
||||||
1543 | } |
||||||
1544 | $this->list = $list; |
||||||
1545 | return $this; |
||||||
1546 | } |
||||||
1547 | |||||||
1548 | public function hasArrayList(): bool |
||||||
1549 | { |
||||||
1550 | return $this->list instanceof ArrayList; |
||||||
1551 | } |
||||||
1552 | |||||||
1553 | public function getArrayList(): ArrayList |
||||||
1554 | { |
||||||
1555 | if (!$this->list instanceof ArrayList) { |
||||||
1556 | throw new RuntimeException("Value is not a ArrayList, it is a: " . get_class($this->list)); |
||||||
0 ignored issues
–
show
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
![]() |
|||||||
1557 | } |
||||||
1558 | return $this->list; |
||||||
0 ignored issues
–
show
|
|||||||
1559 | } |
||||||
1560 | |||||||
1561 | public function hasDataList(): bool |
||||||
1562 | { |
||||||
1563 | return $this->list instanceof DataList; |
||||||
1564 | } |
||||||
1565 | |||||||
1566 | /** |
||||||
1567 | * A properly typed on which you can call byID |
||||||
1568 | * @return ArrayList|DataList |
||||||
1569 | */ |
||||||
1570 | public function getByIDList() |
||||||
1571 | { |
||||||
1572 | return $this->list; |
||||||
1573 | } |
||||||
1574 | |||||||
1575 | public function hasByIDList(): bool |
||||||
1576 | { |
||||||
1577 | return $this->hasDataList() || $this->hasArrayList(); |
||||||
1578 | } |
||||||
1579 | |||||||
1580 | public function getDataList(): DataList |
||||||
1581 | { |
||||||
1582 | if (!$this->list instanceof DataList) { |
||||||
1583 | throw new RuntimeException("Value is not a DataList, it is a: " . get_class($this->list)); |
||||||
0 ignored issues
–
show
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
![]() |
|||||||
1584 | } |
||||||
1585 | return $this->list; |
||||||
0 ignored issues
–
show
|
|||||||
1586 | } |
||||||
1587 | |||||||
1588 | public function getManipulatedData(int $limit, int $offset, array $sort = null, array $filter = null): array |
||||||
1589 | { |
||||||
1590 | if (!$this->hasDataList()) { |
||||||
1591 | $data = $this->list->toNestedArray(); |
||||||
0 ignored issues
–
show
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
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. ![]() |
|||||||
1592 | |||||||
1593 | $lastRow = $this->list->count(); |
||||||
1594 | $lastPage = ceil($lastRow / $limit); |
||||||
1595 | |||||||
1596 | $result = [ |
||||||
1597 | 'last_row' => $lastRow, |
||||||
1598 | 'last_page' => $lastPage, |
||||||
1599 | 'data' => $data, |
||||||
1600 | ]; |
||||||
1601 | |||||||
1602 | return $result; |
||||||
1603 | } |
||||||
1604 | |||||||
1605 | $dataList = $this->getDataList(); |
||||||
1606 | |||||||
1607 | $schema = DataObject::getSchema(); |
||||||
1608 | $dataClass = $dataList->dataClass(); |
||||||
1609 | |||||||
1610 | /** @var DataObject $singleton */ |
||||||
1611 | $singleton = singleton($dataClass); |
||||||
1612 | $opts = $this->getTabulatorOptions($singleton); |
||||||
1613 | $resolutionMap = []; |
||||||
1614 | |||||||
1615 | $sortSql = []; |
||||||
1616 | if ($sort) { |
||||||
1617 | foreach ($sort as $sortValues) { |
||||||
1618 | $cols = array_keys($this->columns); |
||||||
1619 | $field = $sortValues['field']; |
||||||
1620 | $sortField = $field; |
||||||
1621 | $sortClass = $dataClass; |
||||||
1622 | if (!in_array($field, $cols)) { |
||||||
1623 | throw new Exception("Invalid sort field: $field"); |
||||||
1624 | } |
||||||
1625 | $dir = $sortValues['dir']; |
||||||
1626 | if (!in_array($dir, ['asc', 'desc'])) { |
||||||
1627 | throw new Exception("Invalid sort dir: $dir"); |
||||||
1628 | } |
||||||
1629 | |||||||
1630 | // Nested sort |
||||||
1631 | if (str_contains($field, '.')) { |
||||||
1632 | $parts = explode(".", $field); |
||||||
1633 | $relationName = $parts[0]; |
||||||
1634 | |||||||
1635 | // Resolve relation only once in case of multiples similar keys |
||||||
1636 | if (!isset($resolutionMap[$relationName])) { |
||||||
1637 | $resolutionMap[$relationName] = $singleton->relObject($relationName); |
||||||
1638 | } |
||||||
1639 | // Not matching anything (maybe a formatting .Nice ?) |
||||||
1640 | $resolvedObject = $resolutionMap[$relationName] ?? null; |
||||||
1641 | if (!$resolvedObject) { |
||||||
1642 | continue; |
||||||
1643 | } |
||||||
1644 | // Maybe it's an helper method like .Nice and it's not sortable in the query |
||||||
1645 | if (!($resolvedObject instanceof DataList) && !($resolvedObject instanceof DataObject)) { |
||||||
1646 | $field = $parts[0]; |
||||||
0 ignored issues
–
show
|
|||||||
1647 | continue; |
||||||
1648 | } |
||||||
1649 | $sortClass = get_class($resolvedObject); |
||||||
1650 | $sortField = $parts[1]; |
||||||
1651 | $tableName = $schema->tableForField($sortClass, $sortField); |
||||||
1652 | $baseIDColumn = $schema->sqlColumnForField($dataClass, 'ID'); |
||||||
1653 | $dataList = $dataList->leftJoin($tableName, "\"{$relationName}\".\"ID\" = {$baseIDColumn}", $relationName); |
||||||
1654 | } |
||||||
1655 | |||||||
1656 | // Is it an actual field or an expression ? |
||||||
1657 | $sortedField = $schema->tableForField($sortClass, $sortField); |
||||||
1658 | if ($sortedField) { |
||||||
1659 | $sortSql[] = $field . ' ' . $dir; |
||||||
1660 | } |
||||||
1661 | } |
||||||
1662 | } else { |
||||||
1663 | // If we have a sort column |
||||||
1664 | if (isset($this->columns[self::UI_SORT])) { |
||||||
1665 | $sortSql[] = $this->columns[self::UI_SORT]['field'] . ' ASC'; |
||||||
1666 | } |
||||||
1667 | } |
||||||
1668 | if (!empty($sortSql)) { |
||||||
1669 | $dataList = $dataList->sort(implode(", ", $sortSql)); |
||||||
1670 | } |
||||||
1671 | |||||||
1672 | // Filtering is an array of field/type/value arrays |
||||||
1673 | $filters = []; |
||||||
1674 | $anyFilters = []; |
||||||
1675 | $where = []; |
||||||
1676 | $anyWhere = []; |
||||||
1677 | if ($filter) { |
||||||
1678 | $searchAliases = $opts['searchAliases'] ?? []; |
||||||
1679 | $searchAliases = array_flip($searchAliases); |
||||||
1680 | foreach ($filter as $filterValues) { |
||||||
1681 | $cols = array_keys($this->columns); |
||||||
1682 | $field = $filterValues['field']; |
||||||
1683 | if (strpos($field, '__') !== 0 && !in_array($field, $cols)) { |
||||||
1684 | throw new Exception("Invalid filter field: $field"); |
||||||
1685 | } |
||||||
1686 | // If .Nice was used |
||||||
1687 | $field = str_replace('.Nice', '', $field); |
||||||
1688 | |||||||
1689 | $field = $searchAliases[$field] ?? $field; |
||||||
1690 | $value = $filterValues['value']; |
||||||
1691 | $type = $filterValues['type']; |
||||||
1692 | |||||||
1693 | // Some types of fields need custom sql expressions (eg uuids) |
||||||
1694 | $fieldInstance = $singleton->dbObject($field); |
||||||
1695 | if ($fieldInstance && $fieldInstance->hasMethod('filterExpression')) { |
||||||
1696 | $where[] = $fieldInstance->filterExpression($type, $value); |
||||||
1697 | continue; |
||||||
1698 | } |
||||||
1699 | |||||||
1700 | $rawValue = $value; |
||||||
1701 | |||||||
1702 | // Strict value |
||||||
1703 | if ($value === "true") { |
||||||
1704 | $value = true; |
||||||
1705 | } elseif ($value === "false") { |
||||||
1706 | $value = false; |
||||||
1707 | } |
||||||
1708 | |||||||
1709 | switch ($type) { |
||||||
1710 | case "=": |
||||||
1711 | if ($field === "__wildcard") { |
||||||
1712 | // It's a wildcard search |
||||||
1713 | $anyFilters = $this->createWildcardFilters($rawValue); |
||||||
1714 | } elseif ($field === "__quickfilter") { |
||||||
1715 | // It's a quickfilter search |
||||||
1716 | $this->createQuickFilter($rawValue, $dataList); |
||||||
1717 | } else { |
||||||
1718 | $filters["$field"] = $value; |
||||||
1719 | } |
||||||
1720 | break; |
||||||
1721 | case "!=": |
||||||
1722 | $filters["$field:not"] = $value; |
||||||
1723 | break; |
||||||
1724 | case "like": |
||||||
1725 | $filters["$field:PartialMatch:nocase"] = $value; |
||||||
1726 | break; |
||||||
1727 | case "keywords": |
||||||
1728 | $filters["$field:PartialMatch:nocase"] = str_replace(" ", "%", $value); |
||||||
1729 | break; |
||||||
1730 | case "starts": |
||||||
1731 | $filters["$field:StartsWith:nocase"] = $value; |
||||||
1732 | break; |
||||||
1733 | case "ends": |
||||||
1734 | $filters["$field:EndsWith:nocase"] = $value; |
||||||
1735 | break; |
||||||
1736 | case "<": |
||||||
1737 | $filters["$field:LessThan:nocase"] = $value; |
||||||
1738 | break; |
||||||
1739 | case "<=": |
||||||
1740 | $filters["$field:LessThanOrEqual:nocase"] = $value; |
||||||
1741 | break; |
||||||
1742 | case ">": |
||||||
1743 | $filters["$field:GreaterThan:nocase"] = $value; |
||||||
1744 | break; |
||||||
1745 | case ">=": |
||||||
1746 | $filters["$field:GreaterThanOrEqual:nocase"] = $value; |
||||||
1747 | break; |
||||||
1748 | case "in": |
||||||
1749 | $filters["$field"] = $value; |
||||||
1750 | break; |
||||||
1751 | case "regex": |
||||||
1752 | $dataList = $dataList->filters('REGEXP ' . Convert::raw2sql($value)); |
||||||
0 ignored issues
–
show
Are you sure
SilverStripe\Core\Convert::raw2sql($value) of type array|string can be used in concatenation ?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
1753 | break; |
||||||
1754 | default: |
||||||
1755 | throw new Exception("Invalid filter type: $type"); |
||||||
1756 | } |
||||||
1757 | } |
||||||
1758 | } |
||||||
1759 | if (!empty($filters)) { |
||||||
1760 | $dataList = $dataList->filter($filters); |
||||||
1761 | } |
||||||
1762 | if (!empty($anyFilters)) { |
||||||
1763 | $dataList = $dataList->filterAny($anyFilters); |
||||||
1764 | } |
||||||
1765 | if (!empty($where)) { |
||||||
1766 | $dataList = $dataList->where(implode(' AND ', $where)); |
||||||
1767 | } |
||||||
1768 | if (!empty($anyWhere)) { |
||||||
1769 | $dataList = $dataList->where(implode(' OR ', $anyWhere)); |
||||||
1770 | } |
||||||
1771 | |||||||
1772 | $lastRow = $dataList->count(); |
||||||
1773 | $lastPage = ceil($lastRow / $limit); |
||||||
1774 | |||||||
1775 | $data = []; |
||||||
1776 | /** @var DataObject $record */ |
||||||
1777 | foreach ($dataList->limit($limit, $offset) as $record) { |
||||||
1778 | if ($record->hasMethod('canView') && !$record->canView()) { |
||||||
1779 | continue; |
||||||
1780 | } |
||||||
1781 | |||||||
1782 | $item = [ |
||||||
1783 | 'ID' => $record->ID, |
||||||
1784 | ]; |
||||||
1785 | |||||||
1786 | // Add row class |
||||||
1787 | if ($record->hasMethod('TabulatorRowClass')) { |
||||||
1788 | $item['_class'] = $record->TabulatorRowClass(); |
||||||
1789 | } elseif ($record->hasMethod('getRowClass')) { |
||||||
1790 | $item['_class'] = $record->getRowClass(); |
||||||
1791 | } |
||||||
1792 | // Add row color |
||||||
1793 | if ($record->hasMethod('TabulatorRowColor')) { |
||||||
1794 | $item['_color'] = $record->TabulatorRowColor(); |
||||||
1795 | } |
||||||
1796 | |||||||
1797 | $nested = []; |
||||||
1798 | foreach ($this->columns as $col) { |
||||||
1799 | // UI field are skipped |
||||||
1800 | if (empty($col['field'])) { |
||||||
1801 | continue; |
||||||
1802 | } |
||||||
1803 | |||||||
1804 | $field = $col['field']; |
||||||
1805 | |||||||
1806 | // Explode relations or formatters |
||||||
1807 | if (strpos($field, '.') !== false) { |
||||||
1808 | $parts = explode('.', $field); |
||||||
1809 | $classOrField = $parts[0]; |
||||||
1810 | $relationOrMethod = $parts[1]; |
||||||
1811 | // For relations, like Users.count |
||||||
1812 | if ($singleton->getRelationClass($classOrField)) { |
||||||
1813 | $nested[$classOrField][] = $relationOrMethod; |
||||||
1814 | continue; |
||||||
1815 | } else { |
||||||
1816 | // For fields, like SomeValue.Nice |
||||||
1817 | $dbObject = $record->dbObject($classOrField); |
||||||
1818 | if ($dbObject) { |
||||||
1819 | $item[$classOrField] = [ |
||||||
1820 | $relationOrMethod => $dbObject->$relationOrMethod() |
||||||
1821 | ]; |
||||||
1822 | continue; |
||||||
1823 | } |
||||||
1824 | } |
||||||
1825 | } |
||||||
1826 | |||||||
1827 | // Do not override already set fields |
||||||
1828 | if (!isset($item[$field])) { |
||||||
1829 | $getField = 'get' . ucfirst($field); |
||||||
1830 | |||||||
1831 | if ($record->hasMethod($getField)) { |
||||||
1832 | // Prioritize getXyz method |
||||||
1833 | $item[$field] = $record->$getField(); |
||||||
1834 | } elseif ($record->hasMethod($field)) { |
||||||
1835 | // Regular xyz method method |
||||||
1836 | $item[$field] = $record->$field(); |
||||||
1837 | } else { |
||||||
1838 | // Field |
||||||
1839 | $item[$field] = $record->getField($field); |
||||||
1840 | } |
||||||
1841 | } |
||||||
1842 | } |
||||||
1843 | // Fill in nested data, like Users.count |
||||||
1844 | foreach ($nested as $nestedClass => $nestedColumns) { |
||||||
1845 | /** @var DataObject $relObject */ |
||||||
1846 | $relObject = $record->relObject($nestedClass); |
||||||
1847 | $nestedData = []; |
||||||
1848 | foreach ($nestedColumns as $nestedColumn) { |
||||||
1849 | $nestedData[$nestedColumn] = $this->getDataFieldValue($relObject, $nestedColumn); |
||||||
1850 | } |
||||||
1851 | $item[$nestedClass] = $nestedData; |
||||||
1852 | } |
||||||
1853 | $data[] = $item; |
||||||
1854 | } |
||||||
1855 | |||||||
1856 | $result = [ |
||||||
1857 | 'last_row' => $lastRow, |
||||||
1858 | 'last_page' => $lastPage, |
||||||
1859 | 'data' => $data, |
||||||
1860 | ]; |
||||||
1861 | |||||||
1862 | if (Director::isDev()) { |
||||||
1863 | $result['sql'] = $dataList->sql(); |
||||||
1864 | } |
||||||
1865 | |||||||
1866 | return $result; |
||||||
1867 | } |
||||||
1868 | |||||||
1869 | public function QuickFiltersList() |
||||||
1870 | { |
||||||
1871 | $current = $this->StateValue('filter', '__quickfilter'); |
||||||
1872 | $list = new ArrayList(); |
||||||
1873 | foreach ($this->quickFilters as $k => $v) { |
||||||
1874 | $list->push([ |
||||||
1875 | 'Value' => $k, |
||||||
1876 | 'Label' => $v['label'], |
||||||
1877 | 'Selected' => $k == $current |
||||||
1878 | ]); |
||||||
1879 | } |
||||||
1880 | return $list; |
||||||
1881 | } |
||||||
1882 | |||||||
1883 | protected function createQuickFilter($filter, &$list) |
||||||
1884 | { |
||||||
1885 | $qf = $this->quickFilters[$filter] ?? null; |
||||||
1886 | if (!$qf) { |
||||||
1887 | return; |
||||||
1888 | } |
||||||
1889 | |||||||
1890 | $callback = $qf['callback'] ?? null; |
||||||
1891 | if (!$callback) { |
||||||
1892 | return; |
||||||
1893 | } |
||||||
1894 | |||||||
1895 | $callback($list); |
||||||
1896 | } |
||||||
1897 | |||||||
1898 | protected function createWildcardFilters(string $value) |
||||||
1899 | { |
||||||
1900 | $wildcardFields = $this->wildcardFields; |
||||||
1901 | |||||||
1902 | // Create from model |
||||||
1903 | if (empty($wildcardFields)) { |
||||||
1904 | /** @var DataObject $singl */ |
||||||
1905 | $singl = singleton($this->modelClass); |
||||||
1906 | $searchableFields = $singl->searchableFields(); |
||||||
1907 | |||||||
1908 | foreach ($searchableFields as $k => $v) { |
||||||
1909 | $general = $v['general'] ?? true; |
||||||
1910 | if (!$general) { |
||||||
1911 | continue; |
||||||
1912 | } |
||||||
1913 | $wildcardFields[] = $k; |
||||||
1914 | } |
||||||
1915 | } |
||||||
1916 | |||||||
1917 | // Queries can have the format s:... or e:... or =:.... or %:.... |
||||||
1918 | $filter = $this->defaultFilter; |
||||||
1919 | if (strpos($value, ':') === 1) { |
||||||
1920 | $parts = explode(":", $value); |
||||||
1921 | $shortcut = array_shift($parts); |
||||||
1922 | $value = implode(":", $parts); |
||||||
1923 | switch ($shortcut) { |
||||||
1924 | case 's': |
||||||
1925 | $filter = 'StartsWith'; |
||||||
1926 | break; |
||||||
1927 | case 'e': |
||||||
1928 | $filter = 'EndsWith'; |
||||||
1929 | break; |
||||||
1930 | case '=': |
||||||
1931 | $filter = 'ExactMatch'; |
||||||
1932 | break; |
||||||
1933 | case '%': |
||||||
1934 | $filter = 'PartialMatch'; |
||||||
1935 | break; |
||||||
1936 | } |
||||||
1937 | } |
||||||
1938 | |||||||
1939 | // Process value |
||||||
1940 | $baseValue = $value; |
||||||
1941 | $value = str_replace(" ", "%", $value); |
||||||
1942 | $value = str_replace(['.', '_', '-'], ' ', $value); |
||||||
1943 | |||||||
1944 | // Create filters |
||||||
1945 | $anyWhere = []; |
||||||
1946 | foreach ($wildcardFields as $f) { |
||||||
1947 | if (!$value) { |
||||||
1948 | continue; |
||||||
1949 | } |
||||||
1950 | $key = $f . ":" . $filter; |
||||||
1951 | $anyWhere[$key] = $value; |
||||||
1952 | |||||||
1953 | // also look on unfiltered data |
||||||
1954 | if ($value != $baseValue) { |
||||||
1955 | $anyWhere[$key] = $baseValue; |
||||||
1956 | } |
||||||
1957 | } |
||||||
1958 | |||||||
1959 | return $anyWhere; |
||||||
1960 | } |
||||||
1961 | |||||||
1962 | public function getModelClass(): ?string |
||||||
1963 | { |
||||||
1964 | if ($this->modelClass) { |
||||||
1965 | return $this->modelClass; |
||||||
1966 | } |
||||||
1967 | if ($this->list && $this->list instanceof DataList) { |
||||||
1968 | return $this->list->dataClass(); |
||||||
1969 | } |
||||||
1970 | return null; |
||||||
1971 | } |
||||||
1972 | |||||||
1973 | public function setModelClass(string $modelClass): self |
||||||
1974 | { |
||||||
1975 | $this->modelClass = $modelClass; |
||||||
1976 | return $this; |
||||||
1977 | } |
||||||
1978 | |||||||
1979 | |||||||
1980 | public function getDataAttribute(string $k) |
||||||
1981 | { |
||||||
1982 | if (isset($this->dataAttributes[$k])) { |
||||||
1983 | return $this->dataAttributes[$k]; |
||||||
1984 | } |
||||||
1985 | return $this->getAttribute("data-$k"); |
||||||
1986 | } |
||||||
1987 | |||||||
1988 | public function setDataAttribute(string $k, $v): self |
||||||
1989 | { |
||||||
1990 | $this->dataAttributes[$k] = $v; |
||||||
1991 | return $this; |
||||||
1992 | } |
||||||
1993 | |||||||
1994 | public function dataAttributesHTML(): string |
||||||
1995 | { |
||||||
1996 | $parts = []; |
||||||
1997 | foreach ($this->dataAttributes as $k => $v) { |
||||||
1998 | if (!$v) { |
||||||
1999 | continue; |
||||||
2000 | } |
||||||
2001 | if (is_array($v)) { |
||||||
2002 | $v = json_encode($v); |
||||||
2003 | } |
||||||
2004 | $parts[] = "data-$k='$v'"; |
||||||
2005 | } |
||||||
2006 | return implode(" ", $parts); |
||||||
2007 | } |
||||||
2008 | |||||||
2009 | protected function processLink(string $url): string |
||||||
2010 | { |
||||||
2011 | // It's not necessary to process |
||||||
2012 | if ($url == '#') { |
||||||
2013 | return $url; |
||||||
2014 | } |
||||||
2015 | // It's a temporary link on the form |
||||||
2016 | if (strpos($url, 'form:') === 0) { |
||||||
2017 | if (!$this->form) { |
||||||
2018 | $controller = Controller::curr(); |
||||||
2019 | if ($controller->hasMethod('getForm')) { |
||||||
2020 | $form = $controller->getForm(); |
||||||
2021 | $this->form = $form; |
||||||
2022 | } else { |
||||||
2023 | return $url; |
||||||
2024 | } |
||||||
2025 | } |
||||||
2026 | return $this->Link(preg_replace('/^form:/', '', $url)); |
||||||
2027 | } |
||||||
2028 | // It's a temporary link on the controller |
||||||
2029 | if (strpos($url, 'controller:') === 0) { |
||||||
2030 | return $this->ControllerLink(preg_replace('/^controller:/', '', $url)); |
||||||
2031 | } |
||||||
2032 | // It's a custom protocol (mailto: etc) |
||||||
2033 | if (strpos($url, ':') !== false) { |
||||||
2034 | return $url; |
||||||
2035 | } |
||||||
2036 | return $url; |
||||||
2037 | } |
||||||
2038 | |||||||
2039 | protected function processLinks(): void |
||||||
2040 | { |
||||||
2041 | // Process editor and formatter links |
||||||
2042 | foreach ($this->columns as $name => $params) { |
||||||
2043 | if (!empty($params['formatterParams']['url'])) { |
||||||
2044 | $url = $this->processLink($params['formatterParams']['url']); |
||||||
2045 | $this->columns[$name]['formatterParams']['url'] = $url; |
||||||
2046 | } |
||||||
2047 | if (!empty($params['editorParams']['url'])) { |
||||||
2048 | $url = $this->processLink($params['editorParams']['url']); |
||||||
2049 | $this->columns[$name]['editorParams']['url'] = $url; |
||||||
2050 | } |
||||||
2051 | // Set valuesURL automatically if not already set |
||||||
2052 | if (!empty($params['editorParams']['autocomplete'])) { |
||||||
2053 | if (empty($params['editorParams']['valuesURL'])) { |
||||||
2054 | $params = [ |
||||||
2055 | 'Column' => $name, |
||||||
2056 | 'SecurityID' => SecurityToken::getSecurityID(), |
||||||
2057 | ]; |
||||||
2058 | $url = $this->Link('autocomplete') . '?' . http_build_query($params); |
||||||
2059 | $this->columns[$name]['editorParams']['valuesURL'] = $url; |
||||||
2060 | $this->columns[$name]['editorParams']['filterRemote'] = true; |
||||||
2061 | } |
||||||
2062 | } |
||||||
2063 | } |
||||||
2064 | |||||||
2065 | // Other links |
||||||
2066 | $url = $this->getOption('ajaxURL'); |
||||||
2067 | if ($url) { |
||||||
2068 | $this->setOption('ajaxURL', $this->processLink($url)); |
||||||
2069 | } |
||||||
2070 | } |
||||||
2071 | |||||||
2072 | /** |
||||||
2073 | * @link https://github.com/lekoala/formidable-elements/blob/master/src/classes/tabulator/Format/formatters/button.js |
||||||
2074 | */ |
||||||
2075 | public function makeButton(string $urlOrAction, string $icon, string $title): array |
||||||
2076 | { |
||||||
2077 | $opts = [ |
||||||
2078 | "responsive" => 0, |
||||||
2079 | "cssClass" => 'tabulator-cell-btn', |
||||||
2080 | "tooltip" => $title, |
||||||
2081 | "formatter" => "button", |
||||||
2082 | "formatterParams" => [ |
||||||
2083 | "icon" => $icon, |
||||||
2084 | "title" => $title, |
||||||
2085 | "url" => $this->TempLink($urlOrAction), // On the controller by default |
||||||
2086 | ], |
||||||
2087 | "cellClick" => ["__fn" => "SSTabulator.buttonHandler"], |
||||||
2088 | // We need to force its size otherwise Tabulator will assign too much space |
||||||
2089 | "width" => 36 + strlen($title) * 12, |
||||||
2090 | "hozAlign" => "center", |
||||||
2091 | "headerSort" => false, |
||||||
2092 | ]; |
||||||
2093 | return $opts; |
||||||
2094 | } |
||||||
2095 | |||||||
2096 | public function addButtonFromArray(string $action, array $opts = [], string $before = null): self |
||||||
2097 | { |
||||||
2098 | // Insert before given column |
||||||
2099 | if ($before) { |
||||||
2100 | $this->addColumnBefore("action_$action", $opts, $before); |
||||||
2101 | } else { |
||||||
2102 | $this->columns["action_$action"] = $opts; |
||||||
2103 | } |
||||||
2104 | return $this; |
||||||
2105 | } |
||||||
2106 | |||||||
2107 | /** |
||||||
2108 | * @param string $action Action name |
||||||
2109 | * @param string $url Parameters between {} will be interpolated by row values. |
||||||
2110 | * @param string $icon |
||||||
2111 | * @param string $title |
||||||
2112 | * @param string|null $before |
||||||
2113 | * @return self |
||||||
2114 | */ |
||||||
2115 | public function addButton(string $action, string $url, string $icon, string $title, string $before = null): self |
||||||
2116 | { |
||||||
2117 | $opts = $this->makeButton($url, $icon, $title); |
||||||
2118 | $this->addButtonFromArray($action, $opts, $before); |
||||||
2119 | return $this; |
||||||
2120 | } |
||||||
2121 | |||||||
2122 | public function addEditButton() |
||||||
2123 | { |
||||||
2124 | $itemUrl = $this->TempLink('item/{ID}', false); |
||||||
2125 | $this->addButton(self::UI_EDIT, $itemUrl, "edit", _t('TabulatorGrid.Edit', 'Edit')); |
||||||
2126 | $this->editUrl = $this->TempLink("item/{ID}/ajaxEdit", false); |
||||||
2127 | } |
||||||
2128 | |||||||
2129 | public function moveButton(string $action, $pos = self::POS_END): self |
||||||
2130 | { |
||||||
2131 | $keep = null; |
||||||
2132 | foreach ($this->columns as $k => $v) { |
||||||
2133 | if ($k == "action_$action") { |
||||||
2134 | $keep = $this->columns[$k]; |
||||||
2135 | unset($this->columns[$k]); |
||||||
2136 | } |
||||||
2137 | } |
||||||
2138 | if ($keep) { |
||||||
2139 | if ($pos == self::POS_END) { |
||||||
2140 | $this->columns["action_$action"] = $keep; |
||||||
2141 | } |
||||||
2142 | if ($pos == self::POS_START) { |
||||||
2143 | $this->columns = ["action_$action" => $keep] + $this->columns; |
||||||
2144 | } |
||||||
2145 | } |
||||||
2146 | return $this; |
||||||
2147 | } |
||||||
2148 | |||||||
2149 | public function shiftButton(string $action, string $url, string $icon, string $title): self |
||||||
2150 | { |
||||||
2151 | // Find first action |
||||||
2152 | foreach ($this->columns as $name => $options) { |
||||||
2153 | if (strpos($name, 'action_') === 0) { |
||||||
2154 | return $this->addButton($action, $url, $icon, $title, $name); |
||||||
2155 | } |
||||||
2156 | } |
||||||
2157 | return $this->addButton($action, $url, $icon, $title); |
||||||
2158 | } |
||||||
2159 | |||||||
2160 | public function getActions(): array |
||||||
2161 | { |
||||||
2162 | $cols = []; |
||||||
2163 | foreach ($this->columns as $name => $options) { |
||||||
2164 | if (strpos($name, 'action_') === 0) { |
||||||
2165 | $cols[$name] = $options; |
||||||
2166 | } |
||||||
2167 | } |
||||||
2168 | return $cols; |
||||||
2169 | } |
||||||
2170 | |||||||
2171 | public function getUiColumns(): array |
||||||
2172 | { |
||||||
2173 | $cols = []; |
||||||
2174 | foreach ($this->columns as $name => $options) { |
||||||
2175 | if (strpos($name, 'ui_') === 0) { |
||||||
2176 | $cols[$name] = $options; |
||||||
2177 | } |
||||||
2178 | } |
||||||
2179 | return $cols; |
||||||
2180 | } |
||||||
2181 | |||||||
2182 | public function getSystemColumns(): array |
||||||
2183 | { |
||||||
2184 | return array_merge($this->getActions(), $this->getUiColumns()); |
||||||
2185 | } |
||||||
2186 | |||||||
2187 | public function removeButton(string $action): self |
||||||
2188 | { |
||||||
2189 | if ($this->hasButton($action)) { |
||||||
2190 | unset($this->columns["action_$action"]); |
||||||
2191 | } |
||||||
2192 | return $this; |
||||||
2193 | } |
||||||
2194 | |||||||
2195 | public function hasButton(string $action): bool |
||||||
2196 | { |
||||||
2197 | return isset($this->columns["action_$action"]); |
||||||
2198 | } |
||||||
2199 | |||||||
2200 | /** |
||||||
2201 | * @link http://www.tabulator.info/docs/6.2/columns#definition |
||||||
2202 | * @param string $field (Required) this is the key for this column in the data array |
||||||
2203 | * @param string $title (Required) This is the title that will be displayed in the header for this column |
||||||
2204 | * @param array $opts Other options to merge in |
||||||
2205 | * @return $this |
||||||
2206 | */ |
||||||
2207 | public function addColumn(string $field, string $title = null, array $opts = []): self |
||||||
2208 | { |
||||||
2209 | if ($title === null) { |
||||||
2210 | $title = $field; |
||||||
2211 | } |
||||||
2212 | |||||||
2213 | $baseOpts = [ |
||||||
2214 | "field" => $field, |
||||||
2215 | "title" => $title, |
||||||
2216 | ]; |
||||||
2217 | |||||||
2218 | if (!empty($opts)) { |
||||||
2219 | $baseOpts = array_merge($baseOpts, $opts); |
||||||
2220 | } |
||||||
2221 | |||||||
2222 | $this->columns[$field] = $baseOpts; |
||||||
2223 | return $this; |
||||||
2224 | } |
||||||
2225 | |||||||
2226 | /** |
||||||
2227 | * @link http://www.tabulator.info/docs/6.2/columns#definition |
||||||
2228 | * @param array $opts Other options to merge in |
||||||
2229 | * @param ?string $before |
||||||
2230 | * @return $this |
||||||
2231 | */ |
||||||
2232 | public function addColumnFromArray(array $opts = [], $before = null) |
||||||
2233 | { |
||||||
2234 | if (empty($opts['field']) || !isset($opts['title'])) { |
||||||
2235 | throw new Exception("Missing field or title key"); |
||||||
2236 | } |
||||||
2237 | $field = $opts['field']; |
||||||
2238 | |||||||
2239 | if ($before) { |
||||||
2240 | $this->addColumnBefore($field, $opts, $before); |
||||||
2241 | } else { |
||||||
2242 | $this->columns[$field] = $opts; |
||||||
2243 | } |
||||||
2244 | |||||||
2245 | return $this; |
||||||
2246 | } |
||||||
2247 | |||||||
2248 | protected function addColumnBefore($field, $opts, $before) |
||||||
2249 | { |
||||||
2250 | if (array_key_exists($before, $this->columns)) { |
||||||
2251 | $new = []; |
||||||
2252 | foreach ($this->columns as $k => $value) { |
||||||
2253 | if ($k === $before) { |
||||||
2254 | $new[$field] = $opts; |
||||||
2255 | } |
||||||
2256 | $new[$k] = $value; |
||||||
2257 | } |
||||||
2258 | $this->columns = $new; |
||||||
2259 | } |
||||||
2260 | } |
||||||
2261 | |||||||
2262 | public function makeColumnEditable(string $field, string $editor = "input", array $params = []) |
||||||
2263 | { |
||||||
2264 | $col = $this->getColumn($field); |
||||||
2265 | if (!$col) { |
||||||
2266 | throw new InvalidArgumentException("$field is not a valid column"); |
||||||
2267 | } |
||||||
2268 | |||||||
2269 | switch ($editor) { |
||||||
2270 | case 'date': |
||||||
2271 | $editor = "input"; |
||||||
2272 | $params = [ |
||||||
2273 | 'mask' => "9999-99-99", |
||||||
2274 | 'maskAutoFill' => 'true', |
||||||
2275 | ]; |
||||||
2276 | break; |
||||||
2277 | case 'datetime': |
||||||
2278 | $editor = "input"; |
||||||
2279 | $params = [ |
||||||
2280 | 'mask' => "9999-99-99 99:99:99", |
||||||
2281 | 'maskAutoFill' => 'true', |
||||||
2282 | ]; |
||||||
2283 | break; |
||||||
2284 | } |
||||||
2285 | |||||||
2286 | if (empty($col['cssClass'])) { |
||||||
2287 | $col['cssClass'] = 'no-change-track'; |
||||||
2288 | } else { |
||||||
2289 | $col['cssClass'] .= ' no-change-track'; |
||||||
2290 | } |
||||||
2291 | |||||||
2292 | $col['editor'] = $editor; |
||||||
2293 | $col['editorParams'] = $params; |
||||||
2294 | if ($editor == "list") { |
||||||
2295 | if (!empty($params['autocomplete'])) { |
||||||
2296 | $col['headerFilter'] = "input"; // force input |
||||||
2297 | } else { |
||||||
2298 | $col['headerFilterParams'] = $params; // editor is used as base filter editor |
||||||
2299 | } |
||||||
2300 | } |
||||||
2301 | |||||||
2302 | |||||||
2303 | $this->setColumn($field, $col); |
||||||
2304 | } |
||||||
2305 | |||||||
2306 | /** |
||||||
2307 | * Get column details |
||||||
2308 | |||||||
2309 | * @param string $key |
||||||
2310 | */ |
||||||
2311 | public function getColumn(string $key): ?array |
||||||
2312 | { |
||||||
2313 | if (isset($this->columns[$key])) { |
||||||
2314 | return $this->columns[$key]; |
||||||
2315 | } |
||||||
2316 | return null; |
||||||
2317 | } |
||||||
2318 | |||||||
2319 | /** |
||||||
2320 | * Set column details |
||||||
2321 | * |
||||||
2322 | * @param string $key |
||||||
2323 | * @param array $col |
||||||
2324 | */ |
||||||
2325 | public function setColumn(string $key, array $col): self |
||||||
2326 | { |
||||||
2327 | $this->columns[$key] = $col; |
||||||
2328 | return $this; |
||||||
2329 | } |
||||||
2330 | |||||||
2331 | /** |
||||||
2332 | * Update column details |
||||||
2333 | * |
||||||
2334 | * @param string $key |
||||||
2335 | * @param array $col |
||||||
2336 | */ |
||||||
2337 | public function updateColumn(string $key, array $col): self |
||||||
2338 | { |
||||||
2339 | $data = $this->getColumn($key); |
||||||
2340 | if ($data) { |
||||||
2341 | $this->setColumn($key, array_merge($data, $col)); |
||||||
2342 | } |
||||||
2343 | return $this; |
||||||
2344 | } |
||||||
2345 | |||||||
2346 | /** |
||||||
2347 | * Remove a column |
||||||
2348 | * |
||||||
2349 | * @param string $key |
||||||
2350 | */ |
||||||
2351 | public function removeColumn(string $key): void |
||||||
2352 | { |
||||||
2353 | unset($this->columns[$key]); |
||||||
2354 | } |
||||||
2355 | |||||||
2356 | /** |
||||||
2357 | * Remove a column |
||||||
2358 | * |
||||||
2359 | * @param array $keys |
||||||
2360 | */ |
||||||
2361 | public function removeColumns(array $keys): void |
||||||
2362 | { |
||||||
2363 | foreach ($keys as $key) { |
||||||
2364 | $this->removeColumn($key); |
||||||
2365 | } |
||||||
2366 | } |
||||||
2367 | |||||||
2368 | /** |
||||||
2369 | * Get the value of columns |
||||||
2370 | */ |
||||||
2371 | public function getColumns(): array |
||||||
2372 | { |
||||||
2373 | return $this->columns; |
||||||
2374 | } |
||||||
2375 | |||||||
2376 | /** |
||||||
2377 | * Set the value of columns |
||||||
2378 | */ |
||||||
2379 | public function setColumns(array $columns): self |
||||||
2380 | { |
||||||
2381 | $this->columns = $columns; |
||||||
2382 | return $this; |
||||||
2383 | } |
||||||
2384 | |||||||
2385 | public function clearColumns(bool $keepSystem = true): void |
||||||
2386 | { |
||||||
2387 | $sysNames = array_keys($this->getSystemColumns()); |
||||||
2388 | foreach ($this->columns as $k => $v) { |
||||||
2389 | if ($keepSystem && in_array($k, $sysNames)) { |
||||||
2390 | continue; |
||||||
2391 | } |
||||||
2392 | $this->removeColumn($k); |
||||||
2393 | } |
||||||
2394 | } |
||||||
2395 | |||||||
2396 | /** |
||||||
2397 | * This should be the rough equivalent to GridFieldDataColumns::getDisplayFields |
||||||
2398 | */ |
||||||
2399 | public function getDisplayFields(): array |
||||||
2400 | { |
||||||
2401 | $fields = []; |
||||||
2402 | foreach ($this->columns as $col) { |
||||||
2403 | if (empty($col['field'])) { |
||||||
2404 | continue; |
||||||
2405 | } |
||||||
2406 | $fields[$col['field']] = $col['title']; |
||||||
2407 | } |
||||||
2408 | return $fields; |
||||||
2409 | } |
||||||
2410 | |||||||
2411 | /** |
||||||
2412 | * This should be the rough equivalent to GridFieldDataColumns::setDisplayFields |
||||||
2413 | */ |
||||||
2414 | public function setDisplayFields(array $arr): void |
||||||
2415 | { |
||||||
2416 | $currentCols = $this->columns; |
||||||
2417 | $this->clearColumns(); |
||||||
2418 | $actions = array_keys($this->getActions()); |
||||||
2419 | $before = $actions[0] ?? null; |
||||||
2420 | foreach ($arr as $k => $v) { |
||||||
2421 | if (!$k || !$v) { |
||||||
2422 | continue; |
||||||
2423 | } |
||||||
2424 | $currentCol = $currentCols[$k] ?? [ |
||||||
2425 | 'headerSort' => false, |
||||||
2426 | ]; |
||||||
2427 | $this->addColumnFromArray(array_merge($currentCol, [ |
||||||
2428 | 'field' => $k, |
||||||
2429 | 'title' => $v, |
||||||
2430 | ]), $before); |
||||||
2431 | } |
||||||
2432 | } |
||||||
2433 | |||||||
2434 | /** |
||||||
2435 | * Convenience method that get/set fields |
||||||
2436 | */ |
||||||
2437 | public function addDisplayFields(array $arr): void |
||||||
2438 | { |
||||||
2439 | $fields = $this->getDisplayFields(); |
||||||
2440 | $fields = array_merge($fields, $arr); |
||||||
2441 | $this->setDisplayFields($fields); |
||||||
2442 | } |
||||||
2443 | |||||||
2444 | /** |
||||||
2445 | * @param string|AbstractTabulatorTool $tool Pass name or class |
||||||
2446 | * @return AbstractTabulatorTool|null |
||||||
2447 | */ |
||||||
2448 | public function getTool($tool): ?AbstractTabulatorTool |
||||||
2449 | { |
||||||
2450 | if (is_object($tool)) { |
||||||
2451 | $tool = get_class($tool); |
||||||
2452 | } |
||||||
2453 | if (!is_string($tool)) { |
||||||
0 ignored issues
–
show
|
|||||||
2454 | throw new InvalidArgumentException('Tool must be an object or a class name'); |
||||||
2455 | } |
||||||
2456 | foreach ($this->tools as $t) { |
||||||
2457 | if ($t['name'] === $tool) { |
||||||
2458 | return $t['tool']; |
||||||
2459 | } |
||||||
2460 | if ($t['tool'] instanceof $tool) { |
||||||
2461 | return $t['tool']; |
||||||
2462 | } |
||||||
2463 | } |
||||||
2464 | return null; |
||||||
2465 | } |
||||||
2466 | |||||||
2467 | /** |
||||||
2468 | * @param string $pos start|end |
||||||
2469 | * @param AbstractTabulatorTool $tool |
||||||
2470 | * @param string $name |
||||||
2471 | * @return self |
||||||
2472 | */ |
||||||
2473 | public function addTool(string $pos, AbstractTabulatorTool $tool, string $name = ''): self |
||||||
2474 | { |
||||||
2475 | $tool->setTabulatorGrid($this); |
||||||
2476 | if ($tool->getName() && !$name) { |
||||||
2477 | $name = $tool->getName(); |
||||||
2478 | } |
||||||
2479 | $tool->setName($name); |
||||||
2480 | |||||||
2481 | $this->tools[] = [ |
||||||
2482 | 'position' => $pos, |
||||||
2483 | 'tool' => $tool, |
||||||
2484 | 'name' => $name, |
||||||
2485 | ]; |
||||||
2486 | return $this; |
||||||
2487 | } |
||||||
2488 | |||||||
2489 | public function addToolStart(AbstractTabulatorTool $tool, string $name = ''): self |
||||||
2490 | { |
||||||
2491 | return $this->addTool(self::POS_START, $tool, $name); |
||||||
2492 | } |
||||||
2493 | |||||||
2494 | public function addToolEnd(AbstractTabulatorTool $tool, string $name = ''): self |
||||||
2495 | { |
||||||
2496 | return $this->addTool(self::POS_END, $tool, $name); |
||||||
2497 | } |
||||||
2498 | |||||||
2499 | public function removeTool($toolName): self |
||||||
2500 | { |
||||||
2501 | if (is_object($toolName)) { |
||||||
2502 | $toolName = get_class($toolName); |
||||||
2503 | } |
||||||
2504 | if (!is_string($toolName)) { |
||||||
2505 | throw new InvalidArgumentException('Tool must be an object or a class name'); |
||||||
2506 | } |
||||||
2507 | foreach ($this->tools as $idx => $tool) { |
||||||
2508 | if ($tool['name'] === $toolName) { |
||||||
2509 | unset($this->tools[$idx]); |
||||||
2510 | } |
||||||
2511 | if (class_exists($toolName) && $tool['tool'] instanceof $toolName) { |
||||||
2512 | unset($this->tools[$idx]); |
||||||
2513 | } |
||||||
2514 | } |
||||||
2515 | return $this; |
||||||
2516 | } |
||||||
2517 | |||||||
2518 | /** |
||||||
2519 | * @param string|AbstractBulkAction $bulkAction Pass name or class |
||||||
2520 | * @return AbstractBulkAction|null |
||||||
2521 | */ |
||||||
2522 | public function getBulkAction($bulkAction): ?AbstractBulkAction |
||||||
2523 | { |
||||||
2524 | if (is_object($bulkAction)) { |
||||||
2525 | $bulkAction = get_class($bulkAction); |
||||||
2526 | } |
||||||
2527 | if (!is_string($bulkAction)) { |
||||||
0 ignored issues
–
show
|
|||||||
2528 | throw new InvalidArgumentException('BulkAction must be an object or a class name'); |
||||||
2529 | } |
||||||
2530 | foreach ($this->bulkActions as $ba) { |
||||||
2531 | if ($ba->getName() == $bulkAction) { |
||||||
2532 | return $ba; |
||||||
2533 | } |
||||||
2534 | if ($ba instanceof $bulkAction) { |
||||||
2535 | return $ba; |
||||||
2536 | } |
||||||
2537 | } |
||||||
2538 | return null; |
||||||
2539 | } |
||||||
2540 | |||||||
2541 | public function getBulkActions(): array |
||||||
2542 | { |
||||||
2543 | return $this->bulkActions; |
||||||
2544 | } |
||||||
2545 | |||||||
2546 | /** |
||||||
2547 | * @param AbstractBulkAction[] $bulkActions |
||||||
2548 | * @return self |
||||||
2549 | */ |
||||||
2550 | public function setBulkActions(array $bulkActions): self |
||||||
2551 | { |
||||||
2552 | foreach ($bulkActions as $bulkAction) { |
||||||
2553 | $bulkAction->setTabulatorGrid($this); |
||||||
2554 | } |
||||||
2555 | $this->bulkActions = $bulkActions; |
||||||
2556 | return $this; |
||||||
2557 | } |
||||||
2558 | |||||||
2559 | /** |
||||||
2560 | * If you didn't before, you probably want to call wizardSelectable |
||||||
2561 | * to get the actual selection checkbox too |
||||||
2562 | * |
||||||
2563 | * @param AbstractBulkAction $handler |
||||||
2564 | * @return self |
||||||
2565 | */ |
||||||
2566 | public function addBulkAction(AbstractBulkAction $handler): self |
||||||
2567 | { |
||||||
2568 | $handler->setTabulatorGrid($this); |
||||||
2569 | |||||||
2570 | $this->bulkActions[] = $handler; |
||||||
2571 | return $this; |
||||||
2572 | } |
||||||
2573 | |||||||
2574 | public function removeBulkAction($bulkAction): self |
||||||
2575 | { |
||||||
2576 | if (is_object($bulkAction)) { |
||||||
2577 | $bulkAction = get_class($bulkAction); |
||||||
2578 | } |
||||||
2579 | if (!is_string($bulkAction)) { |
||||||
2580 | throw new InvalidArgumentException('Bulk action must be an object or a class name'); |
||||||
2581 | } |
||||||
2582 | foreach ($this->bulkActions as $idx => $ba) { |
||||||
2583 | if ($ba->getName() == $bulkAction) { |
||||||
2584 | unset($this->bulkAction[$idx]); |
||||||
0 ignored issues
–
show
The property
bulkAction does not exist on LeKoala\Tabulator\TabulatorGrid . Since you implemented __get , consider adding a @property annotation.
![]() |
|||||||
2585 | } |
||||||
2586 | if ($ba instanceof $bulkAction) { |
||||||
2587 | unset($this->bulkAction[$idx]); |
||||||
2588 | } |
||||||
2589 | } |
||||||
2590 | return $this; |
||||||
2591 | } |
||||||
2592 | |||||||
2593 | public function getColumnDefault(string $opt) |
||||||
2594 | { |
||||||
2595 | return $this->columnDefaults[$opt] ?? null; |
||||||
2596 | } |
||||||
2597 | |||||||
2598 | public function setColumnDefault(string $opt, $value) |
||||||
2599 | { |
||||||
2600 | $this->columnDefaults[$opt] = $value; |
||||||
2601 | } |
||||||
2602 | |||||||
2603 | public function getColumnDefaults(): array |
||||||
2604 | { |
||||||
2605 | return $this->columnDefaults; |
||||||
2606 | } |
||||||
2607 | |||||||
2608 | public function setColumnDefaults(array $columnDefaults): self |
||||||
2609 | { |
||||||
2610 | $this->columnDefaults = $columnDefaults; |
||||||
2611 | return $this; |
||||||
2612 | } |
||||||
2613 | |||||||
2614 | public function getListeners(): array |
||||||
2615 | { |
||||||
2616 | return $this->listeners; |
||||||
2617 | } |
||||||
2618 | |||||||
2619 | public function setListeners(array $listeners): self |
||||||
2620 | { |
||||||
2621 | $this->listeners = $listeners; |
||||||
2622 | return $this; |
||||||
2623 | } |
||||||
2624 | |||||||
2625 | public function addListener(string $event, string $functionName): self |
||||||
2626 | { |
||||||
2627 | $this->listeners[$event] = $functionName; |
||||||
2628 | return $this; |
||||||
2629 | } |
||||||
2630 | |||||||
2631 | public function removeListener(string $event): self |
||||||
2632 | { |
||||||
2633 | if (isset($this->listeners[$event])) { |
||||||
2634 | unset($this->listeners[$event]); |
||||||
2635 | } |
||||||
2636 | return $this; |
||||||
2637 | } |
||||||
2638 | |||||||
2639 | public function getLinksOptions(): array |
||||||
2640 | { |
||||||
2641 | return $this->linksOptions; |
||||||
2642 | } |
||||||
2643 | |||||||
2644 | public function setLinksOptions(array $linksOptions): self |
||||||
2645 | { |
||||||
2646 | $this->linksOptions = $linksOptions; |
||||||
2647 | return $this; |
||||||
2648 | } |
||||||
2649 | |||||||
2650 | public function registerLinkOption(string $linksOption): self |
||||||
2651 | { |
||||||
2652 | $this->linksOptions[] = $linksOption; |
||||||
2653 | return $this; |
||||||
2654 | } |
||||||
2655 | |||||||
2656 | public function unregisterLinkOption(string $linksOption): self |
||||||
2657 | { |
||||||
2658 | $this->linksOptions = array_diff($this->linksOptions, [$linksOption]); |
||||||
2659 | return $this; |
||||||
2660 | } |
||||||
2661 | |||||||
2662 | /** |
||||||
2663 | * Get the value of pageSize |
||||||
2664 | */ |
||||||
2665 | public function getPageSize(): int |
||||||
2666 | { |
||||||
2667 | return $this->pageSize; |
||||||
2668 | } |
||||||
2669 | |||||||
2670 | /** |
||||||
2671 | * Set the value of pageSize |
||||||
2672 | * |
||||||
2673 | * @param int $pageSize |
||||||
2674 | */ |
||||||
2675 | public function setPageSize(int $pageSize): self |
||||||
2676 | { |
||||||
2677 | $this->pageSize = $pageSize; |
||||||
2678 | return $this; |
||||||
2679 | } |
||||||
2680 | |||||||
2681 | /** |
||||||
2682 | * Get the value of autoloadDataList |
||||||
2683 | */ |
||||||
2684 | public function getAutoloadDataList(): bool |
||||||
2685 | { |
||||||
2686 | return $this->autoloadDataList; |
||||||
2687 | } |
||||||
2688 | |||||||
2689 | /** |
||||||
2690 | * Set the value of autoloadDataList |
||||||
2691 | * |
||||||
2692 | * @param bool $autoloadDataList |
||||||
2693 | */ |
||||||
2694 | public function setAutoloadDataList(bool $autoloadDataList): self |
||||||
2695 | { |
||||||
2696 | $this->autoloadDataList = $autoloadDataList; |
||||||
2697 | return $this; |
||||||
2698 | } |
||||||
2699 | |||||||
2700 | /** |
||||||
2701 | * Set the value of itemRequestClass |
||||||
2702 | */ |
||||||
2703 | public function setItemRequestClass(string $itemRequestClass): self |
||||||
2704 | { |
||||||
2705 | $this->itemRequestClass = $itemRequestClass; |
||||||
2706 | return $this; |
||||||
2707 | } |
||||||
2708 | |||||||
2709 | /** |
||||||
2710 | * Get the value of lazyInit |
||||||
2711 | */ |
||||||
2712 | public function getLazyInit(): bool |
||||||
2713 | { |
||||||
2714 | return $this->lazyInit; |
||||||
2715 | } |
||||||
2716 | |||||||
2717 | /** |
||||||
2718 | * Set the value of lazyInit |
||||||
2719 | */ |
||||||
2720 | public function setLazyInit(bool $lazyInit): self |
||||||
2721 | { |
||||||
2722 | $this->lazyInit = $lazyInit; |
||||||
2723 | return $this; |
||||||
2724 | } |
||||||
2725 | |||||||
2726 | /** |
||||||
2727 | * Get the value of rowClickTriggersAction |
||||||
2728 | */ |
||||||
2729 | public function getRowClickTriggersAction(): bool |
||||||
2730 | { |
||||||
2731 | return $this->rowClickTriggersAction; |
||||||
2732 | } |
||||||
2733 | |||||||
2734 | /** |
||||||
2735 | * Set the value of rowClickTriggersAction |
||||||
2736 | */ |
||||||
2737 | public function setRowClickTriggersAction(bool $rowClickTriggersAction): self |
||||||
2738 | { |
||||||
2739 | $this->rowClickTriggersAction = $rowClickTriggersAction; |
||||||
2740 | return $this; |
||||||
2741 | } |
||||||
2742 | |||||||
2743 | /** |
||||||
2744 | * Get the value of controllerFunction |
||||||
2745 | */ |
||||||
2746 | public function getControllerFunction(): string |
||||||
2747 | { |
||||||
2748 | if (!$this->controllerFunction) { |
||||||
2749 | return $this->getName() ?? "TabulatorGrid"; |
||||||
2750 | } |
||||||
2751 | return $this->controllerFunction; |
||||||
2752 | } |
||||||
2753 | |||||||
2754 | /** |
||||||
2755 | * Set the value of controllerFunction |
||||||
2756 | */ |
||||||
2757 | public function setControllerFunction(string $controllerFunction): self |
||||||
2758 | { |
||||||
2759 | $this->controllerFunction = $controllerFunction; |
||||||
2760 | return $this; |
||||||
2761 | } |
||||||
2762 | |||||||
2763 | /** |
||||||
2764 | * Get the value of editUrl |
||||||
2765 | */ |
||||||
2766 | public function getEditUrl(): string |
||||||
2767 | { |
||||||
2768 | return $this->editUrl; |
||||||
2769 | } |
||||||
2770 | |||||||
2771 | /** |
||||||
2772 | * Set the value of editUrl |
||||||
2773 | */ |
||||||
2774 | public function setEditUrl(string $editUrl): self |
||||||
2775 | { |
||||||
2776 | $this->editUrl = $editUrl; |
||||||
2777 | return $this; |
||||||
2778 | } |
||||||
2779 | |||||||
2780 | /** |
||||||
2781 | * Get the value of moveUrl |
||||||
2782 | */ |
||||||
2783 | public function getMoveUrl(): string |
||||||
2784 | { |
||||||
2785 | return $this->moveUrl; |
||||||
2786 | } |
||||||
2787 | |||||||
2788 | /** |
||||||
2789 | * Set the value of moveUrl |
||||||
2790 | */ |
||||||
2791 | public function setMoveUrl(string $moveUrl): self |
||||||
2792 | { |
||||||
2793 | $this->moveUrl = $moveUrl; |
||||||
2794 | return $this; |
||||||
2795 | } |
||||||
2796 | |||||||
2797 | /** |
||||||
2798 | * Get the value of bulkUrl |
||||||
2799 | */ |
||||||
2800 | public function getBulkUrl(): string |
||||||
2801 | { |
||||||
2802 | return $this->bulkUrl; |
||||||
2803 | } |
||||||
2804 | |||||||
2805 | /** |
||||||
2806 | * Set the value of bulkUrl |
||||||
2807 | */ |
||||||
2808 | public function setBulkUrl(string $bulkUrl): self |
||||||
2809 | { |
||||||
2810 | $this->bulkUrl = $bulkUrl; |
||||||
2811 | return $this; |
||||||
2812 | } |
||||||
2813 | |||||||
2814 | /** |
||||||
2815 | * Get the value of globalSearch |
||||||
2816 | */ |
||||||
2817 | public function getGlobalSearch(): bool |
||||||
2818 | { |
||||||
2819 | return $this->globalSearch; |
||||||
2820 | } |
||||||
2821 | |||||||
2822 | /** |
||||||
2823 | * Set the value of globalSearch |
||||||
2824 | * |
||||||
2825 | * @param bool $globalSearch |
||||||
2826 | */ |
||||||
2827 | public function setGlobalSearch($globalSearch): self |
||||||
2828 | { |
||||||
2829 | $this->globalSearch = $globalSearch; |
||||||
2830 | return $this; |
||||||
2831 | } |
||||||
2832 | |||||||
2833 | /** |
||||||
2834 | * Get the value of wildcardFields |
||||||
2835 | */ |
||||||
2836 | public function getWildcardFields(): array |
||||||
2837 | { |
||||||
2838 | return $this->wildcardFields; |
||||||
2839 | } |
||||||
2840 | |||||||
2841 | /** |
||||||
2842 | * Set the value of wildcardFields |
||||||
2843 | * |
||||||
2844 | * @param array $wildcardFields |
||||||
2845 | */ |
||||||
2846 | public function setWildcardFields($wildcardFields): self |
||||||
2847 | { |
||||||
2848 | $this->wildcardFields = $wildcardFields; |
||||||
2849 | return $this; |
||||||
2850 | } |
||||||
2851 | |||||||
2852 | /** |
||||||
2853 | * Get the value of quickFilters |
||||||
2854 | */ |
||||||
2855 | public function getQuickFilters(): array |
||||||
2856 | { |
||||||
2857 | return $this->quickFilters; |
||||||
2858 | } |
||||||
2859 | |||||||
2860 | /** |
||||||
2861 | * Pass an array with as a key, the name of the filter |
||||||
2862 | * and as a value, an array with two keys: label and callback |
||||||
2863 | * |
||||||
2864 | * For example: |
||||||
2865 | * 'myquickfilter' => [ |
||||||
2866 | * 'label' => 'My Quick Filter', |
||||||
2867 | * 'callback' => function (&$list) { |
||||||
2868 | * ... |
||||||
2869 | * } |
||||||
2870 | * ] |
||||||
2871 | * |
||||||
2872 | * @param array $quickFilters |
||||||
2873 | */ |
||||||
2874 | public function setQuickFilters($quickFilters): self |
||||||
2875 | { |
||||||
2876 | $this->quickFilters = $quickFilters; |
||||||
2877 | return $this; |
||||||
2878 | } |
||||||
2879 | |||||||
2880 | /** |
||||||
2881 | * Get the value of groupLayout |
||||||
2882 | */ |
||||||
2883 | public function getGroupLayout(): bool |
||||||
2884 | { |
||||||
2885 | return $this->groupLayout; |
||||||
2886 | } |
||||||
2887 | |||||||
2888 | /** |
||||||
2889 | * Set the value of groupLayout |
||||||
2890 | * |
||||||
2891 | * @param bool $groupLayout |
||||||
2892 | */ |
||||||
2893 | public function setGroupLayout($groupLayout): self |
||||||
2894 | { |
||||||
2895 | $this->groupLayout = $groupLayout; |
||||||
2896 | return $this; |
||||||
2897 | } |
||||||
2898 | |||||||
2899 | /** |
||||||
2900 | * Get the value of enableGridManipulation |
||||||
2901 | */ |
||||||
2902 | public function getEnableGridManipulation(): bool |
||||||
2903 | { |
||||||
2904 | return $this->enableGridManipulation; |
||||||
2905 | } |
||||||
2906 | |||||||
2907 | /** |
||||||
2908 | * Set the value of enableGridManipulation |
||||||
2909 | * |
||||||
2910 | * @param bool $enableGridManipulation |
||||||
2911 | */ |
||||||
2912 | public function setEnableGridManipulation($enableGridManipulation): self |
||||||
2913 | { |
||||||
2914 | $this->enableGridManipulation = $enableGridManipulation; |
||||||
2915 | return $this; |
||||||
2916 | } |
||||||
2917 | |||||||
2918 | /** |
||||||
2919 | * Get the value of defaultFilter |
||||||
2920 | */ |
||||||
2921 | public function getDefaultFilter(): string |
||||||
2922 | { |
||||||
2923 | return $this->defaultFilter; |
||||||
2924 | } |
||||||
2925 | |||||||
2926 | /** |
||||||
2927 | * Set the value of defaultFilter |
||||||
2928 | * |
||||||
2929 | * @param string $defaultFilter |
||||||
2930 | */ |
||||||
2931 | public function setDefaultFilter($defaultFilter): self |
||||||
2932 | { |
||||||
2933 | $this->defaultFilter = $defaultFilter; |
||||||
2934 | return $this; |
||||||
2935 | } |
||||||
2936 | } |
||||||
2937 |