diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -100,7 +100,6 @@ '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 @@ '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 @@ '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 @@ '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 @@ 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 --- a/src/applications/home/controller/PhabricatorHomeController.php +++ b/src/applications/home/controller/PhabricatorHomeController.php @@ -25,13 +25,10 @@ ->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 @@ } } - $status = array(); - foreach ($applications as $key => $application) { - $status[get_class($application)] = $application->loadStatus($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; - } + $pinned = $user->loadPreferences()->getPinnedApplications( + $applications, + $user); - $tile_groups = array_select_keys( - $tile_groups, - array( - PhabricatorApplication::TILE_FULL, - PhabricatorApplication::TILE_SHOW, - PhabricatorApplication::TILE_HIDE, - )); + // Put "Applications" at the bottom. + $meta_app = 'PhabricatorApplicationApplications'; + $pinned = array_fuse($pinned); + unset($pinned[$meta_app]); + $pinned[$meta_app] = $meta_app; - 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 --- a/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php @@ -19,194 +19,179 @@ $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'); - 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; - } + $app_list = array(); + foreach ($pinned as $app) { + if (isset($apps[$app])) { + $app_list[$app] = $apps[$app]; } - $preferences->setPreference($pref_tiles, $tiles); - $preferences->save(); - - return id(new AphrontRedirectResponse()) - ->setURI($this->getPanelURI('?saved=true')); } - $form = id(new AphrontFormView()) - ->setUser($user); - - $group_map = PhabricatorApplication::getApplicationGroups(); - - $output = array(); + if ($request->getBool('add')) { + $options = array(); + foreach ($apps as $app) { + $options[get_class($app)] = $app->getName(); + } + asort($options); - $app_groups = mgroup($apps, 'getApplicationGroup'); - $app_groups = array_select_keys($app_groups, array_keys($group_map)); + unset($options['PhabricatorApplicationApplications']); - foreach ($app_groups as $group => $apps) { - $group_name = $group_map[$group]; - $rows = array(); + 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(); - foreach ($apps as $app) { - if (!$app->shouldAppearInLaunchView()) { - continue; + return id(new AphrontRedirectResponse()) + ->setURI($this->getPanelURI()); } + } + + $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); + } - $default = $app->getDefaultTileDisplay($user); - if ($default == PhabricatorApplication::TILE_INVISIBLE) { - continue; + $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()); } - $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, - ); + $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); } + } - if (empty($rows)) { - continue; - } + $order = $request->getStrList('order'); + if ($order && $request->validateCSRF()) { + $preferences->setPreference( + PhabricatorUserPreferences::PREFERENCE_APP_PINNED, + $order); + $preferences->save(); - $table = new AphrontTableView($rows); - - $table - ->setClassName('phabricator-settings-homepagetable') - ->setHeaders( - array( - pht('Applications'), - pht('Default'), - pht('Hidden'), - pht('Small'), - pht('Large'), - )) - ->setColumnClasses( - array( - '', - 'fixed', - 'fixed', - 'fixed', - 'fixed', - )); - - - $panel = id(new PHUIObjectBoxView()) - ->setHeaderText($group_name) - ->appendChild($table); - - $output[] = $panel; + return id(new AphrontRedirectResponse()) + ->setURI($this->getPanelURI()); } - $save_button = - id(new AphrontFormSubmitControl()) - ->setValue(pht('Save Preferences')); + $list_id = celerity_generate_unique_node_id(); - $output[] = id(new PHUIBoxView()) - ->addPadding(PHUI::PADDING_LARGE) - ->addClass('phabricator-settings-homepagetable-button') - ->appendChild($save_button); + $list = id(new PHUIObjectItemListView()) + ->setUser($user) + ->setID($list_id) + ->setFlush(true); - $form->appendChild($output); + Javelin::initBehavior( + 'reorder-applications', + array( + 'listID' => $list_id, + 'panelURI' => $this->getPanelURI(), + )); - $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.'))); - } + foreach ($app_list as $key => $application) { + if ($key == 'PhabricatorApplicationApplications') { + continue; + } - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Home Page Preferences')); + $icon = $application->getIconName(); + if (!$icon) { + $icon = 'application'; + } - $form = id(new PHUIBoxView()) - ->addClass('phabricator-settings-homepagetable-wrap') - ->appendChild($form); + $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); + + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('fa-times') + ->setHref($this->getPanelURI().'?unpin='.$key) + ->setWorkflow(true)); + + $item->addSigil('pinned-application'); + $item->setMetadata( + array( + 'applicationClass' => $key, + )); + + $list->addItem($item); + } - return array($header, $error_view, $form); + $header = id(new PHUIHeaderView()) + ->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'))); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($list); + + return $box; } } diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -24,6 +24,7 @@ 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 @@ 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 --- a/src/view/form/control/AphrontFormSelectControl.php +++ b/src/view/form/control/AphrontFormSelectControl.php @@ -7,6 +7,7 @@ } private $options; + private $disabledOptions = array(); public function setOptions(array $options) { $this->options = $options; @@ -17,6 +18,11 @@ 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 @@ '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 @@ $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 @@ 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 --- 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 --- /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(); + }); + +});