81ed486a81e28f4801b4ee5974f7a49104304fa6
[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 'entity_type' => $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 ->addSelect('*', 'directive_name')
120 ->setFormatWhitespace(TRUE)
121 ->setLayoutFormat('shallow')
122 ->addWhere('directive_name', '=', $blockTag)
123 ->execute()->first();
124 if ($embeddedForm['type'] === 'block') {
125 $info['blocks'][$blockTag] = $embeddedForm;
126 }
127 if (!empty($embeddedForm['join_entity'])) {
128 $entities = array_unique(array_merge($entities, [$embeddedForm['join_entity']]));
129 }
130 $scanBlocks($embeddedForm['layout']);
131 }
132 }
133 };
134
135 if ($info['definition']['type'] === 'form') {
136 if ($newForm) {
137 $entities[] = $this->entity;
138 $defaultEntity = AfformAdminMeta::getAfformEntity($this->entity);
139 if (!empty($defaultEntity['boilerplate'])) {
140 $scanBlocks($defaultEntity['boilerplate']);
141 }
142 }
143 else {
144 $scanBlocks($info['definition']['layout']);
145 }
146
147 if (array_intersect($entities, ['Individual', 'Household', 'Organization'])) {
148 $entities[] = 'Contact';
149 }
150
151 // The full contents of blocks used on the form have been loaded. Get basic info about others relevant to these entities.
152 $this->loadAvailableBlocks($entities, $info);
153 }
154
155 if ($info['definition']['type'] === 'block') {
156 $blockEntity = $info['definition']['join_entity'] ?? $info['definition']['entity_type'];
157 if ($blockEntity !== '*') {
158 $entities[] = $blockEntity;
159 }
160 $scanBlocks($info['definition']['layout']);
161 $this->loadAvailableBlocks($entities, $info);
162 }
163
164 if ($info['definition']['type'] === 'search') {
165 $getFieldsMode = 'search';
166 $displayTags = [];
167 if ($newForm) {
168 [$searchName, $displayName] = array_pad(explode('.', $this->entity ?? ''), 2, '');
169 $displayTags[] = ['search-name' => $searchName, 'display-name' => $displayName];
170 }
171 else {
172 foreach (\Civi\Search\Display::getDisplayTypes(['name']) as $displayType) {
173 $displayTags = array_merge($displayTags, \CRM_Utils_Array::findAll($info['definition']['layout'], ['#tag' => $displayType['name']]));
174 }
175 }
176 foreach ($displayTags as $displayTag) {
177 $display = \Civi\Api4\SearchDisplay::get(FALSE)
178 ->addWhere('name', '=', $displayTag['display-name'])
179 ->addWhere('saved_search.name', '=', $displayTag['search-name'])
180 ->addSelect('*', 'type:name', 'type:icon', 'saved_search.name', 'saved_search.api_entity', 'saved_search.api_params')
181 ->execute()->first();
182 $display['calc_fields'] = $this->getCalcFields($display['saved_search.api_entity'], $display['saved_search.api_params']);
183 $info['search_displays'][] = $display;
184 if ($newForm) {
185 $info['definition']['layout'][0]['#children'][] = $displayTag + ['#tag' => $display['type:name']];
186 }
187 $entities[] = $display['saved_search.api_entity'];
188 foreach ($display['saved_search.api_params']['join'] ?? [] as $join) {
189 $entities[] = explode(' AS ', $join[0])[0];
190 }
191 }
192 if (!$newForm) {
193 $scanBlocks($info['definition']['layout']);
194 }
195 $this->loadAvailableBlocks($entities, $info, [['join_entity', 'IS NULL']]);
196 }
197
198 // Optimization - since contact fields are a combination of these three,
199 // we'll combine them client-side rather than sending them via ajax.
200 elseif (array_intersect($entities, ['Individual', 'Household', 'Organization'])) {
201 $entities = array_diff($entities, ['Contact']);
202 }
203
204 foreach (array_diff($entities, $this->skipEntities) as $entity) {
205 $info['entities'][$entity] = AfformAdminMeta::getApiEntity($entity);
206 $info['fields'][$entity] = AfformAdminMeta::getFields($entity, ['action' => $getFieldsMode]);
207 }
208 $info['blocks'] = array_values($info['blocks']);
209
210 $result[] = $info;
211 }
212
213 /**
214 * @param string $name
215 * @return array|null
216 */
217 private function loadForm($name) {
218 return Afform::get($this->checkPermissions)
219 ->setFormatWhitespace(TRUE)
220 ->setLayoutFormat('shallow')
221 ->addWhere('name', '=', $name)
222 ->execute()->first();
223 }
224
225 /**
226 * Get basic info about blocks relevant to these entities.
227 *
228 * @param array $entities
229 * @param array $info
230 * @param array $where
231 * @throws \API_Exception
232 * @throws \Civi\API\Exception\UnauthorizedException
233 */
234 private function loadAvailableBlocks($entities, &$info, $where = []) {
235 $entities = array_diff($entities, $this->skipEntities);
236 if (!$this->skipEntities) {
237 $entities[] = '*';
238 }
239 if ($entities) {
240 $blockInfo = Afform::get($this->checkPermissions)
241 ->addSelect('name', 'title', 'entity_type', 'join_entity', 'directive_name')
242 ->setWhere($where)
243 ->addWhere('type', '=', 'block')
244 ->addWhere('entity_type', 'IN', $entities)
245 ->addWhere('directive_name', 'NOT IN', array_keys($info['blocks']))
246 ->execute();
247 $info['blocks'] = array_merge(array_values($info['blocks']), (array) $blockInfo);
248 }
249 }
250
251 /**
252 * @param string $apiEntity
253 * @param array $apiParams
254 * @return array
255 */
256 private function getCalcFields($apiEntity, $apiParams) {
257 $calcFields = [];
258 $api = \Civi\API\Request::create($apiEntity, 'get', $apiParams);
259 $selectQuery = new \Civi\Api4\Query\Api4SelectQuery($api);
260 $joinMap = $joinCount = [];
261 foreach ($apiParams['join'] ?? [] as $join) {
262 [$entityName, $alias] = explode(' AS ', $join[0]);
263 $num = '';
264 if (!empty($joinCount[$entityName])) {
265 $num = ' ' . (++$joinCount[$entityName]);
266 }
267 else {
268 $joinCount[$entityName] = 1;
269 }
270 $label = CoreUtil::getInfoItem($entityName, 'title');
271 $joinMap[$alias] = $label . $num;
272 }
273
274 foreach ($apiParams['select'] ?? [] as $select) {
275 if (strstr($select, ' AS ')) {
276 $expr = SqlExpression::convert($select, TRUE);
277 $label = $expr::getTitle();
278 foreach ($expr->getFields() as $num => $fieldName) {
279 $field = $selectQuery->getField($fieldName);
280 $joinName = explode('.', $fieldName)[0];
281 $label .= ($num ? ', ' : ': ') . (isset($joinMap[$joinName]) ? $joinMap[$joinName] . ' ' : '') . $field['title'];
282 }
283 $calcFields[] = [
284 '#tag' => 'af-field',
285 'name' => $expr->getAlias(),
286 'defn' => [
287 'label' => $label,
288 'input_type' => 'Text',
289 ],
290 ];
291 }
292 }
293 return $calcFields;
294 }
295
296 /**
297 * @return array[]
298 */
299 public function fields() {
300 return [
301 [
302 'name' => 'definition',
303 'data_type' => 'Array',
304 ],
305 [
306 'name' => 'blocks',
307 'data_type' => 'Array',
308 ],
309 [
310 'name' => 'fields',
311 'data_type' => 'Array',
312 ],
313 [
314 'name' => 'search_displays',
315 'data_type' => 'Array',
316 ],
317 ];
318 }
319
320 /**
321 * @return array
322 */
323 public function getDefinition():array {
324 return $this->definition;
325 }
326
327 /**
328 * @param array $definition
329 */
330 public function setDefinition(array $definition) {
331 $this->definition = $definition;
332 return $this;
333 }
334
335 }