diff --git a/resources/sql/autopatches/20160322.nuance.01.itemcommand.sql b/resources/sql/autopatches/20160322.nuance.01.itemcommand.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20160322.nuance.01.itemcommand.sql @@ -0,0 +1,8 @@ +CREATE TABLE {$NAMESPACE}_nuance.nuance_itemcommand ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + itemPHID VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + command VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}, + parameters LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_item` (itemPHID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; 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 @@ -1440,6 +1440,8 @@ 'NuanceImportCursorPHIDType' => 'applications/nuance/phid/NuanceImportCursorPHIDType.php', 'NuanceItem' => 'applications/nuance/storage/NuanceItem.php', 'NuanceItemActionController' => 'applications/nuance/controller/NuanceItemActionController.php', + 'NuanceItemCommand' => 'applications/nuance/storage/NuanceItemCommand.php', + 'NuanceItemCommandQuery' => 'applications/nuance/query/NuanceItemCommandQuery.php', 'NuanceItemController' => 'applications/nuance/controller/NuanceItemController.php', 'NuanceItemEditor' => 'applications/nuance/editor/NuanceItemEditor.php', 'NuanceItemListController' => 'applications/nuance/controller/NuanceItemListController.php', @@ -5728,6 +5730,11 @@ 'PhabricatorApplicationTransactionInterface', ), 'NuanceItemActionController' => 'NuanceController', + 'NuanceItemCommand' => array( + 'NuanceDAO', + 'PhabricatorPolicyInterface', + ), + 'NuanceItemCommandQuery' => 'NuanceQuery', 'NuanceItemController' => 'NuanceController', 'NuanceItemEditor' => 'PhabricatorApplicationTransactionEditor', 'NuanceItemListController' => 'NuanceItemController', diff --git a/src/applications/nuance/controller/NuanceItemViewController.php b/src/applications/nuance/controller/NuanceItemViewController.php --- a/src/applications/nuance/controller/NuanceItemViewController.php +++ b/src/applications/nuance/controller/NuanceItemViewController.php @@ -26,6 +26,17 @@ $curtain = $this->buildCurtain($item); $content = $this->buildContent($item); + $commands = $this->buildCommands($item); + + $timeline = $this->buildTransactionTimeline( + $item, + new NuanceItemTransactionQuery()); + + $main = array( + $commands, + $content, + $timeline, + ); $header = id(new PHUIHeaderView()) ->setHeader($name); @@ -33,7 +44,7 @@ $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) - ->setMainColumn($content); + ->setMainColumn($main); return $this->newPage() ->setTitle($title) @@ -76,4 +87,36 @@ return $impl->buildItemView($item); } + private function buildCommands(NuanceItem $item) { + $viewer = $this->getViewer(); + + $commands = id(new NuanceItemCommandQuery()) + ->setViewer($viewer) + ->withItemPHIDs(array($item->getPHID())) + ->execute(); + $commands = msort($commands, 'getID'); + + if (!$commands) { + return null; + } + + $rows = array(); + foreach ($commands as $command) { + $rows[] = array( + $command->getCommand(), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('Command'), + )); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Pending Commands')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + } diff --git a/src/applications/nuance/editor/NuanceItemEditor.php b/src/applications/nuance/editor/NuanceItemEditor.php --- a/src/applications/nuance/editor/NuanceItemEditor.php +++ b/src/applications/nuance/editor/NuanceItemEditor.php @@ -19,6 +19,7 @@ $types[] = NuanceItemTransaction::TYPE_REQUESTOR; $types[] = NuanceItemTransaction::TYPE_PROPERTY; $types[] = NuanceItemTransaction::TYPE_QUEUE; + $types[] = NuanceItemTransaction::TYPE_COMMAND; $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorTransactions::TYPE_COMMENT; @@ -45,6 +46,8 @@ $key = $xaction->getMetadataValue( NuanceItemTransaction::PROPERTY_KEY); return $object->getNuanceProperty($key); + case NuanceItemTransaction::TYPE_COMMAND: + return null; } return parent::getCustomTransactionOldValue($object, $xaction); @@ -60,6 +63,7 @@ case NuanceItemTransaction::TYPE_OWNER: case NuanceItemTransaction::TYPE_PROPERTY: case NuanceItemTransaction::TYPE_QUEUE: + case NuanceItemTransaction::TYPE_COMMAND: return $xaction->getNewValue(); } @@ -88,6 +92,8 @@ NuanceItemTransaction::PROPERTY_KEY); $object->setNuanceProperty($key, $xaction->getNewValue()); break; + case NuanceItemTransaction::TYPE_COMMAND: + break; } } @@ -101,6 +107,7 @@ case NuanceItemTransaction::TYPE_OWNER: case NuanceItemTransaction::TYPE_PROPERTY: case NuanceItemTransaction::TYPE_QUEUE: + case NuanceItemTransaction::TYPE_COMMAND: return; } diff --git a/src/applications/nuance/item/NuanceGitHubEventItemType.php b/src/applications/nuance/item/NuanceGitHubEventItemType.php --- a/src/applications/nuance/item/NuanceGitHubEventItemType.php +++ b/src/applications/nuance/item/NuanceGitHubEventItemType.php @@ -147,6 +147,12 @@ public function getItemActions(NuanceItem $item) { $actions = array(); + $actions[] = $this->newItemAction($item, 'sync') + ->setName(pht('Import to Maniphest')) + ->setIcon('fa-anchor') + ->setWorkflow(true) + ->setRenderAsForm(true); + $actions[] = $this->newItemAction($item, 'raw') ->setName(pht('View Raw Event')) ->setWorkflow(true) @@ -156,6 +162,7 @@ } protected function handleAction(NuanceItem $item, $action) { + $viewer = $this->getViewer(); $controller = $this->getController(); switch ($action) { @@ -181,6 +188,9 @@ ->setTitle(pht('GitHub Raw Event')) ->appendForm($form) ->addCancelButton($item->getURI(), pht('Done')); + case 'sync': + $item->issueCommand($viewer->getPHID(), $action); + return id(new AphrontRedirectResponse())->setURI($item->getURI()); } return null; @@ -228,5 +238,9 @@ ->appendChild($property_list); } + protected function handleCommand(NuanceItem $item, $action) { + return null; + } + } diff --git a/src/applications/nuance/item/NuanceItemType.php b/src/applications/nuance/item/NuanceItemType.php --- a/src/applications/nuance/item/NuanceItemType.php +++ b/src/applications/nuance/item/NuanceItemType.php @@ -96,4 +96,43 @@ return null; } + final public function applyCommand( + NuanceItem $item, + NuanceItemCommand $command) { + + $result = $this->handleCommand($item, $command); + + if ($result === null) { + return; + } + + $xaction = id(new NuanceItemTransaction()) + ->setTransactionType(NuanceItemTransaction::TYPE_COMMAND) + ->setNewValue( + array( + 'command' => $command->getCommand(), + 'parameters' => $command->getParameters(), + 'result' => $result, + )); + + $viewer = $this->getViewer(); + + // TODO: Maybe preserve the actor's original content source? + $source = PhabricatorContentSource::newForSource( + PhabricatorContentSource::SOURCE_DAEMON, + array()); + + $editor = id(new NuanceItemEditor()) + ->setActor($viewer) + ->setActingAsPHID($command->getAuthorPHID()) + ->setContentSource($source) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->applyTransactions($item, array($xaction)); + } + + protected function handleCommand(NuanceItem $item, $action) { + return null; + } + } diff --git a/src/applications/nuance/query/NuanceItemCommandQuery.php b/src/applications/nuance/query/NuanceItemCommandQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/nuance/query/NuanceItemCommandQuery.php @@ -0,0 +1,47 @@ +ids = $ids; + return $this; + } + + public function withItemPHIDs(array $item_phids) { + $this->itemPHIDs = $item_phids; + return $this; + } + + public function newResultObject() { + return new NuanceItemCommand(); + } + + 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->itemPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'itemPHID IN (%Ls)', + $this->itemPHIDs); + } + + return $where; + } + +} diff --git a/src/applications/nuance/storage/NuanceItem.php b/src/applications/nuance/storage/NuanceItem.php --- a/src/applications/nuance/storage/NuanceItem.php +++ b/src/applications/nuance/storage/NuanceItem.php @@ -154,6 +154,23 @@ )); } + public function issueCommand( + $author_phid, + $command, + array $parameters = array()) { + + $command = id(NuanceItemCommand::initializeNewCommand()) + ->setItemPHID($this->getPHID()) + ->setAuthorPHID($author_phid) + ->setCommand($command) + ->setParameters($parameters) + ->save(); + + $this->scheduleUpdate(); + + return $this; + } + public function getImplementation() { return $this->assertAttached($this->implementation); } diff --git a/src/applications/nuance/storage/NuanceItemCommand.php b/src/applications/nuance/storage/NuanceItemCommand.php new file mode 100644 --- /dev/null +++ b/src/applications/nuance/storage/NuanceItemCommand.php @@ -0,0 +1,55 @@ + false, + self::CONFIG_SERIALIZATION => array( + 'parameters' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'command' => 'text64', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_item' => array( + 'columns' => array('itemPHID'), + ), + ), + ) + parent::getConfiguration(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::getMostOpenPolicy(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + +} diff --git a/src/applications/nuance/storage/NuanceItemTransaction.php b/src/applications/nuance/storage/NuanceItemTransaction.php --- a/src/applications/nuance/storage/NuanceItemTransaction.php +++ b/src/applications/nuance/storage/NuanceItemTransaction.php @@ -10,6 +10,7 @@ const TYPE_SOURCE = 'nuance.item.source'; const TYPE_PROPERTY = 'nuance.item.property'; const TYPE_QUEUE = 'nuance.item.queue'; + const TYPE_COMMAND = 'nuance.item.command'; public function getApplicationTransactionType() { return NuanceItemPHIDType::TYPECONST; @@ -65,6 +66,12 @@ '%s routed this item to the %s queue.', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); + case self::TYPE_COMMAND: + // TODO: Give item types a chance to render this properly. + return pht( + '%s applied command "%s" to this item.', + $this->renderHandleLink($author_phid), + idx($new, 'command')); } return parent::getTitle(); diff --git a/src/applications/nuance/worker/NuanceItemUpdateWorker.php b/src/applications/nuance/worker/NuanceItemUpdateWorker.php --- a/src/applications/nuance/worker/NuanceItemUpdateWorker.php +++ b/src/applications/nuance/worker/NuanceItemUpdateWorker.php @@ -15,6 +15,7 @@ $item = $this->loadItem($item_phid); $this->updateItem($item); $this->routeItem($item); + $this->applyCommands($item); } catch (Exception $ex) { $lock->unlock(); throw $ex; @@ -51,4 +52,22 @@ ->save(); } + private function applyCommands(NuanceItem $item) { + $viewer = $this->getViewer(); + + $impl = $item->getImplementation(); + $impl->setViewer($viewer); + + $commands = id(new NuanceItemCommandQuery()) + ->setViewer($viewer) + ->withItemPHIDs(array($item->getPHID())) + ->execute(); + $commands = msort($commands, 'getID'); + + foreach ($commands as $command) { + $impl->applyCommand($item, $command); + $command->delete(); + } + } + }