1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\UserAction;
6:
7: use Atk4\Core\Factory;
8: use Atk4\Core\WarnDynamicPropertyTrait;
9: use Atk4\Data\Model\UserAction;
10: use Atk4\Ui\AbstractView;
11: use Atk4\Ui\Button;
12: use Atk4\Ui\Exception;
13: use Atk4\Ui\MenuItem;
14: use Atk4\Ui\View;
15:
16: class ExecutorFactory
17: {
18: use WarnDynamicPropertyTrait;
19:
20: public const JS_EXECUTOR = self::class . '@jsExecutorSeed';
21: public const STEP_EXECUTOR = self::class . '@stepExecutorSeed';
22: public const CONFIRMATION_EXECUTOR = self::class . '@confirmationExecutorClass';
23:
24: public const BASIC_BUTTON = self::class . '@basicButton';
25: public const MODAL_BUTTON = self::class . '@modalExecutorButton';
26: public const TABLE_BUTTON = self::class . '@tableButton';
27: public const CARD_BUTTON = self::class . '@cardButton';
28: public const MENU_ITEM = self::class . '@menuItem';
29: public const TABLE_MENU_ITEM = self::class . '@tableMenuItem';
30:
31: /** @var string */
32: public $buttonPrimaryColor = 'primary';
33:
34: /**
35: * Store basic type of executor to use for create method.
36: * Basic type can be changed or added globally via the registerTypeExecutor method.
37: * A specific model/action executor may be set via the registerExecutor method.
38: *
39: * @var array<string, array>
40: */
41: protected $executorSeed = [
42: self::JS_EXECUTOR => [JsCallbackExecutor::class],
43: self::STEP_EXECUTOR => [ModalExecutor::class],
44: self::CONFIRMATION_EXECUTOR => [ConfirmationExecutor::class],
45: ];
46:
47: /**
48: * Store caption to use for action.
49: * Can be apply globally per action name
50: * or specifically per model/action name.
51: *
52: * Can be set to a callable method in order
53: * to customize the return caption further more.
54: *
55: * @var array<string, string|array<string, string>>
56: */
57: protected $triggerCaption = [
58: self::MODAL_BUTTON => [
59: 'add' => 'Save',
60: 'edit' => 'Save',
61: ],
62: ];
63:
64: /**
65: * Store seed|View|callable method
66: * to use for creating UI View Object
67: * They can be store per either view type or
68: * model/action name.
69: *
70: * @var array<string, array|View>
71: */
72: protected $triggerSeed = [
73: self::TABLE_BUTTON => [
74: 'edit' => [Button::class, null, 'icon' => 'edit'],
75: 'delete' => [Button::class, null, 'icon' => 'red trash'],
76: ],
77: self::MENU_ITEM => [
78: 'add' => [__CLASS__, 'getAddMenuItem'],
79: ],
80: ];
81:
82: /**
83: * Register an executor for a basic type.
84: */
85: public function registerTypeExecutor(string $type, array $seed): void
86: {
87: $this->executorSeed[$type] = $seed;
88: }
89:
90: /**
91: * Register an executor instance for a specific model User action.
92: */
93: public function registerExecutor(UserAction $action, array $seed): void
94: {
95: $this->executorSeed[$this->getModelKey($action)][$action->shortName] = $seed;
96: }
97:
98: /**
99: * Register a trigger for a specific View type.
100: * Trigger can be specify per action or per model/action.
101: *
102: * @param array|View $seed
103: */
104: public function registerTrigger(string $type, $seed, UserAction $action, bool $isSpecific = false): void
105: {
106: if ($isSpecific) {
107: $this->triggerSeed[$type][$this->getModelKey($action)][$action->shortName] = $seed;
108: } else {
109: $this->triggerSeed[$type][$action->shortName] = $seed;
110: }
111: }
112:
113: /**
114: * Set an action trigger type to use it's default seed.
115: */
116: public function useTriggerDefault(string $type): void
117: {
118: $this->triggerSeed[$type] = [];
119: }
120:
121: /**
122: * Register a trigger caption.
123: */
124: public function registerCaption(UserAction $action, string $caption, bool $isSpecific = false, string $type = null): void
125: {
126: if ($isSpecific) {
127: $this->triggerCaption[$this->getModelKey($action)][$action->shortName] = $caption;
128: } elseif ($type) {
129: $this->triggerCaption[$type][$action->shortName] = $caption;
130: } else {
131: $this->triggerCaption[$action->shortName] = $caption;
132: }
133: }
134:
135: /**
136: * @return ($type is self::MENU_ITEM ? MenuItem : ($type is self::TABLE_MENU_ITEM ? MenuItem : Button))
137: */
138: public function createTrigger(UserAction $action, string $type = null): View
139: {
140: return $this->createActionTrigger($action, $type); // @phpstan-ignore-line
141: }
142:
143: public function getCaption(UserAction $action, string $type = null): string
144: {
145: return $this->getActionCaption($action, $type);
146: }
147:
148: /**
149: * @return AbstractView&ExecutorInterface
150: */
151: public function createExecutor(UserAction $action, View $owner, string $requiredType = null): ExecutorInterface
152: {
153: if ($requiredType !== null) {
154: if (!($this->executorSeed[$requiredType] ?? null)) {
155: throw (new Exception('Required executor type is not set'))
156: ->addMoreInfo('type', $requiredType);
157: }
158: $seed = $this->executorSeed[$requiredType];
159: } else {
160: $seed = $this->executorSeed[$this->getModelKey($action)][$action->shortName] ?? null;
161: if ($seed === null) {
162: // if no type is register, determine executor to use base on action properties
163: if ($action->confirmation instanceof \Closure) {
164: $seed = $this->executorSeed[self::CONFIRMATION_EXECUTOR];
165: } else {
166: $seed = (!$action->args && !$action->fields && !$action->preview)
167: ? $this->executorSeed[self::JS_EXECUTOR]
168: : $this->executorSeed[self::STEP_EXECUTOR];
169: }
170: }
171: }
172:
173: /** @var AbstractView&ExecutorInterface */
174: $executor = $owner->add(Factory::factory($seed));
175: $executor->setAction($action);
176:
177: return $executor;
178: }
179:
180: /**
181: * Create executor View for firing model user action.
182: */
183: protected function createActionTrigger(UserAction $action, string $type = null): View
184: {
185: $viewType = array_merge(['default' => [$this, 'getDefaultTrigger']], $this->triggerSeed[$type] ?? []);
186: $seed = $viewType[$this->getModelKey($action)][$action->shortName] ?? null;
187: if ($seed === null) {
188: $seed = $viewType[$action->shortName] ?? null;
189: if ($seed === null) {
190: $seed = $viewType['default'];
191: }
192: }
193:
194: if (is_array($seed) && is_callable($seed)) {
195: $seed = call_user_func($seed, $action, $type);
196: }
197:
198: return Factory::factory($seed);
199: }
200:
201: /**
202: * Return executor default trigger seed based on type.
203: */
204: protected function getDefaultTrigger(UserAction $action, string $type = null): array
205: {
206: switch ($type) {
207: case self::CARD_BUTTON:
208: case self::TABLE_BUTTON:
209: case self::MODAL_BUTTON:
210: $seed = [Button::class, $this->getActionCaption($action, $type)];
211: if ($type === self::MODAL_BUTTON || $type === self::CARD_BUTTON) {
212: $seed['class.' . $this->buttonPrimaryColor] = true;
213: }
214:
215: break;
216: case self::MENU_ITEM:
217: $seed = [MenuItem::class, $this->getActionCaption($action, $type)];
218:
219: break;
220: case self::TABLE_MENU_ITEM:
221: $seed = [MenuItem::class, $this->getActionCaption($action, $type), 'name' => false, 'class.item' => true];
222:
223: break;
224: default:
225: $seed = [Button::class, $this->getActionCaption($action, $type)];
226: }
227:
228: return $seed;
229: }
230:
231: /**
232: * Return action caption set in actionLabel or default.
233: */
234: protected function getActionCaption(UserAction $action, string $type = null): string
235: {
236: $caption = $this->triggerCaption[$type][$action->shortName] ?? null;
237: if ($caption === null) {
238: $caption = $this->triggerCaption[$this->getModelKey($action)][$action->shortName] ?? null;
239: if ($caption === null) {
240: $caption = $this->triggerCaption[$action->shortName] ?? null;
241: if ($caption === null) {
242: $caption = $action->getCaption();
243: }
244: }
245: }
246:
247: if (is_array($caption) && is_callable($caption)) {
248: $caption = call_user_func($caption, $action);
249: }
250:
251: return $caption;
252: }
253:
254: /**
255: * Return Add action seed for menu item.
256: */
257: protected function getAddMenuItem(UserAction $action): array
258: {
259: return [MenuItem::class, $this->getAddActionCaption($action), 'icon' => 'plus'];
260: }
261:
262: /**
263: * Return label for add model UserAction.
264: */
265: protected function getAddActionCaption(UserAction $action): string
266: {
267: return 'Add' . ($action->getModel()->caption ? ' ' . $action->getModel()->caption : '');
268: }
269:
270: protected function getModelKey(UserAction $action): string
271: {
272: return $action->getModel()->getModelCaption();
273: }
274: }
275: