Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15475521
D20279.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
39 KB
Referenced Files
None
Subscribers
None
D20279.diff
View Options
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
@@ -4166,6 +4166,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',
@@ -10268,6 +10281,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
@@ -1111,10 +1111,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());
@@ -1174,6 +1172,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
@@ -276,9 +276,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
Details
Attached
Mime Type
text/plain
Expires
Mon, Apr 7, 8:07 PM (2 d, 9 h ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/uf/io/r2usxnnekli5zakq
Default Alt Text
D20279.diff (39 KB)
Attached To
Mode
D20279: Provide basic scaffolding for workboard column triggers
Attached
Detach File
Event Timeline
Log In to Comment