@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Track how many columns use a particular trigger

Summary:
Ref T5474. In 99% of cases, a separate "archived/active" status for triggers probably doesn't make much sense: there's not much reason to ever disable/archive a trigger explcitly, and the archival rule is really just "is this trigger used by anything?".

(The one reason I can think of to disable a trigger manually is because you want to put something in a column and skip trigger rules, but you can already do this from the task detail page anyway, and disabling the trigger globally is a bad way to accomplish this if it's in use by other columns.)

Instead of adding a separate "status", just track how many columns a trigger is used by and consider it "inactive" if it is not used by any active columns.

Test Plan: This is slightly hard to test exhaustively since you can't share a trigger across multiple columns right now, but: rebuild indexes, poked around the trigger list and trigger details, added/removed triggers.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T5474

Differential Revision: https://secure.phabricator.com/D20308

+360 -11
+8
resources/sql/autopatches/20190322.triggers.01.usage.sql
··· 1 + CREATE TABLE {$NAMESPACE}_project.project_triggerusage ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + triggerPHID VARBINARY(64) NOT NULL, 4 + examplePHID VARBINARY(64), 5 + columnCount INT UNSIGNED NOT NULL, 6 + activeColumnCount INT UNSIGNED NOT NULL, 7 + UNIQUE KEY `key_trigger` (triggerPHID) 8 + ) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
+5
src/__phutil_library_map__.php
··· 4193 4193 'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php', 4194 4194 'PhabricatorProjectTriggerTransactionType' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php', 4195 4195 'PhabricatorProjectTriggerUnknownRule' => 'applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php', 4196 + 'PhabricatorProjectTriggerUsage' => 'applications/project/storage/PhabricatorProjectTriggerUsage.php', 4197 + 'PhabricatorProjectTriggerUsageIndexEngineExtension' => 'applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php', 4196 4198 'PhabricatorProjectTriggerViewController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php', 4197 4199 'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php', 4198 4200 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', ··· 10307 10309 'PhabricatorProjectDAO', 10308 10310 'PhabricatorApplicationTransactionInterface', 10309 10311 'PhabricatorPolicyInterface', 10312 + 'PhabricatorIndexableInterface', 10310 10313 'PhabricatorDestructibleInterface', 10311 10314 ), 10312 10315 'PhabricatorProjectTriggerController' => 'PhabricatorProjectController', ··· 10328 10331 'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 10329 10332 'PhabricatorProjectTriggerTransactionType' => 'PhabricatorModularTransactionType', 10330 10333 'PhabricatorProjectTriggerUnknownRule' => 'PhabricatorProjectTriggerRule', 10334 + 'PhabricatorProjectTriggerUsage' => 'PhabricatorProjectDAO', 10335 + 'PhabricatorProjectTriggerUsageIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 10331 10336 'PhabricatorProjectTriggerViewController' => 'PhabricatorProjectTriggerController', 10332 10337 'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType', 10333 10338 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener',
+23 -4
src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php
··· 71 71 // so we load only the columns they can actually see, but have a list of 72 72 // all the impacted column PHIDs. 73 73 74 + // (We're also exposing the status of columns the user might not be able 75 + // to see. This technically violates policy, but the trigger usage table 76 + // hints at it anyway and it seems unlikely to ever have any security 77 + // impact, but is useful in assessing whether a trigger is really in use 78 + // or not.) 79 + 74 80 $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); 75 81 $all_columns = id(new PhabricatorProjectColumnQuery()) 76 82 ->setViewer($omnipotent_viewer) 77 83 ->withTriggerPHIDs(array($trigger->getPHID())) 78 84 ->execute(); 79 - $column_phids = mpull($all_columns, 'getPHID'); 85 + $column_map = mpull($all_columns, 'getStatus', 'getPHID'); 80 86 81 - if ($column_phids) { 87 + if ($column_map) { 82 88 $visible_columns = id(new PhabricatorProjectColumnQuery()) 83 89 ->setViewer($viewer) 84 - ->withPHIDs($column_phids) 90 + ->withPHIDs(array_keys($column_map)) 85 91 ->execute(); 86 92 $visible_columns = mpull($visible_columns, null, 'getPHID'); 87 93 } else { ··· 89 95 } 90 96 91 97 $rows = array(); 92 - foreach ($column_phids as $column_phid) { 98 + foreach ($column_map as $column_phid => $column_status) { 93 99 $column = idx($visible_columns, $column_phid); 94 100 95 101 if ($column) { ··· 113 119 $column_name = phutil_tag('em', array(), pht('Restricted Column')); 114 120 } 115 121 122 + if ($column_status == PhabricatorProjectColumn::STATUS_ACTIVE) { 123 + $status_icon = id(new PHUIIconView()) 124 + ->setIcon('fa-columns', 'blue') 125 + ->setTooltip(pht('Active Column')); 126 + } else { 127 + $status_icon = id(new PHUIIconView()) 128 + ->setIcon('fa-eye-slash', 'grey') 129 + ->setTooltip(pht('Hidden Column')); 130 + } 131 + 116 132 $rows[] = array( 133 + $status_icon, 117 134 $project_name, 118 135 $column_name, 119 136 ); ··· 123 140 ->setNoDataString(pht('This trigger is not used by any columns.')) 124 141 ->setHeaders( 125 142 array( 143 + null, 126 144 pht('Project'), 127 145 pht('Column'), 128 146 )) 129 147 ->setColumnClasses( 130 148 array( 149 + null, 131 150 null, 132 151 'wide pri', 133 152 ));
+4
src/applications/project/editor/PhabricatorProjectTriggerEditor.php
··· 27 27 return $types; 28 28 } 29 29 30 + protected function supportsSearch() { 31 + return true; 32 + } 33 + 30 34 }
+69
src/applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerUsageIndexEngineExtension 4 + extends PhabricatorIndexEngineExtension { 5 + 6 + const EXTENSIONKEY = 'trigger.usage'; 7 + 8 + public function getExtensionName() { 9 + return pht('Trigger Usage'); 10 + } 11 + 12 + public function shouldIndexObject($object) { 13 + if (!($object instanceof PhabricatorProjectTrigger)) { 14 + return false; 15 + } 16 + 17 + return true; 18 + } 19 + 20 + public function indexObject( 21 + PhabricatorIndexEngine $engine, 22 + $object) { 23 + 24 + $usage_table = new PhabricatorProjectTriggerUsage(); 25 + $column_table = new PhabricatorProjectColumn(); 26 + 27 + $conn_w = $object->establishConnection('w'); 28 + 29 + $active_statuses = array( 30 + PhabricatorProjectColumn::STATUS_ACTIVE, 31 + ); 32 + 33 + // Select summary information to populate the usage index. When picking 34 + // an "examplePHID", we try to pick an active column. 35 + $row = queryfx_one( 36 + $conn_w, 37 + 'SELECT phid, COUNT(*) N, SUM(IF(status IN (%Ls), 1, 0)) M FROM %R 38 + WHERE triggerPHID = %s 39 + ORDER BY IF(status IN (%Ls), 1, 0) DESC, id ASC', 40 + $active_statuses, 41 + $column_table, 42 + $object->getPHID(), 43 + $active_statuses); 44 + if ($row) { 45 + $example_phid = $row['phid']; 46 + $column_count = $row['N']; 47 + $active_count = $row['M']; 48 + } else { 49 + $example_phid = null; 50 + $column_count = 0; 51 + $active_count = 0; 52 + } 53 + 54 + queryfx( 55 + $conn_w, 56 + 'INSERT INTO %R (triggerPHID, examplePHID, columnCount, activeColumnCount) 57 + VALUES (%s, %ns, %d, %d) 58 + ON DUPLICATE KEY UPDATE 59 + examplePHID = VALUES(examplePHID), 60 + columnCount = VALUES(columnCount), 61 + activeColumnCount = VALUES(activeColumnCount)', 62 + $usage_table, 63 + $object->getPHID(), 64 + $example_phid, 65 + $column_count, 66 + $active_count); 67 + } 68 + 69 + }
+86 -2
src/applications/project/query/PhabricatorProjectTriggerQuery.php
··· 5 5 6 6 private $ids; 7 7 private $phids; 8 + private $activeColumnMin; 9 + private $activeColumnMax; 10 + 11 + private $needUsage; 8 12 9 13 public function withIDs(array $ids) { 10 14 $this->ids = $ids; ··· 16 20 return $this; 17 21 } 18 22 23 + public function needUsage($need_usage) { 24 + $this->needUsage = $need_usage; 25 + return $this; 26 + } 27 + 28 + public function withActiveColumnCountBetween($min, $max) { 29 + $this->activeColumnMin = $min; 30 + $this->activeColumnMax = $max; 31 + return $this; 32 + } 33 + 19 34 public function newResultObject() { 20 35 return new PhabricatorProjectTrigger(); 21 36 } ··· 30 45 if ($this->ids !== null) { 31 46 $where[] = qsprintf( 32 47 $conn, 33 - 'id IN (%Ld)', 48 + 'trigger.id IN (%Ld)', 34 49 $this->ids); 35 50 } 36 51 37 52 if ($this->phids !== null) { 38 53 $where[] = qsprintf( 39 54 $conn, 40 - 'phid IN (%Ls)', 55 + 'trigger.phid IN (%Ls)', 41 56 $this->phids); 42 57 } 43 58 59 + if ($this->activeColumnMin !== null) { 60 + $where[] = qsprintf( 61 + $conn, 62 + 'trigger_usage.activeColumnCount >= %d', 63 + $this->activeColumnMin); 64 + } 65 + 66 + if ($this->activeColumnMax !== null) { 67 + $where[] = qsprintf( 68 + $conn, 69 + 'trigger_usage.activeColumnCount <= %d', 70 + $this->activeColumnMax); 71 + } 72 + 44 73 return $where; 45 74 } 46 75 76 + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { 77 + $joins = parent::buildJoinClauseParts($conn); 78 + 79 + if ($this->shouldJoinUsageTable()) { 80 + $joins[] = qsprintf( 81 + $conn, 82 + 'JOIN %R trigger_usage ON trigger.phid = trigger_usage.triggerPHID', 83 + new PhabricatorProjectTriggerUsage()); 84 + } 85 + 86 + return $joins; 87 + } 88 + 89 + private function shouldJoinUsageTable() { 90 + if ($this->activeColumnMin !== null) { 91 + return true; 92 + } 93 + 94 + if ($this->activeColumnMax !== null) { 95 + return true; 96 + } 97 + 98 + return false; 99 + } 100 + 101 + protected function didFilterPage(array $triggers) { 102 + if ($this->needUsage) { 103 + $usage_map = id(new PhabricatorProjectTriggerUsage())->loadAllWhere( 104 + 'triggerPHID IN (%Ls)', 105 + mpull($triggers, 'getPHID')); 106 + $usage_map = mpull($usage_map, null, 'getTriggerPHID'); 107 + 108 + foreach ($triggers as $trigger) { 109 + $trigger_phid = $trigger->getPHID(); 110 + 111 + $usage = idx($usage_map, $trigger_phid); 112 + if (!$usage) { 113 + $usage = id(new PhabricatorProjectTriggerUsage()) 114 + ->setTriggerPHID($trigger_phid) 115 + ->setExamplePHID(null) 116 + ->setColumnCount(0) 117 + ->setActiveColumnCount(0); 118 + } 119 + 120 + $trigger->attachUsage($usage); 121 + } 122 + } 123 + 124 + return $triggers; 125 + } 126 + 47 127 public function getQueryApplicationClass() { 48 128 return 'PhabricatorProjectApplication'; 129 + } 130 + 131 + protected function getPrimaryTableAlias() { 132 + return 'trigger'; 49 133 } 50 134 51 135 }
+84 -4
src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php
··· 12 12 } 13 13 14 14 public function newQuery() { 15 - return new PhabricatorProjectTriggerQuery(); 15 + return id(new PhabricatorProjectTriggerQuery()) 16 + ->needUsage(true); 16 17 } 17 18 18 19 protected function buildCustomSearchFields() { 19 - return array(); 20 + return array( 21 + id(new PhabricatorSearchThreeStateField()) 22 + ->setLabel(pht('Active')) 23 + ->setKey('isActive') 24 + ->setOptions( 25 + pht('(Show All)'), 26 + pht('Show Only Active Triggers'), 27 + pht('Show Only Inactive Triggers')), 28 + ); 20 29 } 21 30 22 31 protected function buildQueryFromParameters(array $map) { 23 32 $query = $this->newQuery(); 24 33 34 + if ($map['isActive'] !== null) { 35 + if ($map['isActive']) { 36 + $query->withActiveColumnCountBetween(1, null); 37 + } else { 38 + $query->withActiveColumnCountBetween(null, 0); 39 + } 40 + } 41 + 25 42 return $query; 26 43 } 27 44 ··· 32 49 protected function getBuiltinQueryNames() { 33 50 $names = array(); 34 51 35 - $names['all'] = pht('All'); 52 + $names['active'] = pht('Active Triggers'); 53 + $names['all'] = pht('All Triggers'); 36 54 37 55 return $names; 38 56 } ··· 42 60 $query->setQueryKey($query_key); 43 61 44 62 switch ($query_key) { 63 + case 'active': 64 + return $query->setParameter('isActive', true); 45 65 case 'all': 46 66 return $query; 47 67 } ··· 56 76 assert_instances_of($triggers, 'PhabricatorProjectTrigger'); 57 77 $viewer = $this->requireViewer(); 58 78 79 + $example_phids = array(); 80 + foreach ($triggers as $trigger) { 81 + $example_phid = $trigger->getUsage()->getExamplePHID(); 82 + if ($example_phid) { 83 + $example_phids[] = $example_phid; 84 + } 85 + } 86 + 87 + $handles = $viewer->loadHandles($example_phids); 88 + 59 89 $list = id(new PHUIObjectItemListView()) 60 90 ->setViewer($viewer); 61 91 foreach ($triggers as $trigger) { 92 + $usage = $trigger->getUsage(); 93 + 94 + $column_handle = null; 95 + $have_column = false; 96 + $example_phid = $usage->getExamplePHID(); 97 + if ($example_phid) { 98 + $column_handle = $handles[$example_phid]; 99 + if ($column_handle->isComplete()) { 100 + if (!$column_handle->getPolicyFiltered()) { 101 + $have_column = true; 102 + } 103 + } 104 + } 105 + 106 + $column_count = $usage->getColumnCount(); 107 + $active_count = $usage->getActiveColumnCount(); 108 + 109 + if ($have_column) { 110 + if ($active_count > 1) { 111 + $usage_description = pht( 112 + 'Used on %s and %s other active column(s).', 113 + $column_handle->renderLink(), 114 + new PhutilNumber($active_count - 1)); 115 + } else if ($column_count > 1) { 116 + $usage_description = pht( 117 + 'Used on %s and %s other column(s).', 118 + $column_handle->renderLink(), 119 + new PhutilNumber($column_count - 1)); 120 + } else { 121 + $usage_description = pht( 122 + 'Used on %s.', 123 + $column_handle->renderLink()); 124 + } 125 + } else { 126 + if ($active_count) { 127 + $usage_description = pht( 128 + 'Used on %s active column(s).', 129 + new PhutilNumber($active_count)); 130 + } else if ($column_count) { 131 + $usage_description = pht( 132 + 'Used on %s column(s).', 133 + new PhutilNumber($column_count)); 134 + } else { 135 + $usage_description = pht( 136 + 'Unused trigger.'); 137 + } 138 + } 139 + 62 140 $item = id(new PHUIObjectItemView()) 63 141 ->setObjectName($trigger->getObjectName()) 64 142 ->setHeader($trigger->getDisplayName()) 65 - ->setHref($trigger->getURI()); 143 + ->setHref($trigger->getURI()) 144 + ->addAttribute($usage_description) 145 + ->setDisabled(!$active_count); 66 146 67 147 $list->addItem($item); 68 148 }
+18
src/applications/project/storage/PhabricatorProjectTrigger.php
··· 5 5 implements 6 6 PhabricatorApplicationTransactionInterface, 7 7 PhabricatorPolicyInterface, 8 + PhabricatorIndexableInterface, 8 9 PhabricatorDestructibleInterface { 9 10 10 11 protected $name; ··· 12 13 protected $editPolicy; 13 14 14 15 private $triggerRules; 16 + private $usage = self::ATTACHABLE; 15 17 16 18 public static function initializeNewTrigger() { 17 19 $default_edit = PhabricatorPolicies::POLICY_USER; ··· 257 259 return $sounds; 258 260 } 259 261 262 + public function getUsage() { 263 + return $this->assertAttached($this->usage); 264 + } 265 + 266 + public function attachUsage(PhabricatorProjectTriggerUsage $usage) { 267 + $this->usage = $usage; 268 + return $this; 269 + } 270 + 260 271 261 272 /* -( PhabricatorApplicationTransactionInterface )------------------------- */ 262 273 ··· 308 319 $conn, 309 320 'UPDATE %R SET triggerPHID = null WHERE triggerPHID = %s', 310 321 new PhabricatorProjectColumn(), 322 + $this->getPHID()); 323 + 324 + // Remove the usage index row for this trigger, if one exists. 325 + queryfx( 326 + $conn, 327 + 'DELETE FROM %R WHERE triggerPHID = %s', 328 + new PhabricatorProjectTriggerUsage(), 311 329 $this->getPHID()); 312 330 313 331 $this->delete();
+28
src/applications/project/storage/PhabricatorProjectTriggerUsage.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerUsage 4 + extends PhabricatorProjectDAO { 5 + 6 + protected $triggerPHID; 7 + protected $examplePHID; 8 + protected $columnCount; 9 + protected $activeColumnCount; 10 + 11 + protected function getConfiguration() { 12 + return array( 13 + self::CONFIG_TIMESTAMPS => false, 14 + self::CONFIG_COLUMN_SCHEMA => array( 15 + 'examplePHID' => 'phid?', 16 + 'columnCount' => 'uint32', 17 + 'activeColumnCount' => 'uint32', 18 + ), 19 + self::CONFIG_KEY_SCHEMA => array( 20 + 'key_trigger' => array( 21 + 'columns' => array('triggerPHID'), 22 + 'unique' => true, 23 + ), 24 + ), 25 + ) + parent::getConfiguration(); 26 + } 27 + 28 + }
+9
src/applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php
··· 13 13 $object->setStatus($value); 14 14 } 15 15 16 + public function applyExternalEffects($object, $value) { 17 + // Update the trigger usage index, which cares about whether columns are 18 + // active or not. 19 + $trigger_phid = $object->getTriggerPHID(); 20 + if ($trigger_phid) { 21 + PhabricatorSearchWorker::queueDocumentForIndexing($trigger_phid); 22 + } 23 + } 24 + 16 25 public function getTitle() { 17 26 $new = $this->getNewValue(); 18 27
+19
src/applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php
··· 13 13 $object->setTriggerPHID($value); 14 14 } 15 15 16 + public function applyExternalEffects($object, $value) { 17 + // After we change the trigger attached to a column, update the search 18 + // indexes for the old and new triggers so we update the usage index. 19 + $old = $this->getOldValue(); 20 + $new = $this->getNewValue(); 21 + 22 + $column_phids = array(); 23 + if ($old) { 24 + $column_phids[] = $old; 25 + } 26 + if ($new) { 27 + $column_phids[] = $new; 28 + } 29 + 30 + foreach ($column_phids as $phid) { 31 + PhabricatorSearchWorker::queueDocumentForIndexing($phid); 32 + } 33 + } 34 + 16 35 public function getTitle() { 17 36 $old = $this->getOldValue(); 18 37 $new = $this->getNewValue();
+7 -1
src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php
··· 136 136 137 137 if ($track_skips) { 138 138 $new_versions = $this->loadIndexVersions($phid); 139 - if ($old_versions !== $new_versions) { 139 + 140 + if (!$old_versions && !$new_versions) { 141 + // If the document doesn't use an index version, both the lists 142 + // of versions will be empty. We still rebuild the index in this 143 + // case. 144 + $count_updated++; 145 + } else if ($old_versions !== $new_versions) { 140 146 $count_updated++; 141 147 } else { 142 148 $count_skipped++;