From 9df0deac96f838106cf12316c5f7db0e742e70c7 Mon Sep 17 00:00:00 2001
From: Coleman Watts <coleman@civicrm.org>
Date: Thu, 30 Jun 2022 14:42:35 -0400
Subject: [PATCH] Afform - Allow picking icon for tab, add CrmUiIconPicker
 widget

Since this is needed in several places, this adds a general purpose crmUiIconPicker widget to crmUi.
It also switches Afforms's hook_civicrm_tabset to use the API, which ensures virtual forms get picked up.
Adds the new icon for CiviGrant as an example.
---
 ang/crmUi.js                                  | 15 ++++++++
 .../ang/afGuiEditor/afGuiEditor.component.js  |  1 +
 .../admin/ang/afGuiEditor/config-form.html    |  3 ++
 ext/afform/core/Civi/Api4/Afform.php          |  4 +++
 ext/afform/core/afform.php                    | 36 +++++++++----------
 ext/civigrant/ang/afsearchGrants.aff.json     |  1 +
 .../common/searchAdminIcons.component.js      |  9 -----
 .../displays/common/searchAdminIcons.html     |  2 +-
 8 files changed, 43 insertions(+), 28 deletions(-)

diff --git a/ang/crmUi.js b/ang/crmUi.js
index 11eb5e28ef31..d8b56772bba0 100644
--- a/ang/crmUi.js
+++ b/ang/crmUi.js
@@ -1170,6 +1170,21 @@
       };
     })
 
+    // Adds an icon picker widget
+    // Example: `<input crm-ui-icon-picker ng-model="model.icon">`
+    .directive('crmUiIconPicker', function($timeout) {
+      return {
+        restrict: 'A',
+        controller: function($element) {
+          CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmIconPicker.js').then(function() {
+            $timeout(function() {
+              $element.crmIconPicker();
+            });
+          });
+        }
+      };
+    })
+
     .run(function($rootScope, $location) {
       /// Example: <button ng-click="goto('home')">Go home!</button>
       $rootScope.goto = function(path) {
diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
index 18283b58a5d9..e99e02c58ce8 100644
--- a/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
+++ b/ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
@@ -95,6 +95,7 @@
           editor.afform.is_dashlet = false;
           editor.afform.title += ' ' + ts('(copy)');
         }
+        editor.afform.icon = editor.afform.icon || 'fa-list-alt';
         $scope.canvasTab = 'layout';
         $scope.layoutHtml = '';
         $scope.entities = {};
diff --git a/ext/afform/admin/ang/afGuiEditor/config-form.html b/ext/afform/admin/ang/afGuiEditor/config-form.html
index cb49a6f4fbf2..ca85a02cb134 100644
--- a/ext/afform/admin/ang/afGuiEditor/config-form.html
+++ b/ext/afform/admin/ang/afGuiEditor/config-form.html
@@ -71,6 +71,9 @@
           <option value="block">{{:: ts('As Block') }}</option>
           <option value="tab">{{:: ts('As Tab') }}</option>
         </select>
+        <div class="form-group" ng-show="editor.afform.contact_summary === 'tab'">
+          <input required ng-model="editor.afform.icon" crm-ui-icon-picker class="form-control">
+        </div>
       </div>
       <p class="help-block">{{:: ts('Placement can be configured using the Contact Layout Editor.') }}</p>
     </div>
diff --git a/ext/afform/core/Civi/Api4/Afform.php b/ext/afform/core/Civi/Api4/Afform.php
index 3c48e9ef0545..15d2aea8d8f2 100644
--- a/ext/afform/core/Civi/Api4/Afform.php
+++ b/ext/afform/core/Civi/Api4/Afform.php
@@ -165,6 +165,10 @@ public static function getFields($checkPermissions = TRUE) {
             'tab' => ts('Contact Summary Tab'),
           ],
         ],
+        [
+          'name' => 'icon',
+          'description' => 'Icon shown in the contact summary tab',
+        ],
         [
           'name' => 'server_route',
         ],
diff --git a/ext/afform/core/afform.php b/ext/afform/core/afform.php
index 898dd8c08e49..19f86de1ad57 100644
--- a/ext/afform/core/afform.php
+++ b/ext/afform/core/afform.php
@@ -178,25 +178,25 @@ function afform_civicrm_tabset($tabsetName, &$tabs, $context) {
   if ($tabsetName !== 'civicrm/contact/view') {
     return;
   }
-  $scanner = \Civi::service('afform_scanner');
+  $afforms = Civi\Api4\Afform::get(FALSE)
+    ->addWhere('contact_summary', '=', 'tab')
+    ->addSelect('name', 'title', 'icon', 'module_name', 'directive_name')
+    ->execute();
   $weight = 111;
-  foreach ($scanner->getMetas() as $afform) {
-    if (!empty($afform['contact_summary']) && $afform['contact_summary'] === 'tab') {
-      $module = _afform_angular_module_name($afform['name']);
-      $tabs[] = [
-        'id' => $afform['name'],
-        'title' => $afform['title'],
-        'weight' => $weight++,
-        'icon' => 'crm-i fa-list-alt',
-        'is_active' => TRUE,
-        'template' => 'afform/contactSummary/AfformTab.tpl',
-        'module' => $module,
-        'directive' => _afform_angular_module_name($afform['name'], 'dash'),
-      ];
-      // If this is the real contact summary page (and not a callback from ContactLayoutEditor), load module.
-      if (empty($context['caller'])) {
-        Civi::service('angularjs.loader')->addModules($module);
-      }
+  foreach ($afforms as $afform) {
+    $tabs[] = [
+      'id' => $afform['name'],
+      'title' => $afform['title'],
+      'weight' => $weight++,
+      'icon' => 'crm-i ' . ($afform['icon'] ?: 'fa-list-alt'),
+      'is_active' => TRUE,
+      'template' => 'afform/contactSummary/AfformTab.tpl',
+      'module' => $afform['module_name'],
+      'directive' => $afform['directive_name'],
+    ];
+    // If this is the real contact summary page (and not a callback from ContactLayoutEditor), load module.
+    if (empty($context['caller'])) {
+      Civi::service('angularjs.loader')->addModules($afform['module_name']);
     }
   }
 }
diff --git a/ext/civigrant/ang/afsearchGrants.aff.json b/ext/civigrant/ang/afsearchGrants.aff.json
index 093d18d9f91b..44b9ee3dcdd2 100644
--- a/ext/civigrant/ang/afsearchGrants.aff.json
+++ b/ext/civigrant/ang/afsearchGrants.aff.json
@@ -2,6 +2,7 @@
     "type": "search",
     "title": "Grants",
     "contact_summary": "tab",
+    "icon": "fa-money",
     "server_route": "",
     "permission": "access CiviGrant"
 }
diff --git a/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminIcons.component.js b/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminIcons.component.js
index d8a1e5eb69e7..6a7d439f9e90 100644
--- a/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminIcons.component.js
+++ b/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminIcons.component.js
@@ -22,12 +22,6 @@
         };
       };
 
-      function initWidgets() {
-        CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmIconPicker.js').then(function() {
-          $('.crm-search-admin-field-icon > input.crm-icon-picker[ng-model]', $element).crmIconPicker();
-        });
-      }
-
       this.$onInit = function() {
         $element.on('hidden.bs.dropdown', function() {
           $timeout(function() {
@@ -51,7 +45,6 @@
         }
         ctrl.iconFields = _.transform(allFields, getIconFields, []);
         ctrl.iconFieldMap = _.indexBy(ctrl.iconFields, 'id');
-        $timeout(initWidgets);
       };
 
       this.onSelectField = function(clause) {
@@ -72,7 +65,6 @@
           searchMeta.pickIcon().then(function(icon) {
             if (icon) {
               ctrl.item.icons.push({icon: icon, side: 'left', if: []});
-              $timeout(initWidgets);
             }
           });
         }
@@ -85,7 +77,6 @@
             item.icon = icon;
             delete item.field;
             item.if = item.if || [];
-            $timeout(initWidgets);
           }
         });
       };
diff --git a/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminIcons.html b/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminIcons.html
index 98d7676884c7..dd698a81fcbc 100644
--- a/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminIcons.html
+++ b/ext/search_kit/ang/crmSearchAdmin/displays/common/searchAdminIcons.html
@@ -17,7 +17,7 @@
     </div>
   </div>
   <div class="form-group crm-search-admin-field-icon" ng-if="icon.icon">
-    <input required ng-model="icon.icon" class="form-control crm-icon-picker">
+    <input required ng-model="icon.icon" crm-ui-icon-picker class="form-control crm-icon-picker">
   </div>
   <select class="form-control" ng-model="icon.side" title="{{:: ts('Show icon on left or right side of the field') }}">
     <option value="left">{{:: ts('Align left') }}</option>