b1d294b44f0929e832bc4c0f3733e92cda8f2b61
[civicrm-core.git] / ext / afform / core / afform.php
1 <?php
2
3 require_once 'afform.civix.php';
4 use CRM_Afform_ExtensionUtil as E;
5
6 /**
7 * Filter the content of $params to only have supported afform fields.
8 *
9 * @param array $params
10 * @return array
11 */
12 function _afform_fields_filter($params) {
13 $result = [];
14 $fields = \Civi\Api4\Afform::getfields(FALSE)->setAction('create')->execute()->indexBy('name');
15 foreach ($fields as $fieldName => $field) {
16 if (array_key_exists($fieldName, $params)) {
17 $result[$fieldName] = $params[$fieldName];
18
19 if ($field['data_type'] === 'Boolean' && !is_bool($params[$fieldName])) {
20 $result[$fieldName] = CRM_Utils_String::strtobool($params[$fieldName]);
21 }
22 }
23 }
24 return $result;
25 }
26
27 /**
28 * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
29 */
30 function afform_civicrm_container($container) {
31 $container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
32 $container->setDefinition('afform_scanner', new \Symfony\Component\DependencyInjection\Definition(
33 'CRM_Afform_AfformScanner',
34 []
35 ))->setPublic(TRUE);
36 }
37
38 /**
39 * Implements hook_civicrm_config().
40 *
41 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_config
42 */
43 function afform_civicrm_config(&$config) {
44 _afform_civix_civicrm_config($config);
45
46 if (isset(Civi::$statics[__FUNCTION__])) {
47 return;
48 }
49 Civi::$statics[__FUNCTION__] = 1;
50
51 $dispatcher = Civi::dispatcher();
52 $dispatcher->addListener('civi.afform.submit', ['\Civi\Api4\Action\Afform\Submit', 'processGenericEntity'], 0);
53 $dispatcher->addListener('civi.afform.submit', ['\Civi\Api4\Action\Afform\Submit', 'preprocessContact'], 10);
54 $dispatcher->addListener('civi.afform.submit', ['\Civi\Api4\Action\Afform\Submit', 'processRelationships'], 1);
55 $dispatcher->addListener('hook_civicrm_angularModules', ['\Civi\Afform\AngularDependencyMapper', 'autoReq'], -1000);
56 $dispatcher->addListener('hook_civicrm_alterAngular', ['\Civi\Afform\AfformMetadataInjector', 'preprocess']);
57 $dispatcher->addListener('hook_civicrm_check', ['\Civi\Afform\StatusChecks', 'hook_civicrm_check']);
58 $dispatcher->addListener('civi.afform.get', ['\Civi\Api4\Action\Afform\Get', 'getCustomGroupBlocks']);
59
60 // Register support for email tokens
61 if (CRM_Extension_System::singleton()->getMapper()->isActiveModule('authx')) {
62 $dispatcher->addListener('hook_civicrm_alterMailContent', ['\Civi\Afform\Tokens', 'applyCkeditorWorkaround']);
63 $dispatcher->addListener('hook_civicrm_tokens', ['\Civi\Afform\Tokens', 'hook_civicrm_tokens']);
64 $dispatcher->addListener('hook_civicrm_tokenValues', ['\Civi\Afform\Tokens', 'hook_civicrm_tokenValues']);
65 }
66 }
67
68 /**
69 * Implements hook_civicrm_install().
70 *
71 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install
72 */
73 function afform_civicrm_install() {
74 _afform_civix_civicrm_install();
75 }
76
77 /**
78 * Implements hook_civicrm_postInstall().
79 *
80 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall
81 */
82 function afform_civicrm_postInstall() {
83 _afform_civix_civicrm_postInstall();
84 }
85
86 /**
87 * Implements hook_civicrm_uninstall().
88 *
89 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall
90 */
91 function afform_civicrm_uninstall() {
92 _afform_civix_civicrm_uninstall();
93 }
94
95 /**
96 * Implements hook_civicrm_enable().
97 *
98 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable
99 */
100 function afform_civicrm_enable() {
101 _afform_civix_civicrm_enable();
102 }
103
104 /**
105 * Implements hook_civicrm_disable().
106 *
107 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable
108 */
109 function afform_civicrm_disable() {
110 _afform_civix_civicrm_disable();
111 }
112
113 /**
114 * Implements hook_civicrm_upgrade().
115 *
116 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade
117 */
118 function afform_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) {
119 return _afform_civix_civicrm_upgrade($op, $queue);
120 }
121
122 /**
123 * Implements hook_civicrm_managed().
124 *
125 * Generate a list of entities to create/deactivate/delete when this module
126 * is installed, disabled, uninstalled.
127 *
128 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed
129 */
130 function afform_civicrm_managed(&$entities, $modules) {
131 if ($modules && !in_array(E::LONG_NAME, $modules, TRUE)) {
132 return;
133 }
134 /** @var \CRM_Afform_AfformScanner $scanner */
135 if (\Civi::container()->has('afform_scanner')) {
136 $scanner = \Civi::service('afform_scanner');
137 }
138 else {
139 // This might happen at oddballs points - e.g. while you're in the middle of re-enabling the ext.
140 // This AfformScanner instance only lives during this method call, and it feeds off the regular cache.
141 $scanner = new CRM_Afform_AfformScanner();
142 }
143 $domains = NULL;
144
145 foreach ($scanner->getMetas() as $afform) {
146 if (empty($afform['name'])) {
147 continue;
148 }
149 if (!empty($afform['is_dashlet'])) {
150 $entities[] = [
151 'module' => E::LONG_NAME,
152 'name' => 'afform_dashlet_' . $afform['name'],
153 'entity' => 'Dashboard',
154 'update' => 'always',
155 // ideal cleanup policy might be to (a) deactivate if used and (b) remove if unused
156 'cleanup' => 'always',
157 'params' => [
158 'version' => 4,
159 'values' => [
160 // Q: Should we loop through all domains?
161 'domain_id' => 'current_domain',
162 'is_active' => TRUE,
163 'name' => $afform['name'],
164 'label' => $afform['title'] ?? E::ts('(Untitled)'),
165 'directive' => _afform_angular_module_name($afform['name'], 'dash'),
166 'permission' => "@afform:" . $afform['name'],
167 'url' => NULL,
168 ],
169 ],
170 ];
171 }
172 if (!empty($afform['navigation']) && !empty($afform['server_route'])) {
173 $domains = $domains ?: \Civi\Api4\Domain::get(FALSE)->addSelect('id')->execute();
174 foreach ($domains as $domain) {
175 $params = [
176 'version' => 4,
177 'values' => [
178 'name' => $afform['name'],
179 'label' => $afform['navigation']['label'] ?: $afform['title'],
180 'permission' => (array) $afform['permission'],
181 'permission_operator' => 'OR',
182 'weight' => $afform['navigation']['weight'] ?? 0,
183 'url' => $afform['server_route'],
184 'is_active' => 1,
185 'icon' => 'crm-i ' . $afform['icon'],
186 'domain_id' => $domain['id'],
187 ],
188 'match' => ['domain_id', 'name'],
189 ];
190 if (!empty($afform['navigation']['parent'])) {
191 $params['values']['parent_id.name'] = $afform['navigation']['parent'];
192 }
193 $entities[] = [
194 'module' => E::LONG_NAME,
195 'name' => 'navigation_' . $afform['name'] . '_' . $domain['id'],
196 'cleanup' => 'always',
197 'update' => 'unmodified',
198 'entity' => 'Navigation',
199 'params' => $params,
200 ];
201 }
202 }
203 }
204 }
205
206 /**
207 * Implements hook_civicrm_tabset().
208 *
209 * Adds afforms as contact summary tabs.
210 */
211 function afform_civicrm_tabset($tabsetName, &$tabs, $context) {
212 if ($tabsetName !== 'civicrm/contact/view') {
213 return;
214 }
215 $afforms = Civi\Api4\Afform::get(FALSE)
216 ->addWhere('contact_summary', '=', 'tab')
217 ->addSelect('name', 'title', 'icon', 'module_name', 'directive_name')
218 ->execute();
219 $weight = 111;
220 foreach ($afforms as $afform) {
221 $tabs[] = [
222 'id' => $afform['name'],
223 'title' => $afform['title'],
224 'weight' => $weight++,
225 'icon' => 'crm-i ' . ($afform['icon'] ?: 'fa-list-alt'),
226 'is_active' => TRUE,
227 'template' => 'afform/contactSummary/AfformTab.tpl',
228 'module' => $afform['module_name'],
229 'directive' => $afform['directive_name'],
230 ];
231 // If this is the real contact summary page (and not a callback from ContactLayoutEditor), load module.
232 if (empty($context['caller'])) {
233 Civi::service('angularjs.loader')->addModules($afform['module_name']);
234 }
235 }
236 }
237
238 /**
239 * Implements hook_civicrm_pageRun().
240 *
241 * Adds afforms as contact summary blocks.
242 */
243 function afform_civicrm_pageRun(&$page) {
244 if (get_class($page) !== 'CRM_Contact_Page_View_Summary') {
245 return;
246 }
247 $scanner = \Civi::service('afform_scanner');
248 $cid = $page->get('cid');
249 $side = 'left';
250 foreach ($scanner->getMetas() as $afform) {
251 if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'block') {
252 $module = _afform_angular_module_name($afform['name']);
253 $block = [
254 'module' => $module,
255 'directive' => _afform_angular_module_name($afform['name'], 'dash'),
256 ];
257 $content = CRM_Core_Smarty::singleton()->fetchWith('afform/contactSummary/AfformBlock.tpl', ['contactId' => $cid, 'block' => $block]);
258 CRM_Core_Region::instance("contact-basic-info-$side")->add([
259 'markup' => '<div class="crm-summary-block">' . $content . '</div>',
260 'weight' => 1,
261 ]);
262 Civi::service('angularjs.loader')->addModules($module);
263 $side = $side === 'left' ? 'right' : 'left';
264 }
265 }
266 }
267
268 /**
269 * Implements hook_civicrm_contactSummaryBlocks().
270 *
271 * @link https://github.com/civicrm/org.civicrm.contactlayout
272 */
273 function afform_civicrm_contactSummaryBlocks(&$blocks) {
274 $afforms = \Civi\Api4\Afform::get(FALSE)
275 ->setSelect(['name', 'title', 'directive_name', 'module_name', 'type', 'type:icon', 'type:label'])
276 ->addWhere('contact_summary', '=', 'block')
277 ->execute();
278 foreach ($afforms as $index => $afform) {
279 // Create a group per afform type
280 $blocks += [
281 "afform_{$afform['type']}" => [
282 'title' => $afform['type:label'],
283 'icon' => $afform['type:icon'],
284 'blocks' => [],
285 ],
286 ];
287 $blocks["afform_{$afform['type']}"]['blocks'][$afform['name']] = [
288 'title' => $afform['title'],
289 'tpl_file' => 'afform/contactSummary/AfformBlock.tpl',
290 'module' => $afform['module_name'],
291 'directive' => $afform['directive_name'],
292 'sample' => [
293 $afform['type:label'],
294 ],
295 'edit' => 'civicrm/admin/afform#/edit/' . $afform['name'],
296 'system_default' => [0, $index % 2],
297 ];
298 }
299 }
300
301 /**
302 * Implements hook_civicrm_angularModules().
303 *
304 * Generate a list of Afform Angular modules.
305 */
306 function afform_civicrm_angularModules(&$angularModules) {
307 $afforms = \Civi\Api4\Afform::get(FALSE)
308 ->setSelect(['name', 'requires', 'module_name', 'directive_name'])
309 ->execute();
310
311 foreach ($afforms as $afform) {
312 $angularModules[$afform['module_name']] = [
313 'ext' => E::LONG_NAME,
314 'js' => ['assetBuilder://afform.js?name=' . urlencode($afform['name'])],
315 'requires' => $afform['requires'],
316 'basePages' => [],
317 'partialsCallback' => '_afform_get_partials',
318 '_afform' => $afform['name'],
319 // TODO: Allow afforms to declare their own theming requirements
320 'bundles' => ['bootstrap3'],
321 'exports' => [
322 $afform['directive_name'] => 'E',
323 ],
324 ];
325 }
326 }
327
328 /**
329 * Callback to retrieve partials for a given afform/angular module.
330 *
331 * @see afform_civicrm_angularModules
332 *
333 * @param string $moduleName
334 * The module name.
335 * @param array $module
336 * The module definition.
337 * @return array
338 * Array(string $filename => string $html).
339 * @throws API_Exception
340 */
341 function _afform_get_partials($moduleName, $module) {
342 $afform = civicrm_api4('Afform', 'get', [
343 'where' => [['name', '=', $module['_afform']]],
344 'select' => ['layout'],
345 'layoutFormat' => 'html',
346 'checkPermissions' => FALSE,
347 ], 0);
348 return [
349 "~/$moduleName/$moduleName.aff.html" => $afform['layout'],
350 ];
351 }
352
353 /**
354 * Implements hook_civicrm_entityTypes().
355 *
356 * Declare entity types provided by this module.
357 *
358 * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_entityTypes
359 */
360 function afform_civicrm_entityTypes(&$entityTypes) {
361 _afform_civix_civicrm_entityTypes($entityTypes);
362 }
363
364 /**
365 * Implements hook_civicrm_buildAsset().
366 */
367 function afform_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
368 if ($asset !== 'afform.js') {
369 return;
370 }
371
372 if (empty($params['name'])) {
373 throw new RuntimeException("Missing required parameter: afform.js?name=NAME");
374 }
375
376 $moduleName = _afform_angular_module_name($params['name'], 'camel');
377 $formMetaData = (array) civicrm_api4('Afform', 'get', [
378 'checkPermissions' => FALSE,
379 'select' => ['redirect', 'name', 'title'],
380 'where' => [['name', '=', $params['name']]],
381 ], 0);
382 $smarty = CRM_Core_Smarty::singleton();
383 $smarty->assign('afform', [
384 'camel' => $moduleName,
385 'meta' => $formMetaData,
386 'templateUrl' => "~/$moduleName/$moduleName.aff.html",
387 ]);
388 $mimeType = 'text/javascript';
389 $content = $smarty->fetch('afform/AfformAngularModule.tpl');
390 }
391
392 /**
393 * Implements hook_civicrm_alterMenu().
394 */
395 function afform_civicrm_alterMenu(&$items) {
396 try {
397 $afforms = \Civi\Api4\Afform::get(FALSE)
398 ->addWhere('server_route', 'IS NOT EMPTY')
399 ->addSelect('name', 'server_route', 'is_public')
400 ->execute()->indexBy('name');
401 }
402 catch (Exception $e) {
403 // During installation...
404 $scanner = new CRM_Afform_AfformScanner();
405 $afforms = $scanner->getMetas();
406 }
407 foreach ($afforms as $name => $meta) {
408 if (!empty($meta['server_route'])) {
409 $items[$meta['server_route']] = [
410 'page_callback' => 'CRM_Afform_Page_AfformBase',
411 'page_arguments' => 'afform=' . urlencode($name),
412 'access_arguments' => [["@afform:$name"], 'and'],
413 'is_public' => $meta['is_public'],
414 ];
415 }
416 }
417 }
418
419 /**
420 * Implements hook_civicrm_permission().
421 *
422 * Define Afform permissions.
423 */
424 function afform_civicrm_permission(&$permissions) {
425 $permissions['administer afform'] = [
426 E::ts('Form Builder: edit and delete forms'),
427 E::ts('Allows non-admin users to create, update and delete forms'),
428 ];
429 }
430
431 /**
432 * Implements hook_civicrm_permission_check().
433 *
434 * This extends the list of permissions available in `CRM_Core_Permission:check()`
435 * by introducing virtual-permissions named `@afform:myForm`. The evaluation
436 * of these virtual-permissions is dependent on the settings for `myForm`.
437 * `myForm` may be exposed/integrated through multiple subsystems (routing,
438 * nav-menu, API, etc), and the use of virtual-permissions makes easy to enforce
439 * consistent permissions across any relevant subsystems.
440 *
441 * @see CRM_Utils_Hook::permission_check()
442 */
443 function afform_civicrm_permission_check($permission, &$granted, $contactId) {
444 if ($permission[0] !== '@') {
445 // Micro-optimization - this function may get hit a lot.
446 return;
447 }
448
449 if (preg_match('/^@afform:(.*)/', $permission, $m)) {
450 $name = $m[1];
451
452 $afform = \Civi\Api4\Afform::get()
453 ->setCheckPermissions(FALSE)
454 ->addWhere('name', '=', $name)
455 ->setSelect(['permission'])
456 ->execute()
457 ->first();
458 if ($afform) {
459 $granted = CRM_Core_Permission::check($afform['permission'], $contactId);
460 }
461 }
462 }
463
464 /**
465 * Implements hook_civicrm_permissionList().
466 *
467 * @see CRM_Utils_Hook::permissionList()
468 */
469 function afform_civicrm_permissionList(&$permissions) {
470 $scanner = Civi::service('afform_scanner');
471 foreach ($scanner->getMetas() as $name => $meta) {
472 $permissions['@afform:' . $name] = [
473 'group' => 'afform',
474 'title' => E::ts('Afform: Inherit permission of %1', [
475 1 => $name,
476 ]),
477 ];
478 }
479 }
480
481 /**
482 * Clear any local/in-memory caches based on afform data.
483 */
484 function _afform_clear() {
485 $container = \Civi::container();
486 $container->get('afform_scanner')->clear();
487 $container->get('angular')->clear();
488 }
489
490 /**
491 * @param string $fileBaseName
492 * Ex: foo-bar
493 * @param string $format
494 * 'camel' or 'dash'.
495 * @return string
496 * Ex: 'FooBar' or 'foo-bar'.
497 * @throws \Exception
498 */
499 function _afform_angular_module_name($fileBaseName, $format = 'camel') {
500 switch ($format) {
501 case 'camel':
502 $camelCase = '';
503 foreach (preg_split('/[-_ ]/', $fileBaseName, -1, PREG_SPLIT_NO_EMPTY) as $shortNamePart) {
504 $camelCase .= ucfirst($shortNamePart);
505 }
506 return strtolower($camelCase[0]) . substr($camelCase, 1);
507
508 case 'dash':
509 return strtolower(implode('-', preg_split('/[-_ ]|(?=[A-Z])/', $fileBaseName, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)));
510
511 default:
512 throw new \Exception("Unrecognized format");
513 }
514 }
515
516 /**
517 * Implements hook_civicrm_alterApiRoutePermissions().
518 *
519 * @see CRM_Utils_Hook::alterApiRoutePermissions
520 */
521 function afform_civicrm_alterApiRoutePermissions(&$permissions, $entity, $action) {
522 if ($entity == 'Afform') {
523 // These actions should be accessible to anonymous users; permissions are checked internally
524 $allowedActions = ['prefill', 'submit', 'submitFile', 'getOptions'];
525 if (in_array($action, $allowedActions, TRUE)) {
526 $permissions = CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION;
527 }
528 }
529 }
530
531 /**
532 * Implements hook_civicrm_preProcess().
533 *
534 * Wordpress only: Adds Afforms to the shortcode dialog (when editing pages/posts).
535 */
536 function afform_civicrm_preProcess($formName, &$form) {
537 if ($formName === 'CRM_Core_Form_ShortCode') {
538 $form->components['afform'] = [
539 'label' => E::ts('Form Builder'),
540 'select' => [
541 'key' => 'name',
542 'entity' => 'Afform',
543 'select' => ['minimumInputLength' => 0],
544 'api' => [
545 'params' => ['type' => ['IN' => ['form', 'search']]],
546 ],
547 ],
548 ];
549 }
550 }
551
552 /**
553 * Implements hook_civicrm_pre().
554 */
555 function afform_civicrm_pre($op, $entity, $id, &$params) {
556 // When deleting a searchDisplay, also delete any Afforms the display is embedded within
557 if ($entity === 'SearchDisplay' && $op === 'delete') {
558 $display = \Civi\Api4\SearchDisplay::get(FALSE)
559 ->addSelect('saved_search_id.name', 'name')
560 ->addWhere('id', '=', $id)
561 ->execute()->first();
562 \Civi\Api4\Afform::revert(FALSE)
563 ->addWhere('search_displays', 'CONTAINS', $display['saved_search_id.name'] . ".{$display['name']}")
564 ->execute();
565 }
566 // When deleting a savedSearch, delete any Afforms which use the default display
567 elseif ($entity === 'SavedSearch' && $op === 'delete') {
568 $search = \Civi\Api4\SavedSearch::get(FALSE)
569 ->addSelect('name')
570 ->addWhere('id', '=', $id)
571 ->execute()->first();
572 \Civi\Api4\Afform::revert(FALSE)
573 ->addWhere('search_displays', 'CONTAINS', $search['name'])
574 ->execute();
575 }
576 }
577
578 /**
579 * Implements hook_civicrm_referenceCounts().
580 */
581 function afform_civicrm_referenceCounts($dao, &$counts) {
582 // Count afforms which contain a search display
583 if (is_a($dao, 'CRM_Search_DAO_SearchDisplay') && $dao->id) {
584 if (empty($dao->saved_search_id) || empty($dao->name)) {
585 $dao->find(TRUE);
586 }
587 $search = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_SavedSearch', $dao->saved_search_id);
588 $afforms = \Civi\Api4\Afform::get(FALSE)
589 ->selectRowCount()
590 ->addWhere('search_displays', 'CONTAINS', "$search.$dao->name")
591 ->execute();
592 if ($afforms->count()) {
593 $counts[] = [
594 'name' => 'Afform',
595 'type' => 'Afform',
596 'count' => $afforms->count(),
597 ];
598 }
599 }
600 // Count afforms which contain any displays from a SavedSearch (including the default display)
601 elseif (is_a($dao, 'CRM_Contact_DAO_SavedSearch') && $dao->id) {
602 if (empty($dao->name)) {
603 $dao->find(TRUE);
604 }
605 $clauses = [
606 ['search_displays', 'CONTAINS', $dao->name],
607 ];
608 try {
609 $displays = civicrm_api4('SearchDisplay', 'get', [
610 'where' => [['saved_search_id', '=', $dao->id]],
611 'select' => 'name',
612 ], ['name']);
613 foreach ($displays as $displayName) {
614 $clauses[] = ['search_displays', 'CONTAINS', $dao->name . '.' . $displayName];
615 }
616 }
617 catch (Exception $e) {
618 // In case SearchKit is not installed, the api call would fail
619 }
620 $afforms = \Civi\Api4\Afform::get(FALSE)
621 ->selectRowCount()
622 ->addClause('OR', $clauses)
623 ->execute();
624 if ($afforms->count()) {
625 $counts[] = [
626 'name' => 'Afform',
627 'type' => 'Afform',
628 'count' => $afforms->count(),
629 ];
630 }
631 }
632 }
633
634 // Wordpress only: Register callback for rendering shortcodes
635 if (function_exists('add_filter')) {
636 add_filter('civicrm_shortcode_get_markup', 'afform_shortcode_content', 10, 4);
637 }
638
639 /**
640 * Wordpress only: Render Afform content for shortcodes.
641 *
642 * @param string $content
643 * HTML Markup
644 * @param array $atts
645 * Shortcode attributes.
646 * @param array $args
647 * Existing shortcode arguments.
648 * @param string $context
649 * How many shortcodes are present on the page: 'single' or 'multiple'.
650 * @return string
651 * Modified markup.
652 */
653 function afform_shortcode_content($content, $atts, $args, $context) {
654 if ($atts['component'] === 'afform') {
655 $afform = civicrm_api4('Afform', 'get', [
656 'select' => ['directive_name', 'module_name'],
657 'where' => [['name', '=', $atts['name']]],
658 ])->first();
659 if ($afform) {
660 Civi::service('angularjs.loader')->addModules($afform['module_name']);
661 $content = "
662 <div class='crm-container' id='bootstrap-theme'>
663 <crm-angular-js modules='{$afform['module_name']}'>
664 <{$afform['directive_name']}></{$afform['directive_name']}>
665 </crm-angular-js>
666 </div>";
667 }
668 }
669 return $content;
670 }