Page MenuHomePhabricator
Paste P2009

Custom Actions "framework"
ActivePublic

Authored by avivey on Sep 21 2016, 7:41 PM.
Tags
None
Referenced Files
F1843268: Custom Actions "framework"
Sep 21 2016, 7:41 PM
Subscribers
None
<?php
/**
* This is just a thin layer on top of the regular Form patern, which mostly
* saves a bit of boilerplate.
* It's not particularly good, but it's better then replicating the code for
* each action.
* It also assumes that each Action is exactly "Apply one Transaction", which is
* an over-simplification.
*/
abstract class MagicReleaseCustomAction extends Phobject {
private $viewer;
abstract protected function getFormTitle(PhabricatorReleaseRelease $release);
abstract protected function getFormPremble(
PhabricatorReleaseRelease $release);
abstract public function isAllowedForTemplateKey($template_key);
public function getSubmitText() {
return pht('Submit');
}
abstract public function getActionItemText();
public function getActionItemIcon() {
return null;
}
public function initialValidationsForRelease(
PhabricatorReleaseRelease $release) {
$errors = array();
$template_key = $release->getReleaseTemplateKey();
if (!$this->isAllowedForTemplateKey($template_key)) {
$errors[] = 'This Release type does not support this action';
return $errors;
}
if ($this->requiresBranches()) {
$branch_name = $release->getBranchNameForAllRepos();
if (!strlen($branch_name)) {
$errors[] =
'This revision doesn\'t have a consistent branches, so we can\'t '.
'update it.';
}
}
if ($this->requiresNoBranches()) {
$refs = $release->getCurrentRefs();
foreach ($refs as $ref) {
if ($release->isBranch($ref)) {
$errors[] =
'This release has branches, so this action does not apply.';
break;
}
}
}
return $errors;
}
protected function requiresBranches() {
return false;
}
protected function requiresNoBranches() {
return false;
}
public function needsEditPermission() {
return true;
}
public function assertPolicy($release) {
// maybe `get required capabilities()?
if ($this->needsEditPermission()) {
// it would be nicer to fold this into $errors[] instead of exception.
PhabricatorPolicyFilter::requireCapability(
$this->getViewer(),
$release,
PhabricatorPolicyCapability::CAN_EDIT);
}
}
/**
* Get initial values to put in the form. Returns array of fields.
*/
abstract public function getDefaultFieldValues(
PhabricatorReleaseRelease $release);
/**
* Build the form. $fields is the output of getDefaultFieldValues() or
* handleForm().
*/
abstract public function buildForm(
PhabricatorReleaseRelease $release,
array $fields);
/**
* Receives the dialog Post; Updates $fields, and any validation errors.
* Doesn't actually take any action.
* return array($fields, $errors)
*/
abstract public function handleFormPost(
PhabricatorReleaseRelease $release,
AphrontRequest $request,
array $fields);
/**
* The transaction will actually do the action.
* This method is only called if both handleFormPost() and
* initialValidationsForRelease() return no errors.
*
* return TransactionType (string) that will run the action.
*/
abstract public function getTransactionType(
PhabricatorReleaseRelease $release,
array $fields);
/**
* Produce the transaction's initial value; This will be updated again
* by the transaction itself.
*/
abstract public function generateTransactionValue(
PhabricatorReleaseRelease $release,
array $fields);
/**
* Where to go after applying the transaction.
*/
public function getResultURI(
PhabricatorReleaseRelease $release,
array $fields,
PhabricatorReleaseReleaseTransaction $xaction) {
return $release->getURI();
}
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
}
final public function getViewer() {
return $this->viewer;
}
}
final class MagicReleaseCustomActionController extends PhabricatorController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$release_id = $request->getURIData('release_id');
$release = id(new PhabricatorReleaseReleaseQuery())
->setViewer($viewer)
->withIDs(array($release_id))
->executeOne();
if (!$release) {
return new Aphront404Response();
}
$action_class = $request->getURIData('action');
$custom_action = is_subclass_of(
$action_class,
'MagicReleaseCustomAction');
if ($custom_action) {
$action = newv($action_class, array());
$action->setViewer($viewer);
} else {
throw new Exception(
pht(
"Action type must be a valid class name and must subclass ".
"%s. '%s' is not a subclass of %s",
'MagicReleaseCustomAction',
$this->strategyClass,
'MagicReleaseCustomAction'));
}
$action->assertPolicy($release);
$fields = $action->getDefaultFieldValues($release);
$errors = $action->initialValidationsForRelease($release);
if ($request->isDialogFormPost()) {
list($fields, $errors2) =
$action->handleFormPost($release, $request, $fields);
$errors = array_merge($errors, $errors2);
if (!$errors) {
$xaction_type = $action->getTransactionType($release, $fields);
$value = $action->generateTransactionValue($release, $fields);
$xaction = id(new PhabricatorReleaseReleaseTransaction())
->setTransactionType($xaction_type)
->setNewValue($value);
$editor = id(new PhabricatorReleaseReleaseEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
try {
$editor->applyTransactions($release, array($xaction));
$uri = $action->getResultURI($release, $fields, $xaction);
return id(new AphrontRedirectResponse())->setURI($uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$errors[] = 'Failed to initiate action! '.$ex->getMessage();
}
}
}
$preamble = $action->getFormPremble($release);
$preamble = new PHUIRemarkupView($viewer, $preamble);
$form = $action->buildForm($release, $fields);
$form->setViewer($viewer);
return $this->newDialog()
->setSubmitURI($request->getRequestURI())
->setTitle($action->getFormTitle($release))
->appendChild($preamble)
->setErrors($errors)
->appendForm($form)
->addSubmitButton($action->getSubmitText())
->addCancelButton('#');
}
}
final class MagicRenderEventListener extends PhabricatorEventListener {
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS:
if ($object instanceof PhabricatorReleaseRelease) {
$this->addReleaseActions($event);
}
break;
}
}
private function addReleaseActions(PhutilEvent $event) {
$release = $event->getValue('object');
$release_id = $release->getID();
$template_key = $release->getReleaseTemplateKey();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$event->getUser(),
$release,
PhabricatorPolicyCapability::CAN_EDIT);
$actions = array();
$custom_actions = id(new PhutilClassMapQuery())
->setAncestorClass('MagicReleaseCustomAction')
->execute();
foreach ($custom_actions as $custom_action) {
if ($custom_action->isAllowedForTemplateKey($template_key)) {
$action_class = get_class($custom_action);
$action = id(new PhabricatorActionView())
->setName($custom_action->getActionItemText())
->setWorkflow(true)
->setIcon($custom_action->getActionItemIcon())
->setHref("/magic/release/{$release_id}/action/{$action_class}/");
if ($custom_action->needsEditPermission()) {
$action->setDisabled(!$can_edit);
}
$actions[] = $action;
}
}
$actions[] = id(new PhabricatorActionView())
->setName('Compare to Another Release')
->setWorkflow(true)
->setIcon('fa-search')
->setHref("/magic/release/{$release_id}/compare/");
$this->addActionMenuItems($event, $actions);
}
}

Event Timeline

avivey changed the visibility from "All Users" to "Public (No Login Required)".

I was writing a lot of "Custom Actions", so I built this wrapper around the form handling pattern that was replicated around a lot.