3 namespace Civi\Api4\Action\Afform
;
5 use Civi\AfformAdmin\AfformAdminMeta
;
7 use Civi\Api4\Utils\CoreUtil
;
8 use Civi\Api4\Query\SqlExpression
;
11 * This action is used by the Afform Admin extension to load metadata for the Admin GUI.
13 * @package Civi\Api4\Action\Afform
15 class LoadAdminData
extends \Civi\Api4\Generic\AbstractAction
{
18 * Any properties already known about the afform
22 protected $definition;
25 * Entity type when creating a new form
31 * A list of entities whose blocks & fields are not needed
34 protected $skipEntities = [];
36 public function _run(\Civi\Api4\Generic\Result
$result) {
37 $info = ['entities' => [], 'fields' => [], 'blocks' => []];
39 $newForm = empty($this->definition
['name']);
42 // Load existing afform if name provided
43 $info['definition'] = $this->loadForm($this->definition
['name']);
46 // Create new blank afform
47 switch ($this->definition
['type']) {
49 $info['definition'] = $this->definition +
[
51 'permission' => 'access CiviCRM',
63 $info['definition'] = $this->definition +
[
65 'entity_type' => $this->entity
,
71 $info['definition'] = $this->definition +
[
73 'permission' => 'access CiviCRM',
86 $getFieldsMode = 'create';
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');
95 * Find all entities by recursing into embedded afforms
96 * @param array $layout
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'];
107 $entities[] = $afEntity['type'];
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);
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;
127 if (!empty($embeddedForm['join_entity'])) {
128 $entities = array_unique(array_merge($entities, [$embeddedForm['join_entity']]));
130 $scanBlocks($embeddedForm['layout']);
135 if ($info['definition']['type'] === 'form') {
137 $entities[] = $this->entity
;
138 $defaultEntity = AfformAdminMeta
::getAfformEntity($this->entity
);
139 if (!empty($defaultEntity['boilerplate'])) {
140 $scanBlocks($defaultEntity['boilerplate']);
144 $scanBlocks($info['definition']['layout']);
147 if (array_intersect($entities, ['Individual', 'Household', 'Organization'])) {
148 $entities[] = 'Contact';
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);
155 if ($info['definition']['type'] === 'block') {
156 $blockEntity = $info['definition']['join_entity'] ??
$info['definition']['entity_type'];
157 if ($blockEntity !== '*') {
158 $entities[] = $blockEntity;
160 $scanBlocks($info['definition']['layout']);
161 $this->loadAvailableBlocks($entities, $info);
164 if ($info['definition']['type'] === 'search') {
165 $getFieldsMode = 'search';
168 [$searchName, $displayName] = array_pad(explode('.', $this->entity ??
''), 2, '');
169 $displayTags[] = ['search-name' => $searchName, 'display-name' => $displayName];
172 foreach (\Civi\Search\Display
::getDisplayTypes(['name']) as $displayType) {
173 $displayTags = array_merge($displayTags, \CRM_Utils_Array
::findAll($info['definition']['layout'], ['#tag' => $displayType['name']]));
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;
185 $info['definition']['layout'][0]['#children'][] = $displayTag +
['#tag' => $display['type:name']];
187 $entities[] = $display['saved_search.api_entity'];
188 foreach ($display['saved_search.api_params']['join'] ??
[] as $join) {
189 $entities[] = explode(' AS ', $join[0])[0];
193 $scanBlocks($info['definition']['layout']);
195 $this->loadAvailableBlocks($entities, $info, [['join_entity', 'IS NULL']]);
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']);
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]);
208 $info['blocks'] = array_values($info['blocks']);
214 * @param string $name
217 private function loadForm($name) {
218 return Afform
::get($this->checkPermissions
)
219 ->setFormatWhitespace(TRUE)
220 ->setLayoutFormat('shallow')
221 ->addWhere('name', '=', $name)
222 ->execute()->first();
226 * Get basic info about blocks relevant to these entities.
228 * @param array $entities
230 * @param array $where
231 * @throws \API_Exception
232 * @throws \Civi\API\Exception\UnauthorizedException
234 private function loadAvailableBlocks($entities, &$info, $where = []) {
235 $entities = array_diff($entities, $this->skipEntities
);
236 if (!$this->skipEntities
) {
240 $blockInfo = Afform
::get($this->checkPermissions
)
241 ->addSelect('name', 'title', 'entity_type', 'join_entity', 'directive_name')
243 ->addWhere('type', '=', 'block')
244 ->addWhere('entity_type', 'IN', $entities)
245 ->addWhere('directive_name', 'NOT IN', array_keys($info['blocks']))
247 $info['blocks'] = array_merge(array_values($info['blocks']), (array) $blockInfo);
252 * @param string $apiEntity
253 * @param array $apiParams
256 private function getCalcFields($apiEntity, $apiParams) {
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]);
264 if (!empty($joinCount[$entityName])) {
265 $num = ' ' . (++
$joinCount[$entityName]);
268 $joinCount[$entityName] = 1;
270 $label = CoreUtil
::getInfoItem($entityName, 'title');
271 $joinMap[$alias] = $label . $num;
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'];
284 '#tag' => 'af-field',
285 'name' => $expr->getAlias(),
288 'input_type' => 'Text',
299 public function fields() {
302 'name' => 'definition',
303 'data_type' => 'Array',
307 'data_type' => 'Array',
311 'data_type' => 'Array',
314 'name' => 'search_displays',
315 'data_type' => 'Array',
323 public function getDefinition():array {
324 return $this->definition
;
328 * @param array $definition
330 public function setDefinition(array $definition) {
331 $this->definition
= $definition;