From 252b6f2260561a77564d175f15ee75f43999f6c7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Mar 2019 08:29:14 -0700 Subject: [PATCH] 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 --- .../20190312.triggers.01.trigger.sql | 9 + .../20190312.triggers.02.xaction.sql | 19 ++ .../20190312.triggers.03.triggerphid.sql | 2 + src/__phutil_library_map__.php | 31 +++ .../PhabricatorProjectApplication.php | 8 + .../PhabricatorProjectBoardViewController.php | 40 +++- .../PhabricatorProjectTriggerController.php | 16 ++ ...habricatorProjectTriggerEditController.php | 197 ++++++++++++++++++ ...habricatorProjectTriggerListController.php | 16 ++ ...habricatorProjectTriggerViewController.php | 168 +++++++++++++++ .../PhabricatorProjectTriggerEditor.php | 30 +++ .../engine/PhabricatorBoardLayoutEngine.php | 1 + .../PhabricatorProjectTriggerPHIDType.php | 45 ++++ .../query/PhabricatorProjectColumnQuery.php | 55 +++++ .../query/PhabricatorProjectTriggerQuery.php | 51 +++++ .../PhabricatorProjectTriggerSearchEngine.php | 75 +++++++ ...bricatorProjectTriggerTransactionQuery.php | 10 + .../storage/PhabricatorProjectColumn.php | 39 ++++ .../storage/PhabricatorProjectTrigger.php | 108 ++++++++++ .../PhabricatorProjectTriggerTransaction.php | 18 ++ ...abricatorProjectTriggerNameTransaction.php | 58 ++++++ ...abricatorProjectTriggerTransactionType.php | 4 + ...PhabricatorApplicationSearchController.php | 5 +- 23 files changed, 999 insertions(+), 6 deletions(-) create mode 100644 resources/sql/autopatches/20190312.triggers.01.trigger.sql create mode 100644 resources/sql/autopatches/20190312.triggers.02.xaction.sql create mode 100644 resources/sql/autopatches/20190312.triggers.03.triggerphid.sql create mode 100644 src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php create mode 100644 src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php create mode 100644 src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php create mode 100644 src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php create mode 100644 src/applications/project/editor/PhabricatorProjectTriggerEditor.php create mode 100644 src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php create mode 100644 src/applications/project/query/PhabricatorProjectTriggerQuery.php create mode 100644 src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php create mode 100644 src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php create mode 100644 src/applications/project/storage/PhabricatorProjectTrigger.php create mode 100644 src/applications/project/storage/PhabricatorProjectTriggerTransaction.php create mode 100644 src/applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php create mode 100644 src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php diff --git a/resources/sql/autopatches/20190312.triggers.01.trigger.sql b/resources/sql/autopatches/20190312.triggers.01.trigger.sql new file mode 100644 index 0000000000..301a3a62cd --- /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 index 0000000000..1a6034c4b1 --- /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 index 0000000000..271d679cfa --- /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 index 876892943f..258ebf1f8f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4166,6 +4166,19 @@ phutil_register_library_map(array( '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 @@ phutil_register_library_map(array( '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 index 0e1a9f37c7..192b40f6cd 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -89,6 +89,14 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { 'background/' => 'PhabricatorProjectBoardBackgroundController', ), + 'trigger/' => array( + $this->getQueryRoutePattern() => + 'PhabricatorProjectTriggerListController', + '(?P[1-9]\d*)/' => + 'PhabricatorProjectTriggerViewController', + $this->getEditRoutePattern('edit/') => + 'PhabricatorProjectTriggerEditController', + ), 'update/(?P[1-9]\d*)/(?P[^/]+)/' => 'PhabricatorProjectUpdateController', 'manage/(?P[1-9]\d*)/' => 'PhabricatorProjectManageController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index a1dcd6ab68..9b882c6ccc 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -1111,10 +1111,8 @@ final class PhabricatorProjectBoardViewController )); } - 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 @@ final class PhabricatorProjectBoardViewController ->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 index 0000000000..ea729e82a4 --- /dev/null +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php @@ -0,0 +1,16 @@ +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 index 0000000000..86f75225d2 --- /dev/null +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php @@ -0,0 +1,197 @@ +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 index 0000000000..62e5430f26 --- /dev/null +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php @@ -0,0 +1,16 @@ +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 index 0000000000..d1966cf106 --- /dev/null +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php @@ -0,0 +1,168 @@ +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 index 0000000000..20098fa370 --- /dev/null +++ b/src/applications/project/editor/PhabricatorProjectTriggerEditor.php @@ -0,0 +1,30 @@ +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 index 0000000000..346b0e69fa --- /dev/null +++ b/src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php @@ -0,0 +1,45 @@ +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 index 441c33e8cb..03169a7827 100644 --- a/src/applications/project/query/PhabricatorProjectColumnQuery.php +++ b/src/applications/project/query/PhabricatorProjectColumnQuery.php @@ -9,6 +9,8 @@ final class PhabricatorProjectColumnQuery private $proxyPHIDs; private $statuses; private $isProxyColumn; + private $triggerPHIDs; + private $needTriggers; public function withIDs(array $ids) { $this->ids = $ids; @@ -40,6 +42,16 @@ final class PhabricatorProjectColumnQuery 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 @@ final class PhabricatorProjectColumnQuery $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 @@ final class PhabricatorProjectColumnQuery $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 index 0000000000..e3fab5b3d0 --- /dev/null +++ b/src/applications/project/query/PhabricatorProjectTriggerQuery.php @@ -0,0 +1,51 @@ +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 index 0000000000..6c6a417723 --- /dev/null +++ b/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php @@ -0,0 +1,75 @@ +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 index 0000000000..9ec4d4a53b --- /dev/null +++ b/src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php @@ -0,0 +1,10 @@ + 'uint32', 'sequence' => 'uint32', 'proxyPHID' => 'phid?', + 'triggerPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( @@ -52,6 +55,9 @@ final class PhabricatorProjectColumn 'columns' => array('projectPHID', 'proxyPHID'), 'unique' => true, ), + 'key_trigger' => array( + 'columns' => array('triggerPHID'), + ), ), ) + parent::getConfiguration(); } @@ -180,6 +186,39 @@ final class PhabricatorProjectColumn 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 index 0000000000..7730d90529 --- /dev/null +++ b/src/applications/project/storage/PhabricatorProjectTrigger.php @@ -0,0 +1,108 @@ +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 index 0000000000..fb94bdc364 --- /dev/null +++ b/src/applications/project/storage/PhabricatorProjectTriggerTransaction.php @@ -0,0 +1,18 @@ +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 index 0000000000..30222e1e2c --- /dev/null +++ b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php @@ -0,0 +1,4 @@ +getObjectList()) {