Add user_dashboard core extension (alpha)
authorcolemanw <coleman@civicrm.org>
Wed, 11 Oct 2023 23:23:44 +0000 (19:23 -0400)
committercolemanw <coleman@civicrm.org>
Thu, 12 Oct 2023 20:21:33 +0000 (16:21 -0400)
Includes search displays to replace the user dashboard, minus the action links

15 files changed:
.gitignore
distmaker/core-ext.txt
ext/user_dashboard/ang/afsearchUserDashboard.aff.php [new file with mode: 0644]
ext/user_dashboard/info.xml [new file with mode: 0644]
ext/user_dashboard/managed/Dashboard_Tag.mgd.php [new file with mode: 0644]
ext/user_dashboard/managed/SavedSearch_UserDashboard_Activities.mgd.php [new file with mode: 0644]
ext/user_dashboard/managed/SavedSearch_UserDashboard_Groups.mgd.php [new file with mode: 0644]
ext/user_dashboard/managed/SavedSearch_UserDashboard_Pledges.mgd.php [new file with mode: 0644]
ext/user_dashboard/managed/UserDashboard_Contributions.mgd.php [new file with mode: 0644]
ext/user_dashboard/managed/UserDashboard_Events.mgd.php [new file with mode: 0644]
ext/user_dashboard/managed/UserDashboard_Memberships.mgd.php [new file with mode: 0644]
ext/user_dashboard/managed/UserDashboard_PCPs.mgd.php [new file with mode: 0644]
ext/user_dashboard/managed/UserDashboard_Relationships.mgd.php [new file with mode: 0644]
ext/user_dashboard/user_dashboard.civix.php [new file with mode: 0644]
ext/user_dashboard/user_dashboard.php [new file with mode: 0644]

index 20ab4f93a8deb54b878ee6bba2e0c564f78dbf5f..231ab1469771a5405c443b464bd9e3f998530786 100644 (file)
@@ -35,6 +35,7 @@
 !/ext/civi_pledge
 !/ext/civi_report
 !/ext/scheduled_communications
+!/ext/user_dashboard
 backdrop/
 bower_components
 CRM/Case/xml/configuration
index 9e2a64087f9eecd2c722fa3974d406bf8de07821..1021333ca7c713d4418d6e80e41dd4b3321a9ad4 100644 (file)
@@ -35,3 +35,4 @@ civi_member
 civi_pledge
 civi_report
 scheduled_communications
+user_dashboard
diff --git a/ext/user_dashboard/ang/afsearchUserDashboard.aff.php b/ext/user_dashboard/ang/afsearchUserDashboard.aff.php
new file mode 100644 (file)
index 0000000..acb8944
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+use CRM_UserDashboard_ExtensionUtil as E;
+
+$afform = [
+  'type' => 'search',
+  'title' => E::ts('User Dashboard'),
+  'server_route' => 'civicrm/user',
+  'permission' => ['access Contact Dashboard'],
+  'layout' => '',
+  // temporary, remove after merging https://github.com/civicrm/civicrm-core/pull/27783
+  'requires' => ['af', 'afCore', 'crmSearchDisplayTable'],
+];
+
+// Add displays for every SavedSearch tagged "UserDashboard"
+$searchDisplays = civicrm_api4('SearchDisplay', 'get', [
+  'checkPermissions' => FALSE,
+  'select' => ['name', 'label', 'type:name', 'saved_search_id.name'],
+  'where' => [
+    ['saved_search_id.is_current', '=', TRUE],
+    ['saved_search_id.tags:name', 'IN', ['UserDashboard']],
+  ],
+  'orderBy' => ['name' => 'ASC'],
+]);
+foreach ($searchDisplays as $display) {
+  $afform['layout'] .= <<<HTML
+    <div af-fieldset="" class="af-container-style-pane" af-title="$display[label]">
+      <{$display['type:name']} search-name="{$display['saved_search_id.name']}" display-name="$display[name]"></{$display['type:name']}>
+    </div>
+  HTML;
+}
+
+return $afform;
diff --git a/ext/user_dashboard/info.xml b/ext/user_dashboard/info.xml
new file mode 100644 (file)
index 0000000..4e87e48
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0"?>
+<extension key="user_dashboard" type="module">
+  <file>user_dashboard</file>
+  <name>User Dashboard</name>
+  <description>Adds a page for users to see their contributions, memberships, etc.</description>
+  <license>AGPL-3.0</license>
+  <maintainer>
+    <author>CiviCRM</author>
+    <email>info@civicrm.org</email>
+  </maintainer>
+  <urls>
+    <url desc="Main Extension Page">http://FIXME</url>
+    <url desc="Documentation">http://FIXME</url>
+    <url desc="Support">http://FIXME</url>
+    <url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
+  </urls>
+  <releaseDate>2023-10-07</releaseDate>
+  <version>1.0</version>
+  <develStage>alpha</develStage>
+  <compatibility>
+    <ver>5.68.alpha1</ver>
+  </compatibility>
+  <comments>This extension is still experimental</comments>
+  <classloader>
+    <psr0 prefix="CRM_" path="."/>
+    <psr4 prefix="Civi\" path="Civi"/>
+  </classloader>
+  <civix>
+    <namespace>CRM/UserDashboard</namespace>
+    <format>23.02.1</format>
+    <angularModule>crmUserDashboard</angularModule>
+  </civix>
+  <mixins>
+    <mixin>mgd-php@1.0.0</mixin>
+  </mixins>
+</extension>
diff --git a/ext/user_dashboard/managed/Dashboard_Tag.mgd.php b/ext/user_dashboard/managed/Dashboard_Tag.mgd.php
new file mode 100644 (file)
index 0000000..753a40b
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+use CRM_UserDashboard_ExtensionUtil as E;
+
+return [
+  [
+    'name' => 'Dashboard_Tag',
+    'entity' => 'Tag',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'label' => E::ts('User Dashboard'),
+        'name' => 'UserDashboard',
+        'description' => E::ts('Search will appear on the User Dashboard page'),
+        'is_reserved' => TRUE,
+        'used_for' => [
+          'civicrm_saved_search',
+        ],
+        'color' => '#5d677b',
+      ],
+      'match' => [
+        'name',
+      ],
+    ],
+  ],
+];
diff --git a/ext/user_dashboard/managed/SavedSearch_UserDashboard_Activities.mgd.php b/ext/user_dashboard/managed/SavedSearch_UserDashboard_Activities.mgd.php
new file mode 100644 (file)
index 0000000..6553865
--- /dev/null
@@ -0,0 +1,175 @@
+<?php
+use CRM_UserDashboard_ExtensionUtil as E;
+
+return [
+  [
+    'name' => 'SavedSearch_UserDashboard_Activities',
+    'entity' => 'SavedSearch',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Activities',
+        'label' => E::ts('User Dashboard - Activities'),
+        'api_entity' => 'Activity',
+        'api_params' => [
+          'version' => 4,
+          'select' => [
+            'subject',
+            'activity_type_id:label',
+            'GROUP_CONCAT(DISTINCT Activity_ActivityContact_Contact_01.sort_name) AS GROUP_CONCAT_Activity_ActivityContact_Contact_01_sort_name',
+            'GROUP_CONCAT(DISTINCT Activity_ActivityContact_Contact_02.sort_name) AS GROUP_CONCAT_Activity_ActivityContact_Contact_02_sort_name',
+            'activity_date_time',
+            'status_id:label',
+          ],
+          'orderBy' => [],
+          'where' => [
+            [
+              'status_id:name',
+              '!=',
+              'Completed',
+            ],
+          ],
+          'groupBy' => [
+            'id',
+          ],
+          'join' => [
+            [
+              'Contact AS Activity_ActivityContact_Contact_01',
+              'LEFT',
+              'ActivityContact',
+              [
+                'id',
+                '=',
+                'Activity_ActivityContact_Contact_01.activity_id',
+              ],
+              [
+                'Activity_ActivityContact_Contact_01.record_type_id:name',
+                '=',
+                '"Activity Source"',
+              ],
+            ],
+            [
+              'Contact AS Activity_ActivityContact_Contact_02',
+              'LEFT',
+              'ActivityContact',
+              [
+                'id',
+                '=',
+                'Activity_ActivityContact_Contact_02.activity_id',
+              ],
+              [
+                'Activity_ActivityContact_Contact_02.record_type_id:name',
+                '=',
+                '"Activity Targets"',
+              ],
+            ],
+            [
+              'Contact AS Activity_ActivityContact_Contact_03',
+              'INNER',
+              'ActivityContact',
+              [
+                'id',
+                '=',
+                'Activity_ActivityContact_Contact_03.activity_id',
+              ],
+              [
+                'Activity_ActivityContact_Contact_03.record_type_id:name',
+                '=',
+                '"Activity Assignees"',
+              ],
+              [
+                'Activity_ActivityContact_Contact_03.id',
+                '=',
+                '"user_contact_id"',
+              ],
+            ],
+          ],
+          'having' => [],
+        ],
+      ],
+      'match' => [
+        'name',
+      ],
+    ],
+  ],
+  [
+    'name' => 'SavedSearch_UserDashboard_Activities_SearchDisplay_UserDashboard_Activities',
+    'entity' => 'SearchDisplay',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Activities',
+        'label' => E::ts('Your Assigned Activities'),
+        'saved_search_id.name' => 'UserDashboard_Activities',
+        'type' => 'table',
+        'settings' => [
+          'description' => NULL,
+          'sort' => [],
+          'limit' => 20,
+          'pager' => [
+            'expose_limit' => TRUE,
+            'hide_single' => TRUE,
+          ],
+          'placeholder' => 1,
+          'columns' => [
+            [
+              'type' => 'field',
+              'key' => 'activity_type_id:label',
+              'dataType' => 'Integer',
+              'label' => E::ts('Type'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'subject',
+              'dataType' => 'String',
+              'label' => E::ts('Subject'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'GROUP_CONCAT_Activity_ActivityContact_Contact_01_sort_name',
+              'dataType' => 'String',
+              'label' => E::ts('Added by'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'GROUP_CONCAT_Activity_ActivityContact_Contact_02_sort_name',
+              'dataType' => 'String',
+              'label' => E::ts('With'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'activity_date_time',
+              'dataType' => 'Timestamp',
+              'label' => E::ts('Date'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'status_id:label',
+              'dataType' => 'Integer',
+              'label' => E::ts('Status'),
+              'sortable' => TRUE,
+            ],
+          ],
+          'actions' => FALSE,
+          'classes' => [
+            'table',
+            'table-striped',
+          ],
+        ],
+      ],
+      'match' => [
+        'name',
+        'saved_search_id',
+      ],
+    ],
+  ],
+];
diff --git a/ext/user_dashboard/managed/SavedSearch_UserDashboard_Groups.mgd.php b/ext/user_dashboard/managed/SavedSearch_UserDashboard_Groups.mgd.php
new file mode 100644 (file)
index 0000000..34c4d43
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+use CRM_UserDashboard_ExtensionUtil as E;
+
+return [
+  [
+    'name' => 'SavedSearch_UserDashboard_Groups',
+    'entity' => 'SavedSearch',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Groups',
+        'label' => E::ts('User Dashboard - Groups'),
+        'api_entity' => 'Group',
+        'api_params' => [
+          'version' => 4,
+          'select' => [
+            'frontend_title',
+            'Group_GroupContact_Contact_01.status:label',
+            'MAX(Group_SubscriptionHistory_group_id_01.date) AS MAX_Group_SubscriptionHistory_group_id_01_date',
+          ],
+          'orderBy' => [],
+          'where' => [
+            [
+              'Group_GroupContact_Contact_01.id',
+              '=',
+              'user_contact_id',
+            ],
+            [
+              'visibility:name',
+              '=',
+              'Public Pages',
+            ],
+            [
+              'is_active',
+              '=',
+              TRUE,
+            ],
+          ],
+          'groupBy' => [
+            'id',
+            'Group_GroupContact_Contact_01.id',
+          ],
+          'join' => [
+            [
+              'Contact AS Group_GroupContact_Contact_01',
+              'LEFT',
+              'GroupContact',
+              [
+                'id',
+                '=',
+                'Group_GroupContact_Contact_01.group_id',
+              ],
+            ],
+            [
+              'SubscriptionHistory AS Group_SubscriptionHistory_group_id_01',
+              'LEFT',
+              [
+                'id',
+                '=',
+                'Group_SubscriptionHistory_group_id_01.group_id',
+              ],
+              [
+                'Group_SubscriptionHistory_group_id_01.contact_id',
+                '=',
+                '"user_contact_id"',
+              ],
+            ],
+          ],
+          'having' => [],
+        ],
+      ],
+      'match' => [
+        'name',
+      ],
+    ],
+  ],
+  [
+    'name' => 'SavedSearch_UserDashboard_Groups_SearchDisplay_UserDashboard_Groups',
+    'entity' => 'SearchDisplay',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Groups',
+        'label' => E::ts('Your Group(s)'),
+        'saved_search_id.name' => 'UserDashboard_Groups',
+        'type' => 'table',
+        'settings' => [
+          'description' => NULL,
+          'sort' => [
+            [
+              'Group_GroupContact_Contact_01.status',
+              'ASC',
+            ],
+          ],
+          'limit' => 20,
+          'pager' => [
+            'expose_limit' => TRUE,
+            'hide_single' => TRUE,
+          ],
+          'placeholder' => 1,
+          'columns' => [
+            [
+              'type' => 'field',
+              'key' => 'frontend_title',
+              'dataType' => 'String',
+              'label' => E::ts('Group'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'Group_GroupContact_Contact_01.status:label',
+              'dataType' => 'String',
+              'label' => E::ts('Status'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'MAX_Group_SubscriptionHistory_group_id_01_date',
+              'dataType' => 'Timestamp',
+              'label' => E::ts('Since'),
+              'sortable' => TRUE,
+            ],
+          ],
+          'actions' => FALSE,
+          'classes' => [
+            'table',
+            'table-striped',
+          ],
+          'cssRules' => [
+            [
+              'disabled',
+              'Group_GroupContact_Contact_01.status',
+              '=',
+              'Removed',
+            ],
+          ],
+        ],
+      ],
+      'match' => [
+        'name',
+        'saved_search_id',
+      ],
+    ],
+  ],
+];
diff --git a/ext/user_dashboard/managed/SavedSearch_UserDashboard_Pledges.mgd.php b/ext/user_dashboard/managed/SavedSearch_UserDashboard_Pledges.mgd.php
new file mode 100644 (file)
index 0000000..9b9f696
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+use CRM_UserDashboard_ExtensionUtil as E;
+
+if (!CRM_Core_Component::isEnabled('CiviPledge')) {
+  return [];
+}
+
+return [
+  [
+    'name' => 'SavedSearch_UserDashboard_Pledges',
+    'entity' => 'SavedSearch',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Pledges',
+        'label' => E::ts('User Dashboard - Pledges'),
+        'api_entity' => 'Pledge',
+        'api_params' => [
+          'version' => 4,
+          'select' => [
+            'amount',
+            'SUM(Pledge_PledgePayment_pledge_id_01.actual_amount) AS SUM_Pledge_PledgePayment_pledge_id_01_actual_amount',
+            'financial_type_id:label',
+            'create_date',
+            'MIN(Pledge_PledgePayment_pledge_id_02.scheduled_date) AS MIN_Pledge_PledgePayment_pledge_id_02_scheduled_date',
+            'MAX(Pledge_PledgePayment_pledge_id_02.scheduled_amount) AS MAX_Pledge_PledgePayment_pledge_id_02_scheduled_amount',
+            'status_id:label',
+          ],
+          'orderBy' => [],
+          'where' => [
+            ['contact_id', '=', 'user_contact_id'],
+          ],
+          'groupBy' => [
+            'id',
+          ],
+          'join' => [
+            [
+              'PledgePayment AS Pledge_PledgePayment_pledge_id_01',
+              'LEFT',
+              ['id', '=', 'Pledge_PledgePayment_pledge_id_01.pledge_id'],
+              ['Pledge_PledgePayment_pledge_id_01.status_id:name', '=', '"Completed"'],
+            ],
+            [
+              'PledgePayment AS Pledge_PledgePayment_pledge_id_02',
+              'LEFT',
+              ['id', '=', 'Pledge_PledgePayment_pledge_id_02.pledge_id'],
+              ['Pledge_PledgePayment_pledge_id_02.status_id:name', '!=', '"Completed"'],
+            ],
+          ],
+          'having' => [],
+        ],
+      ],
+      'match' => [
+        'name',
+      ],
+    ],
+  ],
+  [
+    'name' => 'SavedSearch_UserDashboard_Pledges_SearchDisplay_UserDashboard_Pledges',
+    'entity' => 'SearchDisplay',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Pledges',
+        'label' => E::ts('Your Pledges'),
+        'saved_search_id.name' => 'UserDashboard_Pledges',
+        'type' => 'table',
+        'settings' => [
+          'description' => NULL,
+          'sort' => [],
+          'limit' => 20,
+          'pager' => [
+            'hide_single' => TRUE,
+            'expose_limit' => TRUE,
+          ],
+          'placeholder' => 1,
+          'columns' => [
+            [
+              'type' => 'field',
+              'key' => 'amount',
+              'dataType' => 'Money',
+              'label' => E::ts('Pledged'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'SUM_Pledge_PledgePayment_pledge_id_01_actual_amount',
+              'dataType' => 'Money',
+              'label' => E::ts('Total Paid'),
+              'sortable' => TRUE,
+              'empty_value' => '0',
+            ],
+            [
+              'type' => 'field',
+              'key' => 'financial_type_id:label',
+              'dataType' => 'Integer',
+              'label' => E::ts('Pledged For'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'create_date',
+              'dataType' => 'Timestamp',
+              'label' => E::ts('Pledge Made'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'MIN_Pledge_PledgePayment_pledge_id_02_scheduled_date',
+              'dataType' => 'Timestamp',
+              'label' => E::ts('Next Pay Date'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'MAX_Pledge_PledgePayment_pledge_id_02_scheduled_amount',
+              'dataType' => 'Money',
+              'label' => E::ts('Next Amount'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'status_id:label',
+              'dataType' => 'Integer',
+              'label' => E::ts('Status'),
+              'sortable' => TRUE,
+            ],
+          ],
+          'actions' => FALSE,
+          'classes' => [
+            'table',
+            'table-striped',
+          ],
+        ],
+      ],
+      'match' => [
+        'name',
+        'saved_search_id',
+      ],
+    ],
+  ],
+];
diff --git a/ext/user_dashboard/managed/UserDashboard_Contributions.mgd.php b/ext/user_dashboard/managed/UserDashboard_Contributions.mgd.php
new file mode 100644 (file)
index 0000000..d5b5638
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+use CRM_UserDashboard_ExtensionUtil as E;
+
+if (!CRM_Core_Component::isEnabled('CiviContribute')) {
+  return [];
+}
+
+return [
+  [
+    'name' => 'SavedSearch_UserDashboard_Contributions',
+    'entity' => 'SavedSearch',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Contributions',
+        'label' => E::ts('User Dashboard - Contributions'),
+        'api_entity' => 'Contribution',
+        'api_params' => [
+          'version' => 4,
+          'select' => [
+            'total_amount',
+            'financial_type_id:label',
+            'contribution_status_id:label',
+            'receive_date',
+            'receipt_date',
+          ],
+          'orderBy' => [],
+          'where' => [
+            ['contact_id', '=', 'user_contact_id'],
+          ],
+          'groupBy' => [],
+          'join' => [],
+          'having' => [],
+        ],
+      ],
+      'match' => [
+        'name',
+      ],
+    ],
+  ],
+  [
+    'name' => 'SavedSearch_UserDashboard_Contributions_SearchDisplay_UserDashboard_Contributions',
+    'entity' => 'SearchDisplay',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Contributions',
+        'label' => E::ts('Your Contribution(s)'),
+        'saved_search_id.name' => 'UserDashboard_Contributions',
+        'type' => 'table',
+        'settings' => [
+          'description' => NULL,
+          'sort' => [],
+          'limit' => 20,
+          'pager' => [
+            'hide_single' => TRUE,
+            'expose_limit' => TRUE,
+          ],
+          'placeholder' => 1,
+          'columns' => [
+            [
+              'type' => 'field',
+              'key' => 'total_amount',
+              'dataType' => 'String',
+              'label' => E::ts('Total Amount'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'financial_type_id:label',
+              'dataType' => 'String',
+              'label' => E::ts('Type'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'receive_date',
+              'dataType' => 'Date',
+              'label' => E::ts('Date'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'receipt_date',
+              'dataType' => 'Date',
+              'label' => E::ts('Receipt Sent'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'contribution_status_id:label',
+              'dataType' => 'String',
+              'label' => E::ts('Status'),
+              'sortable' => TRUE,
+            ],
+          ],
+          'actions' => FALSE,
+          'classes' => [
+            'table',
+            'table-striped',
+          ],
+        ],
+      ],
+      'match' => [
+        'name',
+        'saved_search_id',
+      ],
+    ],
+  ],
+];
diff --git a/ext/user_dashboard/managed/UserDashboard_Events.mgd.php b/ext/user_dashboard/managed/UserDashboard_Events.mgd.php
new file mode 100644 (file)
index 0000000..c1cb108
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+use CRM_UserDashboard_ExtensionUtil as E;
+
+if (!CRM_Core_Component::isEnabled('CiviEvent')) {
+  return [];
+}
+
+return [
+  [
+    'name' => 'SavedSearch_UserDashboard_Events',
+    'entity' => 'SavedSearch',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Events',
+        'label' => E::ts('User Dashboard - Events'),
+        'api_entity' => 'Participant',
+        'api_params' => [
+          'version' => 4,
+          'select' => [
+            'event_id.title',
+            'role_id:label',
+            'status_id:label',
+            'event_id.start_date',
+          ],
+          'orderBy' => [],
+          'where' => [
+            ['contact_id', '=', 'user_contact_id'],
+          ],
+          'groupBy' => [],
+          'join' => [],
+          'having' => [],
+        ],
+      ],
+      'match' => [
+        'name',
+      ],
+    ],
+  ],
+  [
+    'name' => 'SavedSearch_UserDashboard_Events_SearchDisplay_UserDashboard_Events',
+    'entity' => 'SearchDisplay',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Events',
+        'label' => E::ts('Your Event(s)'),
+        'saved_search_id.name' => 'UserDashboard_Events',
+        'type' => 'table',
+        'settings' => [
+          'description' => NULL,
+          'sort' => [],
+          'limit' => 20,
+          'pager' => [
+            'hide_single' => TRUE,
+            'expose_limit' => TRUE,
+          ],
+          'placeholder' => 1,
+          'columns' => [
+            [
+              'type' => 'field',
+              'key' => 'event_id.title',
+              'dataType' => 'String',
+              'label' => E::ts('Event'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'event_id.start_date',
+              'dataType' => 'Date',
+              'label' => E::ts('Event Date'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'role_id:label',
+              'dataType' => 'String',
+              'label' => E::ts('Role'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'status_id:label',
+              'dataType' => 'String',
+              'label' => E::ts('Status'),
+              'sortable' => TRUE,
+            ],
+          ],
+          'actions' => FALSE,
+          'classes' => [
+            'table',
+            'table-striped',
+          ],
+        ],
+      ],
+      'match' => [
+        'name',
+        'saved_search_id',
+      ],
+    ],
+  ],
+];
diff --git a/ext/user_dashboard/managed/UserDashboard_Memberships.mgd.php b/ext/user_dashboard/managed/UserDashboard_Memberships.mgd.php
new file mode 100644 (file)
index 0000000..3604070
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+use CRM_UserDashboard_ExtensionUtil as E;
+
+if (!CRM_Core_Component::isEnabled('CiviMember')) {
+  return [];
+}
+
+return [
+  [
+    'name' => 'SavedSearch_UserDashboard_Memberships',
+    'entity' => 'SavedSearch',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Memberships',
+        'label' => E::ts('User Dashboard - Memberships'),
+        'api_entity' => 'Membership',
+        'api_params' => [
+          'version' => 4,
+          'select' => [
+            'membership_type_id:label',
+            'status_id:label',
+            'start_date',
+            'end_date',
+            'join_date',
+          ],
+          'orderBy' => [],
+          'where' => [
+            ['contact_id', '=', 'user_contact_id'],
+          ],
+          'groupBy' => [],
+          'join' => [],
+          'having' => [],
+        ],
+      ],
+      'match' => [
+        'name',
+      ],
+    ],
+  ],
+  [
+    'name' => 'SavedSearch_UserDashboard_Memberships_SearchDisplay_UserDashboard_Memberships',
+    'entity' => 'SearchDisplay',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Memberships',
+        'label' => E::ts('Your Membership(s)'),
+        'saved_search_id.name' => 'UserDashboard_Memberships',
+        'type' => 'table',
+        'settings' => [
+          'description' => NULL,
+          'sort' => [],
+          'limit' => 20,
+          'pager' => [
+            'hide_single' => TRUE,
+            'expose_limit' => TRUE,
+          ],
+          'placeholder' => 1,
+          'columns' => [
+            [
+              'type' => 'field',
+              'key' => 'membership_type_id:label',
+              'dataType' => 'String',
+              'label' => E::ts('Type'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'join_date',
+              'dataType' => 'Date',
+              'label' => E::ts('Member Since'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'start_date',
+              'dataType' => 'Date',
+              'label' => E::ts('Start Date'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'end_date',
+              'dataType' => 'Date',
+              'label' => E::ts('End Date'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'status_id:label',
+              'dataType' => 'String',
+              'label' => E::ts('Status'),
+              'sortable' => TRUE,
+            ],
+          ],
+          'actions' => FALSE,
+          'classes' => [
+            'table',
+            'table-striped',
+          ],
+        ],
+      ],
+      'match' => [
+        'name',
+        'saved_search_id',
+      ],
+    ],
+  ],
+];
diff --git a/ext/user_dashboard/managed/UserDashboard_PCPs.mgd.php b/ext/user_dashboard/managed/UserDashboard_PCPs.mgd.php
new file mode 100644 (file)
index 0000000..4098795
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+use CRM_UserDashboard_ExtensionUtil as E;
+
+if (!CRM_Core_Component::isEnabled('CiviContribute')) {
+  return [];
+}
+
+return [
+  [
+    'name' => 'SavedSearch_UserDashboard_PCPs',
+    'entity' => 'SavedSearch',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_PCPs',
+        'label' => E::ts('User Dashboard - PCPs'),
+        'api_entity' => 'PCP',
+        'api_params' => [
+          'version' => 4,
+          'select' => [
+            'title',
+            'status_id:label',
+            'page_id.frontend_title',
+          ],
+          'orderBy' => [],
+          'where' => [
+            ['contact_id', '=', 'user_contact_id'],
+          ],
+          'groupBy' => [],
+          'join' => [],
+          'having' => [],
+        ],
+      ],
+      'match' => [
+        'name',
+      ],
+    ],
+  ],
+  [
+    'name' => 'SavedSearch_UserDashboard_PCPs_SearchDisplay_UserDashboard_PCPs',
+    'entity' => 'SearchDisplay',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_PCPs',
+        'label' => E::ts('Personal Campaign Pages'),
+        'saved_search_id.name' => 'UserDashboard_PCPs',
+        'type' => 'table',
+        'settings' => [
+          'description' => NULL,
+          'sort' => [],
+          'limit' => 20,
+          'pager' => [
+            'hide_single' => TRUE,
+            'expose_limit' => TRUE,
+          ],
+          'placeholder' => 1,
+          'columns' => [
+            [
+              'type' => 'field',
+              'key' => 'title',
+              'dataType' => 'String',
+              'label' => E::ts('Title'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'status_id:label',
+              'dataType' => 'String',
+              'label' => E::ts('Type'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'page_id.frontend_title',
+              'dataType' => 'String',
+              'label' => E::ts('Campaign'),
+              'sortable' => TRUE,
+            ],
+          ],
+          'actions' => FALSE,
+          'classes' => [
+            'table',
+            'table-striped',
+          ],
+        ],
+      ],
+      'match' => [
+        'name',
+        'saved_search_id',
+      ],
+    ],
+  ],
+];
diff --git a/ext/user_dashboard/managed/UserDashboard_Relationships.mgd.php b/ext/user_dashboard/managed/UserDashboard_Relationships.mgd.php
new file mode 100644 (file)
index 0000000..85c7a65
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+use CRM_UserDashboard_ExtensionUtil as E;
+
+return [
+  [
+    'name' => 'SavedSearch_UserDashboard_Relationships',
+    'entity' => 'SavedSearch',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Relationships',
+        'label' => E::ts('User Dashboard - Relationships'),
+        'api_entity' => 'RelationshipCache',
+        'api_params' => [
+          'version' => 4,
+          'select' => [
+            'near_relation:label',
+            'RelationshipCache_Contact_far_contact_id_01.display_name',
+            'start_date',
+            'RelationshipCache_Contact_far_contact_id_01.address_primary.city',
+            'RelationshipCache_Contact_far_contact_id_01.address_primary.state_province_id:label',
+            'RelationshipCache_Contact_far_contact_id_01.email_primary.email',
+            'RelationshipCache_Contact_far_contact_id_01.phone_primary.phone',
+          ],
+          'orderBy' => [],
+          'where' => [
+            ['near_contact_id', '=', 'user_contact_id'],
+            ['is_current', '=', TRUE],
+          ],
+          'groupBy' => [],
+          'join' => [
+            [
+              'Contact AS RelationshipCache_Contact_far_contact_id_01',
+              'LEFT',
+              ['far_contact_id', '=', 'RelationshipCache_Contact_far_contact_id_01.id'],
+            ],
+          ],
+          'having' => [],
+        ],
+      ],
+      'match' => [
+        'name',
+      ],
+    ],
+  ],
+  [
+    'name' => 'SavedSearch_UserDashboard_Relationships_SearchDisplay_UserDashboard_Relationships',
+    'entity' => 'SearchDisplay',
+    'cleanup' => 'always',
+    'update' => 'unmodified',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'name' => 'UserDashboard_Relationships',
+        'label' => E::ts('Your Contacts / Organizations'),
+        'saved_search_id.name' => 'UserDashboard_Relationships',
+        'type' => 'table',
+        'settings' => [
+          'description' => NULL,
+          'sort' => [],
+          'limit' => 20,
+          'pager' => [
+            'hide_single' => TRUE,
+            'expose_limit' => TRUE,
+          ],
+          'placeholder' => 1,
+          'columns' => [
+            [
+              'type' => 'field',
+              'key' => 'near_relation:label',
+              'dataType' => 'String',
+              'label' => E::ts('Relationship'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'RelationshipCache_Contact_far_contact_id_01.display_name',
+              'dataType' => 'String',
+              'label' => E::ts('With'),
+              'sortable' => TRUE,
+              'icons' => [
+                [
+                  'field' => 'RelationshipCache_Contact_far_contact_id_01.contact_sub_type:icon',
+                  'side' => 'left',
+                ],
+                [
+                  'field' => 'RelationshipCache_Contact_far_contact_id_01.contact_type:icon',
+                  'side' => 'left',
+                ],
+              ],
+            ],
+            [
+              'type' => 'field',
+              'key' => 'start_date',
+              'dataType' => 'Date',
+              'label' => E::ts('Since'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'RelationshipCache_Contact_far_contact_id_01.address_primary.city',
+              'dataType' => 'String',
+              'label' => E::ts('City'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'RelationshipCache_Contact_far_contact_id_01.address_primary.state_province_id:label',
+              'dataType' => 'Integer',
+              'label' => E::ts('State/Prov'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'RelationshipCache_Contact_far_contact_id_01.email_primary.email',
+              'dataType' => 'String',
+              'label' => E::ts('Email'),
+              'sortable' => TRUE,
+            ],
+            [
+              'type' => 'field',
+              'key' => 'RelationshipCache_Contact_far_contact_id_01.phone_primary.phone',
+              'dataType' => 'String',
+              'label' => E::ts('Phone'),
+              'sortable' => TRUE,
+            ],
+          ],
+          'actions' => FALSE,
+          'classes' => [
+            'table',
+            'table-striped',
+          ],
+        ],
+      ],
+      'match' => [
+        'name',
+        'saved_search_id',
+      ],
+    ],
+  ],
+];
diff --git a/ext/user_dashboard/user_dashboard.civix.php b/ext/user_dashboard/user_dashboard.civix.php
new file mode 100644 (file)
index 0000000..3f00bde
--- /dev/null
@@ -0,0 +1,200 @@
+<?php
+
+// AUTO-GENERATED FILE -- Civix may overwrite any changes made to this file
+
+/**
+ * The ExtensionUtil class provides small stubs for accessing resources of this
+ * extension.
+ */
+class CRM_UserDashboard_ExtensionUtil {
+  const SHORT_NAME = 'user_dashboard';
+  const LONG_NAME = 'user_dashboard';
+  const CLASS_PREFIX = 'CRM_UserDashboard';
+
+  /**
+   * Translate a string using the extension's domain.
+   *
+   * If the extension doesn't have a specific translation
+   * for the string, fallback to the default translations.
+   *
+   * @param string $text
+   *   Canonical message text (generally en_US).
+   * @param array $params
+   * @return string
+   *   Translated text.
+   * @see ts
+   */
+  public static function ts($text, $params = []): string {
+    if (!array_key_exists('domain', $params)) {
+      $params['domain'] = [self::LONG_NAME, NULL];
+    }
+    return ts($text, $params);
+  }
+
+  /**
+   * Get the URL of a resource file (in this extension).
+   *
+   * @param string|NULL $file
+   *   Ex: NULL.
+   *   Ex: 'css/foo.css'.
+   * @return string
+   *   Ex: 'http://example.org/sites/default/ext/org.example.foo'.
+   *   Ex: 'http://example.org/sites/default/ext/org.example.foo/css/foo.css'.
+   */
+  public static function url($file = NULL): string {
+    if ($file === NULL) {
+      return rtrim(CRM_Core_Resources::singleton()->getUrl(self::LONG_NAME), '/');
+    }
+    return CRM_Core_Resources::singleton()->getUrl(self::LONG_NAME, $file);
+  }
+
+  /**
+   * Get the path of a resource file (in this extension).
+   *
+   * @param string|NULL $file
+   *   Ex: NULL.
+   *   Ex: 'css/foo.css'.
+   * @return string
+   *   Ex: '/var/www/example.org/sites/default/ext/org.example.foo'.
+   *   Ex: '/var/www/example.org/sites/default/ext/org.example.foo/css/foo.css'.
+   */
+  public static function path($file = NULL) {
+    // return CRM_Core_Resources::singleton()->getPath(self::LONG_NAME, $file);
+    return __DIR__ . ($file === NULL ? '' : (DIRECTORY_SEPARATOR . $file));
+  }
+
+  /**
+   * Get the name of a class within this extension.
+   *
+   * @param string $suffix
+   *   Ex: 'Page_HelloWorld' or 'Page\\HelloWorld'.
+   * @return string
+   *   Ex: 'CRM_Foo_Page_HelloWorld'.
+   */
+  public static function findClass($suffix) {
+    return self::CLASS_PREFIX . '_' . str_replace('\\', '_', $suffix);
+  }
+
+}
+
+use CRM_UserDashboard_ExtensionUtil as E;
+
+/**
+ * (Delegated) Implements hook_civicrm_config().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config
+ */
+function _user_dashboard_civix_civicrm_config($config = NULL) {
+  static $configured = FALSE;
+  if ($configured) {
+    return;
+  }
+  $configured = TRUE;
+
+  $extRoot = __DIR__ . DIRECTORY_SEPARATOR;
+  $include_path = $extRoot . PATH_SEPARATOR . get_include_path();
+  set_include_path($include_path);
+  // Based on <compatibility>, this does not currently require mixin/polyfill.php.
+}
+
+/**
+ * Implements hook_civicrm_install().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install
+ */
+function _user_dashboard_civix_civicrm_install() {
+  _user_dashboard_civix_civicrm_config();
+  // Based on <compatibility>, this does not currently require mixin/polyfill.php.
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_enable().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable
+ */
+function _user_dashboard_civix_civicrm_enable(): void {
+  _user_dashboard_civix_civicrm_config();
+  // Based on <compatibility>, this does not currently require mixin/polyfill.php.
+}
+
+/**
+ * Inserts a navigation menu item at a given place in the hierarchy.
+ *
+ * @param array $menu - menu hierarchy
+ * @param string $path - path to parent of this item, e.g. 'my_extension/submenu'
+ *    'Mailing', or 'Administer/System Settings'
+ * @param array $item - the item to insert (parent/child attributes will be
+ *    filled for you)
+ *
+ * @return bool
+ */
+function _user_dashboard_civix_insert_navigation_menu(&$menu, $path, $item) {
+  // If we are done going down the path, insert menu
+  if (empty($path)) {
+    $menu[] = [
+      'attributes' => array_merge([
+        'label' => $item['name'] ?? NULL,
+        'active' => 1,
+      ], $item),
+    ];
+    return TRUE;
+  }
+  else {
+    // Find an recurse into the next level down
+    $found = FALSE;
+    $path = explode('/', $path);
+    $first = array_shift($path);
+    foreach ($menu as $key => &$entry) {
+      if ($entry['attributes']['name'] == $first) {
+        if (!isset($entry['child'])) {
+          $entry['child'] = [];
+        }
+        $found = _user_dashboard_civix_insert_navigation_menu($entry['child'], implode('/', $path), $item);
+      }
+    }
+    return $found;
+  }
+}
+
+/**
+ * (Delegated) Implements hook_civicrm_navigationMenu().
+ */
+function _user_dashboard_civix_navigationMenu(&$nodes) {
+  if (!is_callable(['CRM_Core_BAO_Navigation', 'fixNavigationMenu'])) {
+    _user_dashboard_civix_fixNavigationMenu($nodes);
+  }
+}
+
+/**
+ * Given a navigation menu, generate navIDs for any items which are
+ * missing them.
+ */
+function _user_dashboard_civix_fixNavigationMenu(&$nodes) {
+  $maxNavID = 1;
+  array_walk_recursive($nodes, function($item, $key) use (&$maxNavID) {
+    if ($key === 'navID') {
+      $maxNavID = max($maxNavID, $item);
+    }
+  });
+  _user_dashboard_civix_fixNavigationMenuItems($nodes, $maxNavID, NULL);
+}
+
+function _user_dashboard_civix_fixNavigationMenuItems(&$nodes, &$maxNavID, $parentID) {
+  $origKeys = array_keys($nodes);
+  foreach ($origKeys as $origKey) {
+    if (!isset($nodes[$origKey]['attributes']['parentID']) && $parentID !== NULL) {
+      $nodes[$origKey]['attributes']['parentID'] = $parentID;
+    }
+    // If no navID, then assign navID and fix key.
+    if (!isset($nodes[$origKey]['attributes']['navID'])) {
+      $newKey = ++$maxNavID;
+      $nodes[$origKey]['attributes']['navID'] = $newKey;
+      $nodes[$newKey] = $nodes[$origKey];
+      unset($nodes[$origKey]);
+      $origKey = $newKey;
+    }
+    if (isset($nodes[$origKey]['child']) && is_array($nodes[$origKey]['child'])) {
+      _user_dashboard_civix_fixNavigationMenuItems($nodes[$origKey]['child'], $maxNavID, $nodes[$origKey]['attributes']['navID']);
+    }
+  }
+}
diff --git a/ext/user_dashboard/user_dashboard.php b/ext/user_dashboard/user_dashboard.php
new file mode 100644 (file)
index 0000000..5cd4495
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+
+require_once 'user_dashboard.civix.php';
+use CRM_UserDashboard_ExtensionUtil as E;
+
+/**
+ * Implements hook_civicrm_config().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config/
+ */
+function user_dashboard_civicrm_config(&$config): void {
+  _user_dashboard_civix_civicrm_config($config);
+}
+
+/**
+ * Implements hook_civicrm_install().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install
+ */
+function user_dashboard_civicrm_install(): void {
+  _user_dashboard_civix_civicrm_install();
+}
+
+/**
+ * Implements hook_civicrm_enable().
+ *
+ * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable
+ */
+function user_dashboard_civicrm_enable(): void {
+  _user_dashboard_civix_civicrm_enable();
+}
+
+/**
+ * Tag SavedSearches with the "UserDashboard" tag.
+ *
+ * The reason for using this hook is that it's write-once (just during insert),
+ * and after that the user can freely tag and untag searches.
+ *
+ * If the tag was part of the .mgd.php file for each search then it would "stick" and
+ * the user would not be able to remove tags without their changes reverting on every managed reconcile.
+ * Also, adding it to the .mgd.php file requires hacking the exported api call with a 'chain', etc.
+ *
+ * @implements CRM_Utils_Hook::post()
+ */
+function user_dashboard_civicrm_post($action, $entity, $id, $savedSearch) {
+  if ($entity !== 'SavedSearch' || $action !== 'create' || !str_starts_with($savedSearch->name, 'UserDashboard_')) {
+    return;
+  }
+
+  // Transition note: the legacy dashboard used a setting ('user_dashboard_options')
+  // to control which panes are enabled.
+  // This new extension uses a tag.
+  // For the next year or so, we'll conditionally tag the searches in this extension based on that setting.
+  // When the transition is complete, the setting can be deleted and this class can be simplified to unconditionally
+  // tag all SavedSearches in this extension.
+  $legacySetting = Civi\Api4\Setting::get(FALSE)
+    ->addSelect('user_dashboard_options:name')
+    ->execute()
+    ->first();
+
+  // If the legacy setting corresponding to this pane is enabled, tag it
+  $settingNames = [
+    'UserDashboard_Activities' => 'Assigned Activities',
+    'UserDashboard_Groups' => 'Groups',
+    'UserDashboard_Pledges' => 'CiviPledge',
+    'UserDashboard_Contributions' => 'CiviContribute',
+    'UserDashboard_Events' => 'CiviEvent',
+    'UserDashboard_Memberships' => 'CiviMember',
+    'UserDashboard_PCPs' => 'PCP',
+    'UserDashboard_Relationships' => 'Permissioned Orgs',
+  ];
+
+  $settingName = $settingNames[$savedSearch->name] ?? NULL;
+
+  if (!$settingName || in_array($settingName, $legacySetting['value'], TRUE)) {
+    Civi\Api4\EntityTag::save(FALSE)
+      ->addRecord(['entity_table' => 'civicrm_saved_search', 'entity_id' => $id, 'tag_id:name' => 'UserDashboard'])
+      ->execute();
+  }
+}