Page MenuHomePhabricator

D20279.id48394.diff
No OneTemporary

D20279.id48394.diff

diff --git a/resources/sql/autopatches/20190312.triggers.01.trigger.sql b/resources/sql/autopatches/20190312.triggers.01.trigger.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20190312.triggers.01.trigger.sql
@@ -0,0 +1,9 @@
+CREATE TABLE {$NAMESPACE}_project.project_trigger (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
+ editPolicy VARBINARY(64) NOT NULL,
+ ruleset LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190312.triggers.02.xaction.sql b/resources/sql/autopatches/20190312.triggers.02.xaction.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20190312.triggers.02.xaction.sql
@@ -0,0 +1,19 @@
+CREATE TABLE {$NAMESPACE}_project.project_triggertransaction (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ authorPHID VARBINARY(64) NOT NULL,
+ objectPHID VARBINARY(64) NOT NULL,
+ viewPolicy VARBINARY(64) NOT NULL,
+ editPolicy VARBINARY(64) NOT NULL,
+ commentPHID VARBINARY(64) DEFAULT NULL,
+ commentVersion INT UNSIGNED NOT NULL,
+ transactionType VARCHAR(32) NOT NULL,
+ oldValue LONGTEXT NOT NULL,
+ newValue LONGTEXT NOT NULL,
+ contentSource LONGTEXT NOT NULL,
+ metadata LONGTEXT NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ UNIQUE KEY `key_phid` (`phid`),
+ KEY `key_object` (`objectPHID`)
+) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190312.triggers.03.triggerphid.sql b/resources/sql/autopatches/20190312.triggers.03.triggerphid.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20190312.triggers.03.triggerphid.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_project.project_column
+ ADD triggerPHID VARBINARY(64);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -4163,6 +4163,19 @@
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
'PhabricatorProjectTransactionType' => 'applications/project/xaction/PhabricatorProjectTransactionType.php',
+ 'PhabricatorProjectTrigger' => 'applications/project/storage/PhabricatorProjectTrigger.php',
+ 'PhabricatorProjectTriggerController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerController.php',
+ 'PhabricatorProjectTriggerEditController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php',
+ 'PhabricatorProjectTriggerEditor' => 'applications/project/editor/PhabricatorProjectTriggerEditor.php',
+ 'PhabricatorProjectTriggerListController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerListController.php',
+ 'PhabricatorProjectTriggerNameTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php',
+ 'PhabricatorProjectTriggerPHIDType' => 'applications/project/phid/PhabricatorProjectTriggerPHIDType.php',
+ 'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php',
+ 'PhabricatorProjectTriggerSearchEngine' => 'applications/project/query/PhabricatorProjectTriggerSearchEngine.php',
+ 'PhabricatorProjectTriggerTransaction' => 'applications/project/storage/PhabricatorProjectTriggerTransaction.php',
+ 'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php',
+ 'PhabricatorProjectTriggerTransactionType' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php',
+ 'PhabricatorProjectTriggerViewController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php',
'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php',
'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php',
'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
@@ -10261,6 +10274,24 @@
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorProjectTransactionType' => 'PhabricatorModularTransactionType',
+ 'PhabricatorProjectTrigger' => array(
+ 'PhabricatorProjectDAO',
+ 'PhabricatorApplicationTransactionInterface',
+ 'PhabricatorPolicyInterface',
+ 'PhabricatorDestructibleInterface',
+ ),
+ 'PhabricatorProjectTriggerController' => 'PhabricatorProjectController',
+ 'PhabricatorProjectTriggerEditController' => 'PhabricatorProjectTriggerController',
+ 'PhabricatorProjectTriggerEditor' => 'PhabricatorApplicationTransactionEditor',
+ 'PhabricatorProjectTriggerListController' => 'PhabricatorProjectTriggerController',
+ 'PhabricatorProjectTriggerNameTransaction' => 'PhabricatorProjectTriggerTransactionType',
+ 'PhabricatorProjectTriggerPHIDType' => 'PhabricatorPHIDType',
+ 'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorProjectTriggerSearchEngine' => 'PhabricatorApplicationSearchEngine',
+ 'PhabricatorProjectTriggerTransaction' => 'PhabricatorModularTransaction',
+ 'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+ 'PhabricatorProjectTriggerTransactionType' => 'PhabricatorModularTransactionType',
+ 'PhabricatorProjectTriggerViewController' => 'PhabricatorProjectTriggerController',
'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener',
'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php
--- a/src/applications/project/application/PhabricatorProjectApplication.php
+++ b/src/applications/project/application/PhabricatorProjectApplication.php
@@ -89,6 +89,14 @@
'background/'
=> 'PhabricatorProjectBoardBackgroundController',
),
+ 'trigger/' => array(
+ $this->getQueryRoutePattern() =>
+ 'PhabricatorProjectTriggerListController',
+ '(?P<id>[1-9]\d*)/' =>
+ 'PhabricatorProjectTriggerViewController',
+ $this->getEditRoutePattern('edit/') =>
+ 'PhabricatorProjectTriggerEditController',
+ ),
'update/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/'
=> 'PhabricatorProjectUpdateController',
'manage/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectManageController',
diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
--- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
@@ -1110,10 +1110,8 @@
));
}
- if (count($specs) > 1) {
- $column_items[] = id(new PhabricatorActionView())
- ->setType(PhabricatorActionView::TYPE_DIVIDER);
- }
+ $column_items[] = id(new PhabricatorActionView())
+ ->setType(PhabricatorActionView::TYPE_DIVIDER);
$batch_edit_uri = $request->getRequestURI();
$batch_edit_uri->replaceQueryParam('batch', $column->getID());
@@ -1173,6 +1171,40 @@
->setWorkflow(true);
}
+ if ($column->canHaveTrigger()) {
+ $column_items[] = id(new PhabricatorActionView())
+ ->setType(PhabricatorActionView::TYPE_DIVIDER);
+
+ $trigger = $column->getTrigger();
+ if (!$trigger) {
+ $set_uri = $this->getApplicationURI(
+ new PhutilURI(
+ 'trigger/edit/',
+ array(
+ 'columnPHID' => $column->getPHID(),
+ )));
+
+ $column_items[] = id(new PhabricatorActionView())
+ ->setIcon('fa-cogs')
+ ->setName(pht('New Trigger...'))
+ ->setHref($set_uri)
+ ->setDisabled(!$can_edit);
+ } else {
+ $column_items[] = id(new PhabricatorActionView())
+ ->setIcon('fa-cogs')
+ ->setName(pht('View Trigger'))
+ ->setHref($trigger->getURI())
+ ->setDisabled(!$can_edit);
+ }
+
+ $column_items[] = id(new PhabricatorActionView())
+ ->setIcon('fa-times')
+ ->setName(pht('Remove Trigger'))
+ ->setHref('#')
+ ->setWorkflow(true)
+ ->setDisabled(!$can_edit || !$trigger);
+ }
+
$column_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($column_items as $item) {
diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php
@@ -0,0 +1,16 @@
+<?php
+
+abstract class PhabricatorProjectTriggerController
+ extends PhabricatorProjectController {
+
+ protected function buildApplicationCrumbs() {
+ $crumbs = parent::buildApplicationCrumbs();
+
+ $crumbs->addTextCrumb(
+ pht('Triggers'),
+ $this->getApplicationURI('trigger/'));
+
+ return $crumbs;
+ }
+
+}
diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php
@@ -0,0 +1,197 @@
+<?php
+
+final class PhabricatorProjectTriggerEditController
+ extends PhabricatorProjectTriggerController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $request = $this->getRequest();
+ $viewer = $request->getViewer();
+
+ $id = $request->getURIData('id');
+ if ($id) {
+ $trigger = id(new PhabricatorProjectTriggerQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$trigger) {
+ return new Aphront404Response();
+ }
+ } else {
+ $trigger = PhabricatorProjectTrigger::initializeNewTrigger();
+ }
+
+ $column_phid = $request->getStr('columnPHID');
+ if ($column_phid) {
+ $column = id(new PhabricatorProjectColumnQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($column_phid))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$column) {
+ return new Aphront404Response();
+ }
+ $board_uri = $column->getBoardURI();
+ } else {
+ $column = null;
+ $board_uri = null;
+ }
+
+ if ($board_uri) {
+ $cancel_uri = $board_uri;
+ } else if ($trigger->getID()) {
+ $cancel_uri = $trigger->getURI();
+ } else {
+ $cancel_uri = $this->getApplicationURI('trigger/');
+ }
+
+ $v_name = $trigger->getName();
+ $v_edit = $trigger->getEditPolicy();
+
+ $e_name = null;
+ $e_edit = null;
+
+ $validation_exception = null;
+ if ($request->isFormPost()) {
+ try {
+ $v_name = $request->getStr('name');
+ $v_edit = $request->getStr('editPolicy');
+
+ $xactions = array();
+ if (!$trigger->getID()) {
+ $xactions[] = $trigger->getApplicationTransactionTemplate()
+ ->setTransactionType(PhabricatorTransactions::TYPE_CREATE)
+ ->setNewValue(true);
+ }
+
+ $xactions[] = $trigger->getApplicationTransactionTemplate()
+ ->setTransactionType(
+ PhabricatorProjectTriggerNameTransaction::TRANSACTIONTYPE)
+ ->setNewValue($v_name);
+
+ $xactions[] = $trigger->getApplicationTransactionTemplate()
+ ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
+ ->setNewValue($v_edit);
+
+ $editor = $trigger->getApplicationTransactionEditor()
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true);
+
+ $editor->applyTransactions($trigger, $xactions);
+
+ $next_uri = $trigger->getURI();
+
+ if ($column) {
+ $column_xactions = array();
+
+ // TODO: Modularize column transactions so we can change the column
+ // trigger here. For now, this does nothing.
+
+ $column_editor = $column->getApplicationTransactionEditor()
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true);
+
+ $column_editor->applyTransactions($column, $column_xactions);
+
+ $next_uri = $column->getBoardURI();
+ }
+
+ return id(new AphrontRedirectResponse())->setURI($next_uri);
+ } catch (PhabricatorApplicationTransactionValidationException $ex) {
+ $validation_exception = $ex;
+
+ $e_name = $ex->getShortMessage(
+ PhabricatorProjectTriggerNameTransaction::TRANSACTIONTYPE);
+
+ $e_edit = $ex->getShortMessage(
+ PhabricatorTransactions::TYPE_EDIT_POLICY);
+
+ $trigger->setEditPolicy($v_edit);
+ }
+ }
+
+ if ($trigger->getID()) {
+ $title = $trigger->getObjectName();
+ $submit = pht('Save Trigger');
+ $header = pht('Edit Trigger: %s', $trigger->getObjectName());
+ } else {
+ $title = pht('New Trigger');
+ $submit = pht('Create Trigger');
+ $header = pht('New Trigger');
+ }
+
+ $form = id(new AphrontFormView())
+ ->setViewer($viewer);
+
+ if ($column) {
+ $form->addHiddenInput('columnPHID', $column->getPHID());
+ }
+
+ $form->appendControl(
+ id(new AphrontFormTextControl())
+ ->setLabel(pht('Name'))
+ ->setName('name')
+ ->setValue($v_name)
+ ->setError($e_name)
+ ->setPlaceholder($trigger->getDefaultName()));
+
+ $policies = id(new PhabricatorPolicyQuery())
+ ->setViewer($viewer)
+ ->setObject($trigger)
+ ->execute();
+
+ $form->appendControl(
+ id(new AphrontFormPolicyControl())
+ ->setName('editPolicy')
+ ->setPolicyObject($trigger)
+ ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
+ ->setPolicies($policies)
+ ->setError($e_edit));
+
+ $form->appendControl(
+ id(new AphrontFormSubmitControl())
+ ->setValue($submit)
+ ->addCancelButton($cancel_uri));
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader($header);
+
+ $box_view = id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setValidationException($validation_exception)
+ ->appendChild($form);
+
+ $column_view = id(new PHUITwoColumnView())
+ ->setFooter($box_view);
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->setBorder(true);
+
+ if ($column) {
+ $crumbs->addTextCrumb(
+ pht(
+ '%s: %s',
+ $column->getProject()->getDisplayName(),
+ $column->getName()),
+ $board_uri);
+ }
+
+ $crumbs->addTextCrumb($title);
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->setCrumbs($crumbs)
+ ->appendChild($column_view);
+ }
+
+}
diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PhabricatorProjectTriggerListController
+ extends PhabricatorProjectTriggerController {
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+ public function handleRequest(AphrontRequest $request) {
+ return id(new PhabricatorProjectTriggerSearchEngine())
+ ->setController($this)
+ ->buildResponse();
+ }
+
+}
diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php
@@ -0,0 +1,168 @@
+<?php
+
+final class PhabricatorProjectTriggerViewController
+ extends PhabricatorProjectTriggerController {
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+ public function handleRequest(AphrontRequest $request) {
+ $request = $this->getRequest();
+ $viewer = $request->getViewer();
+
+ $id = $request->getURIData('id');
+
+ $trigger = id(new PhabricatorProjectTriggerQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->executeOne();
+ if (!$trigger) {
+ return new Aphront404Response();
+ }
+
+ $columns_view = $this->newColumnsView($trigger);
+
+ $title = $trigger->getObjectName();
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader($trigger->getDisplayName());
+
+ $timeline = $this->buildTransactionTimeline(
+ $trigger,
+ new PhabricatorProjectTriggerTransactionQuery());
+ $timeline->setShouldTerminate(true);
+
+ $curtain = $this->newCurtain($trigger);
+
+ $column_view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setCurtain($curtain)
+ ->setMainColumn(
+ array(
+ $columns_view,
+ $timeline,
+ ));
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb($trigger->getObjectName())
+ ->setBorder(true);
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->setCrumbs($crumbs)
+ ->appendChild($column_view);
+ }
+
+ private function newColumnsView(PhabricatorProjectTrigger $trigger) {
+ $viewer = $this->getViewer();
+
+ // NOTE: When showing columns which use this trigger, we want to represent
+ // all columns the trigger is used by: even columns the user can't see.
+
+ // If we hide columns the viewer can't see, they might think that the
+ // trigger isn't widely used and is safe to edit, when it may actually
+ // be in use on workboards they don't have access to.
+
+ // Query the columns with the omnipotent viewer first, then pull out their
+ // PHIDs and throw the actual objects away. Re-query with the real viewer
+ // so we load only the columns they can actually see, but have a list of
+ // all the impacted column PHIDs.
+
+ $omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
+ $all_columns = id(new PhabricatorProjectColumnQuery())
+ ->setViewer($omnipotent_viewer)
+ ->withTriggerPHIDs(array($trigger->getPHID()))
+ ->execute();
+ $column_phids = mpull($all_columns, 'getPHID');
+
+ if ($column_phids) {
+ $visible_columns = id(new PhabricatorProjectColumnQuery())
+ ->setViewer($viewer)
+ ->withPHIDs($column_phids)
+ ->execute();
+ $visible_columns = mpull($visible_columns, null, 'getPHID');
+ } else {
+ $visible_columns = array();
+ }
+
+ $rows = array();
+ foreach ($column_phids as $column_phid) {
+ $column = idx($visible_columns, $column_phid);
+
+ if ($column) {
+ $project = $column->getProject();
+
+ $project_name = phutil_tag(
+ 'a',
+ array(
+ 'href' => $project->getURI(),
+ ),
+ $project->getDisplayName());
+
+ $column_name = phutil_tag(
+ 'a',
+ array(
+ 'href' => $column->getBoardURI(),
+ ),
+ $column->getDisplayName());
+ } else {
+ $project_name = null;
+ $column_name = phutil_tag('em', array(), pht('Restricted Column'));
+ }
+
+ $rows[] = array(
+ $project_name,
+ $column_name,
+ );
+ }
+
+ $table_view = id(new AphrontTableView($rows))
+ ->setNoDataString(pht('This trigger is not used by any columns.'))
+ ->setHeaders(
+ array(
+ pht('Project'),
+ pht('Column'),
+ ))
+ ->setColumnClasses(
+ array(
+ null,
+ 'wide pri',
+ ));
+
+ $header_view = id(new PHUIHeaderView())
+ ->setHeader(pht('Used by Columns'));
+
+ return id(new PHUIObjectBoxView())
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setHeader($header_view)
+ ->setTable($table_view);
+ }
+
+ private function newCurtain(PhabricatorProjectTrigger $trigger) {
+ $viewer = $this->getViewer();
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $trigger,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $curtain = $this->newCurtainView($trigger);
+
+ $edit_uri = $this->getApplicationURI(
+ urisprintf(
+ 'trigger/edit/%d/',
+ $trigger->getID()));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Edit Trigger'))
+ ->setIcon('fa-pencil')
+ ->setHref($edit_uri)
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(!$can_edit));
+
+ return $curtain;
+ }
+
+}
diff --git a/src/applications/project/editor/PhabricatorProjectTriggerEditor.php b/src/applications/project/editor/PhabricatorProjectTriggerEditor.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/editor/PhabricatorProjectTriggerEditor.php
@@ -0,0 +1,30 @@
+<?php
+
+final class PhabricatorProjectTriggerEditor
+ extends PhabricatorApplicationTransactionEditor {
+
+ public function getEditorApplicationClass() {
+ return 'PhabricatorProjectApplication';
+ }
+
+ public function getEditorObjectsDescription() {
+ return pht('Triggers');
+ }
+
+ public function getCreateObjectTitle($author, $object) {
+ return pht('%s created this trigger.', $author);
+ }
+
+ public function getCreateObjectTitleForFeed($author, $object) {
+ return pht('%s created %s.', $author, $object);
+ }
+
+ public function getTransactionTypes() {
+ $types = parent::getTransactionTypes();
+
+ $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
+
+ return $types;
+ }
+
+}
diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php
--- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php
+++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php
@@ -336,6 +336,7 @@
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array_keys($boards))
+ ->needTriggers(true)
->execute();
$columns = msort($columns, 'getOrderingKey');
$columns = mpull($columns, null, 'getPHID');
diff --git a/src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php b/src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php
@@ -0,0 +1,45 @@
+<?php
+
+final class PhabricatorProjectTriggerPHIDType
+ extends PhabricatorPHIDType {
+
+ const TYPECONST = 'WTRG';
+
+ public function getTypeName() {
+ return pht('Trigger');
+ }
+
+ public function getTypeIcon() {
+ return 'fa-exclamation-triangle';
+ }
+
+ public function newObject() {
+ return new PhabricatorProjectTrigger();
+ }
+
+ public function getPHIDTypeApplicationClass() {
+ return 'PhabricatorProjectApplication';
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new PhabricatorProjectTriggerQuery())
+ ->withPHIDs($phids);
+ }
+
+ public function loadHandles(
+ PhabricatorHandleQuery $query,
+ array $handles,
+ array $objects) {
+
+ foreach ($handles as $phid => $handle) {
+ $trigger = $objects[$phid];
+
+ $handle->setName($trigger->getDisplayName());
+ $handle->setURI($trigger->getURI());
+ }
+ }
+
+}
diff --git a/src/applications/project/query/PhabricatorProjectColumnQuery.php b/src/applications/project/query/PhabricatorProjectColumnQuery.php
--- a/src/applications/project/query/PhabricatorProjectColumnQuery.php
+++ b/src/applications/project/query/PhabricatorProjectColumnQuery.php
@@ -9,6 +9,8 @@
private $proxyPHIDs;
private $statuses;
private $isProxyColumn;
+ private $triggerPHIDs;
+ private $needTriggers;
public function withIDs(array $ids) {
$this->ids = $ids;
@@ -40,6 +42,16 @@
return $this;
}
+ public function withTriggerPHIDs(array $trigger_phids) {
+ $this->triggerPHIDs = $trigger_phids;
+ return $this;
+ }
+
+ public function needTriggers($need_triggers) {
+ $this->needTriggers = true;
+ return $this;
+ }
+
public function newResultObject() {
return new PhabricatorProjectColumn();
}
@@ -121,6 +133,42 @@
$column->attachProxy($proxy);
}
+ if ($this->needTriggers) {
+ $trigger_phids = array();
+ foreach ($page as $column) {
+ if ($column->canHaveTrigger()) {
+ $trigger_phid = $column->getTriggerPHID();
+ if ($trigger_phid) {
+ $trigger_phids[] = $trigger_phid;
+ }
+ }
+ }
+
+ if ($trigger_phids) {
+ $triggers = id(new PhabricatorProjectTriggerQuery())
+ ->setViewer($this->getViewer())
+ ->setParentQuery($this)
+ ->withPHIDs(array($this->getPHID()))
+ ->execute();
+ $triggers = mpull($triggers, null, 'getPHID');
+ } else {
+ $triggers = array();
+ }
+
+ foreach ($page as $column) {
+ $trigger = null;
+
+ if ($column->canHaveTrigger()) {
+ $trigger_phid = $column->getTriggerPHID();
+ if ($trigger_phid) {
+ $trigger = idx($triggers, $trigger_phid);
+ }
+ }
+
+ $column->attachTrigger($trigger);
+ }
+ }
+
return $page;
}
@@ -162,6 +210,13 @@
$this->statuses);
}
+ if ($this->triggerPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'triggerPHID IN (%Ls)',
+ $this->triggerPHIDs);
+ }
+
if ($this->isProxyColumn !== null) {
if ($this->isProxyColumn) {
$where[] = qsprintf($conn, 'proxyPHID IS NOT NULL');
diff --git a/src/applications/project/query/PhabricatorProjectTriggerQuery.php b/src/applications/project/query/PhabricatorProjectTriggerQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/query/PhabricatorProjectTriggerQuery.php
@@ -0,0 +1,51 @@
+<?php
+
+final class PhabricatorProjectTriggerQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withPHIDs(array $phids) {
+ $this->phids = $phids;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new PhabricatorProjectTrigger();
+ }
+
+ protected function loadPage() {
+ return $this->loadStandardPage($this->newResultObject());
+ }
+
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
+
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'id IN (%Ld)',
+ $this->ids);
+ }
+
+ if ($this->phids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'phid IN (%Ls)',
+ $this->phids);
+ }
+
+ return $where;
+ }
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorProjectApplication';
+ }
+
+}
diff --git a/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php b/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php
@@ -0,0 +1,75 @@
+<?php
+
+final class PhabricatorProjectTriggerSearchEngine
+ extends PhabricatorApplicationSearchEngine {
+
+ public function getResultTypeDescription() {
+ return pht('Triggers');
+ }
+
+ public function getApplicationClassName() {
+ return 'PhabricatorProjectApplication';
+ }
+
+ public function newQuery() {
+ return new PhabricatorProjectTriggerQuery();
+ }
+
+ protected function buildCustomSearchFields() {
+ return array();
+ }
+
+ protected function buildQueryFromParameters(array $map) {
+ $query = $this->newQuery();
+
+ return $query;
+ }
+
+ protected function getURI($path) {
+ return '/project/trigger/'.$path;
+ }
+
+ protected function getBuiltinQueryNames() {
+ $names = array();
+
+ $names['all'] = pht('All');
+
+ return $names;
+ }
+
+ public function buildSavedQueryFromBuiltin($query_key) {
+ $query = $this->newSavedQuery();
+ $query->setQueryKey($query_key);
+
+ switch ($query_key) {
+ case 'all':
+ return $query;
+ }
+
+ return parent::buildSavedQueryFromBuiltin($query_key);
+ }
+
+ protected function renderResultList(
+ array $triggers,
+ PhabricatorSavedQuery $query,
+ array $handles) {
+ assert_instances_of($triggers, 'PhabricatorProjectTrigger');
+ $viewer = $this->requireViewer();
+
+ $list = id(new PHUIObjectItemListView())
+ ->setViewer($viewer);
+ foreach ($triggers as $trigger) {
+ $item = id(new PHUIObjectItemView())
+ ->setObjectName($trigger->getObjectName())
+ ->setHeader($trigger->getDisplayName())
+ ->setHref($trigger->getURI());
+
+ $list->addItem($item);
+ }
+
+ return id(new PhabricatorApplicationSearchResultView())
+ ->setObjectList($list)
+ ->setNoDataString(pht('No triggers found.'));
+ }
+
+}
diff --git a/src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php b/src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorProjectTriggerTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ public function getTemplateApplicationTransaction() {
+ return new PhabricatorProjectTriggerTransaction();
+ }
+
+}
diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php
--- a/src/applications/project/storage/PhabricatorProjectColumn.php
+++ b/src/applications/project/storage/PhabricatorProjectColumn.php
@@ -18,9 +18,11 @@
protected $proxyPHID;
protected $sequence;
protected $properties = array();
+ protected $triggerPHID;
private $project = self::ATTACHABLE;
private $proxy = self::ATTACHABLE;
+ private $trigger = self::ATTACHABLE;
public static function initializeNewColumn(PhabricatorUser $user) {
return id(new PhabricatorProjectColumn())
@@ -40,6 +42,7 @@
'status' => 'uint32',
'sequence' => 'uint32',
'proxyPHID' => 'phid?',
+ 'triggerPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
@@ -52,6 +55,9 @@
'columns' => array('projectPHID', 'proxyPHID'),
'unique' => true,
),
+ 'key_trigger' => array(
+ 'columns' => array('triggerPHID'),
+ ),
),
) + parent::getConfiguration();
}
@@ -180,6 +186,39 @@
return sprintf('%s%012d', $group, $sequence);
}
+ public function attachTrigger(PhabricatorProjectTrigger $trigger = null) {
+ $this->trigger = $trigger;
+ return $this;
+ }
+
+ public function getTrigger() {
+ return $this->assertAttached($this->trigger);
+ }
+
+ public function canHaveTrigger() {
+ // Backlog columns and proxy (subproject / milestone) columns can't have
+ // triggers because cards routinely end up in these columns through tag
+ // edits rather than drag-and-drop and it would likely be confusing to
+ // have these triggers act only a small fraction of the time.
+
+ if ($this->isDefaultColumn()) {
+ return false;
+ }
+
+ if ($this->getProxy()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getBoardURI() {
+ return urisprintf(
+ '/project/board/%d/',
+ $this->getProject()->getID());
+ }
+
+
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
diff --git a/src/applications/project/storage/PhabricatorProjectTrigger.php b/src/applications/project/storage/PhabricatorProjectTrigger.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/storage/PhabricatorProjectTrigger.php
@@ -0,0 +1,108 @@
+<?php
+
+final class PhabricatorProjectTrigger
+ extends PhabricatorProjectDAO
+ implements
+ PhabricatorApplicationTransactionInterface,
+ PhabricatorPolicyInterface,
+ PhabricatorDestructibleInterface {
+
+ protected $name;
+ protected $ruleset = array();
+ protected $editPolicy;
+
+ public static function initializeNewTrigger() {
+ $default_edit = PhabricatorPolicies::POLICY_USER;
+
+ return id(new self())
+ ->setName('')
+ ->setEditPolicy($default_edit);
+ }
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_AUX_PHID => true,
+ self::CONFIG_SERIALIZATION => array(
+ 'ruleset' => self::SERIALIZATION_JSON,
+ ),
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'name' => 'text255',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function getPHIDType() {
+ return PhabricatorProjectTriggerPHIDType::TYPECONST;
+ }
+
+ public function getDisplayName() {
+ $name = $this->getName();
+ if (strlen($name)) {
+ return $name;
+ }
+
+ return $this->getDefaultName();
+ }
+
+ public function getDefaultName() {
+ return pht('Custom Trigger');
+ }
+
+ public function getURI() {
+ return urisprintf(
+ '/project/trigger/%d/',
+ $this->getID());
+ }
+
+ public function getObjectName() {
+ return pht('Trigger %d', $this->getID());
+ }
+
+
+/* -( PhabricatorApplicationTransactionInterface )------------------------- */
+
+
+ public function getApplicationTransactionEditor() {
+ return new PhabricatorProjectTriggerEditor();
+ }
+
+ public function getApplicationTransactionTemplate() {
+ return new PhabricatorProjectTriggerTransaction();
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ );
+ }
+
+ public function getPolicy($capability) {
+ switch ($capability) {
+ case PhabricatorPolicyCapability::CAN_VIEW:
+ return PhabricatorPolicies::getMostOpenPolicy();
+ case PhabricatorPolicyCapability::CAN_EDIT:
+ return $this->getEditPolicy();
+ }
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ return false;
+ }
+
+
+/* -( PhabricatorDestructibleInterface )----------------------------------- */
+
+
+ public function destroyObjectPermanently(
+ PhabricatorDestructionEngine $engine) {
+ $this->delete();
+ }
+
+}
diff --git a/src/applications/project/storage/PhabricatorProjectTriggerTransaction.php b/src/applications/project/storage/PhabricatorProjectTriggerTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/storage/PhabricatorProjectTriggerTransaction.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorProjectTriggerTransaction
+ extends PhabricatorModularTransaction {
+
+ public function getApplicationName() {
+ return 'project';
+ }
+
+ public function getApplicationTransactionType() {
+ return PhabricatorProjectTriggerPHIDType::TYPECONST;
+ }
+
+ public function getBaseTransactionClass() {
+ return 'PhabricatorProjectTriggerTransactionType';
+ }
+
+}
diff --git a/src/applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php
@@ -0,0 +1,58 @@
+<?php
+
+final class PhabricatorProjectTriggerNameTransaction
+ extends PhabricatorProjectTriggerTransactionType {
+
+ const TRANSACTIONTYPE = 'name';
+
+ public function generateOldValue($object) {
+ return $object->getName();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setName($value);
+ }
+
+ public function getTitle() {
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ if (strlen($old) && strlen($new)) {
+ return pht(
+ '%s renamed this trigger from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldValue(),
+ $this->renderNewValue());
+ } else if (strlen($new)) {
+ return pht(
+ '%s named this trigger %s.',
+ $this->renderAuthor(),
+ $this->renderNewValue());
+ } else {
+ return pht(
+ '%s stripped the name %s from this trigger.',
+ $this->renderAuthor(),
+ $this->renderOldValue());
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $max_length = $object->getColumnMaximumByteLength('name');
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+ $new_length = strlen($new_value);
+ if ($new_length > $max_length) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Trigger names must not be longer than %s characters.',
+ new PhutilNumber($max_length)),
+ $xaction);
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhabricatorProjectTriggerTransactionType
+ extends PhabricatorModularTransactionType {}
diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php
--- a/src/applications/search/controller/PhabricatorApplicationSearchController.php
+++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php
@@ -274,9 +274,10 @@
throw new Exception(
pht(
'SearchEngines must render a "%s" object, but this engine '.
- '(of class "%s") rendered something else.',
+ '(of class "%s") rendered something else ("%s").',
'PhabricatorApplicationSearchResultView',
- get_class($engine)));
+ get_class($engine),
+ phutil_describe_type($list)));
}
if ($list->getObjectList()) {

File Metadata

Mime Type
text/plain
Expires
Mon, Mar 24, 10:45 AM (4 d, 10 h ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/bu/j7/b5xnbnpg6z2l7amc
Default Alt Text
D20279.id48394.diff (39 KB)

Event Timeline