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

Fold task-relationship actions into an accordion dropdown

Summary:
Ref T11179. Alternative to D16152. I think this turned out a bit better than the other one did.

Currently, we render two copies of the menu (one for mobile, one for desktop). A big chunk of this is sharing the nodes instead: when you open the mobile dropdown menu, it steals the nodes from the document. When you close it, it puts them back. Magic! Sneaky!

Test Plan:
{F1695499}

{F1695500}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T11179

Differential Revision: https://secure.phabricator.com/D16157
This commit is contained in:
epriestley 2016-06-20 17:49:38 -07:00
parent 3198aa1659
commit 56d3197fe0
13 changed files with 318 additions and 58 deletions

View file

@ -7,8 +7,8 @@
*/
return array(
'names' => array(
'core.pkg.css' => 'b9e2e1e5',
'core.pkg.js' => '80f86a0a',
'core.pkg.css' => 'f577cd20',
'core.pkg.js' => 'f2139810',
'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => 'b3eea3f5',
'differential.pkg.js' => '4b7d8f19',
@ -124,7 +124,7 @@ return array(
'rsrc/css/phui/phui-badge.css' => '3baef8db',
'rsrc/css/phui/phui-big-info-view.css' => 'bd903741',
'rsrc/css/phui/phui-box.css' => '5c8387cf',
'rsrc/css/phui/phui-button.css' => 'a64a8de6',
'rsrc/css/phui/phui-button.css' => 'e266e0bc',
'rsrc/css/phui/phui-chart.css' => '6bf6f78e',
'rsrc/css/phui/phui-crumbs-view.css' => '6b813619',
'rsrc/css/phui/phui-curtain-view.css' => '7148ae25',
@ -516,14 +516,15 @@ return array(
'rsrc/js/core/behavior-watch-anchor.js' => '9f36c42d',
'rsrc/js/core/behavior-workflow.js' => '0a3f3021',
'rsrc/js/core/phtize.js' => 'd254d646',
'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '54733475',
'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '1aa4c968',
'rsrc/js/phui/behavior-phui-file-upload.js' => 'b003d4fb',
'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836',
'rsrc/js/phui/behavior-phui-profile-menu.js' => '12884df9',
'rsrc/js/phui/behavior-phui-submenu.js' => 'a6f7a73b',
'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8',
'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262',
'rsrc/js/phuix/PHUIXAutocomplete.js' => '9196fb06',
'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca',
'rsrc/js/phuix/PHUIXDropdownMenu.js' => '82e270da',
'rsrc/js/phuix/PHUIXFormControl.js' => 'e15869a8',
'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b',
),
@ -669,11 +670,12 @@ return array(
'javelin-behavior-phabricator-watch-anchor' => '9f36c42d',
'javelin-behavior-pholio-mock-edit' => 'bee502c8',
'javelin-behavior-pholio-mock-view' => 'fbe497e7',
'javelin-behavior-phui-dropdown-menu' => '54733475',
'javelin-behavior-phui-dropdown-menu' => '1aa4c968',
'javelin-behavior-phui-file-upload' => 'b003d4fb',
'javelin-behavior-phui-hovercards' => 'bcaccd64',
'javelin-behavior-phui-object-box-tabs' => '2bfa2836',
'javelin-behavior-phui-profile-menu' => '12884df9',
'javelin-behavior-phui-submenu' => 'a6f7a73b',
'javelin-behavior-policy-control' => 'd0c516d5',
'javelin-behavior-policy-rule-editor' => '5e9f347c',
'javelin-behavior-project-boards' => '14a1faae',
@ -822,7 +824,7 @@ return array(
'phui-badge-view-css' => '3baef8db',
'phui-big-info-view-css' => 'bd903741',
'phui-box-css' => '5c8387cf',
'phui-button-css' => 'a64a8de6',
'phui-button-css' => 'e266e0bc',
'phui-calendar-css' => 'ccabe893',
'phui-calendar-day-css' => 'd1cf6f93',
'phui-calendar-list-css' => '56e6381a',
@ -870,7 +872,7 @@ return array(
'phuix-action-list-view' => 'b5c256b8',
'phuix-action-view' => '8cf6d262',
'phuix-autocomplete' => '9196fb06',
'phuix-dropdown-menu' => 'bd4c8dca',
'phuix-dropdown-menu' => '82e270da',
'phuix-form-control-view' => 'e15869a8',
'phuix-icon-view' => 'bff6884b',
'policy-css' => '957ea14c',
@ -1024,6 +1026,12 @@ return array(
'javelin-workflow',
'javelin-workboard-controller',
),
'1aa4c968' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phuix-dropdown-menu',
),
'1ad0a787' => array(
'javelin-install',
'javelin-reactor',
@ -1275,12 +1283,6 @@ return array(
'javelin-leader',
'javelin-json',
),
54733475 => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phuix-dropdown-menu',
),
'54b612ba' => array(
'javelin-color',
'javelin-install',
@ -1529,6 +1531,13 @@ return array(
'javelin-vector',
'javelin-stratcom',
),
'82e270da' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-vector',
'javelin-stratcom',
),
'834a1173' => array(
'javelin-behavior',
'javelin-scrollbar',
@ -1693,6 +1702,11 @@ return array(
'javelin-uri',
'phabricator-notification',
),
'a6f7a73b' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'a80d0378' => array(
'javelin-behavior',
'javelin-stratcom',
@ -1846,13 +1860,6 @@ return array(
'javelin-vector',
'phui-hovercard',
),
'bd4c8dca' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-vector',
'javelin-stratcom',
),
'bdaf4d04' => array(
'javelin-behavior',
'javelin-dom',

View file

@ -474,8 +474,11 @@ abstract class PhabricatorController extends AphrontController {
public function newCurtainView($object) {
$viewer = $this->getViewer();
$action_id = celerity_generate_unique_node_id();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer);
->setViewer($viewer)
->setID($action_id);
// NOTE: Applications (objects of class PhabricatorApplication) can't
// currently be set here, although they don't need any of the extensions

View file

@ -166,15 +166,6 @@ final class ManiphestTaskDetailController extends ManiphestController {
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Merge Duplicates In'))
->setHref("/search/attach/{$phid}/TASK/merge/")
->setWorkflow(true)
->setIcon('fa-compress')
->setDisabled(!$can_edit)
->setWorkflow(true));
$edit_config = $edit_engine->loadDefaultEditConfiguration();
$can_create = (bool)$edit_config;
@ -195,23 +186,36 @@ final class ManiphestTaskDetailController extends ManiphestController {
$edit_uri = $this->getApplicationURI($edit_uri);
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Create Subtask'))
->setHref($edit_uri)
->setIcon('fa-level-down')
->setDisabled(!$can_create)
->setWorkflow(!$can_create));
$task_submenu = array();
$task_submenu[] = id(new PhabricatorActionView())
->setName(pht('Create Subtask'))
->setHref($edit_uri)
->setIcon('fa-level-down')
->setDisabled(!$can_create)
->setWorkflow(!$can_create);
$task_submenu[] = id(new PhabricatorActionView())
->setName(pht('Edit Blocking Tasks'))
->setHref("/search/attach/{$phid}/TASK/blocks/")
->setWorkflow(true)
->setIcon('fa-link')
->setDisabled(!$can_edit)
->setWorkflow(true);
$task_submenu[] = id(new PhabricatorActionView())
->setName(pht('Merge Duplicates In'))
->setHref("/search/attach/{$phid}/TASK/merge/")
->setWorkflow(true)
->setIcon('fa-compress')
->setDisabled(!$can_edit)
->setWorkflow(true);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Blocking Tasks'))
->setHref("/search/attach/{$phid}/TASK/blocks/")
->setWorkflow(true)
->setIcon('fa-link')
->setDisabled(!$can_edit)
->setWorkflow(true));
->setName(pht('Edit Related Tasks...'))
->setIcon('fa-anchor')
->setSubmenu($task_submenu));
$owner_phid = $task->getOwnerPHID();
$author_phid = $task->getAuthorPHID();

View file

@ -21,6 +21,10 @@ final class PhabricatorActionListView extends AphrontView {
return $this;
}
public function getID() {
return $this->id;
}
public function render() {
$viewer = $this->getViewer();
@ -44,13 +48,20 @@ final class PhabricatorActionListView extends AphrontView {
require_celerity_resource('phabricator-action-list-view-css');
$items = array();
foreach ($actions as $action) {
foreach ($action->getItems() as $item) {
$items[] = $item;
}
}
return phutil_tag(
'ul',
array(
'class' => 'phabricator-action-list-view',
'id' => $this->id,
),
$actions);
$items);
}
public function getDropdownMenuMetadata() {

View file

@ -14,6 +14,10 @@ final class PhabricatorActionView extends AphrontView {
private $metadata;
private $selected;
private $openInNewWindow;
private $submenu = array();
private $hidden;
private $depth;
private $id;
public function setSelected($selected) {
$this->selected = $selected;
@ -95,7 +99,60 @@ final class PhabricatorActionView extends AphrontView {
return $this->openInNewWindow;
}
public function getID() {
if (!$this->id) {
$this->id = celerity_generate_unique_node_id();
}
return $this->id;
}
public function setSubmenu(array $submenu) {
$this->submenu = $submenu;
if (!$this->getHref()) {
$this->setHref('#');
}
return $this;
}
public function getItems($depth = 0) {
$items = array();
$items[] = $this;
foreach ($this->submenu as $action) {
foreach ($action->getItems($depth + 1) as $item) {
$item
->setHidden(true)
->setDepth($depth + 1);
$items[] = $item;
}
}
return $items;
}
public function setHidden($hidden) {
$this->hidden = $hidden;
return $this;
}
public function getHidden() {
return $this->hidden;
}
public function setDepth($depth) {
$this->depth = $depth;
return $this;
}
public function getDepth() {
return $this->depth;
}
public function render() {
$caret_id = celerity_generate_unique_node_id();
$icon = null;
if ($this->icon) {
@ -155,6 +212,18 @@ final class PhabricatorActionView extends AphrontView {
$target = null;
}
if ($this->submenu) {
$caret = javelin_tag(
'span',
array(
'class' => 'caret-right',
'id' => $caret_id,
),
'');
} else {
$caret = null;
}
$item = javelin_tag(
'a',
array(
@ -164,7 +233,7 @@ final class PhabricatorActionView extends AphrontView {
'sigil' => $sigils,
'meta' => $this->metadata,
),
array($icon, $this->name));
array($icon, $this->name, $caret));
}
} else {
$item = phutil_tag(
@ -190,10 +259,47 @@ final class PhabricatorActionView extends AphrontView {
$classes[] = 'phabricator-action-view-selected';
}
return phutil_tag(
if ($this->submenu) {
$classes[] = 'phabricator-action-view-submenu';
}
$style = array();
if ($this->hidden) {
$style[] = 'display: none;';
}
if ($this->depth) {
$indent = ($this->depth * 16);
$style[] = "margin-left: {$indent}px;";
}
$sigil = null;
$meta = null;
if ($this->submenu) {
Javelin::initBehavior('phui-submenu');
$sigil = 'phui-submenu';
$item_ids = array();
foreach ($this->submenu as $subitem) {
$item_ids[] = $subitem->getID();
}
$meta = array(
'itemIDs' => $item_ids,
'caretID' => $caret_id,
);
}
return javelin_tag(
'li',
array(
'id' => $this->getID(),
'class' => implode(' ', $classes),
'style' => implode(' ', $style),
'sigil' => $sigil,
'meta' => $meta,
),
$item);
}

View file

@ -110,6 +110,18 @@ final class PHUIButtonView extends AphrontTagView {
return $this;
}
public function setDropdownMenuID($id) {
Javelin::initBehavior('phui-dropdown-menu');
$this->addSigil('phui-dropdown-menu');
$this->setMetadata(
array(
'menuID' => $id,
));
return $this;
}
protected function getTagAttributes() {
require_celerity_resource('phui-button-css');

View file

@ -24,6 +24,7 @@ final class PHUIHeaderView extends AphrontTagView {
private $badges = array();
private $href;
private $actionList;
private $actionListID;
public function setHeader($header) {
$this->header = $header;
@ -90,6 +91,11 @@ final class PHUIHeaderView extends AphrontTagView {
return $this;
}
public function setActionListID($action_list_id) {
$this->actionListID = $action_list_id;
return $this;
}
public function setPolicyObject(PhabricatorPolicyInterface $object) {
$this->policyObject = $object;
return $this;
@ -189,14 +195,20 @@ final class PHUIHeaderView extends AphrontTagView {
protected function getTagContent() {
if ($this->actionList) {
if ($this->actionList || $this->actionListID) {
$action_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Actions'))
->setHref('#')
->setIcon('fa-bars')
->addClass('phui-mobile-menu')
->setDropdownMenu($this->actionList);
->addClass('phui-mobile-menu');
if ($this->actionList) {
$action_button->setDropdownMenu($this->actionList);
} else if ($this->actionListID) {
$action_button->setDropdownMenuID($this->actionListID);
}
$this->addActionLink($action_button);
}

View file

@ -327,9 +327,7 @@ final class PHUITimelineEventView extends AphrontView {
'sigil' => $sigil,
'aria-haspopup' => 'true',
'aria-expanded' => 'false',
'meta' => array(
'items' => hsprintf('%s', $action_list),
),
'meta' => $action_list->getDropdownMenuMetadata(),
),
array(
$aural,

View file

@ -114,7 +114,7 @@ final class PHUITwoColumnView extends AphrontTagView {
$curtain = $this->getCurtain();
if ($curtain) {
$action_list = $curtain->getActionList();
$this->header->setActionList($action_list);
$this->header->setActionListID($action_list->getID());
}
$header = phutil_tag_div(

View file

@ -264,6 +264,42 @@ a.policy-control .caret {
border-top-color: #000;
}
.phabricator-action-view-submenu .caret-right {
float: right;
margin-top: 4px;
margin-right: 6px;
border-left-color: {$lightgreytext};
}
.phabricator-action-view-submenu .caret {
float: right;
margin-top: 5px;
margin-right: 4px;
border-top: 7px solid {$lightgreytext};
}
.phabricator-action-view-submenu.phui-submenu-open {
background: {$greybackground};
}
.phui-submenu-animate {
animation: phui-submenu-summon 0.25s;
}
@keyframes phui-submenu-summon {
0% {
color: {$lightgreytext};
margin-left: 0;
transform: rotate(12deg);
}
60% {
margin-left: 24px;
transform: rotate(-5deg);
margin-top: 18px;
}
}
/* Icons */
.button.has-icon {
position: relative;

View file

@ -16,17 +16,42 @@ JX.behavior('phui-dropdown-menu', function() {
e.kill();
var list = JX.$H(data.items).getFragment().firstChild;
var list;
var placeholder;
if (data.items) {
list = JX.$H(data.items).getFragment().firstChild;
} else {
list = JX.$(data.menuID);
placeholder = JX.$N('span');
}
var icon = e.getNode('phui-dropdown-menu');
data.menu = new JX.PHUIXDropdownMenu(icon);
data.menu.setContent(list);
data.menu.listen('open', function() {
if (placeholder) {
JX.DOM.replace(list, placeholder);
}
data.menu.setContent(list);
});
data.menu.listen('close', function() {
if (placeholder) {
JX.DOM.replace(placeholder, list);
}
});
data.menu.open();
JX.DOM.listen(list, 'click', 'tag:a', function(e) {
if (!e.isNormalClick()) {
return;
}
if (JX.Stratcom.pass()) {
return;
}
data.menu.close();
});
});

View file

@ -0,0 +1,44 @@
/**
* @provides javelin-behavior-phui-submenu
* @requires javelin-behavior
* javelin-stratcom
* javelin-dom
*/
JX.behavior('phui-submenu', function() {
JX.Stratcom.listen('click', 'phui-submenu', function(e) {
if (!e.isNormalClick()) {
return;
}
var node = e.getNode('phui-submenu');
var data = e.getNodeData('phui-submenu');
e.kill();
data.open = !data.open;
for (var ii = 0; ii < data.itemIDs.length; ii++) {
var id = data.itemIDs[ii];
var item = JX.$(id);
if (data.open) {
JX.DOM.show(item);
} else {
JX.DOM.hide(item);
}
// Add a class so we can animate zany effects.
JX.DOM.alterClass(item, 'phui-submenu-animate', data.open);
}
JX.DOM.alterClass(node, 'phui-submenu-open', data.open);
// Toggle the caret from ">" to "V" when opening the menu, and back again
// when closing it.
var caret = JX.$(data.caretID);
JX.DOM.alterClass(caret, 'caret', data.open);
JX.DOM.alterClass(caret, 'caret-right', !data.open);
});
});

View file

@ -42,7 +42,7 @@ JX.install('PHUIXDropdownMenu', {
JX.Stratcom.listen('keydown', null, JX.bind(this, this._onkey));
},
events: ['open'],
events: ['open', 'close'],
properties: {
width: null,
@ -83,6 +83,8 @@ JX.install('PHUIXDropdownMenu', {
this._open = false;
this._hide();
this.invoke('close');
return this;
},