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 'block' => $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 ->setFormatWhitespace(TRUE)
120 ->setLayoutFormat('shallow')
121 ->addWhere('directive_name', '=', $blockTag)
122 ->execute()->first();
123 if ($embeddedForm['type'] === 'block') {
124 $info['blocks'][$blockTag] = $embeddedForm;
126 if (!empty($embeddedForm['join'])) {
127 $entities = array_unique(array_merge($entities, [$embeddedForm['join']]));
129 $scanBlocks($embeddedForm['layout']);
134 if ($info['definition']['type'] === 'form') {
136 $entities[] = $this->entity
;
137 $defaultEntity = AfformAdminMeta
::getAfformEntity($this->entity
);
138 if (!empty($defaultEntity['boilerplate'])) {
139 $scanBlocks($defaultEntity['boilerplate']);
143 $scanBlocks($info['definition']['layout']);
146 if (array_intersect($entities, ['Individual', 'Household', 'Organization'])) {
147 $entities[] = 'Contact';
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);
154 if ($info['definition']['type'] === 'block') {
155 $blockEntity = $info['definition']['join'] ??
$info['definition']['block'];
156 if ($blockEntity !== '*') {
157 $entities[] = $blockEntity;
159 $scanBlocks($info['definition']['layout']);
160 $this->loadAvailableBlocks($entities, $info);
163 if ($info['definition']['type'] === 'search') {
164 $getFieldsMode = 'search';
167 [$searchName, $displayName] = array_pad(explode('.', $this->entity ??
''), 2, '');
168 $displayTags[] = ['search-name' => $searchName, 'display-name' => $displayName];
171 foreach (\Civi\Search\Display
::getDisplayTypes(['name']) as $displayType) {
172 $displayTags = array_merge($displayTags, \CRM_Utils_Array
::findAll($info['definition']['layout'], ['#tag' => $displayType['name']]));
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;
184 $info['definition']['layout'][0]['#children'][] = $displayTag +
['#tag' => $display['type:name']];
186 $entities[] = $display['saved_search.api_entity'];
187 foreach ($display['saved_search.api_params']['join'] ??
[] as $join) {
188 $entities[] = explode(' AS ', $join[0])[0];
192 $scanBlocks($info['definition']['layout']);
194 $this->loadAvailableBlocks($entities, $info, [['join', 'IS NULL']]);
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']);
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]);
207 $info['blocks'] = array_values($info['blocks']);
213 * @param string $name
216 private function loadForm($name) {
217 return Afform
::get($this->checkPermissions
)
218 ->setFormatWhitespace(TRUE)
219 ->setLayoutFormat('shallow')
220 ->addWhere('name', '=', $name)
221 ->execute()->first();
225 * Get basic info about blocks relevant to these entities.
227 * @param array $entities
229 * @param array $where
230 * @throws \API_Exception
231 * @throws \Civi\API\Exception\UnauthorizedException
233 private function loadAvailableBlocks($entities, &$info, $where = []) {
234 $entities = array_diff($entities, $this->skipEntities
);
235 if (!$this->skipEntities
) {
239 $blockInfo = Afform
::get($this->checkPermissions
)
240 ->addSelect('name', 'title', 'block', 'join', 'directive_name', 'repeat')
242 ->addWhere('type', '=', 'block')
243 ->addWhere('block', 'IN', $entities)
244 ->addWhere('directive_name', 'NOT IN', array_keys($info['blocks']))
246 $info['blocks'] = array_merge(array_values($info['blocks']), (array) $blockInfo);
251 * @param string $apiEntity
252 * @param array $apiParams
255 private function getCalcFields($apiEntity, $apiParams) {
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]);
263 if (!empty($joinCount[$entityName])) {
264 $num = ' ' . (++
$joinCount[$entityName]);
267 $joinCount[$entityName] = 1;
269 $label = CoreUtil
::getInfoItem($entityName, 'title');
270 $joinMap[$alias] = $label . $num;
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'];
280 '#tag' => 'af-field',
281 'name' => $expr->getAlias(),
284 'input_type' => 'Text',
295 public function fields() {
298 'name' => 'definition',
299 'data_type' => 'Array',
303 'data_type' => 'Array',
307 'data_type' => 'Array',
310 'name' => 'search_displays',
311 'data_type' => 'Array',
319 public function getDefinition():array {
320 return $this->definition
;
324 * @param array $definition
326 public function setDefinition(array $definition) {
327 $this->definition
= $definition;