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\Data\Model\UserAction;
10: use Atk4\Ui\Button;
11: use Atk4\Ui\Exception;
12: use Atk4\Ui\Js\JsBlock;
13: use Atk4\Ui\Js\JsExpressionable;
14: use Atk4\Ui\Js\JsFunction;
15: use Atk4\Ui\Js\JsToast;
16: use Atk4\Ui\Loader;
17: use Atk4\Ui\Modal;
18: use Atk4\Ui\Text;
19: use Atk4\Ui\View;
20:
21: /**
22: * Modal executor for action that required a confirmation.
23: */
24: class ConfirmationExecutor extends Modal implements JsExecutorInterface
25: {
26: use CommonExecutorTrait;
27: use HookTrait;
28:
29: /** @var Model\UserAction|null Action to execute */
30: public $action;
31:
32: /** @var Loader|null Loader to add content to modal. */
33: public $loader;
34:
35: /** @var string */
36: public $loaderUi = 'basic segment';
37: /** @var array|View|null Loader shim object or seed. */
38: public $loaderShim;
39: /** @var JsExpressionable|\Closure JS expression to return if action was successful, e.g "new JsToast('Thank you')" */
40: public $jsSuccess;
41:
42: /** @var string CSS class for modal size. */
43: public $size = 'tiny';
44:
45: /** @var string|null */
46: private $step;
47:
48: /** @var Button Ok button */
49: private $ok;
50: /** @var Button Cancel button */
51: private $cancel;
52:
53: #[\Override]
54: protected function init(): void
55: {
56: parent::init();
57:
58: $this->addClass($this->size);
59: }
60:
61: /**
62: * Properly set element ID for this modal.
63: */
64: protected function afterActionInit(): void
65: {
66: // add buttons to modal for next and previous
67: $buttonsView = (new View())->setStyle(['min-height' => '24px']);
68: $this->ok = Button::addTo($buttonsView, ['Ok', 'class.blue' => true]);
69: $this->cancel = Button::addTo($buttonsView, ['Cancel']);
70: $this->add($buttonsView, 'actions');
71: $this->showActions = true;
72:
73: $this->loader = Loader::addTo($this, ['ui' => $this->loaderUi, 'shim' => $this->loaderShim]);
74: $this->loader->loadEvent = false;
75: $this->loader->addClass('atk-hide-loading-content');
76: }
77:
78: /**
79: * @param array<string, string> $urlArgs
80: */
81: private function jsShowAndLoad(array $urlArgs): JsBlock
82: {
83: return new JsBlock([
84: $this->jsShow(),
85: $this->js()->data('closeOnLoadingError', true),
86: $this->loader->jsLoad($urlArgs, [
87: 'method' => 'POST',
88: 'onSuccess' => new JsFunction([], [$this->js()->removeData('closeOnLoadingError')]),
89: ]),
90: ]);
91: }
92:
93: #[\Override]
94: public function jsExecute(array $urlArgs = []): JsBlock
95: {
96: if (!$this->action) {
97: throw new Exception('Action must be set prior to assign trigger');
98: }
99:
100: return $this->jsShowAndLoad($urlArgs);
101: }
102:
103: #[\Override]
104: public function getAction(): UserAction
105: {
106: return $this->action;
107: }
108:
109: #[\Override]
110: public function setAction(Model\UserAction $action)
111: {
112: $this->action = $action;
113: $this->afterActionInit();
114:
115: $this->title ??= $action->getDescription();
116: $this->step = $this->stickyGet('step');
117:
118: $this->jsSetButtonsState($this);
119:
120: return $this;
121: }
122:
123: /**
124: * Perform the current step.
125: */
126: #[\Override]
127: public function executeModelAction(): void
128: {
129: $this->action = $this->executeModelActionLoad($this->action);
130:
131: $this->loader->set(function (Loader $p) {
132: $this->jsSetButtonsState($p);
133: if ($this->step === 'execute') {
134: $this->doFinal($p);
135: } else {
136: $this->doConfirmation($p);
137: }
138: });
139: }
140:
141: /**
142: * Reset button state.
143: */
144: protected function jsSetButtonsState(View $view): void
145: {
146: $view->js(true, $this->ok->js()->off());
147: $view->js(true, $this->cancel->js()->off());
148: }
149:
150: /**
151: * Set modal for displaying confirmation message.
152: * Also apply proper javascript to each button.
153: */
154: public function doConfirmation(View $modal): void
155: {
156: $this->addConfirmation($modal);
157:
158: $modal->js(
159: true,
160: $this->ok->js()->on('click', new JsFunction([], [
161: $this->loader->jsLoad(
162: [
163: 'step' => 'execute',
164: $this->name => $this->action->getEntity()->getId(),
165: ],
166: ['method' => 'POST']
167: ),
168: ]))
169: );
170:
171: $modal->js(
172: true,
173: $this->cancel->js()->on('click', new JsFunction([], [
174: $this->jsHide(),
175: ]))
176: );
177: }
178:
179: /**
180: * Add confirmation message to modal.
181: */
182: protected function addConfirmation(View $view): void
183: {
184: Text::addTo($view)->set($this->action->getConfirmation());
185: }
186:
187: /**
188: * Execute action when all step are completed.
189: */
190: protected function doFinal(View $modal): void
191: {
192: $return = $this->action->execute([]);
193:
194: $modal->js(true, $this->jsGetExecute($return, $this->action->getEntity()->getId()));
195: }
196:
197: /**
198: * Return proper JS statement when action execute.
199: *
200: * @param mixed $obj
201: * @param string|int $id
202: */
203: protected function jsGetExecute($obj, $id): JsBlock
204: {
205: $success = $this->jsSuccess instanceof \Closure
206: ? ($this->jsSuccess)($this, $this->action->getModel(), $id)
207: : $this->jsSuccess;
208:
209: return new JsBlock([
210: $this->jsHide(),
211: $this->ok->js(true)->off(),
212: $this->cancel->js(true)->off(),
213: JsBlock::fromHookResult($this->hook(BasicExecutor::HOOK_AFTER_EXECUTE, [$obj, $id]) // @phpstan-ignore-line
214: ?: ($success ?? new JsToast('Success' . (is_string($obj) ? (': ' . $obj) : '')))),
215: ]);
216: }
217: }
218: