SearchKit - Additional link settings for opening in new tab or popup
authorColeman Watts <coleman@civicrm.org>
Wed, 10 Mar 2021 20:17:43 +0000 (15:17 -0500)
committerColeman Watts <coleman@civicrm.org>
Tue, 16 Mar 2021 06:02:19 +0000 (02:02 -0400)
15 files changed:
css/civicrm.css
ext/search/CRM/Search/Upgrader.php
ext/search/ang/crmSearchAdmin/crmSearchAdminDisplay.component.js
ext/search/ang/crmSearchAdmin/crmSearchAdminLinkGroup.component.js
ext/search/ang/crmSearchAdmin/crmSearchAdminLinkGroup.html
ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.component.js
ext/search/ang/crmSearchAdmin/crmSearchAdminLinkSelect.html
ext/search/ang/crmSearchAdmin/displays/colType/field.html
ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayList.html
ext/search/ang/crmSearchAdmin/displays/searchAdminDisplayTable.html
ext/search/ang/crmSearchDisplay.module.js
ext/search/ang/crmSearchDisplay/colType/buttons.html
ext/search/ang/crmSearchDisplay/colType/links.html
ext/search/ang/crmSearchDisplay/colType/menu.html
ext/search/css/crmSearchAdmin.css

index c928b5fb5ba693cfbcb13ffb652fbceb81c1417f..0114b041321b0d8045c6725a35b81d400ade7960 100644 (file)
   flex-wrap: wrap;
   box-sizing: border-box;
 }
-.crm-container .crm-flex-box > * {
+.crm-flex-box > * {
   flex: 1;
   box-sizing: border-box;
   min-width: 0; /* prevents getting squashed by whitespace:nowrap content */
 }
-.crm-container .crm-flex-box > .crm-flex-2 {
+.crm-container .crm-flex-1 {
+  flex: 1;
+}
+.crm-container .crm-flex-2 {
   flex: 2;
 }
-.crm-container .crm-flex-box > .crm-flex-3 {
+.crm-container .crm-flex-3 {
   flex: 3;
 }
-.crm-container .crm-flex-box > .crm-flex-4 {
+.crm-container .crm-flex-4 {
   flex: 4;
 }
-.crm-container .crm-flex-box > .crm-flex-5 {
+.crm-container .crm-flex-5 {
   flex: 5;
 }
 
index 670d3ca54594c147efd8b4e3a1109e4451326d27..f8fbc76f22b9bc2222fbfda666b31496dfe59a62 100644 (file)
@@ -84,4 +84,30 @@ class CRM_Search_Upgrader extends CRM_Search_Upgrader_Base {
     return TRUE;
   }
 
+  /**
+   * Upgrade 1002 - embellish search display link data
+   * @return bool
+   */
+  public function upgrade_1002() {
+    $this->ctx->log->info('Applying update 1002 - embellish search display link data.');
+    $displays = \Civi\Api4\SearchDisplay::get(FALSE)
+      ->setSelect(['id', 'settings'])
+      ->execute();
+    foreach ($displays as $display) {
+      $update = FALSE;
+      foreach ($display['settings']['columns'] ?? [] as $c => $column) {
+        if (!empty($column['link'])) {
+          $display['settings']['columns'][$c]['link'] = ['path' => $column['link']];
+          $update = TRUE;
+        }
+      }
+      if ($update) {
+        \Civi\Api4\SearchDisplay::update(FALSE)
+          ->setValues($display)
+          ->execute();
+      }
+    }
+    return TRUE;
+  }
+
 }
index 560445ff53f26b18eadd12e3b8ab40f4dec51682..f5806fcf4010751be54f7f0488c476b12a2c4c28 100644 (file)
         return values;
       }
 
+      this.toggleLink = function(column) {
+        if (column.link) {
+          ctrl.onChangeLink(column, column.link.path, '');
+        } else {
+          var defaultLink = ctrl.getLinks()[0];
+          column.link = {path: defaultLink ? defaultLink.path : 'civicrm/'};
+          ctrl.onChangeLink(column, null, column.link.path);
+        }
+      };
+
+      this.onChangeLink = function(column, before, after) {
+        var beforeLink = before && _.findWhere(ctrl.getLinks(), {path: before}),
+          afterLink = after && _.findWhere(ctrl.getLinks(), {path: after});
+        if (!after) {
+          if (beforeLink && column.title === beforeLink.title) {
+            delete column.title;
+          }
+          delete column.link;
+        } else if (afterLink && ((!column.title && !before) || (beforeLink && beforeLink.title === column.title))) {
+          column.title = afterLink.title;
+        } else if (!afterLink && (beforeLink && beforeLink.title === column.title)) {
+          delete column.title;
+        }
+      };
+
       this.getLinks = function() {
         if (!ctrl.links) {
           ctrl.links = buildLinks();
index b528ad0ba614ee43f6ac05bd3d89a8dd8ca70e9f..3ff0962ecbd730bd718f0f47fe2abea446e7b0a5 100644 (file)
 
       this.styles = CRM.crmSearchAdmin.styles;
 
-      this.setValue = function(val, index) {
-        var link = ctrl.getLink(val),
-          item = ctrl.group[index];
-        if (item.path === val) {
-          return;
-        }
-        item.path = val;
-        item.icon = link ? defaultIcons[link.action] : 'fa-external-link';
-        if (val === 'civicrm/') {
-          $timeout(function () {
-            $('tr:eq(' + index + ') input[type=text]', $element).focus();
-          });
-        }
+      this.getStyle = function(item) {
+        return _.findWhere(this.styles, {key: item.style});
       };
 
       this.sortableOptions = {
             ctrl.addItem('civicrm/');
           }
         }
-        $element.on('change', 'select.crm-search-admin-select-path', function() {
+        $element.on('change', 'select.crm-search-admin-add-link', function() {
           var $select = $(this);
           $scope.$apply(function() {
-            if ($select.closest('tfoot').length) {
-              ctrl.addItem($select.val());
-              $select.val('');
-            } else {
-              ctrl.setValue($select.val(), $select.closest('tr').index());
-            }
+            ctrl.addItem($select.val());
+            $select.val('');
           });
         });
       };
index 2846b600739632d0d2df03891a5e4ef549542620..5f5157d48868e6b46931791eaee9fa57fd65ad51 100644 (file)
@@ -3,6 +3,7 @@
     <tr>
       <th class="crm-search-admin-icon-col"></th>
       <th class="crm-search-admin-icon-col">{{:: ts('Icon') }}</th>
+      <th>{{:: ts('Open') }}</th>
       <th>{{:: ts('Text') }}</th>
       <th>{{:: ts('Link') }}</th>
       <th>{{:: ts('Style') }}</th>
     </tr>
   </thead>
   <tbody ui-sortable="$ctrl.sortableOptions" ng-model="$ctrl.group">
-    <tr ng-repeat="item in $ctrl.group" class="bg-{{ item.style }}">
+    <tr ng-repeat="item in $ctrl.group">
       <td class="crm-search-admin-icon-col">
         <i class="crm-i fa-arrows disabled"></i>
       </td>
       <td class="crm-search-admin-icon-col">
         <span class="crm-editable-enabled" ng-click="pickIcon($index)">
-          <i class="{{ item.icon ? 'crm-i ' + item.icon : '' }}"></i>
+          <i class="{{ item.icon ? 'crm-i ' + item.icon : '' }}" style="opacity: 1"></i>
         </span>
       </td>
+      <td>
+        <select class="form-control" ng-model="item.target">
+          <option value>{{:: ts('Normal') }}</option>
+          <option value="_blank">{{:: ts('New tab') }}</option>
+          <option value="crm-popup">{{:: ts('Popup dialog') }}</option>
+        </select>
+      </td>
       <td>
         <input type="text" class="form-control" ng-model="item.text">
       </td>
       <td class="form-inline">
-        <select class="form-control crm-search-admin-select-path" ng-show="$ctrl.links.length">
-          <option ng-repeat="link in $ctrl.links" value="{{ link.path }}" ng-selected="item.path === link.path">
-            {{ link.title }}
-          </option>
-          <option value="civicrm/" ng-selected="item.path && !$ctrl.getLink(item.path)">
-            {{ ts('Other...') }}
-          </option>
-        </select>
-        <input class="form-control" type="text" ng-if="item.path && !$ctrl.getLink(item.path)" ng-model="item.path" ng-model-options="{updateOn: 'blur'}" />
-        <crm-search-admin-token-select ng-if="item.path && !$ctrl.getLink(item.path)" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="item" field="path"></crm-search-admin-token-select>
+        <crm-search-admin-link-select api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" link="item" links="$ctrl.links"></crm-search-admin-link-select>
       </td>
-      <td class="form-inline">
-        <select class="form-control" ng-model="item.style">
-          <option ng-repeat="opt in $ctrl.styles" value="{{ opt.key }}">{{ opt.value }}</option>
-        </select>
+      <td>
+        <div class="btn-group">
+          <button type="button" style="min-width: 85px" class="btn btn-{{ item.style }} dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+            {{ $ctrl.getStyle(item).value }} <span class="caret"></span>
+          </button>
+          <ul class="dropdown-menu dropdown-menu-right">
+            <li ng-repeat="opt in $ctrl.styles">
+              <a href class="bg-{{:: opt.key }}" ng-click="item.style = opt.key">{{:: opt.value }}</a>
+            </li>
+          </ul>
+        </div>
       </td>
       <td class="crm-search-admin-icon-col">
         <button ng-if="$ctrl.group.length > 1" type="button" class="btn btn-xs btn-danger-outline" ng-click="$ctrl.group.splice($index, 1)">
@@ -49,7 +55,7 @@
   <tfoot>
     <tr>
       <td colspan="6" class="form-inline">
-        <select class="form-control crm-search-admin-select-path" ng-show="$ctrl.links.length">
+        <select class="form-control crm-search-admin-add-link" ng-show="$ctrl.links.length">
           <option value="">
             + {{:: ts('Add...') }}
           </option>
index eb8604e9e94d82c7bd733f95ba30a409c2f21a53..6d39b945c8dca698d94ad2b887a728640afcae6e 100644 (file)
@@ -3,10 +3,11 @@
 
   angular.module('crmSearchAdmin').component('crmSearchAdminLinkSelect', {
     bindings: {
-      column: '<',
+      link: '<',
       apiEntity: '<',
       apiParams: '<',
-      links: '<'
+      links: '<',
+      onChange: '&'
     },
     templateUrl: '~/crmSearchAdmin/crmSearchAdminLinkSelect.html',
     controller: function ($scope, $element, $timeout) {
         ctrl = this;
 
       this.setValue = function(val) {
+        ctrl.link  = ctrl.link  || {};
         var link = ctrl.getLink(val),
-          oldLink = ctrl.getLink(ctrl.column.link);
-        if (link) {
-          ctrl.column.link = link.path;
-          ctrl.column.title = link.title;
-          delete ctrl.column.editable;
-        } else {
-          if (val === 'civicrm/') {
-            ctrl.column.link = val;
-            $timeout(function () {
-              $('input[type=text]', $element).focus();
-            });
-          } else {
-            ctrl.column.link = '';
-          }
-          if (oldLink && ctrl.column.title === oldLink.title) {
-            ctrl.column.title = '';
-          }
+          oldVal = ctrl.link.path;
+        ctrl.link.path = val;
+        if (!link) {
+          $timeout(function () {
+            $('input[type=text]', $element).focus();
+          });
         }
-      };
-
-      function onChange() {
-        var val = $('select', $element).val();
-        if (val !== ctrl.column.link) {
-          ctrl.setValue(val);
-        }
-      }
-
-      this.$onInit = function() {
-        $('select', $element).on('change', function() {
-          $scope.$apply(onChange);
-        });
+        ctrl.onChange({before: oldVal, after: val});
       };
 
       this.getLink = function(path) {
index 855f9fdb71e8aeab285eb1bf2029b6af906fc080..9cd67a5a6c33ef3101aeff726ffb60f080603529 100644 (file)
@@ -1,14 +1,20 @@
-<label title="{{ ts('Display as clickable link') }}" >
-  <input type="checkbox" ng-checked="$ctrl.column.link" ng-click="$ctrl.setValue($ctrl.column.link ? '' : ($ctrl.links[0] && $ctrl.links[0].path || 'civicrm/'))" >
-  {{ $ctrl.column.link ? ts('Link:') : ts('Link') }}
-</label>
-<select class="form-control" ng-show="$ctrl.links.length && $ctrl.column.link">
-  <option ng-repeat="link in $ctrl.links" value="{{ link.path }}" ng-selected="$ctrl.column.link === link.path">
-    {{ link.title }}
-  </option>
-  <option value="civicrm/" ng-selected="$ctrl.column.link && !$ctrl.getLink($ctrl.column.link)">
-    {{ ts('Other...') }}
-  </option>
-</select>
-<input class="form-control" type="text" ng-if="$ctrl.column.link && !$ctrl.getLink($ctrl.column.link)" ng-model="$ctrl.column.link" ng-model-options="{updateOn: 'blur'}" />
-<crm-search-admin-token-select ng-if="$ctrl.column.link && !$ctrl.getLink($ctrl.column.link)" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="$ctrl.column" field="link"></crm-search-admin-token-select>
+<div class="crm-flex-1 input-group" >
+  <input type="text" class="form-control" ng-if="!$ctrl.getLink($ctrl.link.path)" ng-model="$ctrl.link.path" ng-model-options="{updateOn: 'blur'}" ng-change="$ctrl.onChange({before: 'civicrm/', after: $ctrl.link.path})">
+  <div class="input-group-btn" style="{{ $ctrl.getLink($ctrl.link.path) ? '' : 'width:27px' }}">
+    <button type="button" class="btn btn-default-outline dropdown-toggle" style="min-width: 200px; text-align: left;" ng-if="$ctrl.getLink($ctrl.link.path)" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+      {{ $ctrl.getLink($ctrl.link.path).title }}
+    </button>
+    <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+      <span class="caret"></span>
+    </button>
+    <ul class="dropdown-menu {{ $ctrl.getLink($ctrl.link.path) ? '' : 'dropdown-menu-right' }}" style="min-width: 223px;">
+      <li ng-repeat="link in $ctrl.links" ng-class="{disabled: $ctrl.link.path === link.path}">
+        <a href ng-click="$ctrl.setValue(link.path)">{{:: link.title }}</a>
+      </li>
+      <li ng-class="{disabled: !$ctrl.getLink($ctrl.link.path)}">
+        <a href ng-click="$ctrl.setValue('civicrm/')">{{:: ts('Other...') }}</a>
+      </li>
+    </ul>
+  </div>
+</div>
+<crm-search-admin-token-select ng-if="!$ctrl.getLink($ctrl.link.path)" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="$ctrl.link" field="path"></crm-search-admin-token-select>
index 5960cc5ef22a4cd5edc04f14fa87dbb77a75171c..7b0969cbd27e8620c4d4c6f6c955b0579912d758 100644 (file)
@@ -1,10 +1,22 @@
-<crm-search-admin-link-select class="form-inline crm-search-admin-flex-row" column="col" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" links=":: $ctrl.parent.getLinks()"></crm-search-admin-link-select>
+<div class="form-inline crm-search-admin-flex-row">
+  <label title="{{ ts('Display as clickable link') }}" >
+    <input type="checkbox" ng-checked="col.link" ng-click="$ctrl.parent.toggleLink(col)" >
+    {{ col.link ? ts('Link:') : ts('Link') }}
+  </label>
+  <select class="form-control" ng-model="$ctrl.link.target" ng-if="col.link">
+    <option value>{{:: ts('Open normally') }}</option>
+    <option value="_blank">{{:: ts('New tab') }}</option>
+    <option value="crm-popup">{{:: ts('Popup dialog') }}</option>
+  </select>
+  <crm-search-admin-link-select ng-if="col.link" link="col.link" on-change="$ctrl.parent.onChangeLink(col, before, after)" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" links=":: $ctrl.parent.getLinks()">
+  </crm-search-admin-link-select>
+</div>
 <div class="form-inline crm-search-admin-flex-row">
   <label>
     <input type="checkbox" ng-checked="col.title" ng-click="col.title = col.title ? null : $ctrl.parent.getFieldLabel(col.key)" >
     {{ col.title ? ts('Tooltip:') : ts('Tooltip') }}
   </label>
-  <input class="form-control" type="text" ng-model="col.title" ng-if="col.title" ng-model-options="{updateOn: 'blur'}" />
+  <input class="form-control crm-flex-1" type="text" ng-model="col.title" ng-if="col.title" ng-model-options="{updateOn: 'blur'}" />
   <crm-search-admin-token-select ng-if="col.title" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="col" field="title"></crm-search-admin-token-select>
 </div>
 <div class="form-inline crm-search-admin-flex-row">
@@ -12,7 +24,7 @@
     <input type="checkbox" ng-checked="col.rewrite" ng-click="$ctrl.parent.toggleRewrite(col)" >
     {{ col.rewrite ? ts('Rewrite:') : ts('Rewrite') }}
   </label>
-  <input type="text" class="form-control" ng-if="col.rewrite" ng-model="col.rewrite" ng-model-options="{updateOn: 'blur'}">
+  <input type="text" class="form-control crm-flex-1" ng-if="col.rewrite" ng-model="col.rewrite" ng-model-options="{updateOn: 'blur'}">
   <crm-search-admin-token-select ng-if="col.rewrite" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="col" field="rewrite"></crm-search-admin-token-select>
 </div>
 <div class="form-inline">
index 382c808751973b28675d3f3ae2b32bf355409509..bf4870cb9310ec1efe59b9cdbaac082be9647cfd 100644 (file)
@@ -40,7 +40,7 @@
           <input type="checkbox" ng-checked="col.label" ng-click="col.label = col.label ? null : $ctrl.parent.getColLabel(col)" >
           {{ col.label ? ts('Label:') : ts('Label') }}
         </label>
-        <input ng-if="col.label" class="form-control" type="text" ng-model="col.label" ng-model-options="{updateOn: 'blur'}">
+        <input ng-if="col.label" class="form-control crm-flex-1" type="text" ng-model="col.label" ng-model-options="{updateOn: 'blur'}">
         <crm-search-admin-token-select ng-if="col.label" api-entity="$ctrl.apiEntity" api-params="$ctrl.apiParams" model="col" field="label"></crm-search-admin-token-select>
       </div>
       <div class="form-inline" ng-if="col.label">
index 7aa5d91f68044c736fd45bea5d527ab4544e31d6..83b9ef670df7ec74a720867c4ba7f41440ba618a 100644 (file)
@@ -21,7 +21,7 @@
       <legend>{{ $ctrl.parent.getColLabel(col) }}</legend>
       <div class="form-inline crm-search-admin-flex-row">
         <label for="crm-search-admin-edit-col-{{ $index }}">{{:: ts('Header:') }}</label>
-        <input id="crm-search-admin-edit-col-{{ $index }}" class="form-control" type="text" ng-model="col.label" >
+        <input id="crm-search-admin-edit-col-{{ $index }}" class="form-control crm-flex-1" type="text" ng-model="col.label" >
         <button type="button" class="btn-xs" ng-click="$ctrl.parent.removeCol($index)" title="{{:: ts('Remove') }}">
           <i class="crm-i fa-ban"></i>
         </button>
index a8f7cb8501c12c70359ad2f8f0eeb8ce1994771d..1a27992c4a5edc767bdb64fc03a0d828c6aef62b 100644 (file)
           displayValue = column.rewrite ? replaceTokens(column.rewrite, rowData, rowMeta) : formatRawValue(column, rowData[key]),
           result = _.escape(displayValue);
         if (column.link) {
-          result = '<a href="' + _.escape(getUrl(column.link, rowData)) + '">' + result + '</a>';
+          var target = '';
+          if (column.link.target) {
+            target = column.link.target === 'crm-popup' ? 'class="crm-popup" ' : 'target="' + column.link.target + '" ';
+          }
+          result = '<a ' + target + 'href="' + _.escape(getUrl(column.link.path, rowData)) + '">' + result + '</a>';
         }
         return result;
       }
index f18030cf3c2a47403b59a65b8b67a758128ed465..7630a9b86352490fe47c207eb2bcdb63ebd51cb7 100644 (file)
@@ -1,5 +1,5 @@
 <span ng-repeat="item in col.links">
-  <a class="btn {{:: col.size }} btn-{{:: item.style }}" href="{{:: displayUtils.getUrl(item.path, row) }}">
+  <a class="btn {{:: col.size }} btn-{{:: item.style }} {{:: item.target }}" target="{{:: item.target }}" href="{{:: displayUtils.getUrl(item.path, row) }}">
     <i ng-if=":: item.icon" class="crm-i {{:: item.icon }}"></i>
     {{:: item.text }}
   </a>
index d0a248047338a2f9bb67515167077b903bff0b73..d84651693d176a988cafbff775c081c3ee24625b 100644 (file)
@@ -1,5 +1,5 @@
 <span ng-repeat="item in col.links">
-  <a class="text-{{:: item.style }}" href="{{:: displayUtils.getUrl(item.path, row) }}">
+  <a class="text-{{:: item.style }} {{:: item.target }}" target="{{:: item.target }}" href="{{:: displayUtils.getUrl(item.path, row) }}">
     <i ng-if=":: item.icon" class="crm-i {{:: item.icon }}"></i>
     {{:: item.text }}
   </a>
index a11b5319be60994e72560a83d4d0919d09ec6704..213f81442425dd185246a33635b8df5dd92d3a7e 100644 (file)
@@ -5,7 +5,7 @@
   </button>
   <ul class="dropdown-menu {{ col.alignment === 'text-right' ? 'dropdown-menu-right' : '' }}" ng-if=":: col.open">
     <li ng-repeat="item in col.links" class="bg-{{:: item.style }}">
-      <a href="{{:: displayUtils.getUrl(item.path, row) }}">
+      <a href="{{:: displayUtils.getUrl(item.path, row) }}" class="{{:: item.target }}" target="{{:: item.target }}">
         <i ng-if=":: item.icon" class="crm-i {{:: item.icon }}"></i>
         {{:: item.text }}
       </a>
index aaf1c5bc2681e7cd9a5dfcc124c7a6a99ae75f44..2a50fe9cf379e26f3b2d45359917a83830566449 100644 (file)
   background-color: rgba(255, 255, 255, .8);
 }
 
+/* A flex-row arranges elements inline, with the 'crm-flex-*' item(s) taking up all remaining space */
 #bootstrap-theme .crm-search-admin-flex-row {
   display: flex;
   align-items: center;
 #bootstrap-theme .crm-search-admin-flex-row > *:not(:first-child) {
   margin-left: 6px;
 }
-#bootstrap-theme .crm-search-admin-flex-row > input[type=text] {
-  flex: 1;
-}
 
 #bootstrap-theme .crm-search-admin-unused-columns fieldset {
   min-height: 60px;