-
Notifications
You must be signed in to change notification settings - Fork 108
/
Copy pathCrud.php
334 lines (283 loc) · 10.8 KB
/
Crud.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
<?php
declare(strict_types=1);
namespace Atk4\Ui;
use Atk4\Core\Factory;
use Atk4\Data\Model;
/**
* Implements a more sophisticated and interactive Data-Table component.
*/
class Crud extends Grid
{
/** @var array of fields to display in Grid */
public $displayFields;
/** @var array|null of fields to edit in Form for Model edit action */
public $editFields;
/** @var array|null of fields to edit in Form for Model add action */
public $addFields;
/** @var array Default notifier to perform when adding or editing is successful * */
public $notifyDefault = [JsToast::class];
/** @var bool|null should we use table column drop-down menu to display user actions? */
public $useMenuActions;
/** @var array Collection of APPLIES_TO_NO_RECORDS Scope Model action menu item */
private $menuItems = [];
/** @var array Model single scope action to include in table action column. Will include all single scope actions if empty. */
public $singleScopeActions = [];
/** @var array Model no_record scope action to include in menu. Will include all no record scope actions if empty. */
public $noRecordScopeActions = [];
/** @var string Message to display when record is add or edit successfully. */
public $saveMsg = 'Record has been saved!';
/** @var string Message to display when record is delete successfully. */
public $deleteMsg = 'Record has been deleted!';
/** @var string Generic display message for no record scope action where model is not loaded. */
public $defaultMsg = 'Done!';
/** @var array Callback containers for model action. */
public $onActions = [];
/** @var mixed recently deleted record id. */
private $deletedId;
protected function init(): void
{
parent::init();
if ($sortBy = $this->getSortBy()) {
$this->stickyGet($this->name . '_sort', $sortBy);
}
}
/**
* Apply ordering to the current model as per the sort parameters.
*/
public function applySort()
{
parent::applySort();
if ($this->getSortBy() && !empty($this->menuItems)) {
foreach ($this->menuItems as $item) {
// Remove previous click handler and attach new one using sort argument.
$this->container->js(true, $item['item']->js()->off('click.atk_crud_item'));
$ex = $item['executor'];
if ($ex instanceof UserAction\JsExecutorInterface) {
$ex->stickyGet($this->name . '_sort', $this->getSortBy());
$this->container->js(true, $item['item']->js()->on('click.atk_crud_item', new JsFunction($ex->jsExecute([]))));
}
}
}
}
/**
* Sets data model of Crud.
*
* @param array<int, string>|null $fields
*/
public function setModel(Model $model, array $fields = null): void
{
$model->assertIsModel();
if ($fields !== null) {
$this->displayFields = $fields;
}
parent::setModel($model, $this->displayFields);
// Grab model id when using delete. Must be set before delete action execute.
$this->model->onHook(Model::HOOK_AFTER_DELETE, function (Model $model) {
$this->deletedId = $model->getId();
});
if ($this->useMenuActions === null) {
$this->useMenuActions = count($model->getUserActions()) > 4;
}
foreach ($this->_getModelActions(Model\UserAction::APPLIES_TO_SINGLE_RECORD) as $action) {
$executor = $this->initActionExecutor($action);
if ($this->useMenuActions) {
$this->addExecutorMenuItem($executor);
} else {
$this->addExecutorButton($executor);
}
}
if ($this->menu) {
foreach ($this->_getModelActions(Model\UserAction::APPLIES_TO_NO_RECORDS) as $k => $action) {
if ($action->enabled) {
$executor = $this->initActionExecutor($action);
$this->menuItems[$k]['item'] = $this->menu->addItem(
$this->getExecutorFactory()->createTrigger($action, $this->getExecutorFactory()::MENU_ITEM)
);
$this->menuItems[$k]['executor'] = $executor;
}
}
$this->setItemsAction();
}
}
/**
* Setup executor for an action.
* First determine what fields action needs,
* then setup executor based on action fields, args and/or preview.
*
* Add hook for onStep 'fields'" Hook can call a callback function
* for UserAction onStep field. Callback will receive executor form where you
* can setup Input field via javascript prior to display form or change form submit event
* handler.
*
* @return object
*/
protected function initActionExecutor(Model\UserAction $action)
{
$executor = $this->getExecutor($action);
$executor->onHook(UserAction\BasicExecutor::HOOK_AFTER_EXECUTE, function ($ex, $return, $id) use ($action) {
return $this->jsExecute($return, $action);
});
if ($executor instanceof UserAction\ModalExecutor) {
foreach ($this->onActions as $onAction) {
$executor->onHook(UserAction\ModalExecutor::HOOK_STEP, function ($ex, $step, $form) use ($onAction, $action) {
$key = key($onAction);
if ($key === $action->short_name && $step === 'fields') {
return $onAction[$key]($form, $ex);
}
});
}
}
return $executor;
}
/**
* Return proper js statement for afterExecute hook on action executor
* depending on return type, model loaded and action scope.
*/
protected function jsExecute($return, Model\UserAction $action): array
{
$js = [];
if ($jsAction = $this->getJsGridAction($action)) {
$js[] = $jsAction;
}
// display msg return by action or depending on action modifier.
if (is_string($return)) {
$js[] = $this->getNotifier($return);
} else {
if ($action->modifier === Model\UserAction::MODIFIER_CREATE || $action->modifier === Model\UserAction::MODIFIER_UPDATE) {
$js[] = $this->getNotifier($this->saveMsg);
} elseif ($action->modifier === Model\UserAction::MODIFIER_DELETE) {
$js[] = $this->getNotifier($this->deleteMsg);
} else {
$js[] = $this->getNotifier($this->defaultMsg);
}
}
return $js;
}
/**
* Return proper js actions depending on action modifier type.
*/
protected function getJsGridAction(Model\UserAction $action): ?JsExpressionable
{
switch ($action->modifier) {
case Model\UserAction::MODIFIER_UPDATE:
case Model\UserAction::MODIFIER_CREATE:
$js = $this->container->jsReload($this->_getReloadArgs());
break;
case Model\UserAction::MODIFIER_DELETE:
// use deleted record id to remove row, fallback to closest tr if id is not available.
$js = $this->deletedId ?
(new Jquery('tr[data-id="' . $this->deletedId . '"]'))->transition('fade left') :
(new Jquery())->closest('tr')->transition('fade left');
break;
default:
$js = null;
}
return $js;
}
/**
* Return jsNotifier object.
* Override this method for setting notifier based on action or model value.
*
* @param string|null $msg the message to display
*
* @return object
*/
protected function getNotifier(string $msg = null)
{
$notifier = Factory::factory($this->notifyDefault);
if ($msg) {
$notifier->setMessage($msg);
}
return $notifier;
}
/**
* Setup js for firing menu action.
*/
protected function setItemsAction()
{
foreach ($this->menuItems as $k => $item) {
$this->container->js(true, $item['item']->on('click.atk_crud_item', $item['executor']));
}
}
/**
* Return proper action executor base on model action.
*
* @return object
*/
protected function getExecutor(Model\UserAction $action)
{
// prioritize Crud addFields over action->fields for Model add action.
if ($action->short_name === 'add' && $this->addFields) {
$action->fields = $this->addFields;
}
// prioritize Crud editFields over action->fields for Model edit action.
if ($action->short_name === 'edit' && $this->editFields) {
$action->fields = $this->editFields;
}
return $this->getExecutorFactory()->create($action, $this);
}
/**
* Return reload argument based on Crud condition.
*
* @return mixed
*/
private function _getReloadArgs()
{
$args[$this->name . '_sort'] = $this->getSortBy();
if ($this->paginator) {
$args[$this->paginator->name] = $this->paginator->getCurrentPage();
}
return $args;
}
/**
* Return proper action need to setup menu or action column.
*/
private function _getModelActions(string $appliesTo): array
{
$actions = [];
if ($appliesTo === Model\UserAction::APPLIES_TO_SINGLE_RECORD && !empty($this->singleScopeActions)) {
foreach ($this->singleScopeActions as $action) {
$actions[] = $this->model->getUserAction($action);
}
} elseif ($appliesTo === Model\UserAction::APPLIES_TO_NO_RECORDS && !empty($this->noRecordScopeActions)) {
foreach ($this->noRecordScopeActions as $action) {
$actions[] = $this->model->getUserAction($action);
}
} else {
$actions = $this->model->getUserActions($appliesTo);
}
return $actions;
}
/**
* Set callback for edit action in Crud.
* Callback function will receive the Edit Form and Executor as param.
*/
public function onFormEdit(\Closure $fx)
{
$this->setOnActions('edit', $fx);
}
/**
* Set callback for add action in Crud.
* Callback function will receive the Add Form and Executor as param.
*/
public function onFormAdd(\Closure $fx)
{
$this->setOnActions('add', $fx);
}
/**
* Set callback for both edit and add action form.
* Callback function will receive Forms and Executor as param.
*/
public function onFormAddEdit(\Closure $fx)
{
$this->onFormEdit($fx);
$this->onFormAdd($fx);
}
/**
* Set onActions.
*/
public function setOnActions(string $actionName, \Closure $fx): void
{
$this->onActions[] = [$actionName => $fx];
}
}