From e8c3fd9d32deab5df9cfb4d972dca7e48b1916dd Mon Sep 17 00:00:00 2001
From: Jaap Jansma <jaap.jansma@civicoop.org>
Date: Tue, 17 Sep 2019 22:17:30 +0200
Subject: [PATCH] Added contact summary as an output

---
 CHANGELOG.md                                  |   5 +-
 .../DataProcessorContactSummaryTab.php        | 257 ++++++++++++++++++
 .../Form/DataProcessorContactSummaryTab.php   | 147 ++++++++++
 .../Page/DataProcessorContactSummaryTab.php   |  57 ++++
 CRM/DataprocessorDashlet/Dashlet.php          |   2 +-
 CRM/DataprocessorDashlet/Page/Dashlet.php     |  15 -
 .../Form/AbstractSearch.php                   |  21 +-
 Civi/DataProcessor/Factory.php                |   1 +
 dataprocessor.php                             |  15 +
 .../Form/DataProcessorContactSummaryTab.tpl   |   4 +
 .../DashletConfiguration.tpl                  |  22 --
 .../DataProcessorContactSearch.tpl            |   5 +-
 .../DataProcessorContactSummaryTab.tpl        |  41 +++
 .../Page/DataProcessorContactSummaryTab.tpl   |  16 ++
 .../Form/Output/UIOutput/CriteriaForm.tpl     |   4 +-
 .../CRM/DataprocessorSearch/Form/Search.tpl   |   8 +
 xml/Menu/dataprocessor.xml                    |   6 +
 17 files changed, 581 insertions(+), 45 deletions(-)
 create mode 100644 CRM/Contact/DataProcessorContactSummaryTab.php
 create mode 100644 CRM/Contact/Form/DataProcessorContactSummaryTab.php
 create mode 100644 CRM/Contact/Page/DataProcessorContactSummaryTab.php
 create mode 100644 templates/CRM/Contact/Form/DataProcessorContactSummaryTab.tpl
 delete mode 100644 templates/CRM/Contact/Form/OutputConfiguration/DashletConfiguration.tpl
 create mode 100644 templates/CRM/Contact/Form/OutputConfiguration/DataProcessorContactSummaryTab.tpl
 create mode 100644 templates/CRM/Contact/Page/DataProcessorContactSummaryTab.tpl

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4e54556..74c6241f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,8 @@
 * Respect selected permissions for outputs 
 * Allow to specify "Is Empty" for various filters.
 * Allow to limit ContactFilter to only show contacts from specific groups.
-* it is now also possible to output a data processor on CiviCRMs dahsboard.
+* Output a data processor as a dashboard.
+* Output a data processor as a tab on the contact summary screen.
 * Added field outputs for simple calculations (substract and total).
 
 # Version 1.0.7
@@ -34,4 +35,4 @@
 
 # Version 1.0.1
 
-Initial release.
\ No newline at end of file
+Initial release.
diff --git a/CRM/Contact/DataProcessorContactSummaryTab.php b/CRM/Contact/DataProcessorContactSummaryTab.php
new file mode 100644
index 00000000..839f37cf
--- /dev/null
+++ b/CRM/Contact/DataProcessorContactSummaryTab.php
@@ -0,0 +1,257 @@
+<?php
+/**
+ * @author Jaap Jansma <jaap.jansma@civicoop.org>
+ * @license AGPL-3.0
+ */
+
+use CRM_Dataprocessor_ExtensionUtil as E;
+use Civi\DataProcessor\Output\UIOutputInterface;
+
+class CRM_Contact_DataProcessorContactSummaryTab implements UIOutputInterface {
+
+  /**
+   * Implements hook_civicrm_tabset().
+   *
+   * Adds the data processor out to the contact summary tabs.
+   *
+   * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_tabset/
+   *
+   * @param $tabsetName
+   * @param $tabs
+   * @param $context
+   */
+  public static function hookTabset($tabsetName, &$tabs, $context) {
+    if ($tabsetName != 'civicrm/contact/view') {
+      return;
+    }
+
+    $factory = dataprocessor_get_factory();
+    // Check whether the factory exists. Usually just after
+    // installation the factory does not exists but then no
+    // outputs exists either. So we can safely return this function.
+    if (!$factory) {
+      return;
+    }
+
+    $maxWeight = 0;
+    foreach($tabs as $tab) {
+      if (isset($tab['weight']) && $tab['weight'] > $maxWeight) {
+        $maxWeight = $tab['weight'];
+      }
+    }
+    $maxWeight++;
+
+    $sql = "SELECT o.*, d.name as data_processor_name
+            FROM civicrm_data_processor d 
+            INNER JOIN civicrm_data_processor_output o ON d.id = o.data_processor_id
+            WHERE d.is_active = 1 AND o.type = 'contact_summary_tab'";
+    $dao = CRM_Core_DAO::executeQuery($sql);
+    while($dao->fetch()) {
+      $outputClass = $factory->getOutputByName($dao->type);
+      if (!$outputClass instanceof \Civi\DataProcessor\Output\UIOutputInterface) {
+        continue;
+      }
+      $output = civicrm_api3('DataProcessorOutput', 'getsingle', ['id' => $dao->id]);
+      $dataprocessor = civicrm_api3('DataProcessor', 'getsingle', ['id' => $dao->data_processor_id]);
+      if (!$outputClass->checkUIPermission($output, $dataprocessor)) {
+        continue;
+      }
+
+      $tab = [
+        'id' => 'dataprocessor_' . $dataprocessor['name'],
+        'title' => $outputClass->getTitleForUiLink($output, $dataprocessor),
+        'icon' => $outputClass->getIconForUiLink($output, $dataprocessor),
+        'url' => CRM_Utils_System::url('civicrm/dataprocessor/page/contactsummary', array('contact_id' => $context['contact_id'], 'data_processor' => $dataprocessor['name'], 'reset' => 1, 'force' => 1)),
+        'class' => '',
+      ];
+      if (isset($output['configuration']['weight']) && strlen($output['configuration']['weight']) && is_numeric($output['configuration']['weight'])) {
+        $tab['weight'] = $output['configuration']['weight'];
+        if ($tab['weight'] > $maxWeight) {
+          $maxWeight = $tab['weight'];
+        }
+      }
+      else {
+        $tab['weight'] = $maxWeight;
+        $maxWeight++;
+      }
+      $tabs[$tab['id']] = $tab;
+    }
+  }
+
+  /**
+   * Returns true when this output has additional configuration
+   *
+   * @return bool
+   */
+  public function hasConfiguration() {
+    return true;
+  }
+
+  /**
+   * When this output type has additional configuration you can add
+   * the fields on the form with this function.
+   *
+   * @param \CRM_Core_Form $form
+   * @param array $output
+   */
+  public function buildConfigurationForm(\CRM_Core_Form $form, $output = []) {
+    $fieldSelect = \CRM_Dataprocessor_Utils_DataSourceFields::getAvailableFilterFieldsInDataSources($output['data_processor_id']);
+
+    $form->add('text', 'title', E::ts('Title'), true);
+    $form->add('select','permission', E::ts('Permission'), \CRM_Core_Permission::basicPermissions(), true, array(
+      'style' => 'min-width:250px',
+      'class' => 'crm-select2 huge',
+      'placeholder' => E::ts('- select -'),
+    ));
+
+    $form->add('select', 'contact_id_field', E::ts('Contact ID field'), $fieldSelect, true, array(
+      'style' => 'min-width:250px',
+      'class' => 'crm-select2 huge',
+      'placeholder' => E::ts('- select -'),
+    ));
+
+    $form->add('wysiwyg', 'help_text', E::ts('Help text for this tab'), array('rows' => 6, 'cols' => 80));
+    $form->add('wysiwyg', 'no_result_text', E::ts('No Result Text'), array('rows' => 2, 'class' => 'huge'), false);
+    $form->add('text', 'weight', E::ts('Weight'));
+    $form->add('text', 'default_limit', E::ts('Default Limit'));
+
+
+    $defaults = array();
+    if ($output) {
+      if (isset($output['permission'])) {
+        $defaults['permission'] = $output['permission'];
+      }
+      if (isset($output['configuration']) && is_array($output['configuration'])) {
+        if (isset($output['configuration']['title'])) {
+          $defaults['title'] = $output['configuration']['title'];
+        }
+        if (isset($output['configuration']['help_text'])) {
+          $defaults['help_text'] = $output['configuration']['help_text'];
+        }
+        if (isset($output['configuration']['no_result_text'])) {
+          $defaults['no_result_text'] = $output['configuration']['no_result_text'];
+        }
+        if (isset($output['configuration']['contact_id_field'])) {
+          $defaults['contact_id_field'] = $output['configuration']['contact_id_field'];
+        }
+        if (isset($output['configuration']['default_limit'])) {
+          $defaults['default_limit'] = $output['configuration']['default_limit'];
+        }
+        if (isset($output['configuration']['weight'])) {
+          $defaults['weight'] = $output['configuration']['weight'];
+        }
+      }
+    }
+    if (!isset($defaults['permission'])) {
+      $defaults['permission'] = 'access CiviCRM';
+    }
+    if (empty($defaults['title'])) {
+      $defaults['title'] = civicrm_api3('DataProcessor', 'getvalue', array('id' => $output['data_processor_id'], 'return' => 'title'));
+    }
+    if (!isset($defaults['no_result_text'])) {
+      $defaults['no_result_text'] = E::ts('No records');
+    }
+    if (!isset($defaults['default_limit'])) {
+      $defaults['default_limit'] = 25;
+    }
+    $form->setDefaults($defaults);
+  }
+
+  /**
+   * When this output type has configuration specify the template file name
+   * for the configuration form.
+   *
+   * @return false|string
+   */
+  public function getConfigurationTemplateFileName() {
+    return "CRM/Contact/Form/OutputConfiguration/DataProcessorContactSummaryTab.tpl";
+  }
+
+  /**
+   * Process the submitted values and create a configuration array
+   *
+   * @param $submittedValues
+   * @param array $output
+   *
+   * @return array $output
+   * @throws \Exception
+   */
+  public function processConfiguration($submittedValues, &$output) {
+    $output['permission'] = $submittedValues['permission'];
+    $configuration['title'] = $submittedValues['title'];
+    $configuration['contact_id_field'] = $submittedValues['contact_id_field'];
+    $configuration['help_text'] = $submittedValues['help_text'];
+    $configuration['no_result_text'] = $submittedValues['no_result_text'];
+    $configuration['weight'] = $submittedValues['weight'];
+    $configuration['default_limit'] = $submittedValues['default_limit'];
+    return $configuration;
+  }
+
+  /**
+   * This function is called prior to removing an output
+   *
+   * @param array $output
+   * @return void
+   * @throws \Exception
+   */
+  public function deleteOutput($output) {
+    // Do nothing
+  }
+
+  /**
+   * Returns the url for the page/form this output will show to the user
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return string
+   */
+  public function getUrlToUi($output, $dataProcessor) {
+    return "civicrm/dataprocessor_contact_summary/{$dataProcessor['name']}";
+  }
+
+  /**
+   * Returns the url for the page/form this output will show to the user
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return string
+   */
+  public function getTitleForUiLink($output, $dataProcessor) {
+    return isset($output['configuration']['title']) ? $output['configuration']['title'] : $dataProcessor['title'];
+  }
+
+  /**
+   * Returns the url for the page/form this output will show to the user
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return string|false
+   */
+  public function getIconForUiLink($output, $dataProcessor) {
+    return false;
+  }
+
+  /**
+   * Returns the callback for the UI.
+   *
+   * @return string
+   */
+  public function getCallbackForUi() {
+    return 'CRM_Contact_Form_DataProcessorContactSummaryTab';
+  }
+
+  /**
+   * Checks whether the current user has access to this output
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return bool
+   */
+  public function checkUIPermission($output, $dataProcessor) {
+    return CRM_Core_Permission::check(array(
+      $output['permission']
+    ));
+  }
+
+
+}
diff --git a/CRM/Contact/Form/DataProcessorContactSummaryTab.php b/CRM/Contact/Form/DataProcessorContactSummaryTab.php
new file mode 100644
index 00000000..a36b2f4a
--- /dev/null
+++ b/CRM/Contact/Form/DataProcessorContactSummaryTab.php
@@ -0,0 +1,147 @@
+<?php
+/**
+ * @author Jaap Jansma <jaap.jansma@civicoop.org>
+ * @license AGPL-3.0
+ */
+
+use Civi\DataProcessor\DataFlow\SqlDataFlow;
+use Civi\DataProcessor\DataFlow\SqlDataFlow\SimpleWhereClause;
+use Civi\DataProcessor\Exception\DataSourceNotFoundException;
+use Civi\DataProcessor\Exception\FieldNotFoundException;
+use Civi\DataProcessor\ProcessorType\AbstractProcessorType;
+use CRM_Dataprocessor_ExtensionUtil as E;
+
+class CRM_Contact_Form_DataProcessorContactSummaryTab extends CRM_DataprocessorSearch_Form_AbstractSearch {
+  public function buildQuickform() {
+    parent::buildQuickform();
+    $this->add('hidden', 'data_processor');
+    $this->setDefaults(array('data_processor' => $this->getDataProcessorName()));
+    $this->assign('no_result_text', $this->dataProcessorOutput['configuration']['no_result_text']);
+  }
+
+  /**
+   * Returns the default row limit.
+   *
+   * @return int
+   */
+  protected function getDefaultLimit() {
+    $defaultLimit = 25;
+    if (!empty($this->dataProcessorOutput['configuration']['default_limit'])) {
+      $defaultLimit = $this->dataProcessorOutput['configuration']['default_limit'];
+    }
+    return $defaultLimit;
+  }
+
+
+  /**
+   * Returns the name of the ID field in the dataset.
+   *
+   * @return string
+   */
+  protected function getIdFieldName() {
+    return false;
+  }
+
+  /**
+   * @return false|string
+   */
+  protected function getEntityTable() {
+    return false;
+  }
+
+  /**
+   * Returns the url for view of the record action
+   *
+   * @param $row
+   *
+   * @return false|string
+   */
+  protected function link($row) {
+    return false;
+  }
+
+  /**
+   * Returns the link text for view of the record action
+   *
+   * @param $row
+   *
+   * @return false|string
+   */
+  protected function linkText($row) {
+    return false;
+  }
+
+  /**
+   * Return the data processor ID
+   *
+   * @return String
+   */
+  protected function getDataProcessorName() {
+    $dataProcessorName = str_replace('civicrm/dataprocessor_contact_summary/', '', CRM_Utils_System::getUrlPath());
+    return $dataProcessorName;
+  }
+
+  /**
+   * Returns the name of the output for this search
+   *
+   * @return string
+   */
+  protected function getOutputName() {
+    return 'contact_summary_tab';
+  }
+
+  /**
+   * Checks whether the output has a valid configuration
+   *
+   * @return bool
+   */
+  protected function isConfigurationValid() {
+    return TRUE;
+  }
+
+  /**
+   * Add buttons for other outputs of this data processor
+   */
+  protected function addExportOutputs() {
+    // Don't add exports
+  }
+
+  /**
+   * Alter the data processor.
+   *
+   * Use this function in child classes to add for example additional filters.
+   *
+   * E.g. The contact summary tab uses this to add additional filtering on the contact id of
+   * the displayed contact.
+   *
+   * @param \Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass
+   */
+  protected function alterDataProcessor(AbstractProcessorType $dataProcessorClass) {
+    $cid = CRM_Utils_Request::retrieve('contact_id', 'Integer', $this, true);
+    list($datasource_name, $field_name) = explode('::', $this->dataProcessorOutput['configuration']['contact_id_field'], 2);
+    $dataSource = $dataProcessorClass->getDataSourceByName($datasource_name);
+    if (!$dataSource) {
+      throw new DataSourceNotFoundException(E::ts("Requires data source '%1' which could not be found. Did you rename or deleted the data source?", array(1=>$datasource_name)));
+    }
+    if (!$dataSource->getAvailableFilterFields()->getFieldSpecificationByName($field_name)) {
+      throw new FieldNotFoundException(E::ts("Requires a field with the name '%1' in the data source '%2'. Did you change the data source type?", array(
+        1 => $field_name,
+        2 => $datasource_name
+      )));
+    }
+    $fieldSpecification  =  clone $dataSource->getAvailableFilterFields()->getFieldSpecificationByName($field_name);
+    if (!$fieldSpecification) {
+      throw new FieldNotFoundException(E::ts("Requires a field with the name '%1' in the data source '%2'. Did you change the data source type?", array(
+        1 => $field_name,
+        2 => $datasource_name
+      )));
+    }
+
+    $fieldSpecification->alias = 'contact_summary_tab_contact_id';
+    $dataFlow = $dataSource->ensureField($fieldSpecification->name);
+    if ($dataFlow && $dataFlow instanceof SqlDataFlow) {
+      $whereClause = new SimpleWhereClause($dataFlow->getName(), $fieldSpecification->name, '=', $cid, $fieldSpecification->type);
+      $dataFlow->addWhereClause($whereClause);
+    }
+  }
+}
diff --git a/CRM/Contact/Page/DataProcessorContactSummaryTab.php b/CRM/Contact/Page/DataProcessorContactSummaryTab.php
new file mode 100644
index 00000000..7be7ce73
--- /dev/null
+++ b/CRM/Contact/Page/DataProcessorContactSummaryTab.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * @author Jaap Jansma <jaap.jansma@civicoop.org>
+ * @license AGPL-3.0
+ */
+
+class CRM_Contact_Page_DataProcessorContactSummaryTab extends CRM_Core_Page {
+
+  /**
+   * @var int
+   */
+  private $outputId;
+
+  /**
+   * @var String
+   */
+  private $dataProcessorName;
+
+  /**
+   * @var array
+   */
+  private $dataProcessor;
+
+  /**
+   * @var Civi\DataProcessor\ProcessorType\AbstractProcessorType
+   */
+  private $dataProcessorClass;
+
+  /**
+   * Pre Process the results
+   *
+   * @return void
+   */
+
+  protected function preProcess() {
+    $this->dataProcessorName = CRM_Utils_Request::retrieve('data_processor', 'String', $this, true);
+    $contact_id = CRM_Utils_Request::retrieve('contact_id', 'Integer', $this, true);
+
+    $this->dataProcessor = civicrm_api3('DataProcessor', 'getsingle', array('name' => $this->dataProcessorName));
+    $this->dataProcessorClass = CRM_Dataprocessor_BAO_DataProcessor::dataProcessorToClass($this->dataProcessor);
+    $this->assign('dataProcessorName', $this->dataProcessorName);
+    $this->assign('contact_id', $contact_id);
+    $this->assign('url', CRM_Utils_System::url("civicrm/dataprocessor_contact_summary/{$this->dataProcessorName}", array('contact_id' => $contact_id, 'reset' => '1', 'snippet' => 'json')));
+  }
+
+  /**
+   * Dataprocessor Output as dashlet.
+   *
+   * @return void
+   */
+
+  public function run() {
+    $this->preProcess();
+    return parent::run();
+  }
+
+}
diff --git a/CRM/DataprocessorDashlet/Dashlet.php b/CRM/DataprocessorDashlet/Dashlet.php
index 3fc2cfc4..a739a647 100644
--- a/CRM/DataprocessorDashlet/Dashlet.php
+++ b/CRM/DataprocessorDashlet/Dashlet.php
@@ -32,7 +32,7 @@ class CRM_DataprocessorDashlet_Dashlet implements Civi\DataProcessor\Output\Outp
       'placeholder' => E::ts('- select -'),
     ));
     $form->add('text', 'default_limit', E::ts('Default Limit'));
-    $form->add('wysiwyg', 'help_text', E::ts('Help text for this search'), array('rows' => 6, 'cols' => 80));
+    $form->add('wysiwyg', 'help_text', E::ts('Help text for this dashlet'), array('rows' => 6, 'cols' => 80));
 
     $defaults = array();
     if ($output) {
diff --git a/CRM/DataprocessorDashlet/Page/Dashlet.php b/CRM/DataprocessorDashlet/Page/Dashlet.php
index af49917e..32fcae97 100644
--- a/CRM/DataprocessorDashlet/Page/Dashlet.php
+++ b/CRM/DataprocessorDashlet/Page/Dashlet.php
@@ -50,22 +50,7 @@ class CRM_DataprocessorDashlet_Page_Dashlet extends CRM_Core_Page {
 
   public function run() {
     $this->preProcess();
-    $this->addColumnHeaders();
-
     return parent::run();
   }
 
-  /**
-   * Add the headers for the columns
-   *
-   */
-  protected function addColumnHeaders() {
-    $columnHeaders = array();
-    foreach($this->dataProcessorClass->getDataFlow()->getOutputFieldHandlers() as $outputFieldHandler) {
-      $field = $outputFieldHandler->getOutputFieldSpecification();
-      $columnHeaders[$field->alias] = $field->title;
-    }
-    $this->assign('columnHeaders', $columnHeaders);
-  }
-
 }
diff --git a/CRM/DataprocessorSearch/Form/AbstractSearch.php b/CRM/DataprocessorSearch/Form/AbstractSearch.php
index aef3e300..0a68234a 100644
--- a/CRM/DataprocessorSearch/Form/AbstractSearch.php
+++ b/CRM/DataprocessorSearch/Form/AbstractSearch.php
@@ -6,6 +6,7 @@
 
 use Civi\DataProcessor\FieldOutputHandler\FieldOutput;
 use Civi\DataProcessor\FieldOutputHandler\Markupable;
+use Civi\DataProcessor\ProcessorType\AbstractProcessorType;
 use CRM_Dataprocessor_ExtensionUtil as E;
 
 abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataprocessor_Form_Output_AbstractUIOutputForm {
@@ -245,6 +246,8 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
       $this->dataProcessorClass->getDataFlow()->addSort($sortField['name'], $sortDirection);
     }
 
+    $this->alterDataProcessor($this->dataProcessorClass);
+
     $pagerParams = $this->getPagerParams();
     $pagerParams['total'] = $this->dataProcessorClass->getDataFlow()->recordCount();
     $pagerParams['pageID'] = $pageId;
@@ -280,7 +283,9 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
           $row['link_text'] = $this->linkText($row);
         }
 
-        $this->addElement('checkbox', $row['checkbox'], NULL, NULL, ['class' => 'select-row']);
+        if (isset($row['checkbox'])) {
+          $this->addElement('checkbox', $row['checkbox'], NULL, NULL, ['class' => 'select-row']);
+        }
 
         if ($row['id'] && $this->usePrevNextCache()) {
           $prevnextData[] = array(
@@ -461,4 +466,18 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
     return $this->dataProcessorOutput['configuration']['title'];
   }
 
+  /**
+   * Alter the data processor.
+   *
+   * Use this function in child classes to add for example additional filters.
+   *
+   * E.g. The contact summary tab uses this to add additional filtering on the contact id of
+   * the displayed contact.
+   *
+   * @param \Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass
+   */
+  protected function alterDataProcessor(AbstractProcessorType $dataProcessorClass) {
+
+  }
+
 }
diff --git a/Civi/DataProcessor/Factory.php b/Civi/DataProcessor/Factory.php
index 3b44dad5..2c96837e 100644
--- a/Civi/DataProcessor/Factory.php
+++ b/Civi/DataProcessor/Factory.php
@@ -119,6 +119,7 @@ class Factory {
     $this->addDataSource('membership_status', 'Civi\DataProcessor\Source\Member\MembershipStatusSource', E::ts('Membership Status'));
     $this->addDataSource('csv', 'Civi\DataProcessor\Source\CSV', E::ts('CSV File'));
     $this->addOutput('api', 'Civi\DataProcessor\Output\Api', E::ts('API'));
+    $this->addOutput('contact_summary_tab', 'CRM_Contact_DataProcessorContactSummaryTab', E::ts('Tab on contact summary'));
     $this->addOutput('dashlet', 'CRM_DataprocessorDashlet_Dashlet', E::ts('Dashlet'));
     $this->addOutput('search', 'CRM_DataprocessorSearch_Search', E::ts('Search'));
     $this->addOutput('contact_search', 'CRM_Contact_DataProcessorContactSearch', E::ts('Contact Search'));
diff --git a/dataprocessor.php b/dataprocessor.php
index badb1701..998e190a 100644
--- a/dataprocessor.php
+++ b/dataprocessor.php
@@ -115,6 +115,21 @@ function dataprocessor_search_action_designer_types(&$types) {
   CRM_DataprocessorSearch_Task::searchActionDesignerTypes($types);
 }
 
+/**
+ * Implements hook_civicrm_tabset().
+ *
+ * Adds the data processor out to the contact summary tabs.
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_tabset/
+ *
+ * @param $tabsetName
+ * @param $tabs
+ * @param $context
+ */
+function dataprocessor_civicrm_tabset($tabsetName, &$tabs, $context) {
+  CRM_Contact_DataProcessorContactSummaryTab::hookTabset($tabsetName, $tabs, $context);
+}
+
 /**
  * Implements hook_civicrm_config().
  *
diff --git a/templates/CRM/Contact/Form/DataProcessorContactSummaryTab.tpl b/templates/CRM/Contact/Form/DataProcessorContactSummaryTab.tpl
new file mode 100644
index 00000000..efa4675c
--- /dev/null
+++ b/templates/CRM/Contact/Form/DataProcessorContactSummaryTab.tpl
@@ -0,0 +1,4 @@
+{crmScope extensionKey='dataprocessor'}
+{capture assign="criteriaFormTitle"}{ts}Filter{/ts}{/capture}
+{include file="CRM/DataprocessorSearch/Form/Search.tpl"}
+{/crmScope}
diff --git a/templates/CRM/Contact/Form/OutputConfiguration/DashletConfiguration.tpl b/templates/CRM/Contact/Form/OutputConfiguration/DashletConfiguration.tpl
deleted file mode 100644
index 3c657772..00000000
--- a/templates/CRM/Contact/Form/OutputConfiguration/DashletConfiguration.tpl
+++ /dev/null
@@ -1,22 +0,0 @@
-{crmScope extensionKey='dataprocessor'}
-
-<div id="dashlet_configuration">
-    <div class="crm-section">
-        <div class="label">{$form.dashlet_title.label}</div>
-        <div class="content">{$form.dashlet_title.html}</div>
-        <div class="clear"></div>
-    </div>
-    <div class="crm-section">
-        <div class="label">{$form.dashlet_name.label}</div>
-        <div class="content">{$form.dashlet_name.html}</div>
-        <div class="clear"></div>
-    </div>
-    <div class="crm-section">
-        <div class="label">{$form.dashlet_active.label}</div>
-        <div class="content">{$form.dashlet_active.html}</div>
-        <div class="clear"></div>
-    </div>
-</div>
-
-{/crmScope}
-
diff --git a/templates/CRM/Contact/Form/OutputConfiguration/DataProcessorContactSearch.tpl b/templates/CRM/Contact/Form/OutputConfiguration/DataProcessorContactSearch.tpl
index 7d0f459c..cb7f1b9a 100644
--- a/templates/CRM/Contact/Form/OutputConfiguration/DataProcessorContactSearch.tpl
+++ b/templates/CRM/Contact/Form/OutputConfiguration/DataProcessorContactSearch.tpl
@@ -1,5 +1,4 @@
 {crmScope extensionKey='dataprocessor'}
-    {include file='CRM/Contact/Form/OutputConfiguration/DashletConfiguration.tpl'}
     <div class="crm-section">
         <div class="label">{$form.title.label}</div>
         <div class="content">{$form.title.html}</div>
@@ -30,5 +29,5 @@
         <div class="content">{$form.help_text.html}</div>
         <div class="clear"></div>
     </div>
-    
-{/crmScope}
\ No newline at end of file
+
+{/crmScope}
diff --git a/templates/CRM/Contact/Form/OutputConfiguration/DataProcessorContactSummaryTab.tpl b/templates/CRM/Contact/Form/OutputConfiguration/DataProcessorContactSummaryTab.tpl
new file mode 100644
index 00000000..3347e891
--- /dev/null
+++ b/templates/CRM/Contact/Form/OutputConfiguration/DataProcessorContactSummaryTab.tpl
@@ -0,0 +1,41 @@
+{crmScope extensionKey='dataprocessor'}
+  <div class="crm-section">
+    <div class="label">{$form.title.label}</div>
+    <div class="content">{$form.title.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.permission.label}</div>
+    <div class="content">{$form.permission.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.weight.label}</div>
+    <div class="content">{$form.weight.html}
+    <p class="description">{ts}Use this to order the tab. The lower the weight to more to the left the tab will be displayed. Keep empty for automatic position.{/ts}</p>
+    </div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.contact_id_field.label}</div>
+    <div class="content">{$form.contact_id_field.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.no_result_text.label}</div>
+    <div class="content">{$form.no_result_text.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section">
+    <div class="label">{$form.help_text.label}</div>
+    <div class="content">{$form.help_text.html}</div>
+    <div class="clear"></div>
+  </div>
+
+  <div class="crm-section">
+    <div class="label">{$form.default_limit.label}</div>
+    <div class="content">{$form.default_limit.html}</div>
+    <div class="clear"></div>
+  </div>
+
+{/crmScope}
diff --git a/templates/CRM/Contact/Page/DataProcessorContactSummaryTab.tpl b/templates/CRM/Contact/Page/DataProcessorContactSummaryTab.tpl
new file mode 100644
index 00000000..21045863
--- /dev/null
+++ b/templates/CRM/Contact/Page/DataProcessorContactSummaryTab.tpl
@@ -0,0 +1,16 @@
+<div id="dataprocessorContactSummaryTab_{$dataProcessorName}"></div>
+
+{literal}
+<script type="text/javascript">
+  (function($) {
+    var target = "#dataprocessorContactSummaryTab_{/literal}{$dataProcessorName}{literal}";
+    var form = CRM.loadForm('{/literal}{$url}{literal}', {
+      "target": target,
+      "dialog": false,
+    }).on('crmFormSuccess', function(event, data) {
+      $(target).crmSnippet('option', 'url', data.userContext).crmSnippet('refresh');
+    });
+
+  })(CRM.$);
+</script>
+{/literal}
diff --git a/templates/CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl b/templates/CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl
index 01a81349..565bebcf 100644
--- a/templates/CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl
+++ b/templates/CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl
@@ -1,7 +1,8 @@
+{crmScope extensionKey='dataprocessor'}
 <div class="crm-form-block crm-search-form-block">
     <div class="crm-accordion-wrapper crm-advanced_search_form-accordion {if (!empty($rows))}collapsed{/if}">
         <div class="crm-accordion-header crm-master-accordion-header">
-            {ts}Edit Search Criteria{/ts}
+            {if isset($criteriaFormTitle)}{$criteriaFormTitle}{else}{ts}Edit Search Criteria{/ts}{/if}
         </div>
         <!-- /.crm-accordion-header -->
         <div class="crm-accordion-body">
@@ -53,3 +54,4 @@
 
 </script>
 {/literal}
+{/crmScope}
diff --git a/templates/CRM/DataprocessorSearch/Form/Search.tpl b/templates/CRM/DataprocessorSearch/Form/Search.tpl
index 4c18315f..283f3d66 100644
--- a/templates/CRM/DataprocessorSearch/Form/Search.tpl
+++ b/templates/CRM/DataprocessorSearch/Form/Search.tpl
@@ -1,3 +1,4 @@
+{crmScope extensionKey='dataprocessor'}
 {include file="CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl"}
 
 {if (isset($output.configuration.help_text) && $output.configuration.help_text)}
@@ -72,4 +73,11 @@
     </div>
 
     {include file="CRM/DataprocessorSearch/Form/ResultsJavascript.tpl"}
+{elseif isset($no_result_text)}
+<div class="crm-content-block">
+  <div class="crm-results-block">
+    {$no_result_text}
+  </div>
+</div>
 {/if}
+{/crmScope}
diff --git a/xml/Menu/dataprocessor.xml b/xml/Menu/dataprocessor.xml
index 9d91780e..693306e0 100644
--- a/xml/Menu/dataprocessor.xml
+++ b/xml/Menu/dataprocessor.xml
@@ -63,6 +63,12 @@
     <access_arguments>access CiviCRM</access_arguments>
     <access_arguments>administer CiviCRM</access_arguments>
   </item>
+  <item>
+    <path>civicrm/dataprocessor/page/contactsummary</path>
+    <page_callback>CRM_Contact_Page_DataProcessorContactSummaryTab</page_callback>
+    <title>DataProcessor</title>
+    <access_arguments>access CiviCRM</access_arguments>
+  </item>
   <item>
     <path>civicrm/dataprocessor/page/dashlet</path>
     <page_callback>CRM_DataprocessorDashlet_Page_Dashlet</page_callback>
-- 
GitLab