1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\UserAction;
6:
7: use Atk4\Core\Factory;
8: use Atk4\Core\HookTrait;
9: use Atk4\Data\Model;
10: use Atk4\Ui\Button;
11: use Atk4\Ui\Header;
12: use Atk4\Ui\Js\JsBlock;
13: use Atk4\Ui\Js\JsChain;
14: use Atk4\Ui\Js\JsToast;
15: use Atk4\Ui\Loader;
16: use Atk4\Ui\View;
17: use Atk4\Ui\VirtualPage;
18:
19: /**
20: * A Step Action Executor that use a VirtualPage.
21: */
22: class VpExecutor extends View implements JsExecutorInterface
23: {
24: use CommonExecutorTrait;
25: use HookTrait;
26: use StepExecutorTrait;
27:
28: public const HOOK_STEP = self::class . '@onStep';
29:
30: /** @var VirtualPage */
31: protected $vp;
32:
33: /** @var string|null */
34: public $title;
35:
36: /** @var Header */
37: public $header;
38:
39: /** @var View */
40: public $stepList;
41:
42: /** @var array<string, string> */
43: public $stepListItems = ['args' => 'Fill argument(s)', 'fields' => 'Edit Record(s)', 'preview' => 'Preview', 'final' => 'Complete'];
44:
45: /** @var array */
46: public $cancelButtonSeed = [Button::class, ['Cancel', 'class.small left floated basic blue' => true, 'icon' => 'left arrow']];
47:
48: #[\Override]
49: protected function init(): void
50: {
51: parent::init();
52:
53: $this->initExecutor();
54: }
55:
56: protected function initExecutor(): void
57: {
58: $this->vp = VirtualPage::addTo($this);
59: /** @var Button $b */
60: $b = $this->vp->add(Factory::factory($this->cancelButtonSeed));
61: $b->link($this->getApp()->url());
62: View::addTo($this->vp, ['ui' => 'clearing divider']);
63:
64: $this->header = Header::addTo($this->vp);
65: $this->stepList = View::addTo($this->vp)->addClass('ui horizontal bulleted link list');
66: }
67:
68: #[\Override]
69: public function getAction(): Model\UserAction
70: {
71: return $this->action;
72: }
73:
74: /**
75: * Make sure modal id is unique.
76: * Since User action can be added via callbacks, we need
77: * to make sure that view id is properly set for loader and button
78: * JS action to run properly.
79: */
80: protected function afterActionInit(): void
81: {
82: $this->loader = Loader::addTo($this->vp, ['ui' => $this->loaderUi, 'shim' => $this->loaderShim]);
83: $this->actionData = $this->loader->jsGetStoreData()['session'];
84: }
85:
86: #[\Override]
87: public function setAction(Model\UserAction $action)
88: {
89: $this->action = $action;
90: $this->afterActionInit();
91:
92: // get necessary step need prior to execute action
93: $this->steps = $this->getSteps();
94: if ($this->steps !== []) {
95: $this->header->set($this->title ?? $action->getDescription());
96: $this->step = $this->stickyGet('step') ?? $this->steps[0];
97: $this->vp->add($this->createButtonBar()->setStyle(['text-align' => 'end']));
98: $this->addStepList();
99: }
100:
101: $this->actionInitialized = true;
102:
103: return $this;
104: }
105:
106: #[\Override]
107: public function jsExecute(array $urlArgs = []): JsBlock
108: {
109: $urlArgs['step'] = $this->step;
110:
111: return new JsBlock([(new JsChain('atk.utils'))->redirect($this->vp->getUrl(), $urlArgs)]);
112: }
113:
114: /**
115: * Perform model action by stepping through args - fields - preview.
116: */
117: #[\Override]
118: public function executeModelAction(): void
119: {
120: $this->action = $this->executeModelActionLoad($this->action);
121:
122: $this->vp->set(function () {
123: $this->jsSetButtonsState($this->loader, $this->step);
124: $this->jsSetListState($this->loader, $this->step);
125: $this->runSteps();
126: });
127: }
128:
129: protected function addStepList(): void
130: {
131: if (count($this->steps) === 1) {
132: return;
133: }
134:
135: foreach ($this->steps as $step) {
136: View::addTo($this->stepList)->set($this->stepListItems[$step])->addClass('item')->setAttr(['data-list-item' => $step]);
137: }
138: }
139:
140: protected function jsSetListState(View $view, string $currentStep): void
141: {
142: $view->js(true, $this->stepList->js()->find('.item')->removeClass('active'));
143: foreach ($this->steps as $step) {
144: if ($step === $currentStep) {
145: $view->js(true, $this->stepList->js()->find('[data-list-item="' . $step . '"]')->addClass('active'));
146: }
147: }
148: }
149:
150: /**
151: * Return proper JS statement need after action execution.
152: *
153: * @param mixed $obj
154: * @param string|int $id
155: */
156: protected function jsGetExecute($obj, $id): JsBlock
157: {
158: $success = $this->jsSuccess instanceof \Closure
159: ? ($this->jsSuccess)($this, $this->action->getModel(), $id, $obj)
160: : $this->jsSuccess;
161:
162: return new JsBlock([
163: JsBlock::fromHookResult($this->hook(BasicExecutor::HOOK_AFTER_EXECUTE, [$obj, $id]) // @phpstan-ignore-line
164: ?: ($success ?? new JsToast('Success' . (is_string($obj) ? (': ' . $obj) : '')))),
165: $this->loader->jsClearStoreData(true),
166: (new JsChain('atk.utils'))->redirect($this->url()),
167: ]);
168: }
169: }
170: