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[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 --- 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + '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 @@ +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 @@ +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 @@ +getObjectList()) {