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\Exception;
10: use Atk4\Ui\Js\JsBlock;
11: use Atk4\Ui\Js\JsFunction;
12: use Atk4\Ui\Js\JsToast;
13: use Atk4\Ui\Loader;
14: use Atk4\Ui\Modal;
15: use Atk4\Ui\View;
16:
17: /**
18: * Modal executor for action.
19: * These are special modal that will divide a model action into steps
20: * and run each step accordingly via a loader setup in modal view.
21: * The step orders are Argument, Field and Preview, prior to execute the model action.
22: *
23: * It will first determine the number of step necessary to run the model
24: * action. When a step is running through the view loader, data collect for each step
25: * are store in browser session storage via javascript. Thus, each request to execute loader,
26: * include step data within the request.
27: *
28: * ModalExecutor modal view may be generated via callbacks.
29: * These modal are added to app->html view if not already added
30: * and the api service take care of generating them when output
31: * in JSON via callback. It is important that these ModalExecutor modals
32: * stay within the page HTML content for loader to run each steps properly.
33: */
34: class ModalExecutor extends Modal implements JsExecutorInterface
35: {
36: use CommonExecutorTrait;
37: use HookTrait;
38: use StepExecutorTrait;
39:
40: public const HOOK_STEP = self::class . '@onStep';
41:
42: #[\Override]
43: protected function init(): void
44: {
45: parent::init();
46:
47: $this->initExecutor();
48: }
49:
50: protected function initExecutor(): void {}
51:
52: #[\Override]
53: public function getAction(): Model\UserAction
54: {
55: return $this->action;
56: }
57:
58: /**
59: * Make sure modal id is unique.
60: * Since User action can be added via callbacks, we need
61: * to make sure that view id is properly set for loader and button
62: * JS action to run properly.
63: */
64: protected function afterActionInit(): void
65: {
66: $this->loader = Loader::addTo($this, ['ui' => $this->loaderUi, 'shim' => $this->loaderShim]);
67: $this->loader->loadEvent = false;
68: $this->loader->addClass('atk-hide-loading-content');
69: $this->actionData = $this->loader->jsGetStoreData()['session'];
70: }
71:
72: #[\Override]
73: public function setAction(Model\UserAction $action)
74: {
75: $this->action = $action;
76: $this->afterActionInit();
77:
78: // get necessary step need prior to execute action
79: $this->steps = $this->getSteps();
80: if ($this->steps !== []) {
81: $this->title ??= $action->getDescription();
82:
83: // get current step
84: $this->step = $this->stickyGet('step') ?? $this->steps[0];
85: }
86:
87: $this->actionInitialized = true;
88:
89: return $this;
90: }
91:
92: /**
93: * Perform model action by stepping through args - fields - preview.
94: */
95: #[\Override]
96: public function executeModelAction(): void
97: {
98: $this->action = $this->executeModelActionLoad($this->action);
99:
100: // add buttons to modal for next and previous
101: $this->addButtonAction($this->createButtonBar());
102: $this->jsSetButtonsState($this->loader, $this->step);
103: $this->runSteps();
104: }
105:
106: /**
107: * @param array<string, string> $urlArgs
108: */
109: private function jsShowAndLoad(array $urlArgs): JsBlock
110: {
111: return new JsBlock([
112: $this->jsShow(),
113: $this->js()->data('closeOnLoadingError', true),
114: $this->loader->jsLoad($urlArgs, [
115: 'method' => 'POST',
116: 'onSuccess' => new JsFunction([], [$this->js()->removeData('closeOnLoadingError')]),
117: ]),
118: ]);
119: }
120:
121: /**
122: * Assign a View that will fire action execution.
123: * If action require steps, it will automatically initialize
124: * proper step to execute first.
125: *
126: * @param array<string, string> $urlArgs
127: *
128: * @return $this
129: */
130: public function assignTrigger(View $view, array $urlArgs = [], string $when = 'click', string $selector = null): self
131: {
132: if (!$this->actionInitialized) {
133: throw new Exception('Action must be set prior to assign trigger');
134: }
135:
136: if ($this->steps !== []) {
137: // use modal for stepping action
138: $urlArgs['step'] = $this->step;
139: if ($this->action->enabled) {
140: $view->on($when, $selector, $this->jsShowAndLoad($urlArgs));
141: } else {
142: $view->addClass('disabled');
143: }
144: }
145:
146: return $this;
147: }
148:
149: #[\Override]
150: public function jsExecute(array $urlArgs = []): JsBlock
151: {
152: if (!$this->actionInitialized) {
153: throw new Exception('Action must be set prior to assign trigger');
154: }
155:
156: $urlArgs['step'] = $this->step;
157:
158: return $this->jsShowAndLoad($urlArgs);
159: }
160:
161: /**
162: * Return proper JS statement need after action execution.
163: *
164: * @param mixed $obj
165: * @param string|int $id
166: */
167: protected function jsGetExecute($obj, $id): JsBlock
168: {
169: $success = $this->jsSuccess instanceof \Closure
170: ? ($this->jsSuccess)($this, $this->action->getModel(), $id, $obj)
171: : $this->jsSuccess;
172:
173: return new JsBlock([
174: $this->jsHide(),
175: JsBlock::fromHookResult($this->hook(BasicExecutor::HOOK_AFTER_EXECUTE, [$obj, $id]) // @phpstan-ignore-line
176: ?: ($success ?? new JsToast('Success' . (is_string($obj) ? (': ' . $obj) : '')))),
177: $this->loader->jsClearStoreData(true),
178: ]);
179: }
180: }
181: