Commit 5dca1569 authored by epriestley's avatar epriestley
Browse files

Preview the effects of a drag-and-drop operation on workboards

Summary:
Ref T10335. Ref T5474. When you drag-and-drop a card on a workboard, show a UI hint which lists all the things that the operation will do.

This shows: column moves; changes because of dragging a card to a different header; and changes which will be caused by triggers.

Not implemented here:

  - Actions are currently shown even if they have no effect. For example, if you drag a "Normal" task to a different column, it says "Change priority to Normal.". I plan to hide actions which have no effect, but figuring this out is a little bit tricky.
  - I'd like to make "trigger effects" vs "non-trigger effects" a little more clear in the future, probably.

Test Plan:
Dragged stuff between columns and headers, and into columns with triggers. Got appropriate preview text hints previewing what the action would do in the UI.

(This is tricky to take a screenshot of since it only shows up while the mouse cursor is down.)

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T10335, T5474

Differential Revision: https://secure.phabricator.com/D20299
parent 149f8cc9
......@@ -10,7 +10,7 @@ return array(
'conpherence.pkg.css' => '3c8a0668',
'conpherence.pkg.js' => '020aebcf',
'core.pkg.css' => 'b797945d',
'core.pkg.js' => 'f9c2509b',
'core.pkg.js' => 'eaca003c',
'differential.pkg.css' => '8d8360fb',
'differential.pkg.js' => '67e02996',
'diffusion.pkg.css' => '42c75c37',
......@@ -178,7 +178,7 @@ return array(
'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308',
'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98',
'rsrc/css/phui/workboards/phui-workcard.css' => '9e9eb0df',
'rsrc/css/phui/workboards/phui-workpanel.css' => 'c5b408ad',
'rsrc/css/phui/workboards/phui-workpanel.css' => 'e5461a51',
'rsrc/css/sprite-login.css' => '18b368a6',
'rsrc/css/sprite-tokens.css' => 'f1896dc5',
'rsrc/css/syntax/syntax-default.css' => '055fc231',
......@@ -408,15 +408,16 @@ return array(
'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f',
'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9',
'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172',
'rsrc/js/application/projects/WorkboardBoard.js' => '9d59f098',
'rsrc/js/application/projects/WorkboardBoard.js' => 'ba6e36b0',
'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8',
'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4',
'rsrc/js/application/projects/WorkboardColumn.js' => 'ec5c5ce0',
'rsrc/js/application/projects/WorkboardColumn.js' => 'c344eb3c',
'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7',
'rsrc/js/application/projects/WorkboardDropEffect.js' => '101121be',
'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d',
'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'b65351bd',
'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b',
'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f',
'rsrc/js/application/projects/behavior-project-boards.js' => '412af9d4',
'rsrc/js/application/projects/behavior-project-boards.js' => 'cd7c9d4f',
'rsrc/js/application/projects/behavior-project-create.js' => '34c53422',
'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9',
'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68',
......@@ -437,7 +438,7 @@ return array(
'rsrc/js/application/uiexample/notification-example.js' => '29819b75',
'rsrc/js/core/Busy.js' => '5202e831',
'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d',
'rsrc/js/core/DraggableList.js' => '8bc7d797',
'rsrc/js/core/DraggableList.js' => 'c9ad6f70',
'rsrc/js/core/Favicon.js' => '7930776a',
'rsrc/js/core/FileUpload.js' => 'ab85e184',
'rsrc/js/core/Hovercard.js' => '074f0783',
......@@ -657,7 +658,7 @@ return array(
'javelin-behavior-phuix-example' => 'c2c500a7',
'javelin-behavior-policy-control' => '0eaa33a9',
'javelin-behavior-policy-rule-editor' => '9347f172',
'javelin-behavior-project-boards' => '412af9d4',
'javelin-behavior-project-boards' => 'cd7c9d4f',
'javelin-behavior-project-create' => '34c53422',
'javelin-behavior-quicksand-blacklist' => '5a6f6a06',
'javelin-behavior-read-only-warning' => 'b9109f8f',
......@@ -729,13 +730,14 @@ return array(
'javelin-view-renderer' => '9aae2b66',
'javelin-view-visitor' => '308f9fe4',
'javelin-websocket' => 'fdc13e4e',
'javelin-workboard-board' => '9d59f098',
'javelin-workboard-board' => 'ba6e36b0',
'javelin-workboard-card' => '0392a5d8',
'javelin-workboard-card-template' => '2a61f8d4',
'javelin-workboard-column' => 'ec5c5ce0',
'javelin-workboard-column' => 'c344eb3c',
'javelin-workboard-controller' => '42c7a5a7',
'javelin-workboard-drop-effect' => '101121be',
'javelin-workboard-header' => '111bfd2d',
'javelin-workboard-header-template' => 'b65351bd',
'javelin-workboard-header-template' => 'ebe83a6b',
'javelin-workboard-order-template' => '03e8891f',
'javelin-workflow' => '958e9045',
'maniphest-report-css' => '3d53188b',
......@@ -761,7 +763,7 @@ return array(
'phabricator-diff-changeset-list' => '04023d82',
'phabricator-diff-inline' => 'a4a14a94',
'phabricator-drag-and-drop-file-upload' => '4370900d',
'phabricator-draggable-list' => '8bc7d797',
'phabricator-draggable-list' => 'c9ad6f70',
'phabricator-fatal-config-template-css' => '20babf50',
'phabricator-favicon' => '7930776a',
'phabricator-feed-css' => 'd8b6e3f8',
......@@ -860,7 +862,7 @@ return array(
'phui-workboard-color-css' => 'e86de308',
'phui-workboard-view-css' => '74fc9d98',
'phui-workcard-view-css' => '9e9eb0df',
'phui-workpanel-view-css' => 'c5b408ad',
'phui-workpanel-view-css' => 'e5461a51',
'phuix-action-list-view' => 'c68f183f',
'phuix-action-view' => 'aaa08f3b',
'phuix-autocomplete' => '8f139ef0',
......@@ -1001,6 +1003,10 @@ return array(
'javelin-workflow',
'phuix-icon-view',
),
'101121be' => array(
'javelin-install',
'javelin-dom',
),
'111bfd2d' => array(
'javelin-install',
),
......@@ -1227,15 +1233,6 @@ return array(
'javelin-behavior',
'javelin-uri',
),
'412af9d4' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'javelin-workboard-controller',
),
'4234f572' => array(
'syntax-default-css',
),
......@@ -1593,14 +1590,6 @@ return array(
'javelin-dom',
'javelin-typeahead-normalizer',
),
'8bc7d797' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'javelin-vector',
'javelin-magical-init',
),
'8c2ed2bf' => array(
'javelin-behavior',
'javelin-dom',
......@@ -1725,18 +1714,6 @@ return array(
'javelin-uri',
'phabricator-textareautils',
),
'9d59f098' => array(
'javelin-install',
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
'javelin-workboard-column',
'javelin-workboard-header-template',
'javelin-workboard-card-template',
'javelin-workboard-order-template',
),
'9f081f05' => array(
'javelin-behavior',
'javelin-dom',
......@@ -1885,9 +1862,6 @@ return array(
'javelin-stratcom',
'javelin-dom',
),
'b65351bd' => array(
'javelin-install',
),
'b7b73831' => array(
'javelin-behavior',
'javelin-dom',
......@@ -1906,6 +1880,18 @@ return array(
'javelin-uri',
'phabricator-notification',
),
'ba6e36b0' => array(
'javelin-install',
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
'javelin-workboard-column',
'javelin-workboard-header-template',
'javelin-workboard-card-template',
'javelin-workboard-order-template',
),
'bdce4d78' => array(
'javelin-install',
'javelin-util',
......@@ -1930,15 +1916,17 @@ return array(
'javelin-dom',
'phuix-button-view',
),
'c344eb3c' => array(
'javelin-install',
'javelin-workboard-card',
'javelin-workboard-header',
),
'c3703a16' => array(
'javelin-behavior',
'javelin-aphlict',
'phabricator-phtize',
'javelin-dom',
),
'c5b408ad' => array(
'phui-workcard-view-css',
),
'c687e867' => array(
'javelin-behavior',
'javelin-dom',
......@@ -1978,6 +1966,24 @@ return array(
'javelin-util',
'phabricator-keyboard-shortcut-manager',
),
'c9ad6f70' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'javelin-vector',
'javelin-magical-init',
),
'cd7c9d4f' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'javelin-workboard-controller',
'javelin-workboard-drop-effect',
),
'cf32921f' => array(
'javelin-behavior',
'javelin-dom',
......@@ -2038,6 +2044,9 @@ return array(
'javelin-dom',
'javelin-history',
),
'e5461a51' => array(
'phui-workcard-view-css',
),
'e562708c' => array(
'javelin-install',
),
......@@ -2068,14 +2077,12 @@ return array(
'javelin-install',
'javelin-event',
),
'ebe83a6b' => array(
'javelin-install',
),
'ec4e31c0' => array(
'phui-timeline-view-css',
),
'ec5c5ce0' => array(
'javelin-install',
'javelin-workboard-card',
'javelin-workboard-header',
),
'ee77366f' => array(
'aphront-dialog-view-css',
),
......
......@@ -4094,6 +4094,7 @@ phutil_register_library_map(array(
'PhabricatorProjectDefaultController' => 'applications/project/controller/PhabricatorProjectDefaultController.php',
'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php',
'PhabricatorProjectDetailsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php',
'PhabricatorProjectDropEffect' => 'applications/project/icon/PhabricatorProjectDropEffect.php',
'PhabricatorProjectEditController' => 'applications/project/controller/PhabricatorProjectEditController.php',
'PhabricatorProjectEditEngine' => 'applications/project/engine/PhabricatorProjectEditEngine.php',
'PhabricatorProjectEditPictureController' => 'applications/project/controller/PhabricatorProjectEditPictureController.php',
......@@ -10219,6 +10220,7 @@ phutil_register_library_map(array(
'PhabricatorProjectDefaultController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField',
'PhabricatorProjectDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectDropEffect' => 'Phobject',
'PhabricatorProjectEditController' => 'PhabricatorProjectController',
'PhabricatorProjectEditEngine' => 'PhabricatorEditEngine',
'PhabricatorProjectEditPictureController' => 'PhabricatorProjectController',
......
......@@ -540,8 +540,8 @@ final class PhabricatorProjectBoardViewController
->setExcludedProjectPHIDs($select_phids);
$templates = array();
$column_maps = array();
$all_tasks = array();
$column_templates = array();
foreach ($visible_columns as $column_phid => $column) {
$column_tasks = $column_phids[$column_phid];
......@@ -606,18 +606,28 @@ final class PhabricatorProjectBoardViewController
'pointLimit' => $column->getPointLimit(),
));
$card_phids = array();
foreach ($column_tasks as $task) {
$object_phid = $task->getPHID();
$card = $rendering_engine->renderCard($object_phid);
$templates[$object_phid] = hsprintf('%s', $card->getItem());
$column_maps[$column_phid][] = $object_phid;
$card_phids[] = $object_phid;
$all_tasks[$object_phid] = $task;
}
$panel->setCards($cards);
$board->addPanel($panel);
$drop_effects = $column->getDropEffects();
$drop_effects = mpull($drop_effects, 'toDictionary');
$column_templates[] = array(
'columnPHID' => $column_phid,
'effects' => $drop_effects,
'cardPHIDs' => $card_phids,
);
}
$order_key = $this->sortKey;
......@@ -661,9 +671,9 @@ final class PhabricatorProjectBoardViewController
'headers' => $headers,
'headerKeys' => $header_keys,
'templateMap' => $templates,
'columnMaps' => $column_maps,
'orderMaps' => $vector_map,
'propertyMaps' => $properties,
'columnTemplates' => $column_templates,
'boardID' => $board_id,
'projectPHID' => $project->getPHID(),
......
<?php
final class PhabricatorProjectDropEffect
extends Phobject {
private $icon;
private $color;
private $content;
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function getIcon() {
return $this->icon;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function getColor() {
return $this->color;
}
public function setContent($content) {
$this->content = $content;
return $this;
}
public function getContent() {
return $this->content;
}
public function toDictionary() {
return array(
'icon' => $this->getIcon(),
'color' => $this->getColor(),
'content' => hsprintf('%s', $this->getContent()),
);
}
}
......@@ -9,6 +9,7 @@ final class PhabricatorProjectColumnHeader
private $name;
private $icon;
private $editProperties;
private $dropEffects = array();
public function setOrderKey($order_key) {
$this->orderKey = $order_key;
......@@ -64,6 +65,15 @@ final class PhabricatorProjectColumnHeader
return $this->editProperties;
}
public function addDropEffect(PhabricatorProjectDropEffect $effect) {
$this->dropEffects[] = $effect;
return $this;
}
public function getDropEffects() {
return $this->dropEffects;
}
public function toDictionary() {
return array(
'order' => $this->getOrderKey(),
......@@ -71,6 +81,7 @@ final class PhabricatorProjectColumnHeader
'template' => hsprintf('%s', $this->newView()),
'vector' => $this->getSortVector(),
'editProperties' => $this->getEditProperties(),
'effects' => mpull($this->getDropEffects(), 'toDictionary'),
);
}
......
......@@ -196,6 +196,10 @@ abstract class PhabricatorProjectColumnOrder
->setOrderKey($this->getColumnOrderKey());
}
final protected function newEffect() {
return new PhabricatorProjectDropEffect();
}
final public function toDictionary() {
return array(
'orderKey' => $this->getColumnOrderKey(),
......
......@@ -122,16 +122,23 @@ final class PhabricatorProjectColumnOwnerOrder
$header_key = $this->newHeaderKeyForOwnerPHID($owner_phid);
$owner_image = null;
$effect_content = null;
if ($owner_phid === null) {
$owner = null;
$sort_vector = $this->newSortVectorForUnowned();
$owner_name = pht('Not Assigned');
$effect_content = pht('Remove task assignee.');
} else {
$owner = idx($owner_users, $owner_phid);
if ($owner) {
$sort_vector = $this->newSortVectorForOwner($owner);
$owner_name = $owner->getUsername();
$owner_image = $owner->getProfileImageURI();
$effect_content = pht(
'Assign task to %s.',
phutil_tag('strong', array(), $owner_name));
} else {
$sort_vector = $this->newSortVectorForOwnerPHID($owner_phid);
$owner_name = pht('Unknown User ("%s")', $owner_phid);
......@@ -159,6 +166,14 @@ final class PhabricatorProjectColumnOwnerOrder
'value' => $owner_phid,
));
if ($effect_content !== null) {
$header->addDropEffect(
$this->newEffect()
->setIcon($owner_icon)
->setColor($owner_color)
->setContent($effect_content));
}
$headers[] = $header;
}
......
......@@ -65,6 +65,14 @@ final class PhabricatorProjectColumnPriorityOrder
$icon_view = id(new PHUIIconView())
->setIcon($priority_icon, $priority_color);
$drop_effect = $this->newEffect()
->setIcon($priority_icon)
->setColor($priority_color)
->setContent(
pht(
'Change priority to %s.',
phutil_tag('strong', array(), $priority_name)));
$header = $this->newHeader()
->setHeaderKey($header_key)
->setSortVector($sort_vector)
......@@ -73,7 +81,8 @@ final class PhabricatorProjectColumnPriorityOrder
->setEditProperties(
array(
'value' => (int)$priority,
));
))
->addDropEffect($drop_effect);
$headers[] = $header;
}
......
......@@ -72,6 +72,14 @@ final class PhabricatorProjectColumnStatusOrder
$icon_view = id(new PHUIIconView())
->setIcon($status_icon, $status_color);
$drop_effect = $this->newEffect()
->setIcon($status_icon)
->setColor($status_color)
->setContent(
pht(
'Change status to %s.',
phutil_tag('strong', array(), $status_name)));
$header = $this->newHeader()
->setHeaderKey($header_key)
->setSortVector($sort_vector)
......@@ -80,7 +88,8 @@ final class PhabricatorProjectColumnStatusOrder
->setEditProperties(
array(
'value' => $status_key,
));
))
->addDropEffect($drop_effect);
$headers[] = $header;
}
......
......@@ -218,6 +218,41 @@ final class PhabricatorProjectColumn
$this->getProject()->getID());
}
public function getDropEffects() {
$effects = array();
$proxy = $this->getProxy();
if ($proxy && $proxy->isMilestone()) {
$effects[] = id(new PhabricatorProjectDropEffect())
->setIcon($proxy->getProxyColumnIcon())
->setColor('violet')
->setContent(
pht(
'Move to milestone %s.',
phutil_tag('strong', array(), $this->getDisplayName())));
} else {
$effects[] = id(new PhabricatorProjectDropEffect())
->setIcon('fa-columns')
->setColor('blue')
->setContent(
pht(
'Move to column %s.',
phutil_tag('strong', array(), $this->getDisplayName())));
}
if ($this->canHaveTrigger()) {
$trigger = $this->getTrigger();
if ($trigger) {
foreach ($trigger->getDropEffects() as $trigger_effect) {
$effects[] = $trigger_effect;
}
}
}
return $effects;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
......
......@@ -170,6 +170,19 @@ final class PhabricatorProjectTrigger
return $this->triggerRules;
}
public function getDropEffects() {
$effects = array();
$rules = $this->getTriggerRules();
foreach ($rules as $rule) {
foreach ($rule->getDropEffects() as $effect) {
$effects[] = $effect;
}
}
return $effects;
}
public function getRulesDescription() {
$rules = $this->getTriggerRules();
if (!$rules) {
......
......@@ -19,4 +19,8 @@ final class PhabricatorProjectTriggerInvalidRule
return array();
}
protected function newDropEffects($value) {
return array();
}
}
......@@ -38,4 +38,21 @@ final class PhabricatorProjectTriggerManiphestStatusRule
);
}
protected function newDropEffects($value) {
$status_name = ManiphestTaskStatus::getTaskStatusName($value);
$status_icon = ManiphestTaskStatus::getStatusIcon($value);
$status_color = ManiphestTaskStatus::getStatusColor($value);
$content = pht(
'Change status to %s.',
phutil_tag('strong', array(), $status_name));
return array(
$this->newEffect()
->setIcon($status_icon)
->setColor($status_color)
->setContent($content),
);
}
}
......@@ -40,6 +40,7 @@ abstract class PhabricatorProjectTriggerRule
abstract public function getDescription();
abstract protected function assertValidRuleValue($value);
abstract protected function newDropTransactions($object, $value);
abstract protected function newDropEffects($value);