1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-26 00:32:42 +01:00

Update Herald rule creation workflow to use more modern UI elements

Summary: Ref T13480. Creating a rule in Herald currently uses the older radio-button flow. Update it to the "clickable menu" flow to simplify it a little bit.

Test Plan: Created new personal, object, and global rules. Hit the object rule error conditions.

Maniphest Tasks: T13480

Differential Revision: https://secure.phabricator.com/D20956
This commit is contained in:
epriestley 2020-01-29 11:33:31 -08:00
parent 4904d7711e
commit 6d4c6924d6
5 changed files with 315 additions and 246 deletions

View file

@ -9,7 +9,7 @@ return array(
'names' => array( 'names' => array(
'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.css' => '3c8a0668',
'conpherence.pkg.js' => '020aebcf', 'conpherence.pkg.js' => '020aebcf',
'core.pkg.css' => '6d9a0ba6', 'core.pkg.css' => '5edb4679',
'core.pkg.js' => '705aec2c', 'core.pkg.js' => '705aec2c',
'differential.pkg.css' => '607c84be', 'differential.pkg.css' => '607c84be',
'differential.pkg.js' => '1b97518d', 'differential.pkg.js' => '1b97518d',
@ -165,7 +165,7 @@ return array(
'rsrc/css/phui/phui-left-right.css' => '68513c34', 'rsrc/css/phui/phui-left-right.css' => '68513c34',
'rsrc/css/phui/phui-lightbox.css' => '4ebf22da', 'rsrc/css/phui/phui-lightbox.css' => '4ebf22da',
'rsrc/css/phui/phui-list.css' => 'b05144dd', 'rsrc/css/phui/phui-list.css' => 'b05144dd',
'rsrc/css/phui/phui-object-box.css' => 'f434b6be', 'rsrc/css/phui/phui-object-box.css' => 'b8d7eea0',
'rsrc/css/phui/phui-pager.css' => 'd022c7ad', 'rsrc/css/phui/phui-pager.css' => 'd022c7ad',
'rsrc/css/phui/phui-pinboard-view.css' => '1f08f5d8', 'rsrc/css/phui/phui-pinboard-view.css' => '1f08f5d8',
'rsrc/css/phui/phui-policy-section-view.css' => '139fdc64', 'rsrc/css/phui/phui-policy-section-view.css' => '139fdc64',
@ -855,7 +855,7 @@ return array(
'phui-left-right-css' => '68513c34', 'phui-left-right-css' => '68513c34',
'phui-lightbox-css' => '4ebf22da', 'phui-lightbox-css' => '4ebf22da',
'phui-list-view-css' => 'b05144dd', 'phui-list-view-css' => 'b05144dd',
'phui-object-box-css' => 'f434b6be', 'phui-object-box-css' => 'b8d7eea0',
'phui-oi-big-ui-css' => 'fa74cc35', 'phui-oi-big-ui-css' => 'fa74cc35',
'phui-oi-color-css' => 'b517bfa0', 'phui-oi-color-css' => 'b517bfa0',
'phui-oi-drag-ui-css' => 'da15d3dc', 'phui-oi-drag-ui-css' => 'da15d3dc',

View file

@ -243,6 +243,12 @@ abstract class HeraldAdapter extends Phobject {
abstract public function getAdapterApplicationClass(); abstract public function getAdapterApplicationClass();
abstract public function getObject(); abstract public function getObject();
public function getAdapterContentIcon() {
$application_class = $this->getAdapterApplicationClass();
$application = newv($application_class, array());
return $application->getIcon();
}
/** /**
* Return a new characteristic object for this adapter. * Return a new characteristic object for this adapter.
* *

View file

@ -3,42 +3,39 @@
final class HeraldNewController extends HeraldController { final class HeraldNewController extends HeraldController {
public function handleRequest(AphrontRequest $request) { public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer(); $viewer = $this->getViewer();
$adapter_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);
$adapter_type = $request->getStr('adapter');
if (!isset($adapter_type_map[$adapter_type])) {
$title = pht('Create Herald Rule');
$content = $this->newAdapterMenu($title);
} else {
$adapter = HeraldAdapter::getAdapterForContentType($adapter_type);
$content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
$rule_type = $request->getStr('type');
if (!isset($rule_type_map[$rule_type])) {
$title = pht(
'Create Herald Rule: %s',
$adapter->getAdapterContentName());
$content = $this->newTypeMenu($adapter, $title);
} else {
if ($rule_type !== HeraldRuleTypeConfig::RULE_TYPE_OBJECT) {
$target_phid = null;
$target_okay = true;
} else {
$object_name = $request->getStr('objectName');
$target_okay = false;
$errors = array(); $errors = array();
$e_type = null;
$e_rule = null;
$e_object = null; $e_object = null;
$step = $request->getInt('step');
if ($request->isFormPost()) { if ($request->isFormPost()) {
$content_type = $request->getStr('content_type'); if (strlen($object_name)) {
if (empty($content_type_map[$content_type])) {
$errors[] = pht('You must choose a content type for this rule.');
$e_type = pht('Required');
$step = 0;
}
if (!$errors && $step > 1) {
$rule_type = $request->getStr('rule_type');
if (empty($rule_type_map[$rule_type])) {
$errors[] = pht('You must choose a rule type for this rule.');
$e_rule = pht('Required');
$step = 1;
}
}
if (!$errors && $step >= 2) {
$target_phid = null;
$object_name = $request->getStr('objectName');
$done = false;
if ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_OBJECT) {
$done = true;
} else if (strlen($object_name)) {
$target_object = id(new PhabricatorObjectQuery()) $target_object = id(new PhabricatorObjectQuery())
->setViewer($viewer) ->setViewer($viewer)
->withNames(array($object_name)) ->withNames(array($object_name))
@ -50,99 +47,255 @@ final class HeraldNewController extends HeraldController {
PhabricatorPolicyCapability::CAN_EDIT); PhabricatorPolicyCapability::CAN_EDIT);
if (!$can_edit) { if (!$can_edit) {
$errors[] = pht( $errors[] = pht(
'You can not create a rule for that object, because you do '. 'You can not create a rule for that object, because you '.
'not have permission to edit it. You can only create rules '. 'do not have permission to edit it. You can only create '.
'for objects you can edit.'); 'rules for objects you can edit.');
$e_object = pht('Not Editable'); $e_object = pht('Not Editable');
$step = 2;
} else { } else {
$adapter = HeraldAdapter::getAdapterForContentType($content_type);
if (!$adapter->canTriggerOnObject($target_object)) { if (!$adapter->canTriggerOnObject($target_object)) {
$errors[] = pht( $errors[] = pht(
'This object is not of an allowed type for the rule. '. 'This object is not of an allowed type for the rule. '.
'Rules can only trigger on certain objects.'); 'Rules can only trigger on certain objects.');
$e_object = pht('Invalid'); $e_object = pht('Invalid');
$step = 2;
} else { } else {
$target_phid = $target_object->getPHID(); $target_phid = $target_object->getPHID();
$done = true;
} }
} }
} else { } else {
$errors[] = pht('No object exists by that name.'); $errors[] = pht('No object exists by that name.');
$e_object = pht('Invalid'); $e_object = pht('Invalid');
$step = 2;
} }
} else if ($step > 2) { } else {
$errors[] = pht( $errors[] = pht(
'You must choose an object to associate this rule with.'); 'You must choose an object to associate this rule with.');
$e_object = pht('Required'); $e_object = pht('Required');
$step = 2;
} }
if (!$errors && $done) { $target_okay = !$errors;
}
}
if (!$target_okay) {
$title = pht('Choose Object');
$content = $this->newTargetForm(
$adapter,
$rule_type,
$object_name,
$errors,
$e_object,
$title);
} else {
$params = array( $params = array(
'content_type' => $content_type, 'content_type' => $adapter_type,
'rule_type' => $rule_type, 'rule_type' => $rule_type,
'targetPHID' => $target_phid, 'targetPHID' => $target_phid,
); );
$uri = new PhutilURI('edit/', $params); $edit_uri = $this->getApplicationURI('edit/');
$uri = $this->getApplicationURI($uri); $edit_uri = new PhutilURI($edit_uri, $params);
return id(new AphrontRedirectResponse())->setURI($uri);
return id(new AphrontRedirectResponse())
->setURI($edit_uri);
} }
} }
} }
$content_type = $request->getStr('content_type'); $crumbs = $this
$rule_type = $request->getStr('rule_type'); ->buildApplicationCrumbs()
->addTextCrumb(pht('Create Rule'))
->setBorder(true);
$form = id(new AphrontFormView()) $view = id(new PHUITwoColumnView())
->setUser($viewer) ->setFooter($content);
->setAction($this->getApplicationURI('new/'));
switch ($step) { return $this->newPage()
case 0: ->setTitle($title)
default: ->setCrumbs($crumbs)
$content_types = $this->renderContentTypeControl( ->appendChild($view);
$content_type_map, }
$e_type);
$form private function newAdapterMenu($title) {
->addHiddenInput('step', 1) $viewer = $this->getViewer();
->appendChild($content_types);
$cancel_text = null; $types = HeraldAdapter::getEnabledAdapterMap($viewer);
$cancel_uri = $this->getApplicationURI();
$title = pht('Create Herald Rule');
break;
case 1:
$rule_types = $this->renderRuleTypeControl(
$rule_type_map,
$e_rule);
$form foreach ($types as $key => $type) {
->addHiddenInput('content_type', $content_type) $types[$key] = HeraldAdapter::getAdapterForContentType($key);
->addHiddenInput('step', 2) }
->appendChild($rule_types);
$params = array( $types = msort($types, 'getAdapterContentName');
'content_type' => $content_type,
'step' => '0', $base_uri = $this->getApplicationURI('create/');
$menu = id(new PHUIObjectItemListView())
->setViewer($viewer)
->setBig(true);
foreach ($types as $key => $adapter) {
$adapter_uri = id(new PhutilURI($base_uri))
->replaceQueryParam('adapter', $key);
$description = $adapter->getAdapterContentDescription();
$description = phutil_escape_html_newlines($description);
$item = id(new PHUIObjectItemView())
->setHeader($adapter->getAdapterContentName())
->setImageIcon($adapter->getAdapterContentIcon())
->addAttribute($description)
->setHref($adapter_uri)
->setClickable(true);
$menu->addItem($item);
}
$box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setObjectList($menu);
return id(new PHUILauncherView())
->appendChild($box);
}
private function newTypeMenu(HeraldAdapter $adapter, $title) {
$viewer = $this->getViewer();
$global_capability = HeraldManageGlobalRulesCapability::CAPABILITY;
$can_global = $this->hasApplicationCapability($global_capability);
if ($can_global) {
$global_note = pht(
'You have permission to create and manage global rules.');
} else {
$global_note = pht(
'You do not have permission to create or manage global rules.');
}
$global_note = phutil_tag('em', array(), $global_note);
$specs = array(
HeraldRuleTypeConfig::RULE_TYPE_PERSONAL => array(
'name' => pht('Personal Rule'),
'icon' => 'fa-user',
'help' => pht(
'Personal rules notify you about events. You own them, but they can '.
'only affect you. Personal rules only trigger for objects you have '.
'permission to see.'),
'enabled' => true,
),
HeraldRuleTypeConfig::RULE_TYPE_OBJECT => array(
'name' => pht('Object Rule'),
'icon' => 'fa-cube',
'help' => pht(
'Object rules notify anyone about events. They are bound to an '.
'object (like a repository) and can only act on that object. You '.
'must be able to edit an object to create object rules for it. '.
'Other users who can edit the object can edit its rules.'),
'enabled' => true,
),
HeraldRuleTypeConfig::RULE_TYPE_GLOBAL => array(
'name' => pht('Global Rule'),
'icon' => 'fa-globe',
'help' => array(
pht(
'Global rules notify anyone about events. Global rules can '.
'bypass access control policies and act on any object.'),
$global_note,
),
'enabled' => $can_global,
),
); );
$cancel_text = pht('Back'); $adapter_type = $adapter->getAdapterContentType();
$cancel_uri = new PhutilURI('new/', $params);
$cancel_uri = $this->getApplicationURI($cancel_uri); $base_uri = new PhutilURI($this->getApplicationURI('create/'));
$title = pht('Create Herald Rule: %s',
idx($content_type_map, $content_type)); $adapter_uri = id(clone $base_uri)
break; ->replaceQueryParam('adapter', $adapter_type);
case 2:
$adapter = HeraldAdapter::getAdapterForContentType($content_type); $menu = id(new PHUIObjectItemListView())
$form ->setUser($viewer)
->addHiddenInput('content_type', $content_type) ->setBig(true);
->addHiddenInput('rule_type', $rule_type)
->addHiddenInput('step', 3) foreach ($specs as $rule_type => $spec) {
$type_uri = id(clone $adapter_uri)
->replaceQueryParam('type', $rule_type);
$name = $spec['name'];
$icon = $spec['icon'];
$description = $spec['help'];
$description = (array)$description;
$enabled = $spec['enabled'];
if ($enabled) {
$enabled = $adapter->supportsRuleType($rule_type);
if (!$enabled) {
$description[] = phutil_tag(
'em',
array(),
pht(
'This rule type is not supported by the selected '.
'content type.'));
}
}
$description = phutil_implode_html(
array(
phutil_tag('br'),
phutil_tag('br'),
),
$description);
$item = id(new PHUIObjectItemView())
->setHeader($name)
->setImageIcon($icon)
->addAttribute($description);
if ($enabled) {
$item
->setHref($type_uri)
->setClickable(true);
} else {
$item->setDisabled(true);
}
$menu->addItem($item);
}
$box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setObjectList($menu);
$box->newTailButton()
->setText(pht('Back to Content Types'))
->setIcon('fa-chevron-left')
->setHref($base_uri);
return id(new PHUILauncherView())
->appendChild($box);
}
private function newTargetForm(
HeraldAdapter $adapter,
$rule_type,
$object_name,
$errors,
$e_object,
$title) {
$viewer = $this->getViewer();
$content_type = $adapter->getAdapterContentType();
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
$params = array(
'adapter' => $content_type,
'type' => $rule_type,
);
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendChild( ->appendChild(
id(new AphrontFormStaticControl()) id(new AphrontFormStaticControl())
->setLabel(pht('Rule for')) ->setLabel(pht('Rule for'))
@ -150,7 +303,7 @@ final class HeraldNewController extends HeraldController {
phutil_tag( phutil_tag(
'strong', 'strong',
array(), array(),
idx($content_type_map, $content_type)))) $adapter->getAdapterContentName())))
->appendChild( ->appendChild(
id(new AphrontFormStaticControl()) id(new AphrontFormStaticControl())
->setLabel(pht('Rule Type')) ->setLabel(pht('Rule Type'))
@ -170,28 +323,23 @@ final class HeraldNewController extends HeraldController {
id(new AphrontFormTextControl()) id(new AphrontFormTextControl())
->setName('objectName') ->setName('objectName')
->setError($e_object) ->setError($e_object)
->setValue($request->getStr('objectName')) ->setValue($object_name)
->setLabel(pht('Object'))); ->setLabel(pht('Object')));
$params = array( foreach ($params as $key => $value) {
'content_type' => $content_type, $form->addHiddenInput($key, $value);
'rule_type' => $rule_type,
'step' => 1,
);
$cancel_text = pht('Back');
$cancel_uri = new PhutilURI('new/', $params);
$cancel_uri = $this->getApplicationURI($cancel_uri);
$title = pht('Create Herald Rule: %s',
idx($content_type_map, $content_type));
break;
} }
$form $cancel_params = $params;
->appendChild( unset($cancel_params['type']);
$cancel_uri = $this->getApplicationURI('new/');
$cancel_uri = new PhutilURI($cancel_uri, $params);
$form->appendChild(
id(new AphrontFormSubmitControl()) id(new AphrontFormSubmitControl())
->setValue(pht('Continue')) ->setValue(pht('Continue'))
->addCancelButton($cancel_uri, $cancel_text)); ->addCancelButton($cancel_uri, pht('Back')));
$form_box = id(new PHUIObjectBoxView()) $form_box = id(new PHUIObjectBoxView())
->setHeaderText($title) ->setHeaderText($title)
@ -199,118 +347,7 @@ final class HeraldNewController extends HeraldController {
->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($form); ->setForm($form);
$crumbs = $this return $form_box;
->buildApplicationCrumbs()
->addTextCrumb(pht('Create Rule'))
->setBorder(true);
$view = id(new PHUITwoColumnView())
->setFooter($form_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
private function renderContentTypeControl(array $content_type_map, $e_type) {
$request = $this->getRequest();
$radio = id(new AphrontFormRadioButtonControl())
->setLabel(pht('New Rule for'))
->setName('content_type')
->setValue($request->getStr('content_type'))
->setError($e_type);
foreach ($content_type_map as $value => $name) {
$adapter = HeraldAdapter::getAdapterForContentType($value);
$radio->addButton(
$value,
$name,
phutil_escape_html_newlines($adapter->getAdapterContentDescription()));
}
return $radio;
}
private function renderRuleTypeControl(array $rule_type_map, $e_rule) {
$request = $this->getRequest();
// Reorder array to put less powerful rules first.
$rule_type_map = array_select_keys(
$rule_type_map,
array(
HeraldRuleTypeConfig::RULE_TYPE_PERSONAL,
HeraldRuleTypeConfig::RULE_TYPE_OBJECT,
HeraldRuleTypeConfig::RULE_TYPE_GLOBAL,
)) + $rule_type_map;
list($can_global, $global_link) = $this->explainApplicationCapability(
HeraldManageGlobalRulesCapability::CAPABILITY,
pht('You have permission to create and manage global rules.'),
pht('You do not have permission to create or manage global rules.'));
$captions = array(
HeraldRuleTypeConfig::RULE_TYPE_PERSONAL =>
pht(
'Personal rules notify you about events. You own them, but they can '.
'only affect you. Personal rules only trigger for objects you have '.
'permission to see.'),
HeraldRuleTypeConfig::RULE_TYPE_OBJECT =>
pht(
'Object rules notify anyone about events. They are bound to an '.
'object (like a repository) and can only act on that object. You '.
'must be able to edit an object to create object rules for it. '.
'Other users who can edit the object can edit its rules.'),
HeraldRuleTypeConfig::RULE_TYPE_GLOBAL =>
array(
pht(
'Global rules notify anyone about events. Global rules can '.
'bypass access control policies and act on any object.'),
$global_link,
),
);
$radio = id(new AphrontFormRadioButtonControl())
->setLabel(pht('Rule Type'))
->setName('rule_type')
->setValue($request->getStr('rule_type'))
->setError($e_rule);
$adapter = HeraldAdapter::getAdapterForContentType(
$request->getStr('content_type'));
foreach ($rule_type_map as $value => $name) {
$caption = idx($captions, $value);
$disabled = ($value == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) &&
(!$can_global);
if (!$adapter->supportsRuleType($value)) {
$disabled = true;
$caption = array(
$caption,
"\n\n",
phutil_tag(
'em',
array(),
pht(
'This rule type is not supported by the selected content type.')),
);
}
$radio->addButton(
$value,
$name,
phutil_escape_html_newlines($caption),
$disabled ? 'disabled' : null,
$disabled);
}
return $radio;
} }
} }

View file

@ -27,6 +27,7 @@ final class PHUIObjectBoxView extends AphrontTagView {
private $showHideOpen; private $showHideOpen;
private $propertyLists = array(); private $propertyLists = array();
private $tailButtons = array();
const COLOR_RED = 'red'; const COLOR_RED = 'red';
const COLOR_BLUE = 'blue'; const COLOR_BLUE = 'blue';
@ -153,6 +154,16 @@ final class PHUIObjectBoxView extends AphrontTagView {
return $this; return $this;
} }
public function newTailButton() {
$button = id(new PHUIButtonView())
->setTag('a')
->setColor(PHUIButtonView::GREY);
$this->tailButtons[] = $button;
return $button;
}
protected function getTagAttributes() { protected function getTagAttributes() {
$classes = array(); $classes = array();
$classes[] = 'phui-box'; $classes[] = 'phui-box';
@ -329,6 +340,15 @@ final class PHUIObjectBoxView extends AphrontTagView {
$content[] = $this->objectList; $content[] = $this->objectList;
} }
if ($this->tailButtons) {
$content[] = phutil_tag(
'div',
array(
'class' => 'phui-object-box-tail-buttons',
),
$this->tailButtons);
}
return $content; return $content;
} }
} }

View file

@ -62,6 +62,12 @@ div.phui-object-box.phui-object-box-flush {
font-size: {$normalfontsize}; font-size: {$normalfontsize};
} }
.phui-object-box-tail-buttons {
padding: 8px;
background: {$lightgreybackground};
border-top: 1px solid {$lightgreyborder};
}
/* - Object Box Colors ------------------------------------------------------ */ /* - Object Box Colors ------------------------------------------------------ */
.phui-box-border.phui-object-box-green { .phui-box-border.phui-object-box-green {