@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.

Provide basic scaffolding for workboard column triggers

Summary:
Depends on D20278. Ref T5474. This change creates some new empty objects that do nothing, and some new views for looking at those objects. There's no actual useful behavior yet.

The "Edit" controller is custom instead of being driven by "EditEngine" because I expect it to be a Herald-style "add new rules" UI, and EditEngine isn't a clean match for those today (although maybe I'll try to move it over).

The general idea here is:

- Triggers are "real" objects with a real PHID.
- Each trigger has a name and a collection of rules, like "Change status to: X" or "Play sound: Y".
- Each column may be bound to a trigger.
- Multiple columns may share the same trigger.
- Later UI refinements will make the cases around "copy trigger" vs "reference the same trigger" vs "create a new ad-hoc trigger" more clear.
- Triggers have their own edit policy.
- Triggers are always world-visible, like Herald rules.

Test Plan: Poked around, created some empty trigger objects, and nothing exploded. This doesn't actually do anything useful yet since triggers can't have any rule behavior and columns can't actually be bound to triggers.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T5474

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

+999 -6
+9
resources/sql/autopatches/20190312.triggers.01.trigger.sql
··· 1 + CREATE TABLE {$NAMESPACE}_project.project_trigger ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARBINARY(64) NOT NULL, 4 + name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, 5 + editPolicy VARBINARY(64) NOT NULL, 6 + ruleset LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, 7 + dateCreated INT UNSIGNED NOT NULL, 8 + dateModified INT UNSIGNED NOT NULL 9 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+19
resources/sql/autopatches/20190312.triggers.02.xaction.sql
··· 1 + CREATE TABLE {$NAMESPACE}_project.project_triggertransaction ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARBINARY(64) NOT NULL, 4 + authorPHID VARBINARY(64) NOT NULL, 5 + objectPHID VARBINARY(64) NOT NULL, 6 + viewPolicy VARBINARY(64) NOT NULL, 7 + editPolicy VARBINARY(64) NOT NULL, 8 + commentPHID VARBINARY(64) DEFAULT NULL, 9 + commentVersion INT UNSIGNED NOT NULL, 10 + transactionType VARCHAR(32) NOT NULL, 11 + oldValue LONGTEXT NOT NULL, 12 + newValue LONGTEXT NOT NULL, 13 + contentSource LONGTEXT NOT NULL, 14 + metadata LONGTEXT NOT NULL, 15 + dateCreated INT UNSIGNED NOT NULL, 16 + dateModified INT UNSIGNED NOT NULL, 17 + UNIQUE KEY `key_phid` (`phid`), 18 + KEY `key_object` (`objectPHID`) 19 + ) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
+2
resources/sql/autopatches/20190312.triggers.03.triggerphid.sql
··· 1 + ALTER TABLE {$NAMESPACE}_project.project_column 2 + ADD triggerPHID VARBINARY(64);
+31
src/__phutil_library_map__.php
··· 4166 4166 'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php', 4167 4167 'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php', 4168 4168 'PhabricatorProjectTransactionType' => 'applications/project/xaction/PhabricatorProjectTransactionType.php', 4169 + 'PhabricatorProjectTrigger' => 'applications/project/storage/PhabricatorProjectTrigger.php', 4170 + 'PhabricatorProjectTriggerController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerController.php', 4171 + 'PhabricatorProjectTriggerEditController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php', 4172 + 'PhabricatorProjectTriggerEditor' => 'applications/project/editor/PhabricatorProjectTriggerEditor.php', 4173 + 'PhabricatorProjectTriggerListController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerListController.php', 4174 + 'PhabricatorProjectTriggerNameTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php', 4175 + 'PhabricatorProjectTriggerPHIDType' => 'applications/project/phid/PhabricatorProjectTriggerPHIDType.php', 4176 + 'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php', 4177 + 'PhabricatorProjectTriggerSearchEngine' => 'applications/project/query/PhabricatorProjectTriggerSearchEngine.php', 4178 + 'PhabricatorProjectTriggerTransaction' => 'applications/project/storage/PhabricatorProjectTriggerTransaction.php', 4179 + 'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php', 4180 + 'PhabricatorProjectTriggerTransactionType' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php', 4181 + 'PhabricatorProjectTriggerViewController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php', 4169 4182 'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php', 4170 4183 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', 4171 4184 'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php', ··· 10268 10281 'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 10269 10282 'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 10270 10283 'PhabricatorProjectTransactionType' => 'PhabricatorModularTransactionType', 10284 + 'PhabricatorProjectTrigger' => array( 10285 + 'PhabricatorProjectDAO', 10286 + 'PhabricatorApplicationTransactionInterface', 10287 + 'PhabricatorPolicyInterface', 10288 + 'PhabricatorDestructibleInterface', 10289 + ), 10290 + 'PhabricatorProjectTriggerController' => 'PhabricatorProjectController', 10291 + 'PhabricatorProjectTriggerEditController' => 'PhabricatorProjectTriggerController', 10292 + 'PhabricatorProjectTriggerEditor' => 'PhabricatorApplicationTransactionEditor', 10293 + 'PhabricatorProjectTriggerListController' => 'PhabricatorProjectTriggerController', 10294 + 'PhabricatorProjectTriggerNameTransaction' => 'PhabricatorProjectTriggerTransactionType', 10295 + 'PhabricatorProjectTriggerPHIDType' => 'PhabricatorPHIDType', 10296 + 'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 10297 + 'PhabricatorProjectTriggerSearchEngine' => 'PhabricatorApplicationSearchEngine', 10298 + 'PhabricatorProjectTriggerTransaction' => 'PhabricatorModularTransaction', 10299 + 'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 10300 + 'PhabricatorProjectTriggerTransactionType' => 'PhabricatorModularTransactionType', 10301 + 'PhabricatorProjectTriggerViewController' => 'PhabricatorProjectTriggerController', 10271 10302 'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType', 10272 10303 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener', 10273 10304 'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
+8
src/applications/project/application/PhabricatorProjectApplication.php
··· 89 89 'background/' 90 90 => 'PhabricatorProjectBoardBackgroundController', 91 91 ), 92 + 'trigger/' => array( 93 + $this->getQueryRoutePattern() => 94 + 'PhabricatorProjectTriggerListController', 95 + '(?P<id>[1-9]\d*)/' => 96 + 'PhabricatorProjectTriggerViewController', 97 + $this->getEditRoutePattern('edit/') => 98 + 'PhabricatorProjectTriggerEditController', 99 + ), 92 100 'update/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/' 93 101 => 'PhabricatorProjectUpdateController', 94 102 'manage/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectManageController',
+36 -4
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 1111 1111 )); 1112 1112 } 1113 1113 1114 - if (count($specs) > 1) { 1115 - $column_items[] = id(new PhabricatorActionView()) 1116 - ->setType(PhabricatorActionView::TYPE_DIVIDER); 1117 - } 1114 + $column_items[] = id(new PhabricatorActionView()) 1115 + ->setType(PhabricatorActionView::TYPE_DIVIDER); 1118 1116 1119 1117 $batch_edit_uri = $request->getRequestURI(); 1120 1118 $batch_edit_uri->replaceQueryParam('batch', $column->getID()); ··· 1172 1170 ->setHref($hide_uri) 1173 1171 ->setDisabled(!$can_hide) 1174 1172 ->setWorkflow(true); 1173 + } 1174 + 1175 + if ($column->canHaveTrigger()) { 1176 + $column_items[] = id(new PhabricatorActionView()) 1177 + ->setType(PhabricatorActionView::TYPE_DIVIDER); 1178 + 1179 + $trigger = $column->getTrigger(); 1180 + if (!$trigger) { 1181 + $set_uri = $this->getApplicationURI( 1182 + new PhutilURI( 1183 + 'trigger/edit/', 1184 + array( 1185 + 'columnPHID' => $column->getPHID(), 1186 + ))); 1187 + 1188 + $column_items[] = id(new PhabricatorActionView()) 1189 + ->setIcon('fa-cogs') 1190 + ->setName(pht('New Trigger...')) 1191 + ->setHref($set_uri) 1192 + ->setDisabled(!$can_edit); 1193 + } else { 1194 + $column_items[] = id(new PhabricatorActionView()) 1195 + ->setIcon('fa-cogs') 1196 + ->setName(pht('View Trigger')) 1197 + ->setHref($trigger->getURI()) 1198 + ->setDisabled(!$can_edit); 1199 + } 1200 + 1201 + $column_items[] = id(new PhabricatorActionView()) 1202 + ->setIcon('fa-times') 1203 + ->setName(pht('Remove Trigger')) 1204 + ->setHref('#') 1205 + ->setWorkflow(true) 1206 + ->setDisabled(!$can_edit || !$trigger); 1175 1207 } 1176 1208 1177 1209 $column_menu = id(new PhabricatorActionListView())
+16
src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorProjectTriggerController 4 + extends PhabricatorProjectController { 5 + 6 + protected function buildApplicationCrumbs() { 7 + $crumbs = parent::buildApplicationCrumbs(); 8 + 9 + $crumbs->addTextCrumb( 10 + pht('Triggers'), 11 + $this->getApplicationURI('trigger/')); 12 + 13 + return $crumbs; 14 + } 15 + 16 + }
+197
src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerEditController 4 + extends PhabricatorProjectTriggerController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $request = $this->getRequest(); 8 + $viewer = $request->getViewer(); 9 + 10 + $id = $request->getURIData('id'); 11 + if ($id) { 12 + $trigger = id(new PhabricatorProjectTriggerQuery()) 13 + ->setViewer($viewer) 14 + ->withIDs(array($id)) 15 + ->requireCapabilities( 16 + array( 17 + PhabricatorPolicyCapability::CAN_VIEW, 18 + PhabricatorPolicyCapability::CAN_EDIT, 19 + )) 20 + ->executeOne(); 21 + if (!$trigger) { 22 + return new Aphront404Response(); 23 + } 24 + } else { 25 + $trigger = PhabricatorProjectTrigger::initializeNewTrigger(); 26 + } 27 + 28 + $column_phid = $request->getStr('columnPHID'); 29 + if ($column_phid) { 30 + $column = id(new PhabricatorProjectColumnQuery()) 31 + ->setViewer($viewer) 32 + ->withPHIDs(array($column_phid)) 33 + ->requireCapabilities( 34 + array( 35 + PhabricatorPolicyCapability::CAN_VIEW, 36 + PhabricatorPolicyCapability::CAN_EDIT, 37 + )) 38 + ->executeOne(); 39 + if (!$column) { 40 + return new Aphront404Response(); 41 + } 42 + $board_uri = $column->getBoardURI(); 43 + } else { 44 + $column = null; 45 + $board_uri = null; 46 + } 47 + 48 + if ($board_uri) { 49 + $cancel_uri = $board_uri; 50 + } else if ($trigger->getID()) { 51 + $cancel_uri = $trigger->getURI(); 52 + } else { 53 + $cancel_uri = $this->getApplicationURI('trigger/'); 54 + } 55 + 56 + $v_name = $trigger->getName(); 57 + $v_edit = $trigger->getEditPolicy(); 58 + 59 + $e_name = null; 60 + $e_edit = null; 61 + 62 + $validation_exception = null; 63 + if ($request->isFormPost()) { 64 + try { 65 + $v_name = $request->getStr('name'); 66 + $v_edit = $request->getStr('editPolicy'); 67 + 68 + $xactions = array(); 69 + if (!$trigger->getID()) { 70 + $xactions[] = $trigger->getApplicationTransactionTemplate() 71 + ->setTransactionType(PhabricatorTransactions::TYPE_CREATE) 72 + ->setNewValue(true); 73 + } 74 + 75 + $xactions[] = $trigger->getApplicationTransactionTemplate() 76 + ->setTransactionType( 77 + PhabricatorProjectTriggerNameTransaction::TRANSACTIONTYPE) 78 + ->setNewValue($v_name); 79 + 80 + $xactions[] = $trigger->getApplicationTransactionTemplate() 81 + ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) 82 + ->setNewValue($v_edit); 83 + 84 + $editor = $trigger->getApplicationTransactionEditor() 85 + ->setActor($viewer) 86 + ->setContentSourceFromRequest($request) 87 + ->setContinueOnNoEffect(true); 88 + 89 + $editor->applyTransactions($trigger, $xactions); 90 + 91 + $next_uri = $trigger->getURI(); 92 + 93 + if ($column) { 94 + $column_xactions = array(); 95 + 96 + // TODO: Modularize column transactions so we can change the column 97 + // trigger here. For now, this does nothing. 98 + 99 + $column_editor = $column->getApplicationTransactionEditor() 100 + ->setActor($viewer) 101 + ->setContentSourceFromRequest($request) 102 + ->setContinueOnNoEffect(true); 103 + 104 + $column_editor->applyTransactions($column, $column_xactions); 105 + 106 + $next_uri = $column->getBoardURI(); 107 + } 108 + 109 + return id(new AphrontRedirectResponse())->setURI($next_uri); 110 + } catch (PhabricatorApplicationTransactionValidationException $ex) { 111 + $validation_exception = $ex; 112 + 113 + $e_name = $ex->getShortMessage( 114 + PhabricatorProjectTriggerNameTransaction::TRANSACTIONTYPE); 115 + 116 + $e_edit = $ex->getShortMessage( 117 + PhabricatorTransactions::TYPE_EDIT_POLICY); 118 + 119 + $trigger->setEditPolicy($v_edit); 120 + } 121 + } 122 + 123 + if ($trigger->getID()) { 124 + $title = $trigger->getObjectName(); 125 + $submit = pht('Save Trigger'); 126 + $header = pht('Edit Trigger: %s', $trigger->getObjectName()); 127 + } else { 128 + $title = pht('New Trigger'); 129 + $submit = pht('Create Trigger'); 130 + $header = pht('New Trigger'); 131 + } 132 + 133 + $form = id(new AphrontFormView()) 134 + ->setViewer($viewer); 135 + 136 + if ($column) { 137 + $form->addHiddenInput('columnPHID', $column->getPHID()); 138 + } 139 + 140 + $form->appendControl( 141 + id(new AphrontFormTextControl()) 142 + ->setLabel(pht('Name')) 143 + ->setName('name') 144 + ->setValue($v_name) 145 + ->setError($e_name) 146 + ->setPlaceholder($trigger->getDefaultName())); 147 + 148 + $policies = id(new PhabricatorPolicyQuery()) 149 + ->setViewer($viewer) 150 + ->setObject($trigger) 151 + ->execute(); 152 + 153 + $form->appendControl( 154 + id(new AphrontFormPolicyControl()) 155 + ->setName('editPolicy') 156 + ->setPolicyObject($trigger) 157 + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) 158 + ->setPolicies($policies) 159 + ->setError($e_edit)); 160 + 161 + $form->appendControl( 162 + id(new AphrontFormSubmitControl()) 163 + ->setValue($submit) 164 + ->addCancelButton($cancel_uri)); 165 + 166 + $header = id(new PHUIHeaderView()) 167 + ->setHeader($header); 168 + 169 + $box_view = id(new PHUIObjectBoxView()) 170 + ->setHeader($header) 171 + ->setValidationException($validation_exception) 172 + ->appendChild($form); 173 + 174 + $column_view = id(new PHUITwoColumnView()) 175 + ->setFooter($box_view); 176 + 177 + $crumbs = $this->buildApplicationCrumbs() 178 + ->setBorder(true); 179 + 180 + if ($column) { 181 + $crumbs->addTextCrumb( 182 + pht( 183 + '%s: %s', 184 + $column->getProject()->getDisplayName(), 185 + $column->getName()), 186 + $board_uri); 187 + } 188 + 189 + $crumbs->addTextCrumb($title); 190 + 191 + return $this->newPage() 192 + ->setTitle($title) 193 + ->setCrumbs($crumbs) 194 + ->appendChild($column_view); 195 + } 196 + 197 + }
+16
src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerListController 4 + extends PhabricatorProjectTriggerController { 5 + 6 + public function shouldAllowPublic() { 7 + return true; 8 + } 9 + 10 + public function handleRequest(AphrontRequest $request) { 11 + return id(new PhabricatorProjectTriggerSearchEngine()) 12 + ->setController($this) 13 + ->buildResponse(); 14 + } 15 + 16 + }
+168
src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerViewController 4 + extends PhabricatorProjectTriggerController { 5 + 6 + public function shouldAllowPublic() { 7 + return true; 8 + } 9 + 10 + public function handleRequest(AphrontRequest $request) { 11 + $request = $this->getRequest(); 12 + $viewer = $request->getViewer(); 13 + 14 + $id = $request->getURIData('id'); 15 + 16 + $trigger = id(new PhabricatorProjectTriggerQuery()) 17 + ->setViewer($viewer) 18 + ->withIDs(array($id)) 19 + ->executeOne(); 20 + if (!$trigger) { 21 + return new Aphront404Response(); 22 + } 23 + 24 + $columns_view = $this->newColumnsView($trigger); 25 + 26 + $title = $trigger->getObjectName(); 27 + 28 + $header = id(new PHUIHeaderView()) 29 + ->setHeader($trigger->getDisplayName()); 30 + 31 + $timeline = $this->buildTransactionTimeline( 32 + $trigger, 33 + new PhabricatorProjectTriggerTransactionQuery()); 34 + $timeline->setShouldTerminate(true); 35 + 36 + $curtain = $this->newCurtain($trigger); 37 + 38 + $column_view = id(new PHUITwoColumnView()) 39 + ->setHeader($header) 40 + ->setCurtain($curtain) 41 + ->setMainColumn( 42 + array( 43 + $columns_view, 44 + $timeline, 45 + )); 46 + 47 + $crumbs = $this->buildApplicationCrumbs() 48 + ->addTextCrumb($trigger->getObjectName()) 49 + ->setBorder(true); 50 + 51 + return $this->newPage() 52 + ->setTitle($title) 53 + ->setCrumbs($crumbs) 54 + ->appendChild($column_view); 55 + } 56 + 57 + private function newColumnsView(PhabricatorProjectTrigger $trigger) { 58 + $viewer = $this->getViewer(); 59 + 60 + // NOTE: When showing columns which use this trigger, we want to represent 61 + // all columns the trigger is used by: even columns the user can't see. 62 + 63 + // If we hide columns the viewer can't see, they might think that the 64 + // trigger isn't widely used and is safe to edit, when it may actually 65 + // be in use on workboards they don't have access to. 66 + 67 + // Query the columns with the omnipotent viewer first, then pull out their 68 + // PHIDs and throw the actual objects away. Re-query with the real viewer 69 + // so we load only the columns they can actually see, but have a list of 70 + // all the impacted column PHIDs. 71 + 72 + $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); 73 + $all_columns = id(new PhabricatorProjectColumnQuery()) 74 + ->setViewer($omnipotent_viewer) 75 + ->withTriggerPHIDs(array($trigger->getPHID())) 76 + ->execute(); 77 + $column_phids = mpull($all_columns, 'getPHID'); 78 + 79 + if ($column_phids) { 80 + $visible_columns = id(new PhabricatorProjectColumnQuery()) 81 + ->setViewer($viewer) 82 + ->withPHIDs($column_phids) 83 + ->execute(); 84 + $visible_columns = mpull($visible_columns, null, 'getPHID'); 85 + } else { 86 + $visible_columns = array(); 87 + } 88 + 89 + $rows = array(); 90 + foreach ($column_phids as $column_phid) { 91 + $column = idx($visible_columns, $column_phid); 92 + 93 + if ($column) { 94 + $project = $column->getProject(); 95 + 96 + $project_name = phutil_tag( 97 + 'a', 98 + array( 99 + 'href' => $project->getURI(), 100 + ), 101 + $project->getDisplayName()); 102 + 103 + $column_name = phutil_tag( 104 + 'a', 105 + array( 106 + 'href' => $column->getBoardURI(), 107 + ), 108 + $column->getDisplayName()); 109 + } else { 110 + $project_name = null; 111 + $column_name = phutil_tag('em', array(), pht('Restricted Column')); 112 + } 113 + 114 + $rows[] = array( 115 + $project_name, 116 + $column_name, 117 + ); 118 + } 119 + 120 + $table_view = id(new AphrontTableView($rows)) 121 + ->setNoDataString(pht('This trigger is not used by any columns.')) 122 + ->setHeaders( 123 + array( 124 + pht('Project'), 125 + pht('Column'), 126 + )) 127 + ->setColumnClasses( 128 + array( 129 + null, 130 + 'wide pri', 131 + )); 132 + 133 + $header_view = id(new PHUIHeaderView()) 134 + ->setHeader(pht('Used by Columns')); 135 + 136 + return id(new PHUIObjectBoxView()) 137 + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 138 + ->setHeader($header_view) 139 + ->setTable($table_view); 140 + } 141 + 142 + private function newCurtain(PhabricatorProjectTrigger $trigger) { 143 + $viewer = $this->getViewer(); 144 + 145 + $can_edit = PhabricatorPolicyFilter::hasCapability( 146 + $viewer, 147 + $trigger, 148 + PhabricatorPolicyCapability::CAN_EDIT); 149 + 150 + $curtain = $this->newCurtainView($trigger); 151 + 152 + $edit_uri = $this->getApplicationURI( 153 + urisprintf( 154 + 'trigger/edit/%d/', 155 + $trigger->getID())); 156 + 157 + $curtain->addAction( 158 + id(new PhabricatorActionView()) 159 + ->setName(pht('Edit Trigger')) 160 + ->setIcon('fa-pencil') 161 + ->setHref($edit_uri) 162 + ->setDisabled(!$can_edit) 163 + ->setWorkflow(!$can_edit)); 164 + 165 + return $curtain; 166 + } 167 + 168 + }
+30
src/applications/project/editor/PhabricatorProjectTriggerEditor.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerEditor 4 + extends PhabricatorApplicationTransactionEditor { 5 + 6 + public function getEditorApplicationClass() { 7 + return 'PhabricatorProjectApplication'; 8 + } 9 + 10 + public function getEditorObjectsDescription() { 11 + return pht('Triggers'); 12 + } 13 + 14 + public function getCreateObjectTitle($author, $object) { 15 + return pht('%s created this trigger.', $author); 16 + } 17 + 18 + public function getCreateObjectTitleForFeed($author, $object) { 19 + return pht('%s created %s.', $author, $object); 20 + } 21 + 22 + public function getTransactionTypes() { 23 + $types = parent::getTransactionTypes(); 24 + 25 + $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; 26 + 27 + return $types; 28 + } 29 + 30 + }
+1
src/applications/project/engine/PhabricatorBoardLayoutEngine.php
··· 336 336 $columns = id(new PhabricatorProjectColumnQuery()) 337 337 ->setViewer($viewer) 338 338 ->withProjectPHIDs(array_keys($boards)) 339 + ->needTriggers(true) 339 340 ->execute(); 340 341 $columns = msort($columns, 'getOrderingKey'); 341 342 $columns = mpull($columns, null, 'getPHID');
+45
src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerPHIDType 4 + extends PhabricatorPHIDType { 5 + 6 + const TYPECONST = 'WTRG'; 7 + 8 + public function getTypeName() { 9 + return pht('Trigger'); 10 + } 11 + 12 + public function getTypeIcon() { 13 + return 'fa-exclamation-triangle'; 14 + } 15 + 16 + public function newObject() { 17 + return new PhabricatorProjectTrigger(); 18 + } 19 + 20 + public function getPHIDTypeApplicationClass() { 21 + return 'PhabricatorProjectApplication'; 22 + } 23 + 24 + protected function buildQueryForObjects( 25 + PhabricatorObjectQuery $query, 26 + array $phids) { 27 + 28 + return id(new PhabricatorProjectTriggerQuery()) 29 + ->withPHIDs($phids); 30 + } 31 + 32 + public function loadHandles( 33 + PhabricatorHandleQuery $query, 34 + array $handles, 35 + array $objects) { 36 + 37 + foreach ($handles as $phid => $handle) { 38 + $trigger = $objects[$phid]; 39 + 40 + $handle->setName($trigger->getDisplayName()); 41 + $handle->setURI($trigger->getURI()); 42 + } 43 + } 44 + 45 + }
+55
src/applications/project/query/PhabricatorProjectColumnQuery.php
··· 9 9 private $proxyPHIDs; 10 10 private $statuses; 11 11 private $isProxyColumn; 12 + private $triggerPHIDs; 13 + private $needTriggers; 12 14 13 15 public function withIDs(array $ids) { 14 16 $this->ids = $ids; ··· 37 39 38 40 public function withIsProxyColumn($is_proxy) { 39 41 $this->isProxyColumn = $is_proxy; 42 + return $this; 43 + } 44 + 45 + public function withTriggerPHIDs(array $trigger_phids) { 46 + $this->triggerPHIDs = $trigger_phids; 47 + return $this; 48 + } 49 + 50 + public function needTriggers($need_triggers) { 51 + $this->needTriggers = true; 40 52 return $this; 41 53 } 42 54 ··· 121 133 $column->attachProxy($proxy); 122 134 } 123 135 136 + if ($this->needTriggers) { 137 + $trigger_phids = array(); 138 + foreach ($page as $column) { 139 + if ($column->canHaveTrigger()) { 140 + $trigger_phid = $column->getTriggerPHID(); 141 + if ($trigger_phid) { 142 + $trigger_phids[] = $trigger_phid; 143 + } 144 + } 145 + } 146 + 147 + if ($trigger_phids) { 148 + $triggers = id(new PhabricatorProjectTriggerQuery()) 149 + ->setViewer($this->getViewer()) 150 + ->setParentQuery($this) 151 + ->withPHIDs(array($this->getPHID())) 152 + ->execute(); 153 + $triggers = mpull($triggers, null, 'getPHID'); 154 + } else { 155 + $triggers = array(); 156 + } 157 + 158 + foreach ($page as $column) { 159 + $trigger = null; 160 + 161 + if ($column->canHaveTrigger()) { 162 + $trigger_phid = $column->getTriggerPHID(); 163 + if ($trigger_phid) { 164 + $trigger = idx($triggers, $trigger_phid); 165 + } 166 + } 167 + 168 + $column->attachTrigger($trigger); 169 + } 170 + } 171 + 124 172 return $page; 125 173 } 126 174 ··· 160 208 $conn, 161 209 'status IN (%Ld)', 162 210 $this->statuses); 211 + } 212 + 213 + if ($this->triggerPHIDs !== null) { 214 + $where[] = qsprintf( 215 + $conn, 216 + 'triggerPHID IN (%Ls)', 217 + $this->triggerPHIDs); 163 218 } 164 219 165 220 if ($this->isProxyColumn !== null) {
+51
src/applications/project/query/PhabricatorProjectTriggerQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerQuery 4 + extends PhabricatorCursorPagedPolicyAwareQuery { 5 + 6 + private $ids; 7 + private $phids; 8 + 9 + public function withIDs(array $ids) { 10 + $this->ids = $ids; 11 + return $this; 12 + } 13 + 14 + public function withPHIDs(array $phids) { 15 + $this->phids = $phids; 16 + return $this; 17 + } 18 + 19 + public function newResultObject() { 20 + return new PhabricatorProjectTrigger(); 21 + } 22 + 23 + protected function loadPage() { 24 + return $this->loadStandardPage($this->newResultObject()); 25 + } 26 + 27 + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 28 + $where = parent::buildWhereClauseParts($conn); 29 + 30 + if ($this->ids !== null) { 31 + $where[] = qsprintf( 32 + $conn, 33 + 'id IN (%Ld)', 34 + $this->ids); 35 + } 36 + 37 + if ($this->phids !== null) { 38 + $where[] = qsprintf( 39 + $conn, 40 + 'phid IN (%Ls)', 41 + $this->phids); 42 + } 43 + 44 + return $where; 45 + } 46 + 47 + public function getQueryApplicationClass() { 48 + return 'PhabricatorProjectApplication'; 49 + } 50 + 51 + }
+75
src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerSearchEngine 4 + extends PhabricatorApplicationSearchEngine { 5 + 6 + public function getResultTypeDescription() { 7 + return pht('Triggers'); 8 + } 9 + 10 + public function getApplicationClassName() { 11 + return 'PhabricatorProjectApplication'; 12 + } 13 + 14 + public function newQuery() { 15 + return new PhabricatorProjectTriggerQuery(); 16 + } 17 + 18 + protected function buildCustomSearchFields() { 19 + return array(); 20 + } 21 + 22 + protected function buildQueryFromParameters(array $map) { 23 + $query = $this->newQuery(); 24 + 25 + return $query; 26 + } 27 + 28 + protected function getURI($path) { 29 + return '/project/trigger/'.$path; 30 + } 31 + 32 + protected function getBuiltinQueryNames() { 33 + $names = array(); 34 + 35 + $names['all'] = pht('All'); 36 + 37 + return $names; 38 + } 39 + 40 + public function buildSavedQueryFromBuiltin($query_key) { 41 + $query = $this->newSavedQuery(); 42 + $query->setQueryKey($query_key); 43 + 44 + switch ($query_key) { 45 + case 'all': 46 + return $query; 47 + } 48 + 49 + return parent::buildSavedQueryFromBuiltin($query_key); 50 + } 51 + 52 + protected function renderResultList( 53 + array $triggers, 54 + PhabricatorSavedQuery $query, 55 + array $handles) { 56 + assert_instances_of($triggers, 'PhabricatorProjectTrigger'); 57 + $viewer = $this->requireViewer(); 58 + 59 + $list = id(new PHUIObjectItemListView()) 60 + ->setViewer($viewer); 61 + foreach ($triggers as $trigger) { 62 + $item = id(new PHUIObjectItemView()) 63 + ->setObjectName($trigger->getObjectName()) 64 + ->setHeader($trigger->getDisplayName()) 65 + ->setHref($trigger->getURI()); 66 + 67 + $list->addItem($item); 68 + } 69 + 70 + return id(new PhabricatorApplicationSearchResultView()) 71 + ->setObjectList($list) 72 + ->setNoDataString(pht('No triggers found.')); 73 + } 74 + 75 + }
+10
src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerTransactionQuery 4 + extends PhabricatorApplicationTransactionQuery { 5 + 6 + public function getTemplateApplicationTransaction() { 7 + return new PhabricatorProjectTriggerTransaction(); 8 + } 9 + 10 + }
+39
src/applications/project/storage/PhabricatorProjectColumn.php
··· 18 18 protected $proxyPHID; 19 19 protected $sequence; 20 20 protected $properties = array(); 21 + protected $triggerPHID; 21 22 22 23 private $project = self::ATTACHABLE; 23 24 private $proxy = self::ATTACHABLE; 25 + private $trigger = self::ATTACHABLE; 24 26 25 27 public static function initializeNewColumn(PhabricatorUser $user) { 26 28 return id(new PhabricatorProjectColumn()) ··· 40 42 'status' => 'uint32', 41 43 'sequence' => 'uint32', 42 44 'proxyPHID' => 'phid?', 45 + 'triggerPHID' => 'phid?', 43 46 ), 44 47 self::CONFIG_KEY_SCHEMA => array( 45 48 'key_status' => array( ··· 51 54 'key_proxy' => array( 52 55 'columns' => array('projectPHID', 'proxyPHID'), 53 56 'unique' => true, 57 + ), 58 + 'key_trigger' => array( 59 + 'columns' => array('triggerPHID'), 54 60 ), 55 61 ), 56 62 ) + parent::getConfiguration(); ··· 179 185 180 186 return sprintf('%s%012d', $group, $sequence); 181 187 } 188 + 189 + public function attachTrigger(PhabricatorProjectTrigger $trigger = null) { 190 + $this->trigger = $trigger; 191 + return $this; 192 + } 193 + 194 + public function getTrigger() { 195 + return $this->assertAttached($this->trigger); 196 + } 197 + 198 + public function canHaveTrigger() { 199 + // Backlog columns and proxy (subproject / milestone) columns can't have 200 + // triggers because cards routinely end up in these columns through tag 201 + // edits rather than drag-and-drop and it would likely be confusing to 202 + // have these triggers act only a small fraction of the time. 203 + 204 + if ($this->isDefaultColumn()) { 205 + return false; 206 + } 207 + 208 + if ($this->getProxy()) { 209 + return false; 210 + } 211 + 212 + return true; 213 + } 214 + 215 + public function getBoardURI() { 216 + return urisprintf( 217 + '/project/board/%d/', 218 + $this->getProject()->getID()); 219 + } 220 + 182 221 183 222 /* -( PhabricatorConduitResultInterface )---------------------------------- */ 184 223
+108
src/applications/project/storage/PhabricatorProjectTrigger.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTrigger 4 + extends PhabricatorProjectDAO 5 + implements 6 + PhabricatorApplicationTransactionInterface, 7 + PhabricatorPolicyInterface, 8 + PhabricatorDestructibleInterface { 9 + 10 + protected $name; 11 + protected $ruleset = array(); 12 + protected $editPolicy; 13 + 14 + public static function initializeNewTrigger() { 15 + $default_edit = PhabricatorPolicies::POLICY_USER; 16 + 17 + return id(new self()) 18 + ->setName('') 19 + ->setEditPolicy($default_edit); 20 + } 21 + 22 + protected function getConfiguration() { 23 + return array( 24 + self::CONFIG_AUX_PHID => true, 25 + self::CONFIG_SERIALIZATION => array( 26 + 'ruleset' => self::SERIALIZATION_JSON, 27 + ), 28 + self::CONFIG_COLUMN_SCHEMA => array( 29 + 'name' => 'text255', 30 + ), 31 + self::CONFIG_KEY_SCHEMA => array( 32 + ), 33 + ) + parent::getConfiguration(); 34 + } 35 + 36 + public function getPHIDType() { 37 + return PhabricatorProjectTriggerPHIDType::TYPECONST; 38 + } 39 + 40 + public function getDisplayName() { 41 + $name = $this->getName(); 42 + if (strlen($name)) { 43 + return $name; 44 + } 45 + 46 + return $this->getDefaultName(); 47 + } 48 + 49 + public function getDefaultName() { 50 + return pht('Custom Trigger'); 51 + } 52 + 53 + public function getURI() { 54 + return urisprintf( 55 + '/project/trigger/%d/', 56 + $this->getID()); 57 + } 58 + 59 + public function getObjectName() { 60 + return pht('Trigger %d', $this->getID()); 61 + } 62 + 63 + 64 + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ 65 + 66 + 67 + public function getApplicationTransactionEditor() { 68 + return new PhabricatorProjectTriggerEditor(); 69 + } 70 + 71 + public function getApplicationTransactionTemplate() { 72 + return new PhabricatorProjectTriggerTransaction(); 73 + } 74 + 75 + 76 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 77 + 78 + 79 + public function getCapabilities() { 80 + return array( 81 + PhabricatorPolicyCapability::CAN_VIEW, 82 + PhabricatorPolicyCapability::CAN_EDIT, 83 + ); 84 + } 85 + 86 + public function getPolicy($capability) { 87 + switch ($capability) { 88 + case PhabricatorPolicyCapability::CAN_VIEW: 89 + return PhabricatorPolicies::getMostOpenPolicy(); 90 + case PhabricatorPolicyCapability::CAN_EDIT: 91 + return $this->getEditPolicy(); 92 + } 93 + } 94 + 95 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 96 + return false; 97 + } 98 + 99 + 100 + /* -( PhabricatorDestructibleInterface )----------------------------------- */ 101 + 102 + 103 + public function destroyObjectPermanently( 104 + PhabricatorDestructionEngine $engine) { 105 + $this->delete(); 106 + } 107 + 108 + }
+18
src/applications/project/storage/PhabricatorProjectTriggerTransaction.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerTransaction 4 + extends PhabricatorModularTransaction { 5 + 6 + public function getApplicationName() { 7 + return 'project'; 8 + } 9 + 10 + public function getApplicationTransactionType() { 11 + return PhabricatorProjectTriggerPHIDType::TYPECONST; 12 + } 13 + 14 + public function getBaseTransactionClass() { 15 + return 'PhabricatorProjectTriggerTransactionType'; 16 + } 17 + 18 + }
+58
src/applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerNameTransaction 4 + extends PhabricatorProjectTriggerTransactionType { 5 + 6 + const TRANSACTIONTYPE = 'name'; 7 + 8 + public function generateOldValue($object) { 9 + return $object->getName(); 10 + } 11 + 12 + public function applyInternalEffects($object, $value) { 13 + $object->setName($value); 14 + } 15 + 16 + public function getTitle() { 17 + $old = $this->getOldValue(); 18 + $new = $this->getNewValue(); 19 + 20 + if (strlen($old) && strlen($new)) { 21 + return pht( 22 + '%s renamed this trigger from %s to %s.', 23 + $this->renderAuthor(), 24 + $this->renderOldValue(), 25 + $this->renderNewValue()); 26 + } else if (strlen($new)) { 27 + return pht( 28 + '%s named this trigger %s.', 29 + $this->renderAuthor(), 30 + $this->renderNewValue()); 31 + } else { 32 + return pht( 33 + '%s stripped the name %s from this trigger.', 34 + $this->renderAuthor(), 35 + $this->renderOldValue()); 36 + } 37 + } 38 + 39 + public function validateTransactions($object, array $xactions) { 40 + $errors = array(); 41 + 42 + $max_length = $object->getColumnMaximumByteLength('name'); 43 + foreach ($xactions as $xaction) { 44 + $new_value = $xaction->getNewValue(); 45 + $new_length = strlen($new_value); 46 + if ($new_length > $max_length) { 47 + $errors[] = $this->newInvalidError( 48 + pht( 49 + 'Trigger names must not be longer than %s characters.', 50 + new PhutilNumber($max_length)), 51 + $xaction); 52 + } 53 + } 54 + 55 + return $errors; 56 + } 57 + 58 + }
+4
src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorProjectTriggerTransactionType 4 + extends PhabricatorModularTransactionType {}
+3 -2
src/applications/search/controller/PhabricatorApplicationSearchController.php
··· 276 276 throw new Exception( 277 277 pht( 278 278 'SearchEngines must render a "%s" object, but this engine '. 279 - '(of class "%s") rendered something else.', 279 + '(of class "%s") rendered something else ("%s").', 280 280 'PhabricatorApplicationSearchResultView', 281 - get_class($engine))); 281 + get_class($engine), 282 + phutil_describe_type($list))); 282 283 } 283 284 284 285 if ($list->getObjectList()) {