Commit 0cbc19f9 authored by Robert Schilling's avatar Robert Schilling

Awesome shortcuts for GitLab

parent 174c00cf
......@@ -10,6 +10,7 @@ v 7.3.0
- Support Unix domain sockets for Redis
- Store session Redis keys in 'session:gitlab:' namespace
- Deprecate LDAP account takeover based on partial LDAP email / GitLab username match
- Keyboard shortcuts for productivity (Robert Schilling)
v 7.2.0
- Explore page
......
......@@ -156,6 +156,9 @@ gem "rack-attack"
# Ace editor
gem 'ace-rails-ap'
# Keyboard shortcuts
gem 'mousetrap-rails'
# Semantic UI Sass for Sidebar
gem 'semantic-ui-sass', '~> 0.16.1.0'
......
......@@ -287,6 +287,7 @@ GEM
mime-types (1.25.1)
mini_portile (0.6.0)
minitest (5.3.5)
mousetrap-rails (1.4.6)
multi_json (1.10.1)
multi_xml (0.5.5)
multipart-post (1.2.0)
......@@ -636,6 +637,7 @@ DEPENDENCIES
launchy
letter_opener
minitest (~> 5.3.0)
mousetrap-rails
mysql2
nprogress-rails
omniauth (~> 1.1.3)
......
......@@ -33,6 +33,12 @@
#= require nprogress-turbolinks
#= require dropzone
#= require semantic-ui/sidebar
#= require mousetrap
#= require shortcuts
#= require shortcuts_navigation
#= require shortcuts_dashboard_navigation
#= require shortcuts_issueable
#= require shortcuts_network
#= require_tree .
window.slugify = (text) ->
......@@ -119,6 +125,13 @@ $ ->
# Initialize select2 selects
$('select.select2').select2(width: 'resolve', dropdownAutoWidth: true)
# Close select2 on escape
$('.js-select2').bind 'select2-close', ->
setTimeout ( ->
$('.select2-container-active').removeClass('select2-container-active')
$(':focus').blur()
), 1
# Initialize tooltips
$('.has_tooltip').tooltip()
......@@ -151,20 +164,6 @@ $ ->
# Show/Hide the profile menu when hovering the account box
$('.account-box').hover -> $(@).toggleClass('hover')
# Focus search field by pressing 's' key
$(document).keypress (e) ->
# Don't do anything if typing in an input
return if $(e.target).is(":input")
switch e.which
when 115
$("#search").focus()
e.preventDefault()
when 63
new Shortcuts()
e.preventDefault()
# Commit show suppressed diff
$(".diff-content").on "click", ".supp_diff_link", ->
$(@).next('table').show()
......
class BranchGraph
class @BranchGraph
constructor: (@element, @options) ->
@preparedCommits = {}
@mtime = 0
......@@ -120,23 +120,32 @@ class BranchGraph
@top.toFront()
bindEvents: ->
drag = {}
element = @element
$(element).scroll (event) =>
@renderPartialGraph()
$(window).on
keydown: (event) =>
# left
element.scrollLeft element.scrollLeft() - 50 if event.keyCode is 37
# top
element.scrollTop element.scrollTop() - 50 if event.keyCode is 38
# right
element.scrollLeft element.scrollLeft() + 50 if event.keyCode is 39
# bottom
element.scrollTop element.scrollTop() + 50 if event.keyCode is 40
@renderPartialGraph()
scrollDown: =>
@element.scrollTop @element.scrollTop() + 50
@renderPartialGraph()
scrollUp: =>
@element.scrollTop @element.scrollTop() - 50
@renderPartialGraph()
scrollLeft: =>
@element.scrollLeft @element.scrollLeft() - 50
@renderPartialGraph()
scrollRight: =>
@element.scrollLeft @element.scrollLeft() + 50
@renderPartialGraph()
scrollBottom: =>
@element.scrollTop @element.find('svg').height()
scrollTop: =>
@element.scrollTop 0
appendLabel: (x, y, commit) ->
return unless commit.refs
......@@ -325,5 +334,3 @@ Raphael::textWrap = (t, width) ->
b = t.getBBox()
h = Math.abs(b.y2) - Math.abs(b.y) + 1
t.attr y: b.y + h
@BranchGraph = BranchGraph
......@@ -15,49 +15,83 @@ class Dispatcher
return false
path = page.split(':')
shortcut_handler = null
switch page
when 'projects:issues:index'
Issues.init()
shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show'
new Issue()
shortcut_handler = new ShortcutsIssueable()
when 'projects:milestones:show'
new Milestone()
when 'projects:issues:new'
GitLab.GfmAutoComplete.setup()
shortcut_handler = new ShortcutsNavigation()
when 'projects:merge_requests:new'
GitLab.GfmAutoComplete.setup()
new Diff()
shortcut_handler = new ShortcutsNavigation()
when 'projects:merge_requests:show'
new Diff()
shortcut_handler = new ShortcutsIssueable()
when "projects:merge_requests:diffs"
new Diff()
when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation()
when 'dashboard:show'
new Dashboard()
new Activities()
when 'projects:commit:show'
new Commit()
new Diff()
shortcut_handler = new ShortcutsNavigation()
when 'projects:commits:show'
shortcut_handler = new ShortcutsNavigation()
when 'groups:show', 'projects:show'
new Activities()
when 'projects:new', 'projects:edit'
shortcut_handler = new ShortcutsNavigation()
when 'projects:new'
new Project()
when 'projects:edit'
new Project()
shortcut_handler = new ShortcutsNavigation()
when 'projects:teams:members:index'
new TeamMembers()
when 'groups:members'
new GroupMembers()
when 'projects:tree:show'
new TreeView()
shortcut_handler = new ShortcutsNavigation()
when 'projects:blob:show'
new BlobView()
shortcut_handler = new ShortcutsNavigation()
when 'projects:labels:new', 'projects:labels:edit'
new Labels()
when 'projects:network:show'
# Ensure we don't create a particular shortcut handler here. This is
# already created, where the network graph is created.
shortcut_handler = true
switch path.first()
when 'admin' then new Admin()
when 'dashboard'
shortcut_handler = new ShortcutsDashboardNavigation()
when 'projects'
new Wikis() if path[1] == 'wikis'
switch path[1]
when 'wikis'
new Wikis()
shortcut_handler = new ShortcutsNavigation()
when 'snippets', 'labels', 'graphs'
shortcut_handler = new ShortcutsNavigation()
when 'team_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation()
# If we haven't installed a custom shortcut handler, install the default one
if not shortcut_handler
new Shortcuts()
initSearch: ->
opts = $('.search-autocomplete-opts')
......
class Network
class @Network
constructor: (opts) ->
$("#filter_ref").click ->
$(this).closest('form').submit()
branch_graph = new BranchGraph($(".network-graph"), opts)
@branch_graph = new BranchGraph($(".network-graph"), opts)
vph = $(window).height() - 250
$('.network-graph').css 'height': (vph + 'px')
@Network = Network
class Shortcuts
class @Shortcuts
constructor: ->
@enabledHelp = []
Mousetrap.reset()
Mousetrap.bind('?', @selectiveHelp)
Mousetrap.bind('s', Shortcuts.focusSearch)
selectiveHelp: (e) =>
Shortcuts.showHelp(e, @enabledHelp)
@showHelp: (e, location) ->
if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show')
else
$.ajax(
url: '/help/shortcuts',
dataType: "script"
dataType: 'script',
success: (e) ->
if location and location.length > 0
for l in location
$(l).show()
else
$('.hidden-shortcut').show()
$('.js-more-help-button').remove()
)
e.preventDefault()
@Shortcuts = Shortcuts
@focusSearch: (e) ->
$('#search').focus()
e.preventDefault()
#= require shortcuts
class @ShortcutsDashboardNavigation extends Shortcuts
constructor: ->
super()
Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-activity'))
Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-projects'))
Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-issues'))
Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-merge_requests'))
@findAndollowLink: (selector) ->
link = $(selector).attr('href')
if link
window.location = link
#= require shortcuts_navigation
class @ShortcutsIssueable extends ShortcutsNavigation
constructor: (isMergeRequest) ->
super()
Mousetrap.bind('a', ->
$('.js-assignee').select2('open')
return false
)
Mousetrap.bind('m', ->
$('.js-milestone').select2('open')
return false
)
if isMergeRequest
@enabledHelp.push('.hidden-shortcut.merge_reuests')
else
@enabledHelp.push('.hidden-shortcut.issues')
#= require shortcuts
class @ShortcutsNavigation extends Shortcuts
constructor: ->
super()
Mousetrap.bind('g a', -> ShortcutsNavigation.findAndollowLink('.shortcuts-activity'))
Mousetrap.bind('g f', -> ShortcutsNavigation.findAndollowLink('.shortcuts-tree'))
Mousetrap.bind('g c', -> ShortcutsNavigation.findAndollowLink('.shortcuts-commits'))
Mousetrap.bind('g n', -> ShortcutsNavigation.findAndollowLink('.shortcuts-network'))
Mousetrap.bind('g g', -> ShortcutsNavigation.findAndollowLink('.shortcuts-graphs'))
Mousetrap.bind('g i', -> ShortcutsNavigation.findAndollowLink('.shortcuts-issues'))
Mousetrap.bind('g m', -> ShortcutsNavigation.findAndollowLink('.shortcuts-merge_requests'))
Mousetrap.bind('g w', -> ShortcutsNavigation.findAndollowLink('.shortcuts-wiki'))
Mousetrap.bind('g s', -> ShortcutsNavigation.findAndollowLink('.shortcuts-snippets'))
@enabledHelp.push('.hidden-shortcut.project')
@findAndollowLink: (selector) ->
link = $(selector).attr('href')
if link
window.location = link
#= require shortcuts_navigation
class @ShortcutsNetwork extends ShortcutsNavigation
constructor: (@graph) ->
super()
Mousetrap.bind(['left', 'h'], @graph.scrollLeft)
Mousetrap.bind(['right', 'l'], @graph.scrollRight)
Mousetrap.bind(['up', 'k'], @graph.scrollUp)
Mousetrap.bind(['down', 'j'], @graph.scrollDown)
Mousetrap.bind(['shift+up', 'shift+k'], @graph.scrollTop)
Mousetrap.bind(['shift+down', 'shift+j'], @graph.scrollBottom)
@enabledHelp.push('.hidden-shortcut.network')
......@@ -16,3 +16,4 @@ body {
.container .content {
margin: 0 0;
}
......@@ -17,3 +17,56 @@
}
}
}
.shortcut-mappings {
font-size: 12px;
color: #555;
tbody:first-child tr:first-child {
padding-top: 0
}
th {
padding-top: 15px;
font-size: 14px;
line-height: 1.5;
color: #333;
text-align: left
}
td {
padding-top: 3px;
padding-bottom: 3px;
vertical-align: top;
line-height: 20px
}
.shortcut {
padding-right: 10px;
color: #999;
text-align: right;
white-space: nowrap
}
.key {
@extend .label;
@extend .label-inverse;
font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
padding: 3px 5px;
}
}
.modal-body {
position: relative;
overflow-y: auto;
padding: 15px;
}
body.modal-open {
overflow: hidden;
}
.modal .modal-dialog {
width: 860px;
}
......@@ -3,30 +3,207 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3 Keyboard Shortcuts
.modal-body
%h5 Global Shortcuts
%p
%span.label.label-inverse s
–
Focus Search
%p
%span.label.label-inverse ?
–
Show this dialog
%h4
Keyboard Shortcuts
%small
= link_to '(Show all)', '#', class: 'js-more-help-button'
.modal-body.shortcuts-cheatsheet
.col-lg-4
%table.shortcut-mappings
%tbody
%tr
%th
%th Global Shortcuts
%tr
%td.shortcut
.key s
%td Focus Search
%tr
%td.shortcut
.key ?
%td Show this dialog
%tbody
%tr
%th
%th Project Files browsing
%tr
%td.shortcut
.key
%i.icon-arrow-up
%td Move selection up
%tr
%td.shortcut
.key
%i.icon-arrow-down
%td Move selection down
%tr
%td.shortcut
.key enter
%td Open Selection
%h5 Project Files browsing
%p
%span.label.label-inverse
%i.icon-arrow-up
–
Move selection up
%p
%span.label.label-inverse
%i.icon-arrow-down
–
Move selection down
%p
%span.label.label-inverse Enter
–
Open selection
.col-lg-4
%table.shortcut-mappings
%tbody{ class: 'hidden-shortcut project', style: 'display:none' }
%tr
%th
%th Global Dashboard
%tr
%td.shortcut
.key g
.key a
%td
Go to the activity feed
%tr
%td.shortcut
.key g
.key p
%td
Go to projects
%tr
%td.shortcut
.key g
.key i
%td
Go to issues
%tr
%td.shortcut
.key g
.key m
%td
Go to merge requests
%tbody
%tr
%th
%th Project
%tr
%td.shortcut
.key g
.key a
%td
Go to the activity feed
%tr
%td.shortcut
.key g
.key f
%td
Go to files
%tr
%td.shortcut
.key g
.key c
%td
Go to commits
%tr
%td.shortcut
.key g
.key n
%td
Go to network graph
%tr
%td.shortcut
.key g
.key g
%td
Go to graphs
%tr
%td.shortcut
.key g
.key i
%td
Go to issues
%tr
%td.shortcut
.key g
.key m
%td
Go to merge requests
%tr
%td.shortcut
.key g
.key s
%td
Go to snippets
.col-lg-4
%table.shortcut-mappings
%tbody{ class: 'hidden-shortcut network', style: 'display:none' }
%tr
%th
%th Network Graph
%tr
%td.shortcut
.key
%i.icon-arrow-left
\/
.key h
%td Scroll left
%tr
%td.shortcut
.key
%i.icon-arrow-right
\/
.key l
%td Scroll right
%tr
%td.shortcut
.key
%i.icon-arrow-up
\/
.key k
%td Scroll up
%tr
%td.shortcut
.key
%i.icon-arrow-down
\/
.key j
%td Scroll down
%tr
%td.shortcut
.key
shift
%i.icon-arrow-up
\/
.key
shift k
%td Scroll to top
%tr
%td.shortcut
.key
shift
%i.icon-arrow-down
\/
.key
shift j
%td Scroll to bottom
%tbody{ class: 'hidden-shortcut issues', style: 'display:none' }
%tr
%th
%th Issues
%tr
%td.shortcut
.key a
%td Change assignee
%tr
%td.shortcut
.key m
%td Change milestone
%tbody{ class: 'hidden-shortcut merge_reuests', style: 'display:none' }
%tr
%th
%th Merge Requests
%tr
%td.shortcut
.key a
%td Change assignee
%tr
%td.shortcut
.key m
%td Change milestone
:javascript
$('.js-more-help-button').click(function(e){
$(this).remove()
$('.hidden-shortcut').show()
e.preventDefault()
});
......@@ -37,8 +37,8 @@
= link_to "getting help", "https://www.gitlab.com/getting-help/"
%li
Use the
= link_to "search bar", '#', onclick: "$('#search').focus();"
= link_to 'search bar', '#', onclick: 'Shortcuts.focusSearch(event)'
on the top of this page
%li
Use
= link_to "shortcuts", '#', onclick: "new Shortcuts()"
= link_to 'shortcuts', '#', onclick: 'Shortcuts.showHelp(event)'
......@@ -8,3 +8,10 @@
= hidden_field_tag :repository_ref, @ref
= submit_tag 'Go' if ENV['RAILS_ENV'] == 'test'
.search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
:javascript
$('.search-input').on('keyup', function(e) {
if (e.keyCode == 27) {
$('.search-input').blur()
}
})
%ul
= nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do
= link_to root_path, title: "Home" do
= link_to root_path, title: 'Home', class: 'shortcuts-activity' do
Activity
= nav_link(path: 'dashboard#projects') do
= link_to projects_dashboard_path do
= link_to projects_dashboard_path, class: 'shortcuts-projects' do
Projects
= nav_link(path: 'dashboard#issues') do
= link_to issues_dashboard_path do
= link_to issues_dashboard_path, class: 'shortcuts-issues' do
Issues
%span.count= current_user.assigned_issues.opened.count
= nav_link(path: 'dashboard#merge_requests') do
= link_to merge_requests_dashboard_path do
= link_to merge_requests_dashboard_path, class: 'shortcuts-merge_requests' do
Merge Requests
%span.count= current_user.assigned_merge_requests.opened.count
= nav_link(controller: :help) do
......
%ul
%ul.project-navigation
= nav_link(path: 'projects#show', html_options: {class: "home"}) do
= link_to project_path(@project), title: "Project" do
Project
= link_to project_path(@project), title: 'Project', class: 'shortcuts-activity' do
Activity
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree)) do
= link_to 'Files', project_tree_path(@project, @ref || @repository.root_ref)
= link_to 'Files', project_tree_path(@project, @ref || @repository.root_ref), class: 'shortcuts-tree'
- if project_nav_tab? :commits
= nav_link(controller: %w(commit commits compare repositories tags branches)) do
= link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref)
= link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref), class: 'shortcuts-commits'
- if project_nav_tab? :network
= nav_link(controller: %w(network)) do
= link_to "Network", project_network_path(@project, @ref || @repository.root_ref)
= link_to "Network", project_network_path(@project, @ref || @repository.root_ref), class: 'shortcuts-network'
- if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do
= link_to "Graphs", project_graph_path(@project, @ref || @repository.root_ref)
= link_to "Graphs", project_graph_path(@project, @ref || @repository.root_ref), class: 'shortcuts-graphs'
- if project_nav_tab? :issues
= nav_link(controller: %w(issues milestones labels)) do
= link_to url_for_project_issues do
= link_to url_for_project_issues, class: 'shortcuts-issues' do