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