1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\UserAction;
6:
7: use Atk4\Core\HookTrait;
8: use Atk4\Data\Model;
9: use Atk4\Ui\Js\Jquery;
10: use Atk4\Ui\Js\JsBlock;
11: use Atk4\Ui\Js\JsExpressionable;
12: use Atk4\Ui\Js\JsToast;
13: use Atk4\Ui\JsCallback;
14: use Atk4\Ui\View;
15:
16: /**
17: * Javascript Action executor.
18: *
19: * Will execute a model action using a JS Event.
20: *
21: * Usage:
22: * When use with View::on method, then JsCallbackExecutor executor is automatically create.
23: * $button->on('click', $model->getUserAction('delete'), [4, 'confirm' => 'This will delete record with ID 4. Are you sure?']);
24: *
25: * Manual setup.
26: * $action = $model->getUserAction('delete')
27: * $ex = JsCallbackExecutor::addTo($app)->setAction($action, [4])
28: * $button->on('click', $ex, ['confirm' => 'This will delete record with id 4. Are you sure?']);
29: */
30: class JsCallbackExecutor extends JsCallback implements ExecutorInterface
31: {
32: use HookTrait;
33:
34: /** @var Model\UserAction The model user action */
35: public $action;
36:
37: /** @var JsExpressionable|\Closure JS expression to return if action was successful, e.g "new JsToast('Thank you')" */
38: public $jsSuccess;
39:
40: #[\Override]
41: public function getAction(): Model\UserAction
42: {
43: return $this->action;
44: }
45:
46: #[\Override]
47: public function setAction(Model\UserAction $action)
48: {
49: $this->action = $action;
50: if (!$this->action->enabled && $this->getOwner() instanceof View) { // @phpstan-ignore-line
51: $this->getOwner()->addClass('disabled');
52: }
53:
54: return $this;
55: }
56:
57: /**
58: * @template T
59: *
60: * @param \Closure(): T $fx
61: * @param array<string, string> $urlArgs
62: *
63: * @return T
64: */
65: protected function invokeFxWithUrlArgs(\Closure $fx, array $urlArgs = [])
66: {
67: $argsOrig = $this->args;
68: $this->args = array_merge($this->args, $urlArgs);
69: try {
70: return $fx();
71: } finally {
72: $this->args = $argsOrig;
73: }
74: }
75:
76: /**
77: * @param array<string, string> $urlArgs
78: */
79: #[\Override]
80: public function jsExecute(array $urlArgs = []): JsBlock
81: {
82: return $this->invokeFxWithUrlArgs(function () { // backup/restore $this->args and merge them with $urlArgs
83: return parent::jsExecute();
84: }, $urlArgs);
85: }
86:
87: #[\Override]
88: public function executeModelAction(): void
89: {
90: $this->invokeFxWithUrlArgs(function () { // backup/restore $this->args mutated in https://github.com/atk4/ui/blob/8926412a31bc17d3ed1e751e67770557fe865935/src/JsCallback.php#L71
91: $this->set(function (Jquery $j, ...$values) {
92: $id = $this->getApp()->uiPersistence->typecastLoadField(
93: $this->action->getModel()->getField($this->action->getModel()->idField),
94: $this->getApp()->tryGetRequestPostParam($this->name)
95: );
96: if ($id && $this->action->appliesTo === Model\UserAction::APPLIES_TO_SINGLE_RECORD) {
97: if ($this->action->isOwnerEntity() && $this->action->getEntity()->getId()) {
98: $this->action->getEntity()->setId($id); // assert ID is the same
99: } else {
100: $this->action = $this->action->getActionForEntity($this->action->getModel()->load($id));
101: }
102: } elseif (!$this->action->isOwnerEntity()
103: && in_array($this->action->appliesTo, [Model\UserAction::APPLIES_TO_NO_RECORDS, Model\UserAction::APPLIES_TO_SINGLE_RECORD], true)
104: ) {
105: $this->action = $this->action->getActionForEntity($this->action->getModel()->createEntity());
106: }
107:
108: $return = $this->action->execute(...$values);
109:
110: $success = $this->jsSuccess instanceof \Closure
111: ? ($this->jsSuccess)($this, $this->action->getModel(), $id, $return)
112: : $this->jsSuccess;
113:
114: $js = JsBlock::fromHookResult($this->hook(BasicExecutor::HOOK_AFTER_EXECUTE, [$return, $id]) // @phpstan-ignore-line
115: ?: ($success ?? new JsToast('Success' . (is_string($return) ? (': ' . $return) : ''))));
116:
117: return $js;
118: }, array_map(static fn () => true, $this->action->args));
119: });
120: }
121: }
122: