Afform - Code cleanup in LoadAdminData API action
[civicrm-core.git] / ext / afform / admin / Civi / Api4 / Action / Afform / LoadAdminData.php
1 <?php
2
3 namespace Civi\Api4\Action\Afform;
4
5 use Civi\AfformAdmin\AfformAdminMeta;
6 use Civi\Api4\Afform;
7 use Civi\Api4\Utils\CoreUtil;
8 use Civi\Api4\Query\SqlExpression;
9
10 /**
11 * This action is used by the Afform Admin extension to load metadata for the Admin GUI.
12 *
13 * @package Civi\Api4\Action\Afform
14 */
15 class LoadAdminData extends \Civi\Api4\Generic\AbstractAction {
16
17 /**
18 * Any properties already known about the afform
19 * @var array
20 * @required
21 */
22 protected $definition;
23
24 /**
25 * Entity type when creating a new form
26 * @var string
27 */
28 protected $entity;
29
30 /**
31 * A list of entities whose blocks & fields are not needed
32 * @var array
33 */
34 protected $skipEntities = [];
35
36 public function _run(\Civi\Api4\Generic\Result $result) {
37 $info = ['entities' => [], 'fields' => [], 'blocks' => []];
38 $entities = [];
39 $newForm = empty($this->definition['name']);
40
41 if (!$newForm) {
42 // Load existing afform if name provided
43 $info['definition'] = $this->loadForm($this->definition['name']);
44 }
45 else {
46 // Create new blank afform
47 switch ($this->definition['type']) {
48 case 'form':
49 $info['definition'] = $this->definition + [
50 'title' => '',
51 'permission' => 'access CiviCRM',
52 'layout' => [
53 [
54 '#tag' => 'af-form',
55 'ctrl' => 'afform',
56 '#children' => [],
57 ],
58 ],
59 ];
60 break;
61
62 case 'block':
63 $info['definition'] = $this->definition + [
64 'title' => '',
65 'block' => $this->entity,
66 'layout' => [],
67 ];
68 break;
69
70 case 'search':
71 $info['definition'] = $this->definition + [
72 'title' => '',
73 'permission' => 'access CiviCRM',
74 'layout' => [
75 [
76 '#tag' => 'div',
77 'af-fieldset' => '',
78 '#children' => [],
79 ],
80 ],
81 ];
82 break;
83 }
84 }
85
86 $getFieldsMode = 'create';
87
88 // Generate list of possibly embedded afform tags to search for
89 $allAfforms = \Civi::service('afform_scanner')->findFilePaths();
90 foreach ($allAfforms as $name => $path) {
91 $allAfforms[$name] = _afform_angular_module_name($name, 'dash');
92 }
93
94 /**
95 * Find all entities by recursing into embedded afforms
96 * @param array $layout
97 */
98 $scanBlocks = function($layout) use (&$scanBlocks, &$info, &$entities, $allAfforms) {
99 // Find declared af-entity tags
100 foreach (\CRM_Utils_Array::findAll($layout, ['#tag' => 'af-entity']) as $afEntity) {
101 // Convert "Contact" to "Individual", "Organization" or "Household"
102 if ($afEntity['type'] === 'Contact' && !empty($afEntity['data'])) {
103 $data = \CRM_Utils_JS::decode($afEntity['data']);
104 $entities[] = $data['contact_type'] ?? $afEntity['type'];
105 }
106 else {
107 $entities[] = $afEntity['type'];
108 }
109 }
110 $joins = array_column(\CRM_Utils_Array::findAll($layout, 'af-join'), 'af-join');
111 $entities = array_unique(array_merge($entities, $joins));
112 $blockTags = array_unique(array_column(\CRM_Utils_Array::findAll($layout, function($el) use ($allAfforms) {
113 return in_array($el['#tag'], $allAfforms);
114 }), '#tag'));
115 foreach ($blockTags as $blockTag) {
116 if (!isset($info['blocks'][$blockTag])) {
117 // Load full contents of block used on the form, then recurse into it
118 $embeddedForm = Afform::get($this->checkPermissions)
119 ->setFormatWhitespace(TRUE)
120 ->setLayoutFormat('shallow')
121 ->addWhere('directive_name', '=', $blockTag)
122 ->execute()->first();
123 if ($embeddedForm['type'] === 'block') {
124 $info['blocks'][$blockTag] = $embeddedForm;
125 }
126 if (!empty($embeddedForm['join'])) {
127 $entities = array_unique(array_merge($entities, [$embeddedForm['join']]));
128 }
129 $scanBlocks($embeddedForm['layout']);
130 }
131 }
132 };
133
134 if ($info['definition']['type'] === 'form') {
135 if ($newForm) {
136 $entities[] = $this->entity;
137 $defaultEntity = AfformAdminMeta::getAfformEntity($this->entity);
138 if (!empty($defaultEntity['boilerplate'])) {
139 $scanBlocks($defaultEntity['boilerplate']);
140 }
141 }
142 else {
143 $scanBlocks($info['definition']['layout']);
144 }
145
146 if (array_intersect($entities, ['Individual', 'Household', 'Organization'])) {
147 $entities[] = 'Contact';
148 }
149
150 // The full contents of blocks used on the form have been loaded. Get basic info about others relevant to these entities.
151 $this->loadAvailableBlocks($entities, $info);
152 }
153
154 if ($info['definition']['type'] === 'block') {
155 $blockEntity = $info['definition']['join'] ?? $info['definition']['block'];
156 if ($blockEntity !== '*') {
157 $entities[] = $blockEntity;
158 }
159 $scanBlocks($info['definition']['layout']);
160 $this->loadAvailableBlocks($entities, $info);
161 }
162
163 if ($info['definition']['type'] === 'search') {
164 $getFieldsMode = 'search';
165 $displayTags = [];
166 if ($newForm) {
167 [$searchName, $displayName] = array_pad(explode('.', $this->entity ?? ''), 2, '');
168 $displayTags[] = ['search-name' => $searchName, 'display-name' => $displayName];
169 }
170 else {
171 foreach (\Civi\Search\Display::getDisplayTypes(['name']) as $displayType) {
172 $displayTags = array_merge($displayTags, \CRM_Utils_Array::findAll($info['definition']['layout'], ['#tag' => $displayType['name']]));
173 }
174 }
175 foreach ($displayTags as $displayTag) {
176 $display = \Civi\Api4\SearchDisplay::get(FALSE)
177 ->addWhere('name', '=', $displayTag['display-name'])
178 ->addWhere('saved_search.name', '=', $displayTag['search-name'])
179 ->addSelect('*', 'type:name', 'type:icon', 'saved_search.name', 'saved_search.api_entity', 'saved_search.api_params')
180 ->execute()->first();
181 $display['calc_fields'] = $this->getCalcFields($display['saved_search.api_entity'], $display['saved_search.api_params']);
182 $info['search_displays'][] = $display;
183 if ($newForm) {
184 $info['definition']['layout'][0]['#children'][] = $displayTag + ['#tag' => $display['type:name']];
185 }
186 $entities[] = $display['saved_search.api_entity'];
187 foreach ($display['saved_search.api_params']['join'] ?? [] as $join) {
188 $entities[] = explode(' AS ', $join[0])[0];
189 }
190 }
191 if (!$newForm) {
192 $scanBlocks($info['definition']['layout']);
193 }
194 $this->loadAvailableBlocks($entities, $info, [['join', 'IS NULL']]);
195 }
196
197 // Optimization - since contact fields are a combination of these three,
198 // we'll combine them client-side rather than sending them via ajax.
199 elseif (array_intersect($entities, ['Individual', 'Household', 'Organization'])) {
200 $entities = array_diff($entities, ['Contact']);
201 }
202
203 foreach (array_diff($entities, $this->skipEntities) as $entity) {
204 $info['entities'][$entity] = AfformAdminMeta::getApiEntity($entity);
205 $info['fields'][$entity] = AfformAdminMeta::getFields($entity, ['action' => $getFieldsMode]);
206 }
207 $info['blocks'] = array_values($info['blocks']);
208
209 $result[] = $info;
210 }
211
212 /**
213 * @param string $name
214 * @return array|null
215 */
216 private function loadForm($name) {
217 return Afform::get($this->checkPermissions)
218 ->setFormatWhitespace(TRUE)
219 ->setLayoutFormat('shallow')
220 ->addWhere('name', '=', $name)
221 ->execute()->first();
222 }
223
224 /**
225 * Get basic info about blocks relevant to these entities.
226 *
227 * @param array $entities
228 * @param array $info
229 * @param array $where
230 * @throws \API_Exception
231 * @throws \Civi\API\Exception\UnauthorizedException
232 */
233 private function loadAvailableBlocks($entities, &$info, $where = []) {
234 $entities = array_diff($entities, $this->skipEntities);
235 if (!$this->skipEntities) {
236 $entities[] = '*';
237 }
238 if ($entities) {
239 $blockInfo = Afform::get($this->checkPermissions)
240 ->addSelect('name', 'title', 'block', 'join', 'directive_name', 'repeat')
241 ->setWhere($where)
242 ->addWhere('type', '=', 'block')
243 ->addWhere('block', 'IN', $entities)
244 ->addWhere('directive_name', 'NOT IN', array_keys($info['blocks']))
245 ->execute();
246 $info['blocks'] = array_merge(array_values($info['blocks']), (array) $blockInfo);
247 }
248 }
249
250 /**
251 * @param string $apiEntity
252 * @param array $apiParams
253 * @return array
254 */
255 private function getCalcFields($apiEntity, $apiParams) {
256 $calcFields = [];
257 $api = \Civi\API\Request::create($apiEntity, 'get', $apiParams);
258 $selectQuery = new \Civi\Api4\Query\Api4SelectQuery($api);
259 $joinMap = $joinCount = [];
260 foreach ($apiParams['join'] ?? [] as $join) {
261 [$entityName, $alias] = explode(' AS ', $join[0]);
262 $num = '';
263 if (!empty($joinCount[$entityName])) {
264 $num = ' ' . (++$joinCount[$entityName]);
265 }
266 else {
267 $joinCount[$entityName] = 1;
268 }
269 $label = CoreUtil::getInfoItem($entityName, 'title');
270 $joinMap[$alias] = $label . $num;
271 }
272
273 foreach ($apiParams['select'] ?? [] as $select) {
274 if (strstr($select, ' AS ')) {
275 $expr = SqlExpression::convert($select, TRUE);
276 $field = $expr->getFields() ? $selectQuery->getField($expr->getFields()[0]) : NULL;
277 $joinName = explode('.', $expr->getFields()[0] ?? '')[0];
278 $label = $expr::getTitle() . ': ' . (isset($joinMap[$joinName]) ? $joinMap[$joinName] . ' ' : '') . $field['title'];
279 $calcFields[] = [
280 '#tag' => 'af-field',
281 'name' => $expr->getAlias(),
282 'defn' => [
283 'label' => $label,
284 'input_type' => 'Text',
285 ],
286 ];
287 }
288 }
289 return $calcFields;
290 }
291
292 /**
293 * @return array[]
294 */
295 public function fields() {
296 return [
297 [
298 'name' => 'definition',
299 'data_type' => 'Array',
300 ],
301 [
302 'name' => 'blocks',
303 'data_type' => 'Array',
304 ],
305 [
306 'name' => 'fields',
307 'data_type' => 'Array',
308 ],
309 [
310 'name' => 'search_displays',
311 'data_type' => 'Array',
312 ],
313 ];
314 }
315
316 /**
317 * @return array
318 */
319 public function getDefinition():array {
320 return $this->definition;
321 }
322
323 /**
324 * @param array $definition
325 */
326 public function setDefinition(array $definition) {
327 $this->definition = $definition;
328 return $this;
329 }
330
331 }