diff --git a/resources/celerity/map.php b/resources/celerity/map.php
index 268675360e..5d5cfc694a 100644
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -100,7 +100,6 @@ return array(
'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd',
'rsrc/css/application/releeph/releeph-request-typeahead.css' => '667a48ae',
'rsrc/css/application/search/search-results.css' => 'f240504c',
- 'rsrc/css/application/settings/settings.css' => 'ea8f5915',
'rsrc/css/application/slowvote/slowvote.css' => '266df6a1',
'rsrc/css/application/subscriptions/subscribers-list.css' => '5bb30c78',
'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
@@ -468,6 +467,7 @@ return array(
'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'c021950a',
'rsrc/js/core/behavior-refresh-csrf.js' => '7814b593',
'rsrc/js/core/behavior-remarkup-preview.js' => 'f7379f45',
+ 'rsrc/js/core/behavior-reorder-applications.js' => 'a8e3795d',
'rsrc/js/core/behavior-reveal-content.js' => '8f24abfc',
'rsrc/js/core/behavior-search-typeahead.js' => '86549ee3',
'rsrc/js/core/behavior-select-on-click.js' => '0e34ca02',
@@ -629,6 +629,7 @@ return array(
'javelin-behavior-releeph-request-state-change' => 'd259e7c9',
'javelin-behavior-releeph-request-typeahead' => 'cd9e7094',
'javelin-behavior-remarkup-preview' => 'f7379f45',
+ 'javelin-behavior-reorder-applications' => 'a8e3795d',
'javelin-behavior-repository-crossreference' => '8ab282be',
'javelin-behavior-search-reorder-queries' => '37871df4',
'javelin-behavior-select-on-click' => '0e34ca02',
@@ -720,7 +721,6 @@ return array(
'phabricator-project-tag-css' => '095c9404',
'phabricator-remarkup-css' => '80c3a48c',
'phabricator-search-results-css' => 'f240504c',
- 'phabricator-settings-css' => 'ea8f5915',
'phabricator-shaped-request' => '7cbe244b',
'phabricator-side-menu-view-css' => 'c1986b85',
'phabricator-slowvote-css' => '266df6a1',
@@ -1612,6 +1612,14 @@ return array(
1 => 'javelin-dom',
2 => 'javelin-stratcom',
),
+ 'a8e3795d' =>
+ array(
+ 0 => 'javelin-behavior',
+ 1 => 'javelin-stratcom',
+ 2 => 'javelin-workflow',
+ 3 => 'javelin-dom',
+ 4 => 'phabricator-draggable-list',
+ ),
'a9aaba0c' =>
array(
0 => 'javelin-behavior',
diff --git a/src/applications/home/controller/PhabricatorHomeController.php b/src/applications/home/controller/PhabricatorHomeController.php
index 0e1223984e..b3295332dc 100644
--- a/src/applications/home/controller/PhabricatorHomeController.php
+++ b/src/applications/home/controller/PhabricatorHomeController.php
@@ -25,13 +25,10 @@ abstract class PhabricatorHomeController extends PhabricatorController {
->setViewer($user)
->withInstalled(true)
->withUnlisted(false)
+ ->withLaunchable(true)
->execute();
foreach ($applications as $key => $application) {
- if (!$application->shouldAppearInLaunchView()) {
- // Remove hidden applications (usually internal stuff).
- unset($applications[$key]);
- }
$invisible = PhabricatorApplication::TILE_INVISIBLE;
if ($application->getDefaultTileDisplay($user) == $invisible) {
// Remove invisible applications (e.g., admin apps for non-admins).
@@ -39,115 +36,45 @@ abstract class PhabricatorHomeController extends PhabricatorController {
}
}
- $status = array();
- foreach ($applications as $key => $application) {
- $status[get_class($application)] = $application->loadStatus($user);
- }
+ $pinned = $user->loadPreferences()->getPinnedApplications(
+ $applications,
+ $user);
- $tile_groups = array();
- $prefs = $user->loadPreferences()->getPreference(
- PhabricatorUserPreferences::PREFERENCE_APP_TILES,
- array());
- foreach ($applications as $key => $application) {
- $display = idx(
- $prefs,
- get_class($application),
- $application->getDefaultTileDisplay($user));
- $tile_groups[$display][] = $application;
- }
+ // Put "Applications" at the bottom.
+ $meta_app = 'PhabricatorApplicationApplications';
+ $pinned = array_fuse($pinned);
+ unset($pinned[$meta_app]);
+ $pinned[$meta_app] = $meta_app;
- $tile_groups = array_select_keys(
- $tile_groups,
- array(
- PhabricatorApplication::TILE_FULL,
- PhabricatorApplication::TILE_SHOW,
- PhabricatorApplication::TILE_HIDE,
- ));
-
- foreach ($tile_groups as $tile_display => $tile_group) {
- if (!$tile_group) {
+ $tiles = array();
+ foreach ($pinned as $pinned_application) {
+ if (empty($applications[$pinned_application])) {
continue;
}
- $is_small_tiles = ($tile_display == PhabricatorApplication::TILE_SHOW) ||
- ($tile_display == PhabricatorApplication::TILE_HIDE);
+ $application = $applications[$pinned_application];
- if ($is_small_tiles) {
- $groups = PhabricatorApplication::getApplicationGroups();
- $tile_group = mgroup($tile_group, 'getApplicationGroup');
- $tile_group = array_select_keys($tile_group, array_keys($groups));
- } else {
- $tile_group = array($tile_group);
- }
+ $tile = id(new PhabricatorApplicationLaunchView())
+ ->setApplication($application)
+ ->setApplicationStatus($application->loadStatus($user))
+ ->setUser($user);
- $is_hide = ($tile_display == PhabricatorApplication::TILE_HIDE);
- if ($is_hide) {
- $show_item_id = celerity_generate_unique_node_id();
- $hide_item_id = celerity_generate_unique_node_id();
-
- $show_item = id(new PHUIListItemView())
- ->setName(pht('Show More Applications'))
- ->setHref('#')
- ->addSigil('reveal-content')
- ->setID($show_item_id);
-
- $hide_item = id(new PHUIListItemView())
- ->setName(pht('Show Fewer Applications'))
- ->setHref('#')
- ->setStyle('display: none')
- ->setID($hide_item_id)
- ->addSigil('reveal-content');
-
- $nav->addMenuItem($show_item);
- $tile_ids = array($hide_item_id);
- }
-
- foreach ($tile_group as $group => $application_list) {
- $tiles = array();
- foreach ($application_list as $key => $application) {
- $tile = id(new PhabricatorApplicationLaunchView())
- ->setApplication($application)
- ->setApplicationStatus(
- idx($status, get_class($application), array()))
- ->setUser($user);
-
- $tiles[] = $tile;
- }
-
- $group_id = celerity_generate_unique_node_id();
- $tile_ids[] = $group_id;
- $nav->addCustomBlock(
- phutil_tag(
- 'div',
- array(
- 'class' => 'application-tile-group',
- 'id' => $group_id,
- 'style' => ($is_hide ? 'display: none' : null),
- ),
- mpull($tiles, 'render')));
- }
-
- if ($is_hide) {
- Javelin::initBehavior('phabricator-reveal-content');
-
- $show_item->setMetadata(
- array(
- 'showIDs' => $tile_ids,
- 'hideIDs' => array($show_item_id),
- ));
- $hide_item->setMetadata(
- array(
- 'showIDs' => array($show_item_id),
- 'hideIDs' => $tile_ids,
- ));
- $nav->addMenuItem($hide_item);
- }
+ $tiles[] = $tile;
}
+ $nav->addCustomBlock(
+ phutil_tag(
+ 'div',
+ array(
+ 'class' => 'application-tile-group',
+ ),
+ $tiles));
+
$nav->addFilter(
'',
pht('Customize Applications...'),
'/settings/panel/home/');
+
$nav->addClass('phabricator-side-menu-home');
$nav->selectFilter(null);
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php
index eeb267cf02..5547c7918e 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php
@@ -19,194 +19,179 @@ final class PhabricatorSettingsPanelHomePreferences
$user = $request->getUser();
$preferences = $user->loadPreferences();
- require_celerity_resource('phabricator-settings-css');
-
$apps = id(new PhabricatorApplicationQuery())
->setViewer($user)
->withInstalled(true)
->withUnlisted(false)
+ ->withLaunchable(true)
->execute();
- $pref_tiles = PhabricatorUserPreferences::PREFERENCE_APP_TILES;
- $tiles = $preferences->getPreference($pref_tiles, array());
+ $pinned = $preferences->getPinnedApplications($apps, $user);
- if ($request->isFormPost()) {
- $values = $request->getArr('tile');
+ $app_list = array();
+ foreach ($pinned as $app) {
+ if (isset($apps[$app])) {
+ $app_list[$app] = $apps[$app];
+ }
+ }
+
+ if ($request->getBool('add')) {
+ $options = array();
foreach ($apps as $app) {
- $key = get_class($app);
- $value = idx($values, $key);
- switch ($value) {
- case PhabricatorApplication::TILE_FULL:
- case PhabricatorApplication::TILE_SHOW:
- case PhabricatorApplication::TILE_HIDE:
- $tiles[$key] = $value;
- break;
- default:
- unset($tiles[$key]);
- break;
+ $options[get_class($app)] = $app->getName();
+ }
+ asort($options);
+
+ unset($options['PhabricatorApplicationApplications']);
+
+ if ($request->isFormPost()) {
+ $pin = $request->getStr('pin');
+ if (isset($options[$pin]) && !in_array($pin, $pinned)) {
+ $pinned[] = $pin;
+ $preferences->setPreference(
+ PhabricatorUserPreferences::PREFERENCE_APP_PINNED,
+ $pinned);
+ $preferences->save();
+
+ return id(new AphrontRedirectResponse())
+ ->setURI($this->getPanelURI());
}
}
- $preferences->setPreference($pref_tiles, $tiles);
+
+ $options_control = id(new AphrontFormSelectControl())
+ ->setName('pin')
+ ->setLabel(pht('Application'))
+ ->setOptions($options)
+ ->setDisabledOptions(array_keys($app_list));
+
+ $form = id(new AphrontFormView())
+ ->setUser($user)
+ ->addHiddenInput('add', 'true')
+ ->appendRemarkupInstructions(
+ pht('Choose an application to pin to your home page.'))
+ ->appendChild($options_control);
+
+ $dialog = id(new AphrontDialogView())
+ ->setUser($user)
+ ->setWidth(AphrontDialogView::WIDTH_FORM)
+ ->setTitle(pht('Pin Application'))
+ ->appendChild($form->buildLayoutView())
+ ->addSubmitButton(pht('Pin Application'))
+ ->addCancelButton($this->getPanelURI());
+
+ return id(new AphrontDialogResponse())->setDialog($dialog);
+ }
+
+ $unpin = $request->getStr('unpin');
+ if ($unpin) {
+ $app = idx($apps, $unpin);
+ if ($app) {
+ if ($request->isFormPost()) {
+ $pinned = array_diff($pinned, array($unpin));
+ $preferences->setPreference(
+ PhabricatorUserPreferences::PREFERENCE_APP_PINNED,
+ $pinned);
+ $preferences->save();
+
+ return id(new AphrontRedirectResponse())
+ ->setURI($this->getPanelURI());
+ }
+
+ $dialog = id(new AphrontDialogView())
+ ->setUser($user)
+ ->setTitle(pht('Unpin Application'))
+ ->appendParagraph(
+ pht(
+ 'Unpin the %s application from your home page?',
+ phutil_tag('strong', array(), $app->getName())))
+ ->addSubmitButton(pht('Unpin Application'))
+ ->addCanceLButton($this->getPanelURI());
+
+ return id(new AphrontDialogResponse())->setDialog($dialog);
+ }
+ }
+
+ $order = $request->getStrList('order');
+ if ($order && $request->validateCSRF()) {
+ $preferences->setPreference(
+ PhabricatorUserPreferences::PREFERENCE_APP_PINNED,
+ $order);
$preferences->save();
return id(new AphrontRedirectResponse())
- ->setURI($this->getPanelURI('?saved=true'));
+ ->setURI($this->getPanelURI());
}
- $form = id(new AphrontFormView())
- ->setUser($user);
+ $list_id = celerity_generate_unique_node_id();
- $group_map = PhabricatorApplication::getApplicationGroups();
+ $list = id(new PHUIObjectItemListView())
+ ->setUser($user)
+ ->setID($list_id)
+ ->setFlush(true);
- $output = array();
+ Javelin::initBehavior(
+ 'reorder-applications',
+ array(
+ 'listID' => $list_id,
+ 'panelURI' => $this->getPanelURI(),
+ ));
- $app_groups = mgroup($apps, 'getApplicationGroup');
- $app_groups = array_select_keys($app_groups, array_keys($group_map));
-
- foreach ($app_groups as $group => $apps) {
- $group_name = $group_map[$group];
- $rows = array();
-
- foreach ($apps as $app) {
- if (!$app->shouldAppearInLaunchView()) {
- continue;
- }
-
- $default = $app->getDefaultTileDisplay($user);
- if ($default == PhabricatorApplication::TILE_INVISIBLE) {
- continue;
- }
-
- $default_name = PhabricatorApplication::getTileDisplayName($default);
-
- $hide = PhabricatorApplication::TILE_HIDE;
- $show = PhabricatorApplication::TILE_SHOW;
- $full = PhabricatorApplication::TILE_FULL;
-
- $key = get_class($app);
-
- $default_radio_button_status =
- (idx($tiles, $key, 'default') == 'default') ? 'checked' : null;
-
- $hide_radio_button_status =
- (idx($tiles, $key, 'default') == $hide) ? 'checked' : null;
-
- $show_radio_button_status =
- (idx($tiles, $key, 'default') == $show) ? 'checked' : null;
-
- $full_radio_button_status =
- (idx($tiles, $key, 'default') == $full) ? 'checked' : null;
-
-
- $default_radio_button = phutil_tag(
- 'input',
- array(
- 'type' => 'radio',
- 'name' => 'tile['.$key.']',
- 'value' => 'default',
- 'checked' => $default_radio_button_status,
- ));
-
- $hide_radio_button = phutil_tag(
- 'input',
- array(
- 'type' => 'radio',
- 'name' => 'tile['.$key.']',
- 'value' => $hide,
- 'checked' => $hide_radio_button_status,
- ));
-
- $show_radio_button = phutil_tag(
- 'input',
- array(
- 'type' => 'radio',
- 'name' => 'tile['.$key.']',
- 'value' => $show,
- 'checked' => $show_radio_button_status,
- ));
-
- $full_radio_button = phutil_tag(
- 'input',
- array(
- 'type' => 'radio',
- 'name' => 'tile['.$key.']',
- 'value' => $full,
- 'checked' => $full_radio_button_status,
- ));
-
- $desc = $app->getShortDescription();
- $app_column = hsprintf(
- "%s
%s, Default: %s",
- $app->getName(), $desc, $default_name);
-
- $rows[] = array(
- $app_column,
- $default_radio_button,
- $hide_radio_button,
- $show_radio_button,
- $full_radio_button,
- );
- }
-
- if (empty($rows)) {
+ foreach ($app_list as $key => $application) {
+ if ($key == 'PhabricatorApplicationApplications') {
continue;
}
- $table = new AphrontTableView($rows);
+ $icon = $application->getIconName();
+ if (!$icon) {
+ $icon = 'application';
+ }
- $table
- ->setClassName('phabricator-settings-homepagetable')
- ->setHeaders(
- array(
- pht('Applications'),
- pht('Default'),
- pht('Hidden'),
- pht('Small'),
- pht('Large'),
- ))
- ->setColumnClasses(
- array(
- '',
- 'fixed',
- 'fixed',
- 'fixed',
- 'fixed',
- ));
+ $icon_view = javelin_tag(
+ 'span',
+ array(
+ 'class' => 'phui-icon-view '.
+ 'sprite-apps-large apps-'.$icon.'-dark-large',
+ 'aural' => false,
+ ),
+ '');
+ $item = id(new PHUIObjectItemView())
+ ->setHeader($application->getName())
+ ->setImageIcon($icon_view)
+ ->addAttribute($application->getShortDescription())
+ ->setGrippable(true);
- $panel = id(new PHUIObjectBoxView())
- ->setHeaderText($group_name)
- ->appendChild($table);
+ $item->addAction(
+ id(new PHUIListItemView())
+ ->setIcon('fa-times')
+ ->setHref($this->getPanelURI().'?unpin='.$key)
+ ->setWorkflow(true));
- $output[] = $panel;
- }
+ $item->addSigil('pinned-application');
+ $item->setMetadata(
+ array(
+ 'applicationClass' => $key,
+ ));
- $save_button =
- id(new AphrontFormSubmitControl())
- ->setValue(pht('Save Preferences'));
-
- $output[] = id(new PHUIBoxView())
- ->addPadding(PHUI::PADDING_LARGE)
- ->addClass('phabricator-settings-homepagetable-button')
- ->appendChild($save_button);
-
- $form->appendChild($output);
-
- $error_view = null;
- if ($request->getStr('saved') === 'true') {
- $error_view = id(new AphrontErrorView())
- ->setTitle(pht('Preferences Saved'))
- ->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
- ->setErrors(array(pht('Your preferences have been saved.')));
+ $list->addItem($item);
}
$header = id(new PHUIHeaderView())
- ->setHeader(pht('Home Page Preferences'));
+ ->setHeader(pht('Pinned Applications'))
+ ->addActionLink(
+ id(new PHUIButtonView())
+ ->setTag('a')
+ ->setText(pht('Pin Application'))
+ ->setHref($this->getPanelURI().'?add=true')
+ ->setWorkflow(true)
+ ->setIcon(
+ id(new PHUIIconView())
+ ->setIconFont('fa-thumb-tack')));
- $form = id(new PHUIBoxView())
- ->addClass('phabricator-settings-homepagetable-wrap')
- ->appendChild($form);
+ $box = id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->appendChild($list);
- return array($header, $error_view, $form);
+ return $box;
}
}
diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php
index ebc6c99a9f..bdafbb588c 100644
--- a/src/applications/settings/storage/PhabricatorUserPreferences.php
+++ b/src/applications/settings/storage/PhabricatorUserPreferences.php
@@ -24,6 +24,7 @@ final class PhabricatorUserPreferences extends PhabricatorUserDAO {
const PREFERENCE_NAV_COLLAPSED = 'nav-collapsed';
const PREFERENCE_NAV_WIDTH = 'nav-width';
const PREFERENCE_APP_TILES = 'app-tiles';
+ const PREFERENCE_APP_PINNED = 'app-pinned';
const PREFERENCE_DIFF_FILETREE = 'diff-filetree';
@@ -55,4 +56,31 @@ final class PhabricatorUserPreferences extends PhabricatorUserDAO {
return $this;
}
+ public function getPinnedApplications(array $apps, PhabricatorUser $viewer) {
+ $pref_pinned = PhabricatorUserPreferences::PREFERENCE_APP_PINNED;
+ $pinned = $this->getPreference($pref_pinned);
+
+ if ($pinned) {
+ return $pinned;
+ }
+
+ $pref_tiles = PhabricatorUserPreferences::PREFERENCE_APP_TILES;
+ $tiles = $this->getPreference($pref_tiles, array());
+
+ $large = array();
+ foreach ($apps as $app) {
+ $tile = $app->getDefaultTileDisplay($viewer);
+
+ if (isset($tiles[get_class($app)])) {
+ $tile = $tiles[get_class($app)];
+ }
+
+ if ($tile == PhabricatorApplication::TILE_FULL) {
+ $large[] = get_class($app);
+ }
+ }
+
+ return $large;
+ }
+
}
diff --git a/src/view/form/control/AphrontFormSelectControl.php b/src/view/form/control/AphrontFormSelectControl.php
index 06c0c43e02..17766ab435 100644
--- a/src/view/form/control/AphrontFormSelectControl.php
+++ b/src/view/form/control/AphrontFormSelectControl.php
@@ -7,6 +7,7 @@ final class AphrontFormSelectControl extends AphrontFormControl {
}
private $options;
+ private $disabledOptions = array();
public function setOptions(array $options) {
$this->options = $options;
@@ -17,6 +18,11 @@ final class AphrontFormSelectControl extends AphrontFormControl {
return $this->options;
}
+ public function setDisabledOptions(array $disabled) {
+ $this->disabledOptions = $disabled;
+ return $this;
+ }
+
protected function renderInput() {
return self::renderSelectTag(
$this->getValue(),
@@ -25,15 +31,17 @@ final class AphrontFormSelectControl extends AphrontFormControl {
'name' => $this->getName(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'id' => $this->getID(),
- ));
+ ),
+ $this->disabledOptions);
}
public static function renderSelectTag(
$selected,
array $options,
- array $attrs = array()) {
+ array $attrs = array(),
+ array $disabled = array()) {
- $option_tags = self::renderOptions($selected, $options);
+ $option_tags = self::renderOptions($selected, $options, $disabled);
return javelin_tag(
'select',
@@ -41,7 +49,12 @@ final class AphrontFormSelectControl extends AphrontFormControl {
$option_tags);
}
- private static function renderOptions($selected, array $options) {
+ private static function renderOptions(
+ $selected,
+ array $options,
+ array $disabled = array()) {
+ $disabled = array_fuse($disabled);
+
$tags = array();
foreach ($options as $value => $thing) {
if (is_array($thing)) {
@@ -57,6 +70,7 @@ final class AphrontFormSelectControl extends AphrontFormControl {
array(
'selected' => ($value == $selected) ? 'selected' : null,
'value' => $value,
+ 'disabled' => isset($disabled[$value]) ? 'disabled' : null,
),
$thing);
}
diff --git a/webroot/rsrc/css/application/settings/settings.css b/webroot/rsrc/css/application/settings/settings.css
deleted file mode 100644
index 28912b68bf..0000000000
--- a/webroot/rsrc/css/application/settings/settings.css
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * @provides phabricator-settings-css
- */
-
-.phabricator-settings-homepagetable .fixed {
- width: 48px;
- text-align: center;
-}
-
-.phabricator-settings-homepagetable td em {
- color: {$lightgreytext};
-}
-
-.phabricator-settings-homepagetable-button .aphront-form-input {
- margin: 0;
- width: auto;
-}
-
-.phabricator-settings-homepagetable-button .aphront-form-control {
- padding: 0;
-}
-
-.phabricator-settings-homepagetable-wrap .phui-form-view {
- padding: 0;
-}
diff --git a/webroot/rsrc/js/core/behavior-reorder-applications.js b/webroot/rsrc/js/core/behavior-reorder-applications.js
new file mode 100644
index 0000000000..63e3b91e92
--- /dev/null
+++ b/webroot/rsrc/js/core/behavior-reorder-applications.js
@@ -0,0 +1,37 @@
+/**
+ * @provides javelin-behavior-reorder-applications
+ * @requires javelin-behavior
+ * javelin-stratcom
+ * javelin-workflow
+ * javelin-dom
+ * phabricator-draggable-list
+ */
+
+JX.behavior('reorder-applications', function(config) {
+
+ var root = JX.$(config.listID);
+
+ var list = new JX.DraggableList('pinned-application', root)
+ .setFindItemsHandler(function() {
+ return JX.DOM.scry(root, 'li', 'pinned-application');
+ });
+
+ list.listen('didDrop', function(node, after) {
+ var nodes = list.findItems();
+ var order = [];
+ var key;
+ for (var ii = 0; ii < nodes.length; ii++) {
+ key = JX.Stratcom.getData(nodes[ii]).applicationClass;
+ if (key) {
+ order.push(key);
+ }
+ }
+
+ list.lock();
+ JX.DOM.alterClass(node, 'drag-sending', true);
+
+ new JX.Workflow(config.panelURI, {order: order.join()})
+ .start();
+ });
+
+});