1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @link http://www.newicon.net/ |
4
|
|
|
* @copyright Copyright (c) 2015-18 Newicon Ltd |
5
|
|
|
* @license http://www.newicon.net/neon/framework/license/ |
6
|
|
|
* @author Steve O'Brien <[email protected]> |
7
|
|
|
* @date 12/12/2015 16:13 |
8
|
|
|
*/ |
9
|
|
|
|
10
|
|
|
namespace neon\core\grid; |
11
|
|
|
|
12
|
|
|
use neon\core\form\Form; |
13
|
|
|
use neon\core\grid\column\Column; |
14
|
|
|
use neon\core\grid\column\IColumn; |
15
|
|
|
use neon\core\helpers\Arr; |
16
|
|
|
use neon\core\helpers\Hash; |
17
|
|
|
use neon\core\interfaces\IProperties; |
18
|
|
|
use neon\core\traits\PropertiesTrait; |
19
|
|
|
use neon\core\web\View; |
20
|
|
|
use yii\base\Component; |
21
|
|
|
use yii\base\InvalidConfigException; |
22
|
|
|
use \yii\helpers\ArrayHelper; |
23
|
|
|
use \neon\core\grid\query\IQuery; |
24
|
|
|
use ReflectionClass; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* The Grid class inherit from this class to create your own advanced neon Grids |
28
|
|
|
* |
29
|
|
|
* The grid goes through 4 main stages to render: |
30
|
|
|
* 1: $this->init(): function is called, you can add columns and settings but its cleaner to use the configure function |
31
|
|
|
* 2: $this->configure(): function is called, this is where you can configure the columns by adding them to the grid |
32
|
|
|
* (3: Deprecated $this->load($_REQUEST): data pertinent to the grid is loaded. Typically this is the request data) |
33
|
|
|
* 4: $this->setup(): the setup function is called, this allows customisation of the grid and columns based on the request data |
34
|
|
|
* 5: $this->run(): Finally run processes and renders the grid |
35
|
|
|
* |
36
|
|
|
* See the self::gridBoot function for more information |
37
|
|
|
* |
38
|
|
|
* If you want properties and filters to persist then make them public properties in child classes for example: |
39
|
|
|
* ```php |
40
|
|
|
* class MyGrid extends Grid { |
41
|
|
|
* public $projectId; |
42
|
|
|
* public function setup() { |
43
|
|
|
* // add query |
44
|
|
|
* $this->getQueryBuilder()->where('project', '=', $this->$projectId); |
45
|
|
|
* } |
46
|
|
|
* } |
47
|
|
|
* ``` |
48
|
|
|
* The grid data persists between requests and setup will be called multiple times - therefore make sure the query |
49
|
|
|
* is built fresh with each call to setup. |
50
|
|
|
* |
51
|
|
|
* @package neon\core\grid |
52
|
|
|
* @author Steve O'Brien <[email protected]> |
53
|
|
|
* |
54
|
|
|
* @property \yii\data\DataProviderInterface $dataProvider |
55
|
|
|
* @property string $id - the grid ID @see $this->getId() |
56
|
|
|
*/ |
57
|
|
|
abstract class GridBase extends Component implements IProperties |
58
|
|
|
{ |
59
|
|
|
use PropertiesTrait; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @inheritdoc |
63
|
|
|
*/ |
64
|
|
|
public function __construct($config = []) |
65
|
|
|
{ |
66
|
|
|
parent::__construct($config); |
67
|
|
|
$this->configure(); |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @var array the configuration for the pager widget. By default, [[LinkPager]] will be |
72
|
|
|
* used to render the pager. You can use a different widget class by configuring the "class" element. |
73
|
|
|
* Note that the widget must support the `pagination` property which will be populated with the |
74
|
|
|
* [[\yii\data\BaseDataProvider::pagination|pagination]] value of the [[dataProvider]]. |
75
|
|
|
* @var \yii\widgets\LinkPager |
76
|
|
|
*/ |
77
|
|
|
public $pager = ['hideOnSinglePage' => false]; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Whether to stripe odd/even rows |
81
|
|
|
* @var bool |
82
|
|
|
*/ |
83
|
|
|
public $stripedRows = true; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Allows to specify a theme name for the grid |
87
|
|
|
* This simply adds the theme string as a css class to the base theme element |
88
|
|
|
* @var string |
89
|
|
|
*/ |
90
|
|
|
public $theme = ''; |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Set whether the grid fills the width of the container or can overflow |
94
|
|
|
* @var boolean |
95
|
|
|
*/ |
96
|
|
|
public $fillWidth = false; |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* TODO: this should not be accessible by the grid directly - use the data provider |
100
|
|
|
* child classes can "know" that the dataprovider has a query object and therefore access it this way |
101
|
|
|
* You can use $this->grid->getDataProvider()->query or use the getDataProvider()->getQueryBuilder() for standard |
102
|
|
|
* filter options |
103
|
|
|
* @deprecated |
104
|
|
|
* @var \yii\db\ActiveQuery |
105
|
|
|
*/ |
106
|
|
|
public $query; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* @var \yii\data\ActiveDataProvider |
110
|
|
|
*/ |
111
|
|
|
protected $_dataProvider; |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Whether we have a data provider |
115
|
|
|
* @return bool |
116
|
|
|
*/ |
117
|
|
|
public function hasDataProvider() |
118
|
|
|
{ |
119
|
|
|
return $this->_dataProvider === null ? false : true; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Store column objects |
124
|
|
|
* @var array |
125
|
|
|
*/ |
126
|
|
|
protected $_columns = []; |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Store filters |
130
|
|
|
* @var array |
131
|
|
|
*/ |
132
|
|
|
protected $_scopes = []; |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* The view file to use to render the grid body |
136
|
|
|
* **NOTE** this can be overridden using the theme pathMap property |
137
|
|
|
* Overriding this property in sub classes should be a last resort |
138
|
|
|
* @var string |
139
|
|
|
*/ |
140
|
|
|
protected $_viewBodyFile = '@neon/core/grid/views/body.tpl'; |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* A unique id for this grid |
144
|
|
|
* it will be used to attach request data to |
145
|
|
|
* @var string |
146
|
|
|
*/ |
147
|
|
|
private $_id; |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* The name of the grid. |
151
|
|
|
* |
152
|
|
|
* @var string |
153
|
|
|
*/ |
154
|
|
|
public $name; |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* The header title for the grid |
158
|
|
|
* |
159
|
|
|
* @var string |
160
|
|
|
*/ |
161
|
|
|
public $title; |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Configures the search url the grid will call this by ajax. |
165
|
|
|
* To return correctly the controller action must return the Grid Html by |
166
|
|
|
* simply instantiating the grid and echoing the run command |
167
|
|
|
* Minimum controller required for an example MyNeonGrid: |
168
|
|
|
* |
169
|
|
|
* ```php |
170
|
|
|
* public function actionGrid() |
171
|
|
|
* { |
172
|
|
|
* $grid = new MyNeonGrid(); |
173
|
|
|
* echo $grid->run(); |
174
|
|
|
* } |
175
|
|
|
* ``` |
176
|
|
|
* |
177
|
|
|
* @var array|string route e.g. ['/controller/index/grid'] |
178
|
|
|
*/ |
179
|
|
|
public $filterUrl = '/core/grid/filter'; |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* DDS filter format - a placeholder for columns to manipulate filters |
183
|
|
|
* |
184
|
|
|
* @var array |
185
|
|
|
*/ |
186
|
|
|
public $filters = []; |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* @var IQuery |
190
|
|
|
*/ |
191
|
|
|
public $queryBuilder = null; |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Store the query builder object for the grid |
195
|
|
|
* |
196
|
|
|
* @var IQuery |
197
|
|
|
*/ |
198
|
|
|
protected $_queryBuilder = null; |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Specify whether or not this grid can export a CSV |
202
|
|
|
* @var bool |
203
|
|
|
*/ |
204
|
|
|
public $canExportCsv = true; |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* The string to display in a cell when empty |
208
|
|
|
* @var string |
209
|
|
|
*/ |
210
|
|
|
public $emptyCellDisplay = '-'; |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* @inheritdoc |
214
|
|
|
*/ |
215
|
|
|
public function getProperties() |
216
|
|
|
{ |
217
|
|
|
/** |
218
|
|
|
* Return all the properties that are needed for generic rendering |
219
|
|
|
* or exporting of this grid in any form. |
220
|
|
|
*/ |
221
|
|
|
return [ |
222
|
|
|
'id', 'pageSize', 'name', 'title', 'canExportCsv', |
223
|
|
|
'theme', 'emptyCellDisplay', 'stripedRows', 'filterUrl' |
224
|
|
|
]; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Get the query builder object responsible for building search queries for columns |
229
|
|
|
* |
230
|
|
|
* @return IQuery |
231
|
|
|
*/ |
232
|
|
|
public function getQueryBuilder() |
233
|
|
|
{ |
234
|
|
|
return $this->getDataProvider()->getQueryBuilder(); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Set the query builder object |
239
|
|
|
* |
240
|
|
|
* @param IQuery $queryBuilder |
241
|
|
|
*/ |
242
|
|
|
public function setQueryBuilder(IQuery $queryBuilder) |
243
|
|
|
{ |
244
|
|
|
$this->_queryBuilder = $queryBuilder; |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* The current scope the grid will render |
249
|
|
|
* if not defined then it will use the first scope defined in the $this->_scopes array |
250
|
|
|
* |
251
|
|
|
* @var string the scope key |
252
|
|
|
*/ |
253
|
|
|
protected $_scope; |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* The key of the default scope for the grid. |
257
|
|
|
* The grid will apply this scope if no request scope exists |
258
|
|
|
* If this is not defined the grid will apply the first scope defined in $this->_scopes |
259
|
|
|
* |
260
|
|
|
* @var string |
261
|
|
|
*/ |
262
|
|
|
public $defaultScope; |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* Set the pagination page number |
266
|
|
|
*/ |
267
|
|
|
public function setPageNumber() |
268
|
|
|
{ |
269
|
|
|
$page = neon()->request->post($this->id); |
270
|
|
|
if (isset($page['page'])) { |
271
|
|
|
$this->getDataProvider()->pagination->setPage($page['page']); |
272
|
|
|
} |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Set the pagination page size |
277
|
|
|
* |
278
|
|
|
* @param int $size |
279
|
|
|
*/ |
280
|
|
|
public function setPageSize($size) |
281
|
|
|
{ |
282
|
|
|
$this->getDataProvider()->pagination->pageSize = $size; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Get hold of the current pagination size |
287
|
|
|
*/ |
288
|
|
|
public function getPageSize() |
289
|
|
|
{ |
290
|
|
|
return $this->getDataProvider()->pagination->pageSize; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Get a scope key string |
295
|
|
|
* This returns the active scope the grid must process |
296
|
|
|
* This is calculated from either request data, a defaultScope parameter or the first in the scopes list |
297
|
|
|
* returns null if a scope is not found |
298
|
|
|
* |
299
|
|
|
* @return string|null |
300
|
|
|
*/ |
301
|
|
|
public function getScope() |
302
|
|
|
{ |
303
|
|
|
if (empty($this->_scopes)) { |
304
|
|
|
return null; |
305
|
|
|
} |
306
|
|
|
// If a scope on the request has been set |
307
|
|
|
// _scope property exists |
308
|
|
|
$postScope = $this->_scope; |
309
|
|
|
$scope = ArrayHelper::getValue($this->_scopes, $postScope); |
310
|
|
|
if ($scope) { |
311
|
|
|
return $scope['key']; |
312
|
|
|
} |
313
|
|
|
// No url request scope defined check if a default is set |
314
|
|
|
if ($this->defaultScope !== null) |
315
|
|
|
return $this->defaultScope; |
316
|
|
|
|
317
|
|
|
// No default scope so return the first in the list as the default |
318
|
|
|
$defaultScope = reset($this->_scopes); |
319
|
|
|
return $defaultScope['key']; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* @deprecated - this function is no longer needed |
324
|
|
|
*/ |
325
|
|
|
public function load() |
326
|
|
|
{} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Whether the specified scope is the currently the active one |
330
|
|
|
* Note that request data must have been loaded - therefore this function will not work |
331
|
|
|
* when used inside the init function |
332
|
|
|
* |
333
|
|
|
* @param string $scopeKey |
334
|
|
|
* @return bool |
335
|
|
|
*/ |
336
|
|
|
public function isScopeActive($scopeKey) |
337
|
|
|
{ |
338
|
|
|
return ($this->getScope() == $scopeKey); |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* Does the grid have the scope defined |
343
|
|
|
* @param string $scopeKey |
344
|
|
|
* @return boolean |
345
|
|
|
*/ |
346
|
|
|
public function hasScope($scopeKey) |
347
|
|
|
{ |
348
|
|
|
return isset($this->_scopes[$scopeKey]); |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* Set the active scope |
353
|
|
|
* |
354
|
|
|
* @param string $scope |
355
|
|
|
*/ |
356
|
|
|
public function setScope($scope) |
357
|
|
|
{ |
358
|
|
|
$this->_scope = $scope; |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* Get the array of scope definitions |
363
|
|
|
* |
364
|
|
|
* @return array |
365
|
|
|
*/ |
366
|
|
|
public function getScopes() |
367
|
|
|
{ |
368
|
|
|
return $this->_scopes; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* Add a filter scope - typically "deleted" | "active" etc |
373
|
|
|
* |
374
|
|
|
* @param string $key |
375
|
|
|
* @param string $name |
376
|
|
|
* @param callable $function |
377
|
|
|
*/ |
378
|
|
|
public function addScope($key, $name, $function=null) |
379
|
|
|
{ |
380
|
|
|
$this->_scopes[$key] = compact('key', 'name', 'function'); |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
/** |
384
|
|
|
* Whether to show the filters |
385
|
|
|
* |
386
|
|
|
* @var bool |
387
|
|
|
*/ |
388
|
|
|
public $filterShow = true; |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* Chainable method to set filterShow property |
392
|
|
|
* |
393
|
|
|
* @param boolean $bool |
394
|
|
|
* @return $this |
395
|
|
|
*/ |
396
|
|
|
public function filterShow($bool) |
397
|
|
|
{ |
398
|
|
|
$this->filterShow = $bool; |
399
|
|
|
return $this; |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
/** |
403
|
|
|
* The default sort (adopt yii terminology) |
404
|
|
|
* columnKey |
405
|
|
|
* |
406
|
|
|
* @var string |
407
|
|
|
*/ |
408
|
|
|
public $sort; |
409
|
|
|
|
410
|
|
|
/** |
411
|
|
|
* A private cache of the grid data |
412
|
|
|
* |
413
|
|
|
* @var array |
414
|
|
|
*/ |
415
|
|
|
private $_gridData = []; |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* Store the form object responsible for each columns filter field |
419
|
|
|
* |
420
|
|
|
* @var null|\neon\core\form\Form |
421
|
|
|
*/ |
422
|
|
|
protected $_filterForm = null; |
423
|
|
|
|
424
|
|
|
/** |
425
|
|
|
* Get a form object representing all the filters |
426
|
|
|
* This calls each column and asks it kindly for a form field object |
427
|
|
|
* that it suitable to render its filter input control |
428
|
|
|
* |
429
|
|
|
* @throws \Exception on incorrect column config |
430
|
|
|
* @return Form |
431
|
|
|
*/ |
432
|
|
|
public function getFilterForm() |
433
|
|
|
{ |
434
|
|
|
if (empty($this->getColumns())) { |
435
|
|
|
throw new \Exception('No grid columns have been defined yet. The filter form is built from the grid columns, make sure you have setup your grid columns before loading request data into the form'); |
436
|
|
|
} |
437
|
|
|
if ($this->_filterForm === null) { |
438
|
|
|
$this->_filterForm = (new Form($this->getId())) |
|
|
|
|
439
|
|
|
->setId($this->getId().'Filter') |
440
|
|
|
->addSubForm('filter'); |
441
|
|
|
foreach($this->getColumns() as $key => $column) { |
442
|
|
|
if ($column->getFilter() && $column->getFilterField() !== false) { |
443
|
|
|
$field = $column->getFilterField(); |
444
|
|
|
if (is_object($field)) |
445
|
|
|
$field = $field->toArray(); |
446
|
|
|
$field['name'] = $key; |
447
|
|
|
$field['dataKey'] = $column->getDbField(); |
448
|
|
|
$this->_filterForm->add($field); |
|
|
|
|
449
|
|
|
} |
450
|
|
|
} |
451
|
|
|
} |
452
|
|
|
return $this->_filterForm; |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
/** |
456
|
|
|
* Return the original grid data that has been sent to the grid |
457
|
|
|
* |
458
|
|
|
* @return array |
459
|
|
|
*/ |
460
|
|
|
public function getGridData() |
461
|
|
|
{ |
462
|
|
|
return $this->_gridData; |
463
|
|
|
} |
464
|
|
|
|
465
|
|
|
/** |
466
|
|
|
* This function is called before request data is loaded (and before setup is ran) |
467
|
|
|
* This is equivalent of putting code into the init function |
468
|
|
|
* This should be used to add columns and grid settings |
469
|
|
|
*/ |
470
|
|
|
public function configure() |
471
|
|
|
{ |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
/** |
475
|
|
|
* Function called as the first step of the run process |
476
|
|
|
* at this point any relevant request data should have been loaded typically which scope is active |
477
|
|
|
* therefore the setup of the grid can be altered based on post data |
478
|
|
|
* Note for filter data to be stored in the filter form the column objects must first be initialized |
479
|
|
|
* (as the filter form is created from the columns on the grid) |
480
|
|
|
* Its best to add columns in the init function and use the setup to tweak them |
481
|
|
|
* you can access an existing column via $this->getColumn($key); or store references in the subclass |
482
|
|
|
* |
483
|
|
|
* @return void |
484
|
|
|
*/ |
485
|
|
|
public function setup() |
486
|
|
|
{ |
487
|
|
|
} |
488
|
|
|
|
489
|
|
|
/** |
490
|
|
|
* Returns the grid name that this model class should use. |
491
|
|
|
* |
492
|
|
|
* The grid name is mainly used by [[\yii\widgets\ActiveForm]] to determine how to name |
493
|
|
|
* the input fields for the attributes in a model. If the form name is "A" and an attribute |
494
|
|
|
* name is "b", then the corresponding input name would be "A[b]". If the form name is |
495
|
|
|
* an empty string, then the input name would be "b". |
496
|
|
|
* |
497
|
|
|
* By default, this method returns the model class name (without the namespace part) |
498
|
|
|
* as the form name. You may override it when the model is used in different forms. |
499
|
|
|
* |
500
|
|
|
* @return string the grid name of this class. |
501
|
|
|
* @throws \Exception |
502
|
|
|
*/ |
503
|
|
|
public function gridName() |
504
|
|
|
{ |
505
|
|
|
$reflector = new ReflectionClass($this); |
506
|
|
|
return $reflector->getShortName(); |
507
|
|
|
} |
508
|
|
|
|
509
|
|
|
/** |
510
|
|
|
* Get an id for the grid |
511
|
|
|
* |
512
|
|
|
* @return string |
513
|
|
|
* @throws \Exception if no id has been set |
514
|
|
|
*/ |
515
|
|
|
public function getId() |
516
|
|
|
{ |
517
|
|
|
if ($this->_id === null) { |
518
|
|
|
$this->_id = $this->gridName(); |
519
|
|
|
} |
520
|
|
|
return $this->_id; |
521
|
|
|
} |
522
|
|
|
|
523
|
|
|
/** |
524
|
|
|
* Set the grid id, the id is used as a key to attach request data to |
525
|
|
|
* and to identify the html block for the grid when rendering |
526
|
|
|
* |
527
|
|
|
* @param string $id |
528
|
|
|
*/ |
529
|
|
|
public function setId($id) |
530
|
|
|
{ |
531
|
|
|
$this->_id = $id; |
532
|
|
|
} |
533
|
|
|
|
534
|
|
|
/** |
535
|
|
|
* Set a bunch of columns by an array definition |
536
|
|
|
* |
537
|
|
|
* ```php |
538
|
|
|
* new Grid(['columns'=>[ |
539
|
|
|
* 'name' => [ |
540
|
|
|
* 'class' => 'neon\core\grid\column\Text', |
541
|
|
|
* 'title' => 'First Name', |
542
|
|
|
* 'dbField' => 'first_name' |
543
|
|
|
* ] |
544
|
|
|
* ]]) |
545
|
|
|
* ``` |
546
|
|
|
* @throws \Exception on incorrect column config |
547
|
|
|
* @param array $columns containing ['key' => ['class' => 'column class', ...]] |
548
|
|
|
*/ |
549
|
|
|
public function setColumns($columns) |
550
|
|
|
{ |
551
|
|
|
foreach($columns as $key => $config) { |
552
|
|
|
$this->addColumn($key, $config); |
553
|
|
|
} |
554
|
|
|
} |
555
|
|
|
|
556
|
|
|
/** |
557
|
|
|
* Add a text column to the grid |
558
|
|
|
* |
559
|
|
|
* @param string $key |
560
|
|
|
* @param array $config |
561
|
|
|
* @throws \Exception on incorrect column config |
562
|
|
|
* @return \neon\core\grid\column\Text |
563
|
|
|
*/ |
564
|
|
|
public function addTextColumn($key, $config=[]) |
565
|
|
|
{ |
566
|
|
|
$config['class'] = isset($config['class']) ? $config['class'] : 'neon\core\grid\column\Text'; |
567
|
|
|
return $this->addColumn($key, $config); |
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
/** |
571
|
|
|
* Add a Date column to the grid |
572
|
|
|
* |
573
|
|
|
* @param string $key |
574
|
|
|
* @param array $config |
575
|
|
|
* @throws \Exception on incorrect column config |
576
|
|
|
* @return \neon\core\grid\column\Date |
577
|
|
|
*/ |
578
|
|
|
public function addDateColumn($key, $config=[]) |
579
|
|
|
{ |
580
|
|
|
$config['class'] = isset($config['class']) ? $config['class'] : 'neon\core\grid\column\Date'; |
581
|
|
|
return $this->addColumn($key, $config); |
582
|
|
|
} |
583
|
|
|
|
584
|
|
|
/** |
585
|
|
|
* Add a int column to the grid |
586
|
|
|
* |
587
|
|
|
* @param string $key |
588
|
|
|
* @param array $config |
589
|
|
|
* @throws \Exception on incorrect column config |
590
|
|
|
* @return \neon\core\grid\column\IntColumn |
591
|
|
|
*/ |
592
|
|
|
public function addIntColumn($key, $config=[]) |
593
|
|
|
{ |
594
|
|
|
$config['class'] = isset($config['class']) ? $config['class'] : 'neon\core\grid\column\IntColumn'; |
595
|
|
|
return $this->addColumn($key, $config); |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
/** |
599
|
|
|
* Add a check column to the grid |
600
|
|
|
* |
601
|
|
|
* @param $key |
602
|
|
|
* @param array $config |
603
|
|
|
* @return \neon\core\grid\column\Check |
604
|
|
|
* @throws \Exception |
605
|
|
|
*/ |
606
|
|
|
public function addCheckColumn($key, $config=[]) |
607
|
|
|
{ |
608
|
|
|
$config['class'] = isset($config['class']) ? $config['class'] : 'neon\core\grid\column\Check'; |
609
|
|
|
return $this->addColumn($key, $config); |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
/** |
613
|
|
|
* Add an action column to the grid |
614
|
|
|
* |
615
|
|
|
* @param $key |
616
|
|
|
* @param $config |
617
|
|
|
* @throws \Exception on incorrect column config |
618
|
|
|
* @return \neon\core\grid\column\Button |
619
|
|
|
*/ |
620
|
|
|
public function addButtonColumn($key, $config=[]) |
621
|
|
|
{ |
622
|
|
|
$config['class'] = isset($config['class']) ? $config['class'] : 'neon\core\grid\column\Button'; |
623
|
|
|
return $this->addColumn($key, $config); |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
/** |
627
|
|
|
* Create a column based on its field definition |
628
|
|
|
* |
629
|
|
|
* @param string $key - a key for the column |
630
|
|
|
* @param string $class - the class reference |
631
|
|
|
* @param string $member - the member reference string typically from Daedalus. |
632
|
|
|
* If this is null, then the key is used as the member |
633
|
|
|
* @param string $phoebeType - the phoebe type for the form definition. Defaults to 'daedalus' |
634
|
|
|
* @throws \Exception |
635
|
|
|
* @return Column |
636
|
|
|
*/ |
637
|
|
|
public function addColumnFromDefinition($key, $class, $member=null, $phoebeType='daedalus') |
638
|
|
|
{ |
639
|
|
|
static $formsDefinitionCache=[]; |
640
|
|
|
$cacheKey = $class.$phoebeType; |
641
|
|
|
if (!array_key_exists($cacheKey, $formsDefinitionCache)) { |
642
|
|
|
$formDefinition = neon('phoebe')->getFormDefinition($phoebeType, $class); |
643
|
|
|
$formsDefinitionCache[$cacheKey] = $formDefinition; |
644
|
|
|
} |
645
|
|
|
$form = new \neon\core\form\Form($formsDefinitionCache[$cacheKey]); |
646
|
|
|
if ($member === null) |
647
|
|
|
$member = $key; |
648
|
|
|
return $this->addColumn($key, [ |
649
|
|
|
'title' => $form->getField($member)->getLabel(), |
650
|
|
|
'class' => 'neon\core\grid\column\Column', |
651
|
|
|
'member' => $form->getField($member), |
652
|
|
|
]); |
653
|
|
|
} |
654
|
|
|
|
655
|
|
|
/** |
656
|
|
|
* @var array |
657
|
|
|
*/ |
658
|
|
|
protected $_columnGroups = []; |
659
|
|
|
|
660
|
|
|
/** |
661
|
|
|
* @return array |
662
|
|
|
*/ |
663
|
|
|
public function getColumnGroups() |
664
|
|
|
{ |
665
|
|
|
return $this->_columnGroups; |
666
|
|
|
} |
667
|
|
|
|
668
|
|
|
/** |
669
|
|
|
* @param $array |
670
|
|
|
*/ |
671
|
|
|
public function setColumnGroups($array) |
672
|
|
|
{ |
673
|
|
|
$this->_columnGroups = $array; |
674
|
|
|
} |
675
|
|
|
|
676
|
|
|
/** |
677
|
|
|
* Add a column to the grid |
678
|
|
|
* |
679
|
|
|
* @param string $key - the internal key for the column will also be used as the key to look up the value in the |
680
|
|
|
* row data. If you need this to be different then you can set the dbField option of the column to specify the |
681
|
|
|
* precise key in the column data to return |
682
|
|
|
* @param array $config |
683
|
|
|
* @return \neon\core\grid\column\Column |
684
|
|
|
* @throws \Exception if no `$config['class']` is set |
685
|
|
|
*/ |
686
|
|
|
public function addColumn($key, $config=[]) |
687
|
|
|
{ |
688
|
|
|
if (!isset($config['class'])) { |
689
|
|
|
// 99% of the time you just want the standard Column class |
690
|
|
|
$config['class'] = \neon\core\grid\column\Column::class; |
691
|
|
|
} |
692
|
|
|
$class = Arr::remove($config, 'class'); |
693
|
|
|
unset($config['class']); |
694
|
|
|
$this->_columns[$key] = \Neon::createObject(array_merge([ |
695
|
|
|
'class' => $class, |
696
|
|
|
'key' => $key, |
697
|
|
|
'grid' => $this |
698
|
|
|
], $config)); |
699
|
|
|
return $this->_columns[$key]; |
700
|
|
|
} |
701
|
|
|
|
702
|
|
|
/** |
703
|
|
|
* Remove a column from the grid |
704
|
|
|
* |
705
|
|
|
* @param $key |
706
|
|
|
*/ |
707
|
|
|
public function removeColumn($key) |
708
|
|
|
{ |
709
|
|
|
if ($this->columnExists($key)) |
710
|
|
|
unset($this->_columns[$key]); |
711
|
|
|
} |
712
|
|
|
|
713
|
|
|
/** |
714
|
|
|
* Set the data provider used to get the data |
715
|
|
|
* |
716
|
|
|
* @param \yii\data\BaseDataProvider $provider |
717
|
|
|
*/ |
718
|
|
|
public function setDataProvider($provider) |
719
|
|
|
{ |
720
|
|
|
$this->_dataProvider = $provider; |
|
|
|
|
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
/** |
724
|
|
|
* Get the grids Data provider object |
725
|
|
|
* |
726
|
|
|
* @return \yii\data\ActiveDataProvider |
727
|
|
|
*/ |
728
|
|
|
abstract public function getDataProvider(); |
729
|
|
|
|
730
|
|
|
/** |
731
|
|
|
* Get the columns. |
732
|
|
|
* Returns an array of column objects implementing IColumn interface key => column object |
733
|
|
|
* |
734
|
|
|
* @return \neon\core\grid\column\IColumn[] |
735
|
|
|
*/ |
736
|
|
|
public function getColumns() |
737
|
|
|
{ |
738
|
|
|
return $this->_columns; |
739
|
|
|
} |
740
|
|
|
|
741
|
|
|
/** |
742
|
|
|
* Whether the grid has any data set |
743
|
|
|
* |
744
|
|
|
* @return bool |
745
|
|
|
*/ |
746
|
|
|
public function hasData() |
747
|
|
|
{ |
748
|
|
|
return ! empty($this->_gridData); |
749
|
|
|
} |
750
|
|
|
|
751
|
|
|
/** |
752
|
|
|
* We must refresh the data provider before calling scopes |
753
|
|
|
* This removes scopes that have been added previously but keeps initial setup query conditions |
754
|
|
|
*/ |
755
|
|
|
public function refreshDataProvider() |
756
|
|
|
{ |
757
|
|
|
if ($this->_dataProvider) |
758
|
|
|
$this->_dataProvider = clone $this->getDataProvider(); |
759
|
|
|
} |
760
|
|
|
|
761
|
|
|
/** |
762
|
|
|
* The main render function to draw the grid and run the search etc |
763
|
|
|
* @throws \Exception |
764
|
|
|
* @return string |
765
|
|
|
*/ |
766
|
|
|
public function run() |
767
|
|
|
{ |
768
|
|
|
$this->gridBoot(); |
769
|
|
|
$this->setPageNumber(); |
770
|
|
|
$this->registerAssets(); |
771
|
|
|
// executes the query and stores the data results in models accessible via `$this->getDataProvider()->getModels()` |
772
|
|
|
$this->getDataProvider()->prepare(); |
773
|
|
|
// enable columns to prepare for display (i.e. load maps based on current result set) |
774
|
|
|
$this->prepareDisplay(); |
775
|
|
|
return $this->getGridRenderer()->doRender(); |
776
|
|
|
} |
777
|
|
|
|
778
|
|
|
/** |
779
|
|
|
* @throws InvalidConfigException |
780
|
|
|
*/ |
781
|
|
|
public function gridBoot() |
782
|
|
|
{ |
783
|
|
|
if (! $this->hasData()) |
784
|
|
|
$this->_gridData = Arr::get($_REQUEST, $this->id, null); |
785
|
|
|
// load the active scope |
786
|
|
|
$this->_scope = Arr::get($this->_gridData, "scope", []); |
|
|
|
|
787
|
|
|
// load request data into the form |
788
|
|
|
// note: columns should have already been added to the grid otherwise the form will be empty |
789
|
|
|
$this->getFilterForm()->load(Arr::get($this->_gridData, "filter", [])); |
790
|
|
|
// enable the grid to configure columns visibility and filters based on active scopes and request data |
791
|
|
|
$this->setup(); |
792
|
|
|
// force the data provider to load (only necessary when this is a subclass of YiiGridView |
793
|
|
|
$this->getDataProvider(); |
794
|
|
|
$this->refreshDataProvider(); |
795
|
|
|
$this->processActionsOnSelected(); |
796
|
|
|
$this->processScopes(); |
797
|
|
|
$this->processSearch(); |
798
|
|
|
$this->processSort(); |
799
|
|
|
} |
800
|
|
|
|
801
|
|
|
/** |
802
|
|
|
* Return the string url to the export link |
803
|
|
|
* @return string |
804
|
|
|
*/ |
805
|
|
|
public function exportCsvUrl() |
806
|
|
|
{ |
807
|
|
|
return url(['/core/grid/export-csv', 'token' => Hash::setObjectToToken($this)]); |
808
|
|
|
} |
809
|
|
|
|
810
|
|
|
/** |
811
|
|
|
* Export the grid data in a format suitable for CSV |
812
|
|
|
* @param int $page the starting page in the data |
813
|
|
|
* @param int $pageSize the number of rows in a page. Maximum 1000. |
814
|
|
|
* @param bool $asRows false if you want the data imploded into a string |
815
|
|
|
* and true if you want the data returned as an array |
816
|
|
|
* @return string | array depending on value of $asRows |
817
|
|
|
*/ |
818
|
|
|
public function exportCsv($page=0, $pageSize=1000, $asRows=false) |
819
|
|
|
{ |
820
|
|
|
// check we're allowed to do this |
821
|
|
|
if (!$this->canExportCsv) |
822
|
|
|
return null; |
823
|
|
|
|
824
|
|
|
$this->gridBoot(); |
825
|
|
|
// Get header column names |
826
|
|
|
$headerCols = []; |
827
|
|
|
$columns = $this->getColumns(); |
828
|
|
|
|
829
|
|
|
foreach ($columns as $colKey => $column) { |
830
|
|
|
$headerCols[] = $this->prepareCellContentForCSV($column->getTitle()); |
831
|
|
|
} |
832
|
|
|
// For some reason excel throws an SYLK file error if the first cell is ID uppercase |
833
|
|
|
|
834
|
|
|
if (isset($headerCols[0]) && $headerCols[0] == 'ID') { |
835
|
|
|
$headerCols[0] = 'Id'; |
836
|
|
|
} |
837
|
|
|
|
838
|
|
|
// Render the rows |
839
|
|
|
$dataProvider = $this->getDataProvider(); |
840
|
|
|
$pagination = $dataProvider->pagination; |
|
|
|
|
841
|
|
|
$pagination->page=$page; |
842
|
|
|
$pagination->pageSize = min(1000,$pageSize); |
843
|
|
|
$models = $dataProvider->getModels(); |
844
|
|
|
if (empty($models)) |
845
|
|
|
return null; |
846
|
|
|
|
847
|
|
|
$rows = []; |
848
|
|
|
foreach($models as $index => $row) { |
849
|
|
|
$singleRow = []; |
850
|
|
|
// use the columns set as not all columns exists in row data e.g. derived ones |
851
|
|
|
foreach ($columns as $colKey => $column) { |
852
|
|
|
$singleRow[] = $this->prepareCellContentForCSV($column->callRenderDataCellContentFunction($row, $colKey, 0)); |
853
|
|
|
} |
854
|
|
|
$rows[] = implode(',', $singleRow); |
855
|
|
|
} |
856
|
|
|
|
857
|
|
|
// clear the models to prevent ooming |
858
|
|
|
$dataProvider->models = null; |
859
|
|
|
|
860
|
|
|
// Add headers columns as the first row if we are on the first page |
861
|
|
|
if ($page==0) |
862
|
|
|
array_unshift($rows, implode(',', $headerCols)); |
863
|
|
|
|
864
|
|
|
return $asRows ? $rows : implode("\n", $rows); |
865
|
|
|
} |
866
|
|
|
|
867
|
|
|
/** |
868
|
|
|
* Get a grid renderer object |
869
|
|
|
*/ |
870
|
|
|
public function getGridRenderer() |
871
|
|
|
{ |
872
|
|
|
return new GridRenderer($this); |
873
|
|
|
} |
874
|
|
|
|
875
|
|
|
/** |
876
|
|
|
* Prepare the columns for display |
877
|
|
|
*/ |
878
|
|
|
public function prepareDisplay() |
879
|
|
|
{ |
880
|
|
|
foreach ($this->getColumns() as $column) { |
881
|
|
|
$column->prepareDisplay(); |
882
|
|
|
} |
883
|
|
|
} |
884
|
|
|
|
885
|
|
|
/** |
886
|
|
|
* Checks if there are selected row actions and runs them |
887
|
|
|
*/ |
888
|
|
|
public function processActionsOnSelected() |
889
|
|
|
{ |
890
|
|
|
// run actions on any selected rows |
891
|
|
|
// TODO: tidy up running actions |
892
|
|
|
if (isset($_POST[$this->id]['action']) && isset($_POST[$this->id]['action_index']) && $_POST[$this->id]['action'] != 'undefined') { |
893
|
|
|
$rows = explode(',', $_POST[$this->id]['action_index']); |
894
|
|
|
foreach ($rows as $id) { |
895
|
|
|
call_user_func_array(array($this, $_POST[$this->id]['action']), array($id)); |
896
|
|
|
} |
897
|
|
|
} |
898
|
|
|
} |
899
|
|
|
|
900
|
|
|
/** |
901
|
|
|
* Applies scope filter criteria |
902
|
|
|
* @throws \Exception |
903
|
|
|
*/ |
904
|
|
|
public function processScopes() |
905
|
|
|
{ |
906
|
|
|
$this->_processScopeCounts(); |
907
|
|
|
// apply default scope |
908
|
|
|
if (!empty($this->getScopes())) { |
909
|
|
|
$activeScope = $this->getScope(); |
910
|
|
|
// remove previous scopes added |
911
|
|
|
$scope = ArrayHelper::getValue($this->getScopes(), $activeScope); |
912
|
|
|
if ($scope) { |
913
|
|
|
// before calling a scope we will tell the query builder to label any rules added |
914
|
|
|
// by the scope - this allows us to remove them later |
915
|
|
|
$this->callScope($scope, $this->getQueryBuilder()); |
916
|
|
|
} |
917
|
|
|
} |
918
|
|
|
} |
919
|
|
|
|
920
|
|
|
private $_scopeCounts = []; |
921
|
|
|
|
922
|
|
|
/** |
923
|
|
|
* Perform a count for each scope and store ready for rendering. |
924
|
|
|
* Populates $this->_scopeCounts with total count for each scope. |
925
|
|
|
* This function must be called before applying the default scope otherwise we cannot easily remove the |
926
|
|
|
* default scope from the query builder object. Thus ending up with inaccurate counts |
927
|
|
|
* |
928
|
|
|
* Note - counts are extremely expensive, and can take a long time to return |
929
|
|
|
* for database tables with items in the millions of rows especially if joins |
930
|
|
|
* are involved. So counts should not be made more than once if possible. |
931
|
|
|
* |
932
|
|
|
* @throws \Exception if no scope function exists for specified key |
933
|
|
|
*/ |
934
|
|
|
private function _processScopeCounts() |
935
|
|
|
{ |
936
|
|
|
foreach ($this->getScopes() as $key => $scope) { |
937
|
|
|
// counts across whole tables are very very expensive. Don't count more than once |
938
|
|
|
if (isset($this->_scopeCounts[$key])) |
939
|
|
|
continue; |
940
|
|
|
$dp = clone $this->getDataProvider(); |
941
|
|
|
// call the scope function defined by `'scope'.$key` functions in child classes |
942
|
|
|
$this->callScope($scope, $dp->getQueryBuilder()); |
943
|
|
|
// reset cached counts |
944
|
|
|
$dp->setTotalCount(null); |
945
|
|
|
$this->_scopeCounts[$key] = $dp->getTotalCount(); |
946
|
|
|
} |
947
|
|
|
} |
948
|
|
|
|
949
|
|
|
/** |
950
|
|
|
* return the count for a given scope key |
951
|
|
|
* @param string $key |
952
|
|
|
* @return int |
953
|
|
|
*/ |
954
|
|
|
public function getScopeTotalCount($key) |
955
|
|
|
{ |
956
|
|
|
return isset($this->_scopeCounts[$key]) ? $this->_scopeCounts[$key] : '?'; |
957
|
|
|
} |
958
|
|
|
|
959
|
|
|
/** |
960
|
|
|
* Checks search data and applied correct filter modifications to the query/filter object |
961
|
|
|
* |
962
|
|
|
* @throws InvalidConfigException |
963
|
|
|
*/ |
964
|
|
|
public function processSearch() |
965
|
|
|
{ |
966
|
|
|
$data = $this->getGridData(); |
967
|
|
|
if (!isset($data['filter'])) |
968
|
|
|
return; |
969
|
|
|
foreach ($this->getColumns() as $key => $column) { |
970
|
|
|
// check there is a form filter field |
971
|
|
|
// check that the filter field has request data |
972
|
|
|
if ($column->getFilter() && $column->hasRequestData()) { |
973
|
|
|
// if there is valid search data: |
974
|
|
|
if ($column->getSearchFunction() !== null) { |
975
|
|
|
$column->callSearchFunction(); |
976
|
|
|
} else { |
977
|
|
|
$column->processSearch($this->getQueryBuilder()); |
978
|
|
|
} |
979
|
|
|
} |
980
|
|
|
} |
981
|
|
|
} |
982
|
|
|
|
983
|
|
|
/** |
984
|
|
|
* Attach sort information to the data provider. |
985
|
|
|
* Typically a column will be identified to be sorted, this information can be |
986
|
|
|
* found out from `$this->getSortInfo()` |
987
|
|
|
* |
988
|
|
|
* @see getSortInfo() |
989
|
|
|
*/ |
990
|
|
|
public function processSort() |
991
|
|
|
{ |
992
|
|
|
list ($columnKey, $desc) = $this->getSortInfo(); |
993
|
|
|
if ($columnKey && $this->columnExists($columnKey)) { |
994
|
|
|
// lookup the column db field to apply the sorting to |
995
|
|
|
$column = $this->getColumn($columnKey); |
996
|
|
|
$column->processSort($this->getQueryBuilder(), $desc); |
997
|
|
|
} |
998
|
|
|
} |
999
|
|
|
|
1000
|
|
|
/** |
1001
|
|
|
* Whether a column currently has a sort set |
1002
|
|
|
* |
1003
|
|
|
* @param $columnKey |
1004
|
|
|
* @return bool |
1005
|
|
|
*/ |
1006
|
|
|
public function hasSort($columnKey) |
1007
|
|
|
{ |
1008
|
|
|
list ($columnSort, $desc) = $this->getSortInfo(); |
1009
|
|
|
return $columnSort == $columnKey; |
1010
|
|
|
} |
1011
|
|
|
|
1012
|
|
|
/** |
1013
|
|
|
* Whether the current sort is in descending order |
1014
|
|
|
* |
1015
|
|
|
* @return mixed |
1016
|
|
|
*/ |
1017
|
|
|
public function hasSortDescending() |
1018
|
|
|
{ |
1019
|
|
|
list ($columnSort, $desc) = $this->getSortInfo(); |
1020
|
|
|
return $desc; |
1021
|
|
|
} |
1022
|
|
|
|
1023
|
|
|
/** |
1024
|
|
|
* example useage: |
1025
|
|
|
* |
1026
|
|
|
* ```PHP |
1027
|
|
|
* list($columnKey, $descending) = $this->getSortInfo(); |
1028
|
|
|
* ``` |
1029
|
|
|
* Note that the column key (the first key in the returned array) will be null |
1030
|
|
|
* if there is currently no sort |
1031
|
|
|
* |
1032
|
|
|
* @return array first key columnKey the second a boolean representing descending |
1033
|
|
|
*/ |
1034
|
|
|
public function getSortInfo() |
1035
|
|
|
{ |
1036
|
|
|
$sort = ArrayHelper::getValue($this->getGridData(), 'sort'); |
1037
|
|
|
$descending = false; |
1038
|
|
|
if (strncmp($sort, '-', 1) === 0) { |
1039
|
|
|
$descending = true; |
1040
|
|
|
$sort = substr($sort, 1); |
1041
|
|
|
} |
1042
|
|
|
return [$sort, $descending]; |
1043
|
|
|
} |
1044
|
|
|
|
1045
|
|
|
/** |
1046
|
|
|
* @param $columnKey |
1047
|
|
|
* @return \neon\core\grid\column\Column |
1048
|
|
|
* @throws \Exception |
1049
|
|
|
*/ |
1050
|
|
|
public function getColumn($columnKey) |
1051
|
|
|
{ |
1052
|
|
|
if (!isset($this->_columns[$columnKey])) |
1053
|
|
|
throw new \Exception("No column with key '$columnKey' exists in the grid"); |
1054
|
|
|
return $this->_columns[$columnKey]; |
1055
|
|
|
} |
1056
|
|
|
|
1057
|
|
|
/** |
1058
|
|
|
* Check a column exists |
1059
|
|
|
* @param string $columnKey - the column key |
1060
|
|
|
* @return bool |
1061
|
|
|
*/ |
1062
|
|
|
public function columnExists($columnKey) |
1063
|
|
|
{ |
1064
|
|
|
return isset($this->_columns[$columnKey]); |
1065
|
|
|
} |
1066
|
|
|
|
1067
|
|
|
/** |
1068
|
|
|
* Calls the scope function to action the scope filter |
1069
|
|
|
* |
1070
|
|
|
* @param array $scope the scope array containing ['function' => ..., 'key' => '...'] |
1071
|
|
|
* @param IQuery $query |
1072
|
|
|
* @throws \Exception |
1073
|
|
|
*/ |
1074
|
|
|
public function callScope($scope, IQuery $query) |
1075
|
|
|
{ |
1076
|
|
|
if ($scope['function'] == null) { |
1077
|
|
|
$func = 'scope' . ucfirst($scope['key']); |
1078
|
|
|
if ($this->hasMethod($func, false)) { |
1079
|
|
|
call_user_func(array($this, $func), $query); |
1080
|
|
|
} else { |
1081
|
|
|
throw new \Exception("A scope function with name '$func' is not defined in the grid class."); |
1082
|
|
|
} |
1083
|
|
|
} else { |
1084
|
|
|
call_user_func($scope['function'], $query); |
1085
|
|
|
} |
1086
|
|
|
} |
1087
|
|
|
|
1088
|
|
|
/** |
1089
|
|
|
* Get the grid row data |
1090
|
|
|
* |
1091
|
|
|
* @return array |
1092
|
|
|
*/ |
1093
|
|
|
public function getRows() |
1094
|
|
|
{ |
1095
|
|
|
return $this->getDataProvider()->getModels(); |
1096
|
|
|
} |
1097
|
|
|
|
1098
|
|
|
public function getFilterUrl() |
1099
|
|
|
{ |
1100
|
|
|
$filterUrl = is_array($this->filterUrl) ? $this->filterUrl : [$this->filterUrl]; |
1101
|
|
|
$filterUrl['token'] = Hash::setObjectToToken($this); |
1102
|
|
|
return url($filterUrl); |
1103
|
|
|
} |
1104
|
|
|
|
1105
|
|
|
/** |
1106
|
|
|
* Registers client assets |
1107
|
|
|
*/ |
1108
|
|
|
protected function registerAssets() |
1109
|
|
|
{ |
1110
|
|
|
GridAssets::register(neon()->view); |
1111
|
|
|
$options = []; |
1112
|
|
|
$options['filterUrl'] = $this->getFilterUrl(); |
1113
|
|
|
neon()->view->registerJs('(new Vue()).$mount("#'.$this->id.'");', View::POS_END, $this->id.'vue'); |
1114
|
|
|
neon()->view->registerJs('$("#'.$this->id.'").neonGrid('.json_encode($options).')', View::POS_READY, $this->id.'neonGrid'); |
1115
|
|
|
} |
1116
|
|
|
|
1117
|
|
|
/** |
1118
|
|
|
* Gets a debug result message showing the query performed to |
1119
|
|
|
* fetch the current set of grid results |
1120
|
|
|
* |
1121
|
|
|
* @throws \Exception |
1122
|
|
|
* @return string |
1123
|
|
|
*/ |
1124
|
|
|
public function showQuery() |
1125
|
|
|
{ |
1126
|
|
|
return json_encode($this->getQueryBuilder()->getFilter()); |
1127
|
|
|
} |
1128
|
|
|
|
1129
|
|
|
/** |
1130
|
|
|
* Get an array of only visible columns |
1131
|
|
|
* format: |
1132
|
|
|
* ``` |
1133
|
|
|
* [ |
1134
|
|
|
* ['key' => {column object}], |
1135
|
|
|
* ] |
1136
|
|
|
* ``` |
1137
|
|
|
* |
1138
|
|
|
* @return array |
1139
|
|
|
*/ |
1140
|
|
|
public function getColumnsVisible() |
1141
|
|
|
{ |
1142
|
|
|
$columns = []; |
1143
|
|
|
foreach($this->getColumns() as $key => $column) { |
1144
|
|
|
if ($column->visible) |
|
|
|
|
1145
|
|
|
$columns[$key] = $column; |
1146
|
|
|
} |
1147
|
|
|
return $columns; |
1148
|
|
|
} |
1149
|
|
|
|
1150
|
|
|
/** |
1151
|
|
|
* When outputting the object as a string call the run function |
1152
|
|
|
* |
1153
|
|
|
* @return string |
1154
|
|
|
*/ |
1155
|
|
|
public function __toString() |
1156
|
|
|
{ |
1157
|
|
|
return $this->run(); |
1158
|
|
|
} |
1159
|
|
|
|
1160
|
|
|
/** |
1161
|
|
|
* Key of the column this grid is indexed by |
1162
|
|
|
* |
1163
|
|
|
* @var string |
1164
|
|
|
*/ |
1165
|
|
|
protected $_indexedByColumn; |
1166
|
|
|
|
1167
|
|
|
/** |
1168
|
|
|
* Set a column as the index of the grid |
1169
|
|
|
* An index column assumes that the value in of that column can be used to find the row of data from the data store |
1170
|
|
|
* This is useful for grid actions passing the value of that rows index column - the row id to the action |
1171
|
|
|
* |
1172
|
|
|
* @param string $columnKey |
1173
|
|
|
* @throws \Exception if no column exists with the $columnKey in the $this->_columns array |
1174
|
|
|
*/ |
1175
|
|
|
public function setIndexedByColumn($columnKey) |
1176
|
|
|
{ |
1177
|
|
|
if (!isset($this->_columns[$columnKey])) { |
1178
|
|
|
throw new \Exception("No column exists with key '$columnKey'"); |
1179
|
|
|
} |
1180
|
|
|
$this->_indexedByColumn = $columnKey; |
1181
|
|
|
} |
1182
|
|
|
|
1183
|
|
|
/** |
1184
|
|
|
* Get the column that has been set as the index |
1185
|
|
|
* @return IColumn |
1186
|
|
|
* @throws \Exception - if no index column exists |
1187
|
|
|
*/ |
1188
|
|
|
public function getIndexColumn() |
1189
|
|
|
{ |
1190
|
|
|
if (!isset($this->_columns[$this->_indexedByColumn])) |
1191
|
|
|
throw new \Exception("No index column exists with key '$this->_indexedByColumn'."); |
1192
|
|
|
return $this->_columns[$this->_indexedByColumn]; |
1193
|
|
|
} |
1194
|
|
|
|
1195
|
|
|
/** |
1196
|
|
|
* Whether ter is a column on the grid that has been set to be the index |
1197
|
|
|
* @return bool |
1198
|
|
|
*/ |
1199
|
|
|
public function hasIndexColumn() |
1200
|
|
|
{ |
1201
|
|
|
return isset($this->_columns[$this->_indexedByColumn]); |
1202
|
|
|
} |
1203
|
|
|
|
1204
|
|
|
/** |
1205
|
|
|
* Replace field tags in a string with the values from the row of data. A field tag is denoted with a leading colon. |
1206
|
|
|
* For example a row containing ['name' => 'steve'] and a string of 'hello {$name}' will give 'hello steve' |
1207
|
|
|
* |
1208
|
|
|
* @param string|array $string - a string or array of strings containing field tags e.g. ':field' |
1209
|
|
|
* @param array $row - The row data |
1210
|
|
|
* @return string |
1211
|
|
|
*/ |
1212
|
|
|
public function replaceRowTagsWithValues($string, $row) |
1213
|
|
|
{ |
1214
|
|
|
$tags = $this->getRowSearchTags($row); |
1215
|
|
|
return str_replace(array_keys($tags), array_values($tags), $string); |
1216
|
|
|
} |
1217
|
|
|
|
1218
|
|
|
/** |
1219
|
|
|
* Generates an array of search tags based on the keys of the row |
1220
|
|
|
* A tag is simply the key of a row item wrapped with a particular string |
1221
|
|
|
* It assumes that each row in the grid will have a consistent keys |
1222
|
|
|
* |
1223
|
|
|
* @param array $row |
1224
|
|
|
* @return array |
1225
|
|
|
*/ |
1226
|
|
|
protected function getRowSearchTags($row) |
1227
|
|
|
{ |
1228
|
|
|
$tags = []; |
1229
|
|
|
foreach ($row as $key => $val) { |
1230
|
|
|
if (!is_array($val)){ |
1231
|
|
|
$tags['{{' . $key . '}}'] = $val; |
1232
|
|
|
} |
1233
|
|
|
} |
1234
|
|
|
return $tags; |
1235
|
|
|
} |
1236
|
|
|
|
1237
|
|
|
/** |
1238
|
|
|
* Prepare the content for CSV export by removing tags, html entities etc |
1239
|
|
|
* @param string $content the content to be converted |
1240
|
|
|
*/ |
1241
|
|
|
protected function prepareCellContentForCSV($content) |
1242
|
|
|
{ |
1243
|
|
|
/** |
1244
|
|
|
* From experimentation with real data: |
1245
|
|
|
* grid cells and headers can have html tags added |
1246
|
|
|
* To export properly to csv, any html tags need to be removed and |
1247
|
|
|
* html entities reverted. These can leave newlines in the data so |
1248
|
|
|
* need to clear those out too. |
1249
|
|
|
*/ |
1250
|
|
|
$plain = html_entity_decode( |
1251
|
|
|
str_replace(["\n", '"'], ['', '\''], strip_tags($content)), |
1252
|
|
|
ENT_HTML401 | ENT_QUOTES | ENT_HTML5); |
1253
|
|
|
if ($plain === $this->emptyCellDisplay) |
1254
|
|
|
$plain = ''; |
1255
|
|
|
return '"'.$plain.'"'; |
1256
|
|
|
} |
1257
|
|
|
} |
1258
|
|
|
|
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..