summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorZeger-Jan van de Weg <zegerjan@gitlab.com>2016-03-29 17:26:59 +0200
committerZeger-Jan van de Weg <zegerjan@gitlab.com>2016-03-29 17:26:59 +0200
commit3339513ca67c50a231b8906a33eccc0d209270a5 (patch)
treee9dbe77b814abd59062ba5dcdd3f454011d758cc /app
parent15a6633999c81387245cabf129dd2fbb04650c95 (diff)
parent54957d6932c2b159e01b60ee1d4e191cfdf5b713 (diff)
downloadgitlab-ce-3339513ca67c50a231b8906a33eccc0d209270a5.tar.gz
Merge branch 'master' into assign-to-issuable-opener
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/activities.js.coffee18
-rw-r--r--app/assets/javascripts/api.js.coffee16
-rw-r--r--app/assets/javascripts/application.js.coffee69
-rw-r--r--app/assets/javascripts/aside.js.coffee1
-rw-r--r--app/assets/javascripts/autosave.js.coffee6
-rw-r--r--app/assets/javascripts/awards_handler.coffee79
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js.coffee47
-rw-r--r--app/assets/javascripts/breakpoints.coffee37
-rw-r--r--app/assets/javascripts/ci/build.coffee13
-rw-r--r--app/assets/javascripts/dashboard.js.coffee31
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee22
-rw-r--r--app/assets/javascripts/gl_crop.js.coffee152
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee331
-rw-r--r--app/assets/javascripts/issuable_context.js.coffee49
-rw-r--r--app/assets/javascripts/issuable_form.js.coffee52
-rw-r--r--app/assets/javascripts/issue_status_select.js.coffee11
-rw-r--r--app/assets/javascripts/issues.js.coffee26
-rw-r--r--app/assets/javascripts/labels_select.js.coffee249
-rw-r--r--app/assets/javascripts/lib/animate.js.coffee13
-rw-r--r--app/assets/javascripts/logo.js.coffee2
-rw-r--r--app/assets/javascripts/markdown_preview.js.coffee46
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee39
-rw-r--r--app/assets/javascripts/milestone.js.coffee33
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee107
-rw-r--r--app/assets/javascripts/notes.js.coffee140
-rw-r--r--app/assets/javascripts/pager.js.coffee3
-rw-r--r--app/assets/javascripts/profile.js.coffee68
-rw-r--r--app/assets/javascripts/project.js.coffee1
-rw-r--r--app/assets/javascripts/project_new.js.coffee13
-rw-r--r--app/assets/javascripts/projects_list.js.coffee55
-rw-r--r--app/assets/javascripts/shortcuts.js.coffee19
-rw-r--r--app/assets/javascripts/shortcuts_issuable.coffee8
-rw-r--r--app/assets/javascripts/sidebar.js.coffee25
-rw-r--r--app/assets/javascripts/stat_graph_contributors_util.js.coffee2
-rw-r--r--app/assets/javascripts/subscription.js.coffee34
-rw-r--r--app/assets/javascripts/todos.js.coffee56
-rw-r--r--app/assets/javascripts/user.js.coffee11
-rw-r--r--app/assets/javascripts/user_tabs.js.coffee146
-rw-r--r--app/assets/javascripts/users_select.js.coffee186
-rw-r--r--app/assets/javascripts/wikis.js.coffee3
-rw-r--r--app/assets/stylesheets/application.scss8
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/blocks.scss32
-rw-r--r--app/assets/stylesheets/framework/buttons.scss53
-rw-r--r--app/assets/stylesheets/framework/calendar.scss8
-rw-r--r--app/assets/stylesheets/framework/callout.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss68
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss375
-rw-r--r--app/assets/stylesheets/framework/files.scss17
-rw-r--r--app/assets/stylesheets/framework/filters.scss25
-rw-r--r--app/assets/stylesheets/framework/fonts.scss4
-rw-r--r--app/assets/stylesheets/framework/forms.scss16
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss30
-rw-r--r--app/assets/stylesheets/framework/header.scss32
-rw-r--r--app/assets/stylesheets/framework/highlight.scss8
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss20
-rw-r--r--app/assets/stylesheets/framework/jquery.scss22
-rw-r--r--app/assets/stylesheets/framework/layout.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss28
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss10
-rw-r--r--app/assets/stylesheets/framework/mobile.scss6
-rw-r--r--app/assets/stylesheets/framework/nav.scss64
-rw-r--r--app/assets/stylesheets/framework/progress.scss5
-rw-r--r--app/assets/stylesheets/framework/selects.scss152
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss160
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss12
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss8
-rw-r--r--app/assets/stylesheets/framework/typography.scss42
-rw-r--r--app/assets/stylesheets/framework/variables.scss181
-rw-r--r--app/assets/stylesheets/framework/zen.scss2
-rw-r--r--app/assets/stylesheets/highlight/dark.scss18
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss2
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss2
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss2
-rw-r--r--app/assets/stylesheets/highlight/white.scss80
-rw-r--r--app/assets/stylesheets/notify.scss24
-rw-r--r--app/assets/stylesheets/pages/admin.scss6
-rw-r--r--app/assets/stylesheets/pages/appearances.scss11
-rw-r--r--app/assets/stylesheets/pages/awards.scss210
-rw-r--r--app/assets/stylesheets/pages/builds.scss25
-rw-r--r--app/assets/stylesheets/pages/commit.scss3
-rw-r--r--app/assets/stylesheets/pages/commits.scss13
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss6
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss3
-rw-r--r--app/assets/stylesheets/pages/diff.scss54
-rw-r--r--app/assets/stylesheets/pages/editor.scss4
-rw-r--r--app/assets/stylesheets/pages/emojis.scss164
-rw-r--r--app/assets/stylesheets/pages/events.scss21
-rw-r--r--app/assets/stylesheets/pages/graph.scss4
-rw-r--r--app/assets/stylesheets/pages/import.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss236
-rw-r--r--app/assets/stylesheets/pages/issues.scss43
-rw-r--r--app/assets/stylesheets/pages/labels.scss50
-rw-r--r--app/assets/stylesheets/pages/login.scss16
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss21
-rw-r--r--app/assets/stylesheets/pages/milestone.scss18
-rw-r--r--app/assets/stylesheets/pages/note_form.scss6
-rw-r--r--app/assets/stylesheets/pages/notes.scss56
-rw-r--r--app/assets/stylesheets/pages/notifications.scss18
-rw-r--r--app/assets/stylesheets/pages/profile.scss173
-rw-r--r--app/assets/stylesheets/pages/projects.scss110
-rw-r--r--app/assets/stylesheets/pages/runners.scss2
-rw-r--r--app/assets/stylesheets/pages/search.scss12
-rw-r--r--app/assets/stylesheets/pages/sherlock.scss4
-rw-r--r--app/assets/stylesheets/pages/snippets.scss34
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss2
-rw-r--r--app/assets/stylesheets/pages/status.scss92
-rw-r--r--app/assets/stylesheets/pages/todos.scss51
-rw-r--r--app/assets/stylesheets/pages/tree.scss6
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss11
-rw-r--r--app/assets/stylesheets/pages/wiki.scss5
-rw-r--r--app/assets/stylesheets/pages/xterm.scss62
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb2
-rw-r--r--app/controllers/admin/appearances_controller.rb57
-rw-r--r--app/controllers/admin/application_settings_controller.rb1
-rw-r--r--app/controllers/admin/groups_controller.rb10
-rw-r--r--app/controllers/admin/labels_controller.rb2
-rw-r--r--app/controllers/admin/projects_controller.rb7
-rw-r--r--app/controllers/admin/users_controller.rb6
-rw-r--r--app/controllers/application_controller.rb65
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/ci/projects_controller.rb12
-rw-r--r--app/controllers/concerns/continue_params.rb13
-rw-r--r--app/controllers/concerns/filter_projects.rb15
-rw-r--r--app/controllers/concerns/global_milestones.rb1
-rw-r--r--app/controllers/concerns/issuable_actions.rb23
-rw-r--r--app/controllers/concerns/issues_action.rb4
-rw-r--r--app/controllers/concerns/merge_requests_action.rb4
-rw-r--r--app/controllers/concerns/toggle_subscription_action.rb17
-rw-r--r--app/controllers/dashboard/application_controller.rb6
-rw-r--r--app/controllers/dashboard/groups_controller.rb2
-rw-r--r--app/controllers/dashboard/labels_controller.rb9
-rw-r--r--app/controllers/dashboard/milestones_controller.rb15
-rw-r--r--app/controllers/dashboard/projects_controller.rb26
-rw-r--r--app/controllers/dashboard/snippets_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb17
-rw-r--r--app/controllers/dashboard_controller.rb4
-rw-r--r--app/controllers/emojis_controller.rb6
-rw-r--r--app/controllers/explore/groups_controller.rb4
-rw-r--r--app/controllers/explore/projects_controller.rb17
-rw-r--r--app/controllers/explore/snippets_controller.rb2
-rw-r--r--app/controllers/groups/application_controller.rb27
-rw-r--r--app/controllers/groups/avatars_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb3
-rw-r--r--app/controllers/groups/milestones_controller.rb18
-rw-r--r--app/controllers/groups_controller.rb50
-rw-r--r--app/controllers/namespaces_controller.rb2
-rw-r--r--app/controllers/oauth/applications_controller.rb24
-rw-r--r--app/controllers/oauth/authorizations_controller.rb1
-rw-r--r--app/controllers/passwords_controller.rb8
-rw-r--r--app/controllers/profiles/keys_controller.rb8
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb12
-rw-r--r--app/controllers/profiles_controller.rb27
-rw-r--r--app/controllers/projects/application_controller.rb72
-rw-r--r--app/controllers/projects/avatars_controller.rb9
-rw-r--r--app/controllers/projects/badges_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb23
-rw-r--r--app/controllers/projects/commit_controller.rb9
-rw-r--r--app/controllers/projects/compare_controller.rb12
-rw-r--r--app/controllers/projects/forks_controller.rb35
-rw-r--r--app/controllers/projects/group_links_controller.rb23
-rw-r--r--app/controllers/projects/imports_controller.rb12
-rw-r--r--app/controllers/projects/issues_controller.rb43
-rw-r--r--app/controllers/projects/labels_controller.rb18
-rw-r--r--app/controllers/projects/merge_requests_controller.rb32
-rw-r--r--app/controllers/projects/milestones_controller.rb13
-rw-r--r--app/controllers/projects/project_members_controller.rb1
-rw-r--r--app/controllers/projects/raw_controller.rb15
-rw-r--r--app/controllers/projects/refs_controller.rb6
-rw-r--r--app/controllers/projects/snippets_controller.rb8
-rw-r--r--app/controllers/projects/tags_controller.rb9
-rw-r--r--app/controllers/projects/uploads_controller.rb6
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb30
-rw-r--r--app/controllers/root_controller.rb4
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb18
-rw-r--r--app/controllers/snippets_controller.rb2
-rw-r--r--app/controllers/uploads_controller.rb5
-rw-r--r--app/controllers/users_controller.rb70
-rw-r--r--app/finders/contributed_projects_finder.rb24
-rw-r--r--app/finders/group_projects_finder.rb42
-rw-r--r--app/finders/groups_finder.rb18
-rw-r--r--app/finders/issuable_finder.rb29
-rw-r--r--app/finders/issues_finder.rb6
-rw-r--r--app/finders/joined_groups_finder.rb24
-rw-r--r--app/finders/personal_projects_finder.rb28
-rw-r--r--app/finders/projects_finder.rb72
-rw-r--r--app/finders/snippets_finder.rb6
-rw-r--r--app/finders/union_finder.rb11
-rw-r--r--app/helpers/appearances_helper.rb28
-rw-r--r--app/helpers/application_helper.rb22
-rw-r--r--app/helpers/auth_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb45
-rw-r--r--app/helpers/button_helper.rb30
-rw-r--r--app/helpers/ci_status_helper.rb14
-rw-r--r--app/helpers/commits_helper.rb17
-rw-r--r--app/helpers/diff_helper.rb36
-rw-r--r--app/helpers/dropdowns_helper.rb101
-rw-r--r--app/helpers/events_helper.rb12
-rw-r--r--app/helpers/explore_helper.rb12
-rw-r--r--app/helpers/gitlab_markdown_helper.rb2
-rw-r--r--app/helpers/groups_helper.rb4
-rw-r--r--app/helpers/icons_helper.rb11
-rw-r--r--app/helpers/issuables_helper.rb33
-rw-r--r--app/helpers/issues_helper.rb17
-rw-r--r--app/helpers/labels_helper.rb45
-rw-r--r--app/helpers/milestones_helper.rb59
-rw-r--r--app/helpers/nav_helper.rb1
-rw-r--r--app/helpers/preferences_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb18
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/snippets_helper.rb10
-rw-r--r--app/helpers/sorting_helper.rb10
-rw-r--r--app/helpers/todos_helper.rb11
-rw-r--r--app/helpers/visibility_level_helper.rb37
-rw-r--r--app/mailers/emails/issues.rb34
-rw-r--r--app/mailers/emails/merge_requests.rb49
-rw-r--r--app/mailers/emails/profile.rb5
-rw-r--r--app/models/ability.rb154
-rw-r--r--app/models/abuse_report.rb4
-rw-r--r--app/models/appearance.rb9
-rw-r--r--app/models/application_setting.rb1
-rw-r--r--app/models/blob.rb3
-rw-r--r--app/models/ci/build.rb34
-rw-r--r--app/models/ci/commit.rb34
-rw-r--r--app/models/ci/runner.rb20
-rw-r--r--app/models/commit.rb20
-rw-r--r--app/models/commit_status.rb20
-rw-r--r--app/models/concerns/internal_id.rb5
-rw-r--r--app/models/concerns/issuable.rb58
-rw-r--r--app/models/concerns/milestoneish.rb29
-rw-r--r--app/models/concerns/subscribable.rb44
-rw-r--r--app/models/diff_line.rb3
-rw-r--r--app/models/event.rb10
-rw-r--r--app/models/global_label.rb7
-rw-r--r--app/models/global_milestone.rb55
-rw-r--r--app/models/group.rb51
-rw-r--r--app/models/issue.rb56
-rw-r--r--app/models/key.rb3
-rw-r--r--app/models/label.rb59
-rw-r--r--app/models/merge_request.rb83
-rw-r--r--app/models/merge_request_diff.rb88
-rw-r--r--app/models/milestone.rb58
-rw-r--r--app/models/namespace.rb12
-rw-r--r--app/models/note.rb87
-rw-r--r--app/models/personal_snippet.rb1
-rw-r--r--app/models/project.rb146
-rw-r--r--app/models/project_group_link.rb36
-rw-r--r--app/models/project_services/ci_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb6
-rw-r--r--app/models/project_snippet.rb3
-rw-r--r--app/models/project_team.rb52
-rw-r--r--app/models/project_wiki.rb20
-rw-r--r--app/models/repository.rb219
-rw-r--r--app/models/snippet.rb31
-rw-r--r--app/models/subscription.rb2
-rw-r--r--app/models/todo.rb32
-rw-r--r--app/models/user.rb57
-rw-r--r--app/models/wiki_page.rb2
-rw-r--r--app/services/base_service.rb7
-rw-r--r--app/services/ci/create_builds_service.rb2
-rw-r--r--app/services/ci/image_for_build_service.rb2
-rw-r--r--app/services/commits/revert_service.rb29
-rw-r--r--app/services/compare_service.rb12
-rw-r--r--app/services/create_commit_builds_service.rb1
-rw-r--r--app/services/create_snippet_service.rb3
-rw-r--r--app/services/delete_user_service.rb24
-rw-r--r--app/services/destroy_group_service.rb4
-rw-r--r--app/services/git_push_service.rb37
-rw-r--r--app/services/git_tag_push_service.rb2
-rw-r--r--app/services/groups/base_service.rb9
-rw-r--r--app/services/groups/create_service.rb21
-rw-r--r--app/services/groups/update_service.rb20
-rw-r--r--app/services/issuable_base_service.rb17
-rw-r--r--app/services/issues/close_service.rb6
-rw-r--r--app/services/issues/create_service.rb2
-rw-r--r--app/services/issues/move_service.rb97
-rw-r--r--app/services/issues/update_service.rb9
-rw-r--r--app/services/merge_requests/base_service.rb13
-rw-r--r--app/services/merge_requests/build_service.rb35
-rw-r--r--app/services/merge_requests/merge_when_build_succeeds_service.rb12
-rw-r--r--app/services/merge_requests/post_merge_service.rb4
-rw-r--r--app/services/merge_requests/update_service.rb13
-rw-r--r--app/services/notes/create_service.rb1
-rw-r--r--app/services/notification_service.rb90
-rw-r--r--app/services/projects/autocomplete_service.rb6
-rw-r--r--app/services/projects/create_service.rb10
-rw-r--r--app/services/projects/destroy_service.rb6
-rw-r--r--app/services/projects/housekeeping_service.rb29
-rw-r--r--app/services/projects/update_service.rb29
-rw-r--r--app/services/search/global_service.rb3
-rw-r--r--app/services/search/project_service.rb3
-rw-r--r--app/services/search/snippet_service.rb5
-rw-r--r--app/services/system_note_service.rb48
-rw-r--r--app/services/todo_service.rb65
-rw-r--r--app/services/update_snippet_service.rb1
-rw-r--r--app/validators/url_validator.rb3
-rw-r--r--app/views/abuse_reports/new.html.haml4
-rw-r--r--app/views/admin/appearances/_form.html.haml58
-rw-r--r--app/views/admin/appearances/preview.html.haml29
-rw-r--r--app/views/admin/appearances/show.html.haml7
-rw-r--r--app/views/admin/application_settings/_form.html.haml4
-rw-r--r--app/views/admin/applications/_delete_form.html.haml2
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml4
-rw-r--r--app/views/admin/builds/_build.html.haml21
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/groups/index.html.haml3
-rw-r--r--app/views/admin/groups/show.html.haml21
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml2
-rw-r--r--app/views/admin/users/_form.html.haml8
-rw-r--r--app/views/admin/users/index.html.haml10
-rw-r--r--app/views/admin/users/keys.html.haml2
-rw-r--r--app/views/admin/users/show.html.haml4
-rw-r--r--app/views/ci/commits/_commit.html.haml32
-rw-r--r--app/views/ci/projects/index.html.haml20
-rw-r--r--app/views/dashboard/_projects_head.html.haml4
-rw-r--r--app/views/dashboard/issues.html.haml2
-rw-r--r--app/views/dashboard/milestones/_issue.html.haml10
-rw-r--r--app/views/dashboard/milestones/_issues.html.haml6
-rw-r--r--app/views/dashboard/milestones/_merge_request.html.haml10
-rw-r--r--app/views/dashboard/milestones/_merge_requests.html.haml6
-rw-r--r--app/views/dashboard/milestones/_milestone.html.haml31
-rw-r--r--app/views/dashboard/milestones/show.html.haml106
-rw-r--r--app/views/dashboard/projects/_projects.html.haml7
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml4
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/dashboard/todos/_todo.html.haml17
-rw-r--r--app/views/dashboard/todos/index.html.haml14
-rw-r--r--app/views/devise/sessions/_new_crowd.html.haml2
-rw-r--r--app/views/devise/sessions/new.html.haml6
-rw-r--r--app/views/doorkeeper/applications/_delete_form.html.haml8
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml31
-rw-r--r--app/views/doorkeeper/applications/index.html.haml98
-rw-r--r--app/views/doorkeeper/applications/new.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/error.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/show.html.haml2
-rw-r--r--app/views/emojis/index.html.haml11
-rw-r--r--app/views/events/_commit.html.haml2
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/events/_event_last_push.html.haml2
-rw-r--r--app/views/events/event/_common.html.haml2
-rw-r--r--app/views/events/event/_push.html.haml2
-rw-r--r--app/views/explore/projects/_dropdown.html.haml20
-rw-r--r--app/views/explore/projects/_filter.html.haml73
-rw-r--r--app/views/explore/projects/_projects.html.haml7
-rw-r--r--app/views/explore/projects/index.html.haml4
-rw-r--r--app/views/groups/_activities.html.haml12
-rw-r--r--app/views/groups/_projects.html.haml12
-rw-r--r--app/views/groups/_shared_projects.html.haml1
-rw-r--r--app/views/groups/activity.html.haml9
-rw-r--r--app/views/groups/edit.html.haml12
-rw-r--r--app/views/groups/group_members/_group_member.html.haml3
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/groups/milestones/_issue.html.haml10
-rw-r--r--app/views/groups/milestones/_issues.html.haml6
-rw-r--r--app/views/groups/milestones/_merge_request.html.haml10
-rw-r--r--app/views/groups/milestones/_merge_requests.html.haml6
-rw-r--r--app/views/groups/milestones/_milestone.html.haml34
-rw-r--r--app/views/groups/milestones/new.html.haml6
-rw-r--r--app/views/groups/milestones/show.html.haml114
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/show.html.haml61
-rw-r--r--app/views/help/_shortcuts.html.haml16
-rw-r--r--app/views/help/ui.html.haml443
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/application.html.haml6
-rw-r--r--app/views/layouts/ci/_page.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml61
-rw-r--r--app/views/layouts/header/_public.html.haml10
-rw-r--r--app/views/layouts/nav/_admin.html.haml5
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml2
-rw-r--r--app/views/layouts/nav/_explore.html.haml2
-rw-r--r--app/views/layouts/nav/_group.html.haml67
-rw-r--r--app/views/layouts/nav/_profile.html.haml2
-rw-r--r--app/views/layouts/nav/_project.html.haml4
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml10
-rw-r--r--app/views/layouts/notify.html.haml43
-rw-r--r--app/views/notify/_reassigned_issuable_email.text.erb2
-rw-r--r--app/views/notify/_relabeled_issuable_email.html.haml3
-rw-r--r--app/views/notify/_relabeled_issuable_email.text.erb3
-rw-r--r--app/views/notify/issue_moved_email.html.haml6
-rw-r--r--app/views/notify/issue_moved_email.text.erb4
-rw-r--r--app/views/notify/relabeled_issue_email.html.haml1
-rw-r--r--app/views/notify/relabeled_issue_email.text.erb1
-rw-r--r--app/views/notify/relabeled_merge_request_email.html.haml1
-rw-r--r--app/views/notify/relabeled_merge_request_email.text.erb1
-rw-r--r--app/views/profiles/_event_table.html.haml28
-rw-r--r--app/views/profiles/accounts/show.html.haml211
-rw-r--r--app/views/profiles/applications.html.haml70
-rw-r--r--app/views/profiles/audit_log.html.haml13
-rw-r--r--app/views/profiles/emails/index.html.haml93
-rw-r--r--app/views/profiles/keys/_form.html.haml14
-rw-r--r--app/views/profiles/keys/_key.html.haml25
-rw-r--r--app/views/profiles/keys/_key_details.html.haml2
-rw-r--r--app/views/profiles/keys/_key_table.html.haml20
-rw-r--r--app/views/profiles/keys/index.html.haml29
-rw-r--r--app/views/profiles/keys/new.html.haml17
-rw-r--r--app/views/profiles/notifications/_settings.html.haml4
-rw-r--r--app/views/profiles/notifications/show.html.haml123
-rw-r--r--app/views/profiles/passwords/edit.html.haml52
-rw-r--r--app/views/profiles/preferences/show.html.haml99
-rw-r--r--app/views/profiles/show.html.haml181
-rw-r--r--app/views/profiles/two_factor_auths/new.html.haml78
-rw-r--r--app/views/projects/_builds_settings.html.haml68
-rw-r--r--app/views/projects/_home_panel.html.haml18
-rw-r--r--app/views/projects/blame/show.html.haml6
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/_image.html.haml2
-rw-r--r--app/views/projects/blob/_new_dir.html.haml2
-rw-r--r--app/views/projects/blob/_remove.html.haml2
-rw-r--r--app/views/projects/blob/_upload.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml4
-rw-r--r--app/views/projects/branches/destroy.js.haml2
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/builds/index.html.haml8
-rw-r--r--app/views/projects/builds/show.html.haml21
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml6
-rw-r--r--app/views/projects/buttons/_notifications.html.haml2
-rw-r--r--app/views/projects/buttons/_star.html.haml4
-rw-r--r--app/views/projects/ci/builds/_build.html.haml76
-rw-r--r--app/views/projects/commit/_builds.html.haml7
-rw-r--r--app/views/projects/commit_statuses/_commit_status.html.haml79
-rw-r--r--app/views/projects/commits/_commit_list.html.haml13
-rw-r--r--app/views/projects/commits/_commits.html.haml10
-rw-r--r--app/views/projects/commits/_head.html.haml4
-rw-r--r--app/views/projects/compare/_form.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml13
-rw-r--r--app/views/projects/diffs/_file.html.haml24
-rw-r--r--app/views/projects/diffs/_image.html.haml5
-rw-r--r--app/views/projects/diffs/_line.html.haml26
-rw-r--r--app/views/projects/diffs/_text_file.html.haml23
-rw-r--r--app/views/projects/diffs/_warning.html.haml13
-rw-r--r--app/views/projects/edit.html.haml67
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/find_file/show.html.haml2
-rw-r--r--app/views/projects/forks/_projects.html.haml2
-rw-r--r--app/views/projects/forks/index.html.haml31
-rw-r--r--app/views/projects/forks/new.html.haml4
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml58
-rw-r--r--app/views/projects/go_import.html.haml5
-rw-r--r--app/views/projects/group_links/index.html.haml41
-rw-r--r--app/views/projects/hooks/index.html.haml10
-rw-r--r--app/views/projects/issues/_discussion.html.haml4
-rw-r--r--app/views/projects/issues/_form.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml19
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml4
-rw-r--r--app/views/projects/issues/_new_branch.html.haml5
-rw-r--r--app/views/projects/issues/_related_branches.html.haml15
-rw-r--r--app/views/projects/issues/index.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml65
-rw-r--r--app/views/projects/labels/_form.html.haml4
-rw-r--r--app/views/projects/labels/_label.html.haml16
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml4
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml22
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml29
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml10
-rw-r--r--app/views/projects/merge_requests/_show.html.haml10
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml5
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml33
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_wip.html.haml10
-rw-r--r--app/views/projects/milestones/_form.html.haml6
-rw-r--r--app/views/projects/milestones/_issue.html.haml10
-rw-r--r--app/views/projects/milestones/_issues.html.haml7
-rw-r--r--app/views/projects/milestones/_merge_request.html.haml8
-rw-r--r--app/views/projects/milestones/_merge_requests.html.haml5
-rw-r--r--app/views/projects/milestones/_milestone.html.haml36
-rw-r--r--app/views/projects/milestones/show.html.haml101
-rw-r--r--app/views/projects/notes/_edit_form.html.haml6
-rw-r--r--app/views/projects/notes/_form.html.haml9
-rw-r--r--app/views/projects/notes/_note.html.haml10
-rw-r--r--app/views/projects/project_members/_shared_group_members.html.haml21
-rw-r--r--app/views/projects/project_members/index.html.haml3
-rw-r--r--app/views/projects/refs/logs_tree.js.haml7
-rw-r--r--app/views/projects/releases/edit.html.haml4
-rw-r--r--app/views/projects/repositories/_download_archive.html.haml2
-rw-r--r--app/views/projects/tags/_download.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml4
-rw-r--r--app/views/projects/tags/destroy.js.haml3
-rw-r--r--app/views/projects/tags/new.html.haml4
-rw-r--r--app/views/projects/tags/show.html.haml8
-rw-r--r--app/views/projects/tree/_tree_header.html.haml4
-rw-r--r--app/views/projects/wikis/_form.html.haml4
-rw-r--r--app/views/projects/wikis/_main_links.html.haml23
-rw-r--r--app/views/projects/wikis/_nav.html.haml2
-rw-r--r--app/views/projects/wikis/_new.html.haml13
-rw-r--r--app/views/projects/wikis/edit.html.haml22
-rw-r--r--app/views/projects/wikis/history.html.haml13
-rw-r--r--app/views/projects/wikis/pages.html.haml13
-rw-r--r--app/views/projects/wikis/show.html.haml11
-rw-r--r--app/views/search/_filter.html.haml46
-rw-r--r--app/views/search/_form.html.haml2
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml1
-rw-r--r--app/views/search/results/_milestone.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml2
-rw-r--r--app/views/search/results/_wiki_blob.html.haml4
-rw-r--r--app/views/shared/_clone_panel.html.haml6
-rw-r--r--app/views/shared/_commit_message_container.html.haml2
-rw-r--r--app/views/shared/_group_tips.html.haml1
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/_label_row.html.haml2
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/_no_ssh.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml20
-rw-r--r--app/views/shared/groups/_list.html.haml6
-rw-r--r--app/views/shared/issuable/_filter.html.haml36
-rw-r--r--app/views/shared/issuable/_form.html.haml68
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml44
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml16
-rw-r--r--app/views/shared/issuable/_participants.html.haml17
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml134
-rw-r--r--app/views/shared/milestones/_issuable.html.haml27
-rw-r--r--app/views/shared/milestones/_issuables.html.haml16
-rw-r--r--app/views/shared/milestones/_issues_tab.html.haml10
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml18
-rw-r--r--app/views/shared/milestones/_merge_requests_tab.haml12
-rw-r--r--app/views/shared/milestones/_milestone.html.haml45
-rw-r--r--app/views/shared/milestones/_participants_tab.html.haml8
-rw-r--r--app/views/shared/milestones/_summary.html.haml28
-rw-r--r--app/views/shared/milestones/_tabs.html.haml30
-rw-r--r--app/views/shared/milestones/_top.html.haml58
-rw-r--r--app/views/shared/projects/_dropdown.html.haml22
-rw-r--r--app/views/shared/projects/_list.html.haml28
-rw-r--r--app/views/shared/projects/_project.html.haml45
-rw-r--r--app/views/shared/snippets/_blob.html.haml2
-rw-r--r--app/views/shared/snippets/_header.html.haml2
-rw-r--r--app/views/shared/snippets/_snippet.html.haml7
-rw-r--r--app/views/snippets/_snippets.html.haml2
-rw-r--r--app/views/users/show.html.haml199
-rw-r--r--app/views/votes/_votes_block.html.haml36
-rw-r--r--app/workers/delete_user_worker.rb10
-rw-r--r--app/workers/gitlab_shell_one_shot_worker.rb10
-rw-r--r--app/workers/irker_worker.rb2
-rw-r--r--app/workers/post_receive.rb46
-rw-r--r--app/workers/repository_fork_worker.rb5
-rw-r--r--app/workers/repository_import_worker.rb2
548 files changed, 9672 insertions, 5050 deletions
diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee
index 3b6b453ac51..5092e824e65 100644
--- a/app/assets/javascripts/activities.js.coffee
+++ b/app/assets/javascripts/activities.js.coffee
@@ -1,7 +1,7 @@
class @Activities
constructor: ->
Pager.init 20, true
- $(".event-filter a").bind "click", (event) =>
+ $(".event-filter-link").on "click", (event) =>
event.preventDefault()
@toggleFilter($(event.currentTarget))
@reloadActivities()
@@ -12,18 +12,10 @@ class @Activities
toggleFilter: (sender) ->
- sender.closest('li').toggleClass "active"
+ $('.event-filter .active').removeClass "active"
event_filters = $.cookie("event_filter")
filter = sender.attr("id").split("_")[0]
- if event_filters
- event_filters = event_filters.split(",")
- else
- event_filters = new Array()
+ $.cookie "event_filter", (if event_filters isnt filter then filter else ""), { path: '/' }
- index = event_filters.indexOf(filter)
- if index is -1
- event_filters.push filter
- else
- event_filters.splice index, 1
-
- $.cookie "event_filter", event_filters.join(","), { path: '/' }
+ if event_filters isnt filter
+ sender.closest('li').toggleClass "active"
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 3e0fdb3f795..f3ed9a66715 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -4,6 +4,7 @@
namespaces_path: "/api/:version/namespaces.json"
group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json"
+ labels_path: "/api/:version/projects/:id/labels"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
@@ -61,6 +62,21 @@
).done (projects) ->
callback(projects)
+ newLabel: (project_id, data, callback) ->
+ url = Api.buildUrl(Api.labels_path)
+ url = url.replace(':id', project_id)
+
+ data.private_token = gon.api_token
+ $.ajax(
+ url: url
+ type: "POST"
+ data: data
+ dataType: "json"
+ ).done (label) ->
+ callback(label)
+ .error (message) ->
+ callback(message.responseJSON)
+
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 367bd098bfd..f01c67e9474 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -7,6 +7,7 @@
#= require jquery
#= require jquery-ui/autocomplete
#= require jquery-ui/datepicker
+#= require jquery-ui/draggable
#= require jquery-ui/effect-highlight
#= require jquery-ui/sortable
#= require jquery_ujs
@@ -31,8 +32,6 @@
#= require ace/ace
#= require ace/ext-searchbox
#= require underscore
-#= require nprogress
-#= require nprogress-turbolinks
#= require dropzone
#= require mousetrap
#= require mousetrap/pause
@@ -44,6 +43,7 @@
#= require jquery.nicescroll
#= require_tree .
#= require fuzzaldrin-plus
+#= require cropper
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
@@ -109,6 +109,8 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
+ bootstrapBreakpoint = bp.getBreakpointSize()
+
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents
@@ -138,7 +140,7 @@ $ ->
# Initialize tooltips
$('body').tooltip(
- selector: '.has_tooltip, [data-toggle="tooltip"]'
+ selector: '.has-tooltip, [data-toggle="tooltip"]'
placement: (_, el) ->
$el = $(el)
$el.data('placement') || 'bottom'
@@ -210,82 +212,68 @@ $ ->
$this = $(this)
$this.attr 'value', $this.val()
return
-
+
$(document)
.off 'keyup', 'input[type="search"]'
.on 'keyup', 'input[type="search"]' , (e) ->
$this = $(this)
$this.attr 'value', $this.val()
+ $sidebarGutterToggle = $('.js-sidebar-toggle')
+ $navIconToggle = $('.toggle-nav-collapse')
+
$(document)
.off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs'
- $gutterIcon = $('.gutter-toggle').find('i')
+ $gutterIcon = $sidebarGutterToggle.find('i')
if $gutterIcon.hasClass('fa-angle-double-right')
- $gutterIcon.closest('a').trigger('click')
+ $sidebarGutterToggle.trigger('click')
+
+ $navIcon = $navIconToggle.find('.fa')
+ if $navIcon.hasClass('fa-angle-left')
+ $navIconToggle.trigger('click')
$(document)
- .off 'click', 'aside .gutter-toggle'
- .on 'click', 'aside .gutter-toggle', (e) ->
+ .off 'click', '.js-sidebar-toggle'
+ .on 'click', '.js-sidebar-toggle', (e, triggered) ->
e.preventDefault()
$this = $(this)
$thisIcon = $this.find 'i'
+ $allGutterToggleIcons = $('.js-sidebar-toggle i')
if $thisIcon.hasClass('fa-angle-double-right')
- $thisIcon
+ $allGutterToggleIcons
.removeClass('fa-angle-double-right')
.addClass('fa-angle-double-left')
- $this
- .closest('aside')
+ $('aside.right-sidebar')
.removeClass('right-sidebar-expanded')
.addClass('right-sidebar-collapsed')
$('.page-with-sidebar')
.removeClass('right-sidebar-expanded')
.addClass('right-sidebar-collapsed')
else
- $thisIcon
+ $allGutterToggleIcons
.removeClass('fa-angle-double-left')
.addClass('fa-angle-double-right')
- $this
- .closest('aside')
+ $('aside.right-sidebar')
.removeClass('right-sidebar-collapsed')
.addClass('right-sidebar-expanded')
$('.page-with-sidebar')
.removeClass('right-sidebar-collapsed')
.addClass('right-sidebar-expanded')
- $.cookie("collapsed_gutter",
- $('.right-sidebar')
- .hasClass('right-sidebar-collapsed'), { path: '/' })
-
- bootstrapBreakpoint = undefined;
- checkBootstrapBreakpoints = ->
- if $('.device-xs').is(':visible')
- bootstrapBreakpoint = "xs"
- else if $('.device-sm').is(':visible')
- bootstrapBreakpoint = "sm"
- else if $('.device-md').is(':visible')
- bootstrapBreakpoint = "md"
- else if $('.device-lg').is(':visible')
- bootstrapBreakpoint = "lg"
-
- setBootstrapBreakpoints = ->
- if $('.device-xs').length
- return
-
- $("body")
- .append('<div class="device-xs visible-xs"></div>'+
- '<div class="device-sm visible-sm"></div>'+
- '<div class="device-md visible-md"></div>'+
- '<div class="device-lg visible-lg"></div>')
- checkBootstrapBreakpoints()
+ if not triggered
+ $.cookie("collapsed_gutter",
+ $('.right-sidebar')
+ .hasClass('right-sidebar-collapsed'), { path: '/' })
fitSidebarForSize = ->
oldBootstrapBreakpoint = bootstrapBreakpoint
- checkBootstrapBreakpoints()
+ bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint != oldBootstrapBreakpoint
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
checkInitialSidebarSize = ->
+ bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint is "xs" or "sm"
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
@@ -294,6 +282,5 @@ $ ->
.on "resize", (e) ->
fitSidebarForSize()
- setBootstrapBreakpoints()
checkInitialSidebarSize()
new Aside()
diff --git a/app/assets/javascripts/aside.js.coffee b/app/assets/javascripts/aside.js.coffee
index 85473101944..66ab5054326 100644
--- a/app/assets/javascripts/aside.js.coffee
+++ b/app/assets/javascripts/aside.js.coffee
@@ -5,7 +5,6 @@ class @Aside
e.preventDefault()
btn = $(e.currentTarget)
icon = btn.find('i')
- console.log('1')
if icon.hasClass('fa-angle-left')
btn.parent().find('section').hide()
diff --git a/app/assets/javascripts/autosave.js.coffee b/app/assets/javascripts/autosave.js.coffee
index 5d3fe81da74..28f8e103664 100644
--- a/app/assets/javascripts/autosave.js.coffee
+++ b/app/assets/javascripts/autosave.js.coffee
@@ -16,11 +16,11 @@ class @Autosave
try
text = window.localStorage.getItem @key
- catch
+ catch e
return
@field.val text if text?.length > 0
- @field.trigger "input"
+ @field.trigger "input"
save: ->
return unless window.localStorage?
@@ -35,5 +35,5 @@ class @Autosave
reset: ->
return unless window.localStorage?
- try
+ try
window.localStorage.removeItem @key
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index 360acb864f6..47b080406d4 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -1,25 +1,54 @@
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
- $(".add-award").click (event)->
+ $(".js-add-award").on "click", (event) =>
event.stopPropagation()
event.preventDefault()
- $(".emoji-menu").show()
- $("#emoji_search").focus()
+
+ @showEmojiMenu()
$("html").on 'click', (event) ->
if !$(event.target).closest(".emoji-menu").length
if $(".emoji-menu").is(":visible")
- $(".emoji-menu").hide()
+ $(".emoji-menu").removeClass "is-visible"
+
+ $(".awards")
+ .off "click"
+ .on "click", ".js-emoji-btn", @handleClick
@renderFrequentlyUsedBlock()
- @setupSearch()
+
+ handleClick: (e) ->
+ e.preventDefault()
+ emoji = $(this)
+ .find(".icon")
+ .data "emoji"
+ awards_handler.addAward emoji
+
+ showEmojiMenu: ->
+ if $(".emoji-menu").length
+ if $(".emoji-menu").is ".is-visible"
+ $(".emoji-menu").removeClass "is-visible"
+ $("#emoji_search").blur()
+ else
+ $(".emoji-menu").addClass "is-visible"
+ $("#emoji_search").focus()
+ else
+ $('.js-add-award').addClass "is-loading"
+ $.get "/emojis", (response) =>
+ $('.js-add-award').removeClass "is-loading"
+ $(".js-award-holder").append response
+ setTimeout =>
+ $(".emoji-menu").addClass "is-visible"
+ $("#emoji_search").focus()
+ @setupSearch()
+ , 200
addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
- $(".emoji-menu").hide()
+ $(".emoji-menu").removeClass "is-visible"
addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji)
@@ -29,7 +58,7 @@ class @AwardsHandler
if @isActive(emoji)
@decrementCounter(emoji)
else
- counter = @findEmojiIcon(emoji).siblings(".counter")
+ counter = @findEmojiIcon(emoji).siblings(".js-counter")
counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
@@ -43,7 +72,7 @@ class @AwardsHandler
@findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) ->
- counter = @findEmojiIcon(emoji).siblings(".counter")
+ counter = @findEmojiIcon(emoji).siblings(".js-counter")
emojiIcon = counter.parent()
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
@@ -60,9 +89,13 @@ class @AwardsHandler
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
- authors = award_block.attr("data-original-title").split(", ")
+ authors = award_block
+ .attr("data-original-title")
+ .split(", ")
authors.splice(authors.indexOf("me"),1)
- award_block.closest(".award").attr("data-original-title", authors.join(", "))
+ award_block
+ .closest(".js-emoji-btn")
+ .attr("data-original-title", authors.join(", "))
@resetTooltip(award_block)
addMeToAuthorList: (emoji) ->
@@ -88,14 +121,18 @@ class @AwardsHandler
emojiCssClass = @resolveNameToCssClass(emoji)
nodes = []
- nodes.push("<div class='award active' title='me'>")
- nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>")
- nodes.push("<div class='counter'>1</div>")
- nodes.push("</div>")
-
- emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji)
-
- $(".award").tooltip()
+ nodes.push(
+ "<button class='btn award-control js-emoji-btn has-tooltip active' title='me'>",
+ "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
+ "<span class='award-control-text js-counter'>1</span>",
+ "</button>"
+ )
+
+ emoji_node = $(nodes.join("\n"))
+ .insertBefore(".js-award-holder")
+ .find(".emoji-icon")
+ .data("emoji", emoji)
+ $('.award-control').tooltip()
resolveNameToCssClass: (emoji) ->
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
@@ -118,7 +155,7 @@ class @AwardsHandler
callback.call()
findEmojiIcon: (emoji) ->
- $(".award [data-emoji='#{emoji}']")
+ $(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: ->
$('body, html').animate({
@@ -154,13 +191,13 @@ class @AwardsHandler
term = $(ev.target).val()
# Clean previous search results
- $("ul.emoji-search,h5.emoji-search").remove()
+ $("ul.emoji-menu-search, h5.emoji-search").remove()
if term
# Generate a search result block
h5 = $("<h5>").text("Search results").addClass("emoji-search")
found_emojis = @searchEmojis(term).show()
- ul = $("<ul>").addClass("emoji-search").append(found_emojis)
+ ul = $("<ul>").addClass("emoji-menu-list emoji-menu-search").append(found_emojis)
$(".emoji-menu-content ul, .emoji-menu-content h5").hide()
$(".emoji-menu-content").append(h5).append(ul)
else
diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee
index 4ec8531d580..6e29d374267 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js.coffee
+++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee
@@ -1,29 +1,52 @@
# Quick Submit behavior
#
-# When an input field with the `js-quick-submit` class receives a "Meta+Enter"
-# (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, its parent form is
-# submitted.
+# When a child field of a form with a `js-quick-submit` class receives a
+# "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
+# is submitted.
#
#= require extensions/jquery
#
# ### Example Markup
#
-# <form action="/foo">
-# <input type="text" class="js-quick-submit" />
-# <textarea class="js-quick-submit"></textarea>
+# <form action="/foo" class="js-quick-submit">
+# <input type="text" />
+# <textarea></textarea>
+# <input type="submit" value="Submit" />
# </form>
#
+isMac = ->
+ navigator.userAgent.match(/Macintosh/)
+
+keyCodeIs = (e, keyCode) ->
+ return false if (e.originalEvent && e.originalEvent.repeat) || e.repeat
+ return e.keyCode == keyCode
+
$(document).on 'keydown.quick_submit', '.js-quick-submit', (e) ->
- return if (e.originalEvent && e.originalEvent.repeat) || e.repeat
- return unless e.keyCode == 13 # Enter
+ return unless keyCodeIs(e, 13) # Enter
- if navigator.userAgent.match(/Macintosh/)
- return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey)
- else
- return unless (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey)
+ return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey)
e.preventDefault()
$form = $(e.target).closest('form')
$form.find('input[type=submit], button[type=submit]').disable()
$form.submit()
+
+# If the user tabs to a submit button on a `js-quick-submit` form, display a
+# tooltip to let them know they could've used the hotkey
+$(document).on 'keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', (e) ->
+ return unless keyCodeIs(e, 9) # Tab
+
+ if isMac()
+ title = "You can also press &#8984;-Enter"
+ else
+ title = "You can also press Ctrl-Enter"
+
+ $this = $(@)
+ $this.tooltip(
+ container: 'body'
+ html: 'true'
+ placement: 'auto top'
+ title: title
+ trigger: 'manual'
+ ).tooltip('show').one('blur', -> $this.tooltip('hide'))
diff --git a/app/assets/javascripts/breakpoints.coffee b/app/assets/javascripts/breakpoints.coffee
new file mode 100644
index 00000000000..5457430f921
--- /dev/null
+++ b/app/assets/javascripts/breakpoints.coffee
@@ -0,0 +1,37 @@
+class @Breakpoints
+ instance = null;
+
+ class BreakpointInstance
+ BREAKPOINTS = ["xs", "sm", "md", "lg"]
+
+ constructor: ->
+ @setup()
+
+ setup: ->
+ allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
+ ".device-#{breakpoint}"
+ return if $(allDeviceSelector.join(",")).length
+
+ # Create all the elements
+ els = $.map BREAKPOINTS, (breakpoint) ->
+ "<div class='device-#{breakpoint} visible-#{breakpoint}'></div>"
+ $("body").append els.join('')
+
+ visibleDevice: ->
+ allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
+ ".device-#{breakpoint}"
+ $(allDeviceSelector.join(",")).filter(":visible")
+
+ getBreakpointSize: ->
+ $visibleDevice = @visibleDevice
+ # the page refreshed via turbolinks
+ if not $visibleDevice().length
+ @setup()
+ $visibleDevice = @visibleDevice()
+ return $visibleDevice.attr("class").split("visible-")[1]
+
+ @get: ->
+ return instance ?= new BreakpointInstance
+
+$ =>
+ @bp = Breakpoints.get()
diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
index 44d5ddb7d95..7afe8bf79e2 100644
--- a/app/assets/javascripts/ci/build.coffee
+++ b/app/assets/javascripts/ci/build.coffee
@@ -4,6 +4,8 @@ class CiBuild
constructor: (build_url, build_status) ->
clearInterval(CiBuild.interval)
+ @initScrollButtonAffix()
+
if build_status == "running" || build_status == "pending"
#
# Bind autoscroll button to follow build output
@@ -38,4 +40,15 @@ class CiBuild
checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
+ initScrollButtonAffix: ->
+ $buildScroll = $('#js-build-scroll')
+ $body = $('body')
+ $buildTrace = $('#build-trace')
+
+ $buildScroll.affix(
+ offset:
+ bottom: ->
+ $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
+ )
+
@CiBuild = CiBuild
diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee
deleted file mode 100644
index 62143e66cfe..00000000000
--- a/app/assets/javascripts/dashboard.js.coffee
+++ /dev/null
@@ -1,31 +0,0 @@
-@Dashboard =
- init: ->
- $(".projects-list-filter").off('keyup')
- this.initSearch()
-
- initSearch: ->
- @timer = null
- $(".projects-list-filter").on('keyup', ->
- clearTimeout(@timer)
- @timer = setTimeout(Dashboard.filterResults, 500)
- )
-
- filterResults: =>
- $('.projects-list-holder').fadeTo(250, 0.5)
-
- form = null
- form = $("form#project-filter-form")
- search = $(".projects-list-filter").val()
- project_filter_url = form.attr('action') + '?' + form.serialize()
-
- $.ajax
- type: "GET"
- url: form.attr('action')
- data: form.serialize()
- complete: ->
- $('.projects-list-holder').fadeTo(250, 1)
- success: (data) ->
- $('.projects-list-holder').replaceWith(data.html)
- # Change url so if user reload a page - search results are saved
- history.replaceState {page: project_filter_url}, document.title, project_filter_url
- dataType: "json"
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index b17f8e51470..f5e1ca9860d 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -14,10 +14,7 @@ class Dispatcher
path = page.split(':')
shortcut_handler = null
-
switch page
- when 'explore:projects:index', 'explore:projects:starred', 'explore:projects:trending'
- Dashboard.init()
when 'projects:issues:index'
Issues.init()
shortcut_handler = new ShortcutsNavigation()
@@ -25,8 +22,10 @@ class Dispatcher
new Issue()
shortcut_handler = new ShortcutsIssuable()
new ZenMode()
- when 'projects:milestones:show'
+ when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone()
+ when 'dashboard:todos:index'
+ new Todos()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
new DropzoneInput($('.milestone-form'))
@@ -59,8 +58,6 @@ class Dispatcher
when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation()
MergeRequests.init()
- when 'dashboard:show', 'root:show'
- Dashboard.init()
when 'dashboard:activity'
new Activities()
when 'dashboard:projects:starred'
@@ -76,8 +73,11 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
when 'projects:show'
shortcut_handler = new ShortcutsNavigation()
- when 'groups:show'
+
+ new TreeView() if $('#tree-slider').length
+ when 'groups:activity'
new Activities()
+ when 'groups:show'
shortcut_handler = new ShortcutsNavigation()
when 'groups:group_members:index'
new GroupMembers()
@@ -88,10 +88,11 @@ class Dispatcher
when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new'
new GroupAvatar()
when 'projects:tree:show'
+ shortcut_handler = new ShortcutsNavigation()
new TreeView()
when 'projects:find_file:show'
shortcut_handler = true
- when 'projects:blob:show'
+ when 'projects:blob:show', 'projects:blame:show'
new LineHighlighter()
shortcut_handler = new ShortcutsNavigation()
when 'projects:labels:new', 'projects:labels:edit'
@@ -104,9 +105,8 @@ class Dispatcher
new ProjectFork()
when 'projects:artifacts:browse'
new BuildArtifacts()
- when 'users:show'
- new User()
- new Activities()
+ when 'projects:group_links:index'
+ new GroupsSelect()
switch path.first()
when 'admin'
diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee
new file mode 100644
index 00000000000..df9bfdfa6cc
--- /dev/null
+++ b/app/assets/javascripts/gl_crop.js.coffee
@@ -0,0 +1,152 @@
+class GitLabCrop
+ # Matches everything but the file name
+ FILENAMEREGEX = /^.*[\\\/]/
+
+ constructor: (input, opts = {}) ->
+ @fileInput = $(input)
+
+ # We should rename to avoid spec to fail
+ # Form will submit the proper input filed with a file using FormData
+ @fileInput
+ .attr('name', "#{@fileInput.attr('name')}-trigger")
+ .attr('id', "#{@fileInput.attr('id')}-trigger")
+
+ # Set defaults
+ {
+ @exportWidth = 200
+ @exportHeight = 200
+ @cropBoxWidth = 200
+ @cropBoxHeight = 200
+ @form = @fileInput.parents('form')
+
+ # Required params
+ @filename
+ @previewImage
+ @modalCrop
+ @pickImageEl
+ @uploadImageBtn
+ @modalCropImg
+ } = opts
+
+ # Ensure needed elements are jquery objects
+ # If selector is provided we will convert them to a jQuery Object
+ @filename = @getElement(@filename)
+ @previewImage = @getElement(@previewImage)
+ @pickImageEl = @getElement(@pickImageEl)
+
+ # Modal elements usually are outside the @form element
+ @modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop
+ @uploadImageBtn = if _.isString(@uploadImageBtn) then $(@uploadImageBtn) else @uploadImageBtn
+ @modalCropImg = if _.isString(@modalCropImg) then $(@modalCropImg) else @modalCropImg
+
+ @cropActionsBtn = @modalCrop.find('[data-method]')
+
+ @bindEvents()
+
+ getElement: (selector) ->
+ $(selector, @form)
+
+ bindEvents: ->
+ _this = @
+ @fileInput.on 'change', (e) ->
+ _this.onFileInputChange(e, @)
+
+ @pickImageEl.on 'click', @onPickImageClick
+ @modalCrop.on 'shown.bs.modal', @onModalShow
+ @modalCrop.on 'hidden.bs.modal', @onModalHide
+ @uploadImageBtn.on 'click', @onUploadImageBtnClick
+ @cropActionsBtn.on 'click', (e) ->
+ btn = @
+ _this.onActionBtnClick(btn)
+ @croppedImageBlob = null
+
+ onPickImageClick: =>
+ @fileInput.trigger('click')
+
+ onModalShow: =>
+ _this = @
+ @modalCropImg.cropper(
+ viewMode: 1
+ center: false
+ aspectRatio: 1
+ modal: true
+ scalable: false
+ rotatable: false
+ zoomable: true
+ dragMode: 'move'
+ guides: false
+ zoomOnTouch: false
+ zoomOnWheel: false
+ cropBoxMovable: false
+ cropBoxResizable: false
+ toggleDragModeOnDblclick: false
+ built: ->
+ $image = $(@)
+ container = $image.cropper 'getContainerData'
+ cropBoxWidth = _this.cropBoxWidth;
+ cropBoxHeight = _this.cropBoxHeight;
+
+ $image.cropper('setCropBoxData',
+ width: cropBoxWidth,
+ height: cropBoxHeight,
+ left: (container.width - cropBoxWidth) / 2,
+ top: (container.height - cropBoxHeight) / 2
+ )
+ )
+
+
+ onModalHide: =>
+ @modalCropImg
+ .attr('src', '') # Remove attached image
+ .cropper('destroy') # Destroy cropper instance
+
+ onUploadImageBtnClick: (e) =>
+ e.preventDefault()
+ @setBlob()
+ @setPreview()
+ @modalCrop.modal('hide')
+ @fileInput.val('')
+
+ onActionBtnClick: (btn) ->
+ data = $(btn).data()
+
+ if @modalCropImg.data('cropper') && data.method
+ result = @modalCropImg.cropper data.method, data.option
+
+ onFileInputChange: (e, input) ->
+ @readFile(input)
+
+ readFile: (input) ->
+ _this = @
+ reader = new FileReader
+ reader.onload = ->
+ _this.modalCropImg.attr('src', reader.result)
+ _this.modalCrop.modal('show')
+
+ reader.readAsDataURL(input.files[0])
+
+ dataURLtoBlob: (dataURL) ->
+ binary = atob(dataURL.split(',')[1])
+ array = []
+ for v, k in binary
+ array.push(binary.charCodeAt(k))
+ new Blob([new Uint8Array(array)], type: 'image/png')
+
+ setPreview: ->
+ @previewImage.attr('src', @dataURL)
+ filename = @fileInput.val().replace(FILENAMEREGEX, '')
+ @filename.text(filename)
+
+ setBlob: ->
+ @dataURL = @modalCropImg.cropper('getCroppedCanvas',
+ width: 200
+ height: 200
+ ).toDataURL('image/png')
+ @croppedImageBlob = @dataURLtoBlob(@dataURL)
+
+ getBlob: ->
+ @croppedImageBlob
+
+$.fn.glCrop = (opts) ->
+ return @.each ->
+ $(@).data('glcrop', new GitLabCrop(@, opts))
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
new file mode 100644
index 00000000000..2b56ab2e6de
--- /dev/null
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -0,0 +1,331 @@
+class GitLabDropdownFilter
+ BLUR_KEYCODES = [27, 40]
+ HAS_VALUE_CLASS = "has-value"
+
+ constructor: (@input, @options) ->
+ $inputContainer = @input.parent()
+ $clearButton = $inputContainer.find('.js-dropdown-input-clear')
+
+ # Clear click
+ $clearButton.on 'click', (e) =>
+ e.preventDefault()
+ e.stopPropagation()
+ @input
+ .val('')
+ .trigger('keyup')
+ .focus()
+
+ # Key events
+ timeout = ""
+ @input.on "keyup", (e) =>
+ if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS
+ $inputContainer.addClass HAS_VALUE_CLASS
+ else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS
+ $inputContainer.removeClass HAS_VALUE_CLASS
+
+ if e.keyCode is 13 and @input.val() isnt ""
+ if @options.enterCallback
+ @options.enterCallback()
+ return
+
+ clearTimeout timeout
+ timeout = setTimeout =>
+ blur_field = @shouldBlur e.keyCode
+ search_text = @input.val()
+
+ if blur_field
+ @input.blur()
+
+ if @options.remote
+ @options.query search_text, (data) =>
+ @options.callback(data)
+ else
+ @filter search_text
+ , 250
+
+ shouldBlur: (keyCode) ->
+ return BLUR_KEYCODES.indexOf(keyCode) >= 0
+
+ filter: (search_text) ->
+ data = @options.data()
+ results = data
+
+ if search_text isnt ""
+ results = fuzzaldrinPlus.filter(data, search_text,
+ key: @options.keys
+ )
+
+ @options.callback results
+
+class GitLabDropdownRemote
+ constructor: (@dataEndpoint, @options) ->
+
+ execute: ->
+ if typeof @dataEndpoint is "string"
+ @fetchData()
+ else if typeof @dataEndpoint is "function"
+ if @options.beforeSend
+ @options.beforeSend()
+
+ # Fetch the data by calling the data funcfion
+ @dataEndpoint "", (data) =>
+ if @options.success
+ @options.success(data)
+
+ if @options.beforeSend
+ @options.beforeSend()
+
+ # Fetch the data through ajax if the data is a string
+ fetchData: ->
+ $.ajax(
+ url: @dataEndpoint,
+ dataType: @options.dataType,
+ beforeSend: =>
+ if @options.beforeSend
+ @options.beforeSend()
+ success: (data) =>
+ if @options.success
+ @options.success(data)
+ )
+
+class GitLabDropdown
+ LOADING_CLASS = "is-loading"
+ PAGE_TWO_CLASS = "is-page-two"
+ ACTIVE_CLASS = "is-active"
+
+ constructor: (@el, @options) ->
+ self = @
+ @dropdown = $(@el).parent()
+ search_fields = if @options.search then @options.search.fields else [];
+
+ if @options.data
+ # Remote data
+ @remote = new GitLabDropdownRemote @options.data, {
+ dataType: @options.dataType,
+ beforeSend: @toggleLoading.bind(@)
+ success: (data) =>
+ @fullData = data
+
+ @parseData @fullData
+ }
+
+ # Init filiterable
+ if @options.filterable
+ @input = @dropdown.find('.dropdown-input .dropdown-input-field')
+
+ @filter = new GitLabDropdownFilter @input,
+ remote: @options.filterRemote
+ query: @options.data
+ keys: @options.search.fields
+ data: =>
+ return @fullData
+ callback: (data) =>
+ @parseData data
+ @highlightRow 1
+ enterCallback: =>
+ @selectFirstRow()
+
+ # Event listeners
+
+ @dropdown.on "shown.bs.dropdown", @opened
+ @dropdown.on "hidden.bs.dropdown", @hidden
+ @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
+ e.preventDefault()
+ e.stopPropagation()
+
+ @togglePage()
+
+ if @options.selectable
+ selector = ".dropdown-content a"
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content a"
+
+ @dropdown.on "click", selector, (e) ->
+ e.preventDefault()
+ self.rowClicked $(@)
+
+ if self.options.clicked
+ self.options.clicked.call(@,e)
+
+ toggleLoading: ->
+ $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
+
+ togglePage: ->
+ menu = $('.dropdown-menu', @dropdown)
+
+ if menu.hasClass(PAGE_TWO_CLASS)
+ if @remote
+ @remote.execute()
+
+ menu.toggleClass PAGE_TWO_CLASS
+
+ parseData: (data) ->
+ @renderedData = data
+
+ # Render each row
+ html = $.map data, (obj) =>
+ return @renderItem(obj)
+
+ if @options.filterable and data.length is 0
+ # render no matching results
+ html = [@noResults()]
+
+ # Render the full menu
+ full_html = @renderMenu(html.join(""))
+
+ @appendMenu(full_html)
+
+ shouldPropagate: (e) =>
+ if @options.multiSelect
+ $target = $(e.target)
+ if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon')
+ e.stopPropagation()
+ return false
+ else
+ return true
+
+ opened: =>
+ contentHtml = $('.dropdown-content', @dropdown).html()
+ if @remote && contentHtml is ""
+ @remote.execute()
+
+ if @options.filterable
+ @dropdown.find(".dropdown-input-field").focus()
+
+ hidden: (e) =>
+ if @options.filterable
+ @dropdown
+ .find(".dropdown-input-field")
+ .blur()
+ .val("")
+ .trigger("keyup")
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
+
+ if @options.hidden
+ @options.hidden.call(@,e)
+
+
+ # Render the full menu
+ renderMenu: (html) ->
+ menu_html = ""
+
+ if @options.renderMenu
+ menu_html = @options.renderMenu(html)
+ else
+ menu_html = "<ul>#{html}</ul>"
+
+ return menu_html
+
+ # Append the menu into the dropdown
+ appendMenu: (html) ->
+ selector = '.dropdown-content'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content"
+
+ $(selector, @dropdown).html html
+
+ # Render the row
+ renderItem: (data) ->
+ html = ""
+
+ return "<li class='divider'></li>" if data is "divider"
+
+ if @options.renderRow
+ # Call the render function
+ html = @options.renderRow(data)
+ else
+ selected = if @options.isSelected then @options.isSelected(data) else false
+ if not selected
+ value = if @options.id then @options.id(data) else data.id
+ fieldName = @options.fieldName
+ field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
+ if field.length
+ selected = true
+
+ url = if @options.url then @options.url(data) else "#"
+ text = if @options.text then @options.text(data) else ""
+ cssClass = "";
+
+ if selected
+ cssClass = "is-active"
+
+ html = "<li>"
+ html += "<a href='#{url}' class='#{cssClass}'>"
+ html += text
+ html += "</a>"
+ html += "</li>"
+
+ return html
+
+ noResults: ->
+ html = "<li>"
+ html += "<a href='#' class='dropdown-menu-empty-link is-focused'>"
+ html += "No matching results."
+ html += "</a>"
+ html += "</li>"
+
+ highlightRow: (index) ->
+ if @input.val() isnt ""
+ selector = '.dropdown-content li:first-child a'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content li:first-child a"
+
+ $(selector).addClass 'is-focused'
+
+ rowClicked: (el) ->
+ fieldName = @options.fieldName
+ selectedIndex = el.parent().index()
+ if @renderedData
+ selectedObject = @renderedData[selectedIndex]
+ value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
+ field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
+ if el.hasClass(ACTIVE_CLASS)
+ el.removeClass(ACTIVE_CLASS)
+ field.remove()
+ else
+ fieldName = @options.fieldName
+ selectedIndex = el.parent().index()
+ if @renderedData
+ selectedObject = @renderedData[selectedIndex]
+ selectedObject.selected = true
+ value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
+
+ if !value?
+ field.remove()
+
+ if not @options.multiSelect
+ @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
+ @dropdown.parent().find("input[name='#{fieldName}']").remove()
+
+ # Toggle active class for the tick mark
+ el.toggleClass "is-active"
+
+ # Toggle the dropdown label
+ if @options.toggleLabel
+ $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
+ if value?
+ if !field.length
+ # Create hidden input for form
+ input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
+ if @options.inputId?
+ input = $(input)
+ .attr('id', @options.inputId)
+ @dropdown.before input
+
+ selectFirstRow: ->
+ selector = '.dropdown-content li:first-child a'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content li:first-child a"
+
+ # simulate a click on the first link
+ $(selector).trigger "click"
+
+$.fn.glDropdown = (opts) ->
+ return @.each ->
+ new GitLabDropdown @, opts
diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee
index e52b73f94f6..6fc924d3d66 100644
--- a/app/assets/javascripts/issuable_context.js.coffee
+++ b/app/assets/javascripts/issuable_context.js.coffee
@@ -1,8 +1,7 @@
-#= require jquery.waitforimages
-
class @IssuableContext
- constructor: ->
- new UsersSelect()
+ constructor: (currentUser) ->
+ @initParticipants()
+ new UsersSelect(currentUser)
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
$(".issuable-sidebar .inline-update").on "change", "select", ->
@@ -11,9 +10,43 @@ class @IssuableContext
$(this).submit()
$(document).on "click",".edit-link", (e) ->
- block = $(@).parents('.block')
- block.find('.selectbox').show()
- block.find('.value').hide()
- block.find('.js-select2').select2("open")
+ $block = $(@).parents('.block')
+ $selectbox = $block.find('.selectbox')
+ if $selectbox.is(':visible')
+ $selectbox.hide()
+ $block.find('.value').show()
+ else
+ $selectbox.show()
+ $block.find('.value').hide()
+
+ if $selectbox.is(':visible')
+ setTimeout (->
+ $block.find('.dropdown-menu-toggle').trigger 'click'
+ ), 0
+
$(".right-sidebar").niceScroll()
+
+ initParticipants: ->
+ _this = @
+ $(document).on "click", ".js-participants-more", @toggleHiddenParticipants
+
+ $(".js-participants-author").each (i) ->
+ if i >= _this.PARTICIPANTS_ROW_COUNT
+ $(@)
+ .addClass "js-participants-hidden"
+ .hide()
+
+ toggleHiddenParticipants: (e) ->
+ e.preventDefault()
+
+ currentText = $(this).text().trim()
+ lessText = $(this).data("less-text")
+ originalText = $(this).data("original-text")
+
+ if currentText is originalText
+ $(this).text(lessText)
+ else
+ $(this).text(originalText)
+
+ $(".js-participants-hidden").toggle()
diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee
index 48c249943f2..7a788f761b7 100644
--- a/app/assets/javascripts/issuable_form.js.coffee
+++ b/app/assets/javascripts/issuable_form.js.coffee
@@ -1,4 +1,7 @@
class @IssuableForm
+ issueMoveConfirmMsg: 'Are you sure you want to move this issue to another project?'
+ wipRegex: /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i
+
constructor: (@form) ->
GitLab.GfmAutoComplete.setup()
new UsersSelect()
@@ -6,14 +9,17 @@ class @IssuableForm
@titleField = @form.find("input[name*='[title]']")
@descriptionField = @form.find("textarea[name*='[description]']")
+ @issueMoveField = @form.find("#move_to_project_id")
return unless @titleField.length && @descriptionField.length
@initAutosave()
- @form.on "submit", @resetAutosave
+ @form.on "submit", @handleSubmit
@form.on "click", ".btn-cancel", @resetAutosave
+ @initWip()
+
initAutosave: ->
new Autosave @titleField, [
document.location.pathname,
@@ -27,6 +33,50 @@ class @IssuableForm
"description"
]
+ handleSubmit: =>
+ if (parseInt(@issueMoveField?.val()) ? 0) > 0
+ return false unless confirm(@issueMoveConfirmMsg)
+
+ @resetAutosave()
+
resetAutosave: =>
@titleField.data("autosave").reset()
@descriptionField.data("autosave").reset()
+
+ initWip: ->
+ @$wipExplanation = @form.find(".js-wip-explanation")
+ @$noWipExplanation = @form.find(".js-no-wip-explanation")
+ return unless @$wipExplanation.length and @$noWipExplanation.length
+
+ @form.on "click", ".js-toggle-wip", @toggleWip
+
+ @titleField.on "keyup blur", @renderWipExplanation
+
+ @renderWipExplanation()
+
+ workInProgress: ->
+ @wipRegex.test @titleField.val()
+
+ renderWipExplanation: =>
+ if @workInProgress()
+ @$wipExplanation.show()
+ @$noWipExplanation.hide()
+ else
+ @$wipExplanation.hide()
+ @$noWipExplanation.show()
+
+ toggleWip: (event) =>
+ event.preventDefault()
+
+ if @workInProgress()
+ @removeWip()
+ else
+ @addWip()
+
+ @renderWipExplanation()
+
+ removeWip: ->
+ @titleField.val @titleField.val().replace(@wipRegex, "")
+
+ addWip: ->
+ @titleField.val "WIP: #{@titleField.val()}"
diff --git a/app/assets/javascripts/issue_status_select.js.coffee b/app/assets/javascripts/issue_status_select.js.coffee
new file mode 100644
index 00000000000..c5740f27ddd
--- /dev/null
+++ b/app/assets/javascripts/issue_status_select.js.coffee
@@ -0,0 +1,11 @@
+class @IssueStatusSelect
+ constructor: ->
+ $('.js-issue-status').each (i, el) ->
+ fieldName = $(el).data("field-name")
+
+ $(el).glDropdown(
+ selectable: true
+ fieldName: fieldName
+ id: (obj, el) ->
+ $(el).data("id")
+ )
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index a0acf3028bf..1127b289264 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -41,24 +41,28 @@
@timer = null
$("#issue_search").keyup ->
clearTimeout(@timer)
- @timer = setTimeout(Issues.filterResults, 500)
+ @timer = setTimeout( ->
+ Issues.filterResults $("#issue_search_form")
+ , 500)
- filterResults: =>
- form = $("#issue_search_form")
- search = $("#issue_search").val()
- $('.issues-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '?' + form.serialize()
+ filterResults: (form) =>
+ $('.issues-holder, .merge-requests-holder').css("opacity", '0.5')
+ formAction = form.attr('action')
+ formData = form.serialize()
+ issuesUrl = formAction
+ issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}")
+ issuesUrl += formData
$.ajax
type: "GET"
- url: form.attr('action')
- data: form.serialize()
+ url: formAction
+ data: formData
complete: ->
- $('.issues-holder').css("opacity", '1.0')
+ $('.issues-holder, .merge-requests-holder').css("opacity", '1.0')
success: (data) ->
- $('.issues-holder').html(data.html)
+ $('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
- history.replaceState {page: issues_url}, document.title, issues_url
+ history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload()
dataType: "json"
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
new file mode 100644
index 00000000000..b5c7af9a8ad
--- /dev/null
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -0,0 +1,249 @@
+class @LabelsSelect
+ constructor: ->
+ $('.js-label-select').each (i, dropdown) ->
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ labelUrl = $dropdown.data('labels')
+ issueUpdateURL = $dropdown.data('issueUpdate')
+ selectedLabel = $dropdown.data('selected')
+ if selectedLabel?
+ selectedLabel = selectedLabel.split(',')
+ newLabelField = $('#new_label_name')
+ newColorField = $('#new_label_color')
+ showNo = $dropdown.data('show-no')
+ showAny = $dropdown.data('show-any')
+ defaultLabel = $dropdown.data('default-label')
+ abilityName = $dropdown.data('ability-name')
+ $selectbox = $dropdown.closest('.selectbox')
+ $block = $selectbox.closest('.block')
+ $value = $block.find('.value')
+ $loading = $block.find('.block-loading').fadeOut()
+
+ if newLabelField.length
+ $newLabelCreateButton = $('.js-new-label-btn')
+ $colorPreview = $('.js-dropdown-label-color-preview')
+ $newLabelError = $dropdown.parent().find('.js-label-error')
+ $newLabelError.hide()
+
+ # Suggested colors in the dropdown to chose from pre-chosen colors
+ $('.suggest-colors-dropdown a').on 'click', (e) ->
+
+ issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL?
+ if issueUpdateURL
+ labelHTMLTemplate = _.template(
+ '<% _.each(labels, function(label){ %>
+ <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>">
+ <span class="label color-label" style="background-color: <%= label.color %>;">
+ <%= label.title %>
+ </span>
+ </a>
+ <% }); %>'
+ );
+ labelNoneHTMLTemplate = _.template('<div class="light">None</div>')
+
+ if newLabelField.length and $dropdown.hasClass 'js-extra-options'
+ $('.suggest-colors-dropdown a').on "click", (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+ newColorField
+ .val($(this).data('color'))
+ .trigger('change')
+ $colorPreview
+ .css 'background-color', $(this).data('color')
+ .parent()
+ .addClass 'is-active'
+
+ # Cancel button takes back to first page
+ resetForm = ->
+ newLabelField
+ .val ''
+ .trigger 'change'
+ newColorField
+ .val ''
+ .trigger 'change'
+ $colorPreview
+ .css 'background-color', ''
+ .parent()
+ .removeClass 'is-active'
+
+ $('.dropdown-menu-back').on 'click', ->
+ resetForm()
+
+ $('.js-cancel-label-btn').on 'click', (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+ resetForm()
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
+
+ # Listen for change and keyup events on label and color field
+ # This allows us to enable the button when ready
+ enableLabelCreateButton = ->
+ if newLabelField.val() isnt '' and newColorField.val() isnt ''
+ $newLabelError.hide()
+ $('.js-new-label-btn').disable()
+
+ # Create new label with API
+ Api.newLabel projectId, {
+ name: newLabelField.val()
+ color: newColorField.val()
+ }, (label) ->
+ $('.js-new-label-btn').enable()
+
+ if label.message?
+ $newLabelError
+ .text label.message
+ .show()
+ else
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
+
+ $newLabelCreateButton.enable()
+ else
+ $newLabelCreateButton.disable()
+
+ newLabelField.on 'keyup change', enableLabelCreateButton
+
+ newColorField.on 'keyup change', enableLabelCreateButton
+
+ # Send the API call to create the label
+ $newLabelCreateButton
+ .disable()
+ .on 'click', (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+
+ if newLabelField.val() isnt '' and newColorField.val() isnt ''
+ $newLabelError.hide()
+ $('.js-new-label-btn').disable()
+
+ # Create new label with API
+ Api.newLabel projectId, {
+ name: newLabelField.val()
+ color: newColorField.val()
+ }, (label) ->
+ $('.js-new-label-btn').enable()
+
+ if label.message?
+ $newLabelError
+ .text label.message
+ .show()
+ else
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
+
+ saveLabelData = ->
+ selected = $dropdown
+ .closest('.selectbox')
+ .find("input[name='#{$dropdown.data('field-name')}']")
+ .map(->
+ @value
+ ).get()
+ data = {}
+ data[abilityName] = {}
+ data[abilityName].label_ids = selected
+ if not selected.length
+ data[abilityName].label_ids = ['']
+ $loading.fadeIn()
+ $.ajax(
+ type: 'PUT'
+ url: issueUpdateURL
+ dataType: 'JSON'
+ data: data
+ ).done (data) ->
+ $loading.fadeOut()
+ $selectbox.hide()
+ data.issueURLSplit = issueURLSplit
+ if not data.labels.length
+ template = labelNoneHTMLTemplate()
+ else
+ template = labelHTMLTemplate(data)
+ href = $value
+ .show()
+ .html(template)
+ $value
+ .find('a')
+ .each((i) ->
+ setTimeout(=>
+ glAnimate($(@), 'pulse')
+ ,200 * i
+ )
+ )
+
+
+ $dropdown.glDropdown(
+ data: (term, callback) ->
+ $.ajax(
+ url: labelUrl
+ ).done (data) ->
+ if $dropdown.hasClass 'js-extra-options'
+ if showNo
+ data.unshift(
+ id: 0
+ title: 'No Label'
+ )
+
+ if showAny
+ data.unshift(
+ isAny: true
+ title: 'Any Label'
+ )
+
+ if data.length > 2
+ data.splice 2, 0, 'divider'
+ callback data
+
+ renderRow: (label) ->
+ selectedClass = ''
+ if $selectbox.find("input[type='hidden']\
+ [name='#{$dropdown.data('field-name')}']\
+ [value='#{label.id}']").length
+ selectedClass = 'is-active'
+
+ color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
+
+ "<li>
+ <a href='#' class='#{selectedClass}'>
+ #{color}
+ #{label.title}
+ </a>
+ </li>"
+ filterable: true
+ search:
+ fields: ['title']
+ selectable: true
+
+ toggleLabel: (selected) ->
+ if selected and selected.title isnt 'Any Label'
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
+ id: (label) ->
+ if label.isAny?
+ ''
+ else if $dropdown.hasClass "js-filter-submit"
+ label.title
+ else
+ label.id
+
+ hidden: ->
+ $selectbox.hide()
+ $value.show()
+ if $dropdown.hasClass 'js-multiselect'
+ saveLabelData()
+
+ multiSelect: $dropdown.hasClass 'js-multiselect'
+
+ clicked: ->
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ else
+ if $dropdown.hasClass 'js-multiselect'
+ return
+ else
+ saveLabelData()
+ )
diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/animate.js.coffee
new file mode 100644
index 00000000000..8f892b5a2b9
--- /dev/null
+++ b/app/assets/javascripts/lib/animate.js.coffee
@@ -0,0 +1,13 @@
+((w) ->
+
+ w.glAnimate = ($el, animation, done) ->
+ $el
+ .removeClass()
+ .addClass(animation + ' animated')
+ .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', ->
+ $(this).removeClass()
+ return
+ return
+ return
+
+) window \ No newline at end of file
diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee
index 35b2fbbba07..d14b7139237 100644
--- a/app/assets/javascripts/logo.js.coffee
+++ b/app/assets/javascripts/logo.js.coffee
@@ -1,4 +1,4 @@
-NProgress.configure(showSpinner: false)
+Turbolinks.enableProgressBar();
defaultClass = 'tanuki-shape'
pieces = [
diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee
index 98fc8f17340..2a0b9479445 100644
--- a/app/assets/javascripts/markdown_preview.js.coffee
+++ b/app/assets/javascripts/markdown_preview.js.coffee
@@ -6,6 +6,7 @@
class @MarkdownPreview
# Minimum number of users referenced before triggering a warning
referenceThreshold: 10
+ ajaxCache: {}
showPreview: (form) ->
preview = form.find('.js-md-preview')
@@ -24,12 +25,16 @@ class @MarkdownPreview
renderMarkdown: (text, success) ->
return unless window.markdown_preview_path
+ return success(@ajaxCache.response) if text == @ajaxCache.text
+
$.ajax
type: 'POST'
url: window.markdown_preview_path
data: { text: text }
dataType: 'json'
- success: success
+ success: (response) =>
+ @ajaxCache = text: text, response: response
+ success(response)
hideReferencedUsers: (form) ->
referencedUsers = form.find('.referenced-users')
@@ -49,6 +54,7 @@ markdownPreview = new MarkdownPreview()
previewButtonSelector = '.js-md-preview-button'
writeButtonSelector = '.js-md-write-button'
+lastTextareaPreviewed = null
$.fn.setupMarkdownPreview = ->
$form = $(this)
@@ -58,10 +64,10 @@ $.fn.setupMarkdownPreview = ->
form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form)
form_textarea.on 'blur', -> markdownPreview.showPreview($form)
-$(document).on 'click', previewButtonSelector, (e) ->
- e.preventDefault()
+$(document).on 'markdown-preview:show', (e, $form) ->
+ return unless $form
- $form = $(this).closest('form')
+ lastTextareaPreviewed = $form.find('textarea.markdown-area')
# toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active')
@@ -73,10 +79,10 @@ $(document).on 'click', previewButtonSelector, (e) ->
markdownPreview.showPreview($form)
-$(document).on 'click', writeButtonSelector, (e) ->
- e.preventDefault()
+$(document).on 'markdown-preview:hide', (e, $form) ->
+ return unless $form
- $form = $(this).closest('form')
+ lastTextareaPreviewed = null
# toggle tabs
$form.find(writeButtonSelector).parent().addClass('active')
@@ -84,4 +90,30 @@ $(document).on 'click', writeButtonSelector, (e) ->
# toggle content
$form.find('.md-write-holder').show()
+ $form.find('textarea.markdown-area').focus()
$form.find('.md-preview-holder').hide()
+
+$(document).on 'markdown-preview:toggle', (e, keyboardEvent) ->
+ $target = $(keyboardEvent.target)
+
+ if $target.is('textarea.markdown-area')
+ $(document).triggerHandler('markdown-preview:show', [$target.closest('form')])
+ keyboardEvent.preventDefault()
+ else if lastTextareaPreviewed
+ $target = lastTextareaPreviewed
+ $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')])
+ keyboardEvent.preventDefault()
+
+$(document).on 'click', previewButtonSelector, (e) ->
+ e.preventDefault()
+
+ $form = $(this).closest('form')
+
+ $(document).triggerHandler('markdown-preview:show', [$form])
+
+$(document).on 'click', writeButtonSelector, (e) ->
+ e.preventDefault()
+
+ $form = $(this).closest('form')
+
+ $(document).triggerHandler('markdown-preview:hide', [$form])
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index b10e1db7f3f..839e6ec2c08 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -3,6 +3,8 @@
# Handles persisting and restoring the current tab selection and lazily-loading
# content on the MergeRequests#show page.
#
+#= require jquery.cookie
+#
# ### Example Markup
#
# <ul class="nav-links merge-request-tabs">
@@ -68,10 +70,15 @@ class @MergeRequestTabs
if action == 'commits'
@loadCommits($target.attr('href'))
+ @expandView()
else if action == 'diffs'
@loadDiff($target.attr('href'))
+ @shrinkView()
else if action == 'builds'
@loadBuilds($target.attr('href'))
+ @expandView()
+ else
+ @expandView()
@setCurrentAction(action)
@@ -145,7 +152,9 @@ class @MergeRequestTabs
url: "#{source}.json" + @_location.search
success: (data) =>
document.querySelector("div#diffs").innerHTML = data.html
+ $('.js-timeago').timeago()
$('div#diffs .js-syntax-highlight').syntaxHighlight()
+ @expandViewContainer() if @diffViewType() is 'parallel'
@diffsLoaded = true
@scrollToElement("#diffs")
@@ -177,3 +186,33 @@ class @MergeRequestTabs
options = $.extend({}, defaults, options)
$.ajax(options)
+
+ # Returns diff view type
+ diffViewType: ->
+ $('.inline-parallel-buttons a.active').data('view-type')
+
+ expandViewContainer: ->
+ $('.container-fluid').removeClass('container-limited')
+
+ shrinkView: ->
+ $gutterIcon = $('.js-sidebar-toggle i:visible')
+
+ # Wait until listeners are set
+ setTimeout( ->
+ # Only when sidebar is expanded
+ if $gutterIcon.is('.fa-angle-double-right')
+ $gutterIcon.closest('a').trigger('click', [true])
+ , 0)
+
+ # Expand the issuable sidebar unless the user explicitly collapsed it
+ expandView: ->
+ return if $.cookie('collapsed_gutter') == 'true'
+
+ $gutterIcon = $('.js-sidebar-toggle i:visible')
+
+ # Wait until listeners are set
+ setTimeout( ->
+ # Only when sidebar is collapsed
+ if $gutterIcon.is('.fa-angle-double-left')
+ $gutterIcon.closest('a').trigger('click', [true])
+ , 0)
diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee
index 31f6c6d3d47..0037a3a21c2 100644
--- a/app/assets/javascripts/milestone.js.coffee
+++ b/app/assets/javascripts/milestone.js.coffee
@@ -62,15 +62,24 @@ class @Milestone
dataType: "json"
constructor: ->
+ oldMouseStart = $.ui.sortable.prototype._mouseStart
+ $.ui.sortable.prototype._mouseStart = (event, overrideHandle, noActivation) ->
+ this._trigger "beforeStart", event, this._uiHash()
+ oldMouseStart.apply this, [event, overrideHandle, noActivation]
+
@bindIssuesSorting()
@bindMergeRequestSorting()
- @bindTabsSwitching
+ @bindTabsSwitching()
bindIssuesSorting: ->
$("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable(
connectWith: ".issues-sortable-list",
dropOnEmpty: true,
items: "li:not(.ui-sort-disabled)",
+ beforeStart: (event, ui) ->
+ $(".issues-sortable-list").css "min-height", ui.item.outerHeight()
+ stop: (event, ui) ->
+ $(".issues-sortable-list").css "min-height", "0px"
update: (event, ui) ->
data = $(this).sortable("serialize")
Milestone.sortIssues(data)
@@ -95,11 +104,24 @@ class @Milestone
).disableSelection()
+ bindTabsSwitching: ->
+ $('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
+ currentTabClass = $(e.target).data('show')
+ previousTabClass = $(e.relatedTarget).data('show')
+
+ $(previousTabClass).hide()
+ $(currentTabClass).removeClass('hidden')
+ $(currentTabClass).show()
+
bindMergeRequestSorting: ->
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable(
connectWith: ".merge_requests-sortable-list",
dropOnEmpty: true,
items: "li:not(.ui-sort-disabled)",
+ beforeStart: (event, ui) ->
+ $(".merge_requests-sortable-list").css "min-height", ui.item.outerHeight()
+ stop: (event, ui) ->
+ $(".merge_requests-sortable-list").css "min-height", "0px"
update: (event, ui) ->
data = $(this).sortable("serialize")
Milestone.sortMergeRequests(data)
@@ -123,12 +145,3 @@ class @Milestone
Milestone.updateMergeRequest(ui.item, merge_request_url, data)
).disableSelection()
-
- bindMergeRequestSorting: ->
- $('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
- currentTabClass = $(e.target).data('show')
- previousTabClass = $(e.relatedTarget).data('show')
-
- $(previousTabClass).hide()
- $(currentTabClass).removeClass('hidden')
- $(currentTabClass).show()
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
new file mode 100644
index 00000000000..d1746c38e74
--- /dev/null
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -0,0 +1,107 @@
+class @MilestoneSelect
+ constructor: (currentProject) ->
+ if currentProject?
+ _this = @
+ @currentProject = JSON.parse(currentProject)
+ $('.js-milestone-select').each (i, dropdown) ->
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ milestonesUrl = $dropdown.data('milestones')
+ issueUpdateURL = $dropdown.data('issueUpdate')
+ selectedMilestone = $dropdown.data('selected')
+ showNo = $dropdown.data('show-no')
+ showAny = $dropdown.data('show-any')
+ useId = $dropdown.data('use-id')
+ defaultLabel = $dropdown.data('default-label')
+ issuableId = $dropdown.data('issuable-id')
+ abilityName = $dropdown.data('ability-name')
+ $selectbox = $dropdown.closest('.selectbox')
+ $block = $selectbox.closest('.block')
+ $value = $block.find('.value')
+ $loading = $block.find('.block-loading').fadeOut()
+
+ if issueUpdateURL
+ milestoneLinkTemplate = _.template(
+ '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= title %></a>'
+ )
+
+ milestoneLinkNoneTemplate = '<div class="light">None</div>'
+
+ $dropdown.glDropdown(
+ data: (term, callback) ->
+ $.ajax(
+ url: milestonesUrl
+ ).done (data) ->
+ if $dropdown.hasClass "js-extra-options"
+ if showNo
+ data.unshift(
+ id: '0'
+ title: 'No Milestone'
+ )
+
+ if showAny
+ data.unshift(
+ isAny: true
+ title: 'Any Milestone'
+ )
+
+ if data.length > 2
+ data.splice 2, 0, 'divider'
+ callback(data)
+ filterable: true
+ search:
+ fields: ['title']
+ selectable: true
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
+ text: (milestone) ->
+ milestone.title
+ id: (milestone) ->
+ if !useId
+ if !milestone.isAny?
+ milestone.title
+ else
+ ''
+ else
+ milestone.id
+ isSelected: (milestone) ->
+ milestone.title is selectedMilestone
+ hidden: ->
+ $selectbox.hide()
+ $value.show()
+ clicked: (e) ->
+ if $dropdown.hasClass 'js-filter-bulk-update'
+ return
+
+ if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.parents('form').submit()
+ else
+ selected = $selectbox
+ .find('input[type="hidden"]')
+ .val()
+ data = {}
+ data[abilityName] = {}
+ data[abilityName].milestone_id = selected
+ $loading
+ .fadeIn()
+ $.ajax(
+ type: 'PUT'
+ url: issueUpdateURL
+ data: data
+ ).done (data) ->
+ $loading.fadeOut()
+ $selectbox.hide()
+ $milestoneLink = $value
+ .show()
+ .find('a')
+ if data.milestone?
+ data.milestone.namespace = _this.currentProject.namespace
+ data.milestone.path = _this.currentProject.path
+ $value.html(milestoneLinkTemplate(data.milestone))
+ else
+ $value.html(milestoneLinkNoneTemplate)
+ ) \ No newline at end of file
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index 3347ab65c90..ff06c57f2b5 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -16,11 +16,13 @@ class @Notes
@view = view
@noteable_url = document.URL
@notesCountBadge ||= $(".issuable-details").find(".notes-tab .badge")
+ @basePollingInterval = 15000
+ @maxPollingSteps = 4
- @initRefresh()
- @setupMainTargetNoteForm()
@cleanBinding()
@addBinding()
+ @setPollingInterval()
+ @setupMainTargetNoteForm()
@initTaskList()
addBinding: ->
@@ -28,8 +30,11 @@ class @Notes
$(document).on "ajax:success", ".js-main-target-form", @addNote
$(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
+ # catch note ajax errors
+ $(document).on "ajax:error", ".js-main-target-form", @addNoteError
+
# change note in UI after update
- $(document).on "ajax:success", "form.edit_note", @updateNote
+ $(document).on "ajax:success", "form.edit-note", @updateNote
# Edit note link
$(document).on "click", ".js-note-edit", @showEditForm
@@ -37,7 +42,7 @@ class @Notes
# Reopen and close actions for Issue/MR combined with note form submit
$(document).on "click", ".js-comment-button", @updateCloseButton
- $(document).on "keyup", ".js-note-text", @updateTargetButtons
+ $(document).on "keyup input", ".js-note-text", @updateTargetButtons
# remove a note (in general)
$(document).on "click", ".js-note-delete", @removeNote
@@ -49,6 +54,9 @@ class @Notes
$(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton
$(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm
+ # reset main target form when clicking discard
+ $(document).on "click", ".js-note-discard", @resetMainTargetForm
+
# update the file name when an attachment is selected
$(document).on "change", ".js-note-attachment-input", @updateFormAttachment
@@ -70,7 +78,7 @@ class @Notes
cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form"
- $(document).off "ajax:success", "form.edit_note"
+ $(document).off "ajax:success", "form.edit-note"
$(document).off "click", ".js-note-edit"
$(document).off "click", ".note-edit-cancel"
$(document).off "click", ".js-note-delete"
@@ -83,6 +91,7 @@ class @Notes
$(document).off "keyup", ".js-note-text"
$(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close"
+ $(document).off "click", ".js-note-discard"
$('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container'
@@ -91,9 +100,11 @@ class @Notes
clearInterval(Notes.interval)
Notes.interval = setInterval =>
@refresh()
- , 15000
+ , @pollingInterval
refresh: ->
+ return if @refreshing is true
+ refreshing = true
if not document.hidden and document.URL.indexOf(@noteable_url) is 0
@getContent()
@@ -105,12 +116,31 @@ class @Notes
success: (data) =>
notes = data.notes
@last_fetched_at = data.last_fetched_at
+ @setPollingInterval(data.notes.length)
$.each notes, (i, note) =>
if note.discussion_with_diff_html?
@renderDiscussionNote(note)
else
@renderNote(note)
+ always: =>
+ @refreshing = false
+ ###
+ Increase @pollingInterval up to 120 seconds on every function call,
+ if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
+ will reset to @basePollingInterval.
+
+ Note: this function is used to gradually increase the polling interval
+ if there aren't new notes coming from the server
+ ###
+ setPollingInterval: (shouldReset = true) ->
+ nthInterval = @basePollingInterval * Math.pow(2, @maxPollingSteps - 1)
+ if shouldReset
+ @pollingInterval = @basePollingInterval
+ else if @pollingInterval < nthInterval
+ @pollingInterval *= 2
+
+ @initRefresh()
###
Render note in main comments area.
@@ -196,7 +226,7 @@ class @Notes
Resets text and preview.
Resets buttons.
###
- resetMainTargetForm: ->
+ resetMainTargetForm: (e) =>
form = $(".js-main-target-form")
# remove validation errors
@@ -208,6 +238,8 @@ class @Notes
form.find(".js-note-text").data("autosave").reset()
+ @updateTargetButtons(e)
+
reenableTargetFormSubmitButton: ->
form = $(".js-main-target-form")
@@ -251,8 +283,10 @@ class @Notes
form.removeClass "js-new-note-form"
form.find('.div-dropzone').remove()
+ # hide discard button
+ form.find('.js-note-discard').hide()
+
# setup preview buttons
- form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
previewButton = form.find(".js-md-preview-button")
textarea = form.find(".js-note-text")
@@ -286,6 +320,10 @@ class @Notes
addNote: (xhr, note, status) =>
@renderNote(note)
+ addNoteError: (xhr, note, status) =>
+ flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert')
+ flash.pinTo('.md-area')
+
###
Called in response to the new note form being submitted
@@ -305,6 +343,7 @@ class @Notes
updateNote: (_xhr, note, _status) =>
# Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html)
+ $('.js-timeago', $html).timeago()
$html.syntaxHighlight()
$html.find('.js-task-list-container').taskList('enable')
@@ -322,24 +361,26 @@ class @Notes
showEditForm: (e) ->
e.preventDefault()
note = $(this).closest(".note")
- note.find(".note-body > .note-text").hide()
- note.find(".note-header").hide()
- base_form = note.find(".note-edit-form")
- form = base_form.clone().insertAfter(base_form)
- form.addClass('current-note-edit-form gfm-form')
- form.find('.div-dropzone').remove()
+ note.addClass "is-editting"
+ form = note.find(".note-edit-form")
+ isNewForm = form.is(':not(.gfm-form)')
+ if isNewForm
+ form.addClass('gfm-form')
+ form.addClass('current-note-edit-form')
# Show the attachment delete link
note.find(".js-note-attachment-delete").show()
# Setup markdown form
- GitLab.GfmAutoComplete.setup()
- new DropzoneInput(form)
+ if isNewForm
+ GitLab.GfmAutoComplete.setup()
+ new DropzoneInput(form)
- form.show()
textarea = form.find("textarea")
textarea.focus()
- autosize(textarea)
+
+ if isNewForm
+ autosize(textarea)
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
# The textarea has the correct value, Chrome just won't show it unless we
@@ -348,7 +389,8 @@ class @Notes
textarea.val ""
textarea.val value
- disableButtonIfEmptyField textarea, form.find(".js-comment-button")
+ if isNewForm
+ disableButtonIfEmptyField textarea, form.find(".js-comment-button")
###
Called in response to clicking the edit note link
@@ -358,9 +400,9 @@ class @Notes
cancelEdit: (e) ->
e.preventDefault()
note = $(this).closest(".note")
- note.find(".note-body > .note-text").show()
- note.find(".note-header").show()
- note.find(".current-note-edit-form").remove()
+ note.removeClass "is-editting"
+ note.find(".current-note-edit-form")
+ .removeClass("current-note-edit-form")
###
Called in response to deleting a note of any kind.
@@ -439,6 +481,11 @@ class @Notes
form.find("#note_line_code").val dataHolder.data("lineCode")
form.find("#note_noteable_type").val dataHolder.data("noteableType")
form.find("#note_noteable_id").val dataHolder.data("noteableId")
+ form.find('.js-note-discard')
+ .show()
+ .removeClass('js-note-discard')
+ .addClass('js-close-discussion-note-form')
+ .text(form.find('.js-close-discussion-note-form').data('cancel-text'))
@setupNoteForm form
form.find(".js-note-text").focus()
form.addClass "js-discussion-note-form"
@@ -538,21 +585,52 @@ class @Notes
updateCloseButton: (e) =>
textarea = $(e.target)
form = textarea.parents('form')
- form.find('.js-note-target-close').text('Close')
+ closebtn = form.find('.js-note-target-close')
+ closebtn.text(closebtn.data('original-text'))
updateTargetButtons: (e) =>
textarea = $(e.target)
form = textarea.parents('form')
+ reopenbtn = form.find('.js-note-target-reopen')
+ closebtn = form.find('.js-note-target-close')
+ discardbtn = form.find('.js-note-discard')
+
if textarea.val().trim().length > 0
- form.find('.js-note-target-reopen').text('Comment & reopen')
- form.find('.js-note-target-close').text('Comment & close')
- form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen')
- form.find('.js-note-target-close').addClass('btn-comment-and-close')
+ reopentext = reopenbtn.data('alternative-text')
+ closetext = closebtn.data('alternative-text')
+
+ if reopenbtn.text() isnt reopentext
+ reopenbtn.text(reopentext)
+
+ if closebtn.text() isnt closetext
+ closebtn.text(closetext)
+
+ if reopenbtn.is(':not(.btn-comment-and-reopen)')
+ reopenbtn.addClass('btn-comment-and-reopen')
+
+ if closebtn.is(':not(.btn-comment-and-close)')
+ closebtn.addClass('btn-comment-and-close')
+
+ if discardbtn.is(':hidden')
+ discardbtn.show()
else
- form.find('.js-note-target-reopen').text('Reopen')
- form.find('.js-note-target-close').text('Close')
- form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen')
- form.find('.js-note-target-close').removeClass('btn-comment-and-close')
+ reopentext = reopenbtn.data('original-text')
+ closetext = closebtn.data('original-text')
+
+ if reopenbtn.text() isnt reopentext
+ reopenbtn.text(reopentext)
+
+ if closebtn.text() isnt closetext
+ closebtn.text(closetext)
+
+ if reopenbtn.is('.btn-comment-and-reopen')
+ reopenbtn.removeClass('btn-comment-and-reopen')
+
+ if closebtn.is('.btn-comment-and-close')
+ closebtn.removeClass('btn-comment-and-close')
+
+ if discardbtn.is(':visible')
+ discardbtn.hide()
initTaskList: ->
@enableTaskList()
diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee
index d639303aed3..0ff83b7f0c8 100644
--- a/app/assets/javascripts/pager.js.coffee
+++ b/app/assets/javascripts/pager.js.coffee
@@ -1,6 +1,7 @@
@Pager =
init: (@limit = 0, preload, @disable = false) ->
- @loading = $(".loading")
+ @loading = $('.loading').first()
+
if preload
@offset = 0
@getOld()
diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee
index bb0b66b86e1..ae87c6c4e40 100644
--- a/app/assets/javascripts/profile.js.coffee
+++ b/app/assets/javascripts/profile.js.coffee
@@ -1,26 +1,72 @@
class @Profile
- constructor: ->
+ constructor: (opts = {}) ->
+ {
+ @form = $('.edit-user')
+ } = opts
+
# Automatically submit the Preferences form when any of its radio buttons change
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit()
- $('.update-username form').on 'ajax:before', ->
- $('.loading-gif').show()
+ $('.update-username').on 'ajax:before', ->
+ $('.loading-username').show()
$(this).find('.update-success').hide()
$(this).find('.update-failed').hide()
- $('.update-username form').on 'ajax:complete', ->
+ $('.update-username').on 'ajax:complete', ->
+ $('.loading-username').hide()
$(this).find('.btn-save').enable()
$(this).find('.loading-gif').hide()
$('.update-notifications').on 'ajax:complete', ->
$(this).find('.btn-save').enable()
- $('.js-choose-user-avatar-button').bind "click", ->
- form = $(this).closest("form")
- form.find(".js-user-avatar-input").click()
+ @bindEvents()
+
+ cropOpts =
+ filename: '.js-avatar-filename'
+ previewImage: '.avatar-image .avatar'
+ modalCrop: '.modal-profile-crop'
+ pickImageEl: '.js-choose-user-avatar-button'
+ uploadImageBtn: '.js-upload-user-avatar'
+ modalCropImg: '.modal-profile-crop-image'
+
+ @avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data 'glcrop'
+
+ bindEvents: ->
+ @form.on 'submit', @onSubmitForm
+
+ onSubmitForm: (e) =>
+ e.preventDefault()
+ @saveForm()
+
+ saveForm: ->
+ self = @
+
+ formData = new FormData(@form[0])
+ formData.append('user[avatar]', @avatarGlCrop.getBlob(), 'avatar.png')
+
+ $.ajax
+ url: @form.attr('action')
+ type: @form.attr('method')
+ data: formData
+ dataType: "json"
+ processData: false
+ contentType: false
+ success: (response) ->
+ new Flash(response.message, 'notice')
+ error: (jqXHR) ->
+ new Flash(jqXHR.responseJSON.message, 'alert')
+ complete: ->
+ window.scrollTo 0, 0
+ # Enable submit button after requests ends
+ self.form.find(':input[disabled]').enable()
+
+$ ->
+ # Extract the SSH Key title from its comment
+ $(document).on 'focusout.ssh_key', '#key_key', ->
+ $title = $('#key_title')
+ comment = $(@).val().match(/^\S+ \S+ (.+)\n?$/)
- $('.js-user-avatar-input').bind "change", ->
- form = $(this).closest("form")
- filename = $(this).val().replace(/^.*[\\\/]/, '')
- form.find(".js-avatar-filename").text(filename)
+ if comment && comment.length > 1 && $title.val() == ''
+ $title.val(comment[1]).change()
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
index 76bc4ff42a2..87d313ed67c 100644
--- a/app/assets/javascripts/project.js.coffee
+++ b/app/assets/javascripts/project.js.coffee
@@ -11,7 +11,6 @@ class @Project
$(@).toggleClass('active')
url = $("#project_clone").val()
- console.log("url",url)
# Update the input field
$('#project_clone').val(url)
diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee
index fecdb9fc2e7..63dee4ed5d7 100644
--- a/app/assets/javascripts/project_new.js.coffee
+++ b/app/assets/javascripts/project_new.js.coffee
@@ -3,3 +3,16 @@ class @ProjectNew
$('.project-edit-container').on 'ajax:before', =>
$('.project-edit-container').hide()
$('.save-project-loader').show()
+ @toggleSettings()
+ @toggleSettingsOnclick()
+
+
+ toggleSettings: ->
+ checked = $("#project_builds_enabled").prop("checked")
+ if checked
+ $('.builds-feature').show()
+ else
+ $('.builds-feature').hide()
+
+ toggleSettingsOnclick: ->
+ $("#project_builds_enabled").on 'click', @toggleSettings
diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee
index eab34be652a..e4c4bf3b273 100644
--- a/app/assets/javascripts/projects_list.js.coffee
+++ b/app/assets/javascripts/projects_list.js.coffee
@@ -1,26 +1,37 @@
-class @ProjectsList
- constructor: ->
- $(".projects-list .js-expand").on 'click', (e) ->
- e.preventDefault()
- list = $(this).closest('.projects-list')
+@ProjectsList =
+ init: ->
+ $(".projects-list-filter").off('keyup')
+ this.initSearch()
+ this.initPagination()
- $("#filter_projects").on 'keyup', ->
- ProjectsList.filter_results($("#filter_projects"))
+ initSearch: ->
+ @timer = null
+ $(".projects-list-filter").on('keyup', ->
+ clearTimeout(@timer)
+ @timer = setTimeout(ProjectsList.filterResults, 500)
+ )
- @filter_results: ($element) ->
- terms = $element.val()
- filterSelector = $element.data('filter-selector') || 'span.filter-title'
+ filterResults: =>
+ $('.projects-list-holder').fadeTo(250, 0.5)
- if not terms
- $(".projects-list li").show()
- $('.gl-pagination').show()
- else
- $(".projects-list li").each (index) ->
- $this = $(this)
- name = $this.find(filterSelector).text()
+ form = null
+ form = $("form#project-filter-form")
+ search = $(".projects-list-filter").val()
+ project_filter_url = form.attr('action') + '?' + form.serialize()
- if name.toLowerCase().indexOf(terms.toLowerCase()) == -1
- $this.hide()
- else
- $this.show()
- $('.gl-pagination').hide()
+ $.ajax
+ type: "GET"
+ url: form.attr('action')
+ data: form.serialize()
+ complete: ->
+ $('.projects-list-holder').fadeTo(250, 1)
+ success: (data) ->
+ $('.projects-list-holder').replaceWith(data.html)
+ # Change url so if user reload a page - search results are saved
+ history.replaceState {page: project_filter_url}, document.title, project_filter_url
+ dataType: "json"
+
+ initPagination: ->
+ $('.projects-list-holder .pagination').on('ajax:success', (e, data) ->
+ $('.projects-list-holder').replaceWith(data.html)
+ )
diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee
index f141fb69c3e..100e3aac535 100644
--- a/app/assets/javascripts/shortcuts.js.coffee
+++ b/app/assets/javascripts/shortcuts.js.coffee
@@ -4,17 +4,23 @@ class @Shortcuts
Mousetrap.reset()
Mousetrap.bind('?', @selectiveHelp)
Mousetrap.bind('s', Shortcuts.focusSearch)
+ Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL?
selectiveHelp: (e) =>
Shortcuts.showHelp(e, @enabledHelp)
+ toggleMarkdownPreview: (e) =>
+ $(document).triggerHandler('markdown-preview:toggle', [e])
+
@showHelp: (e, location) ->
if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show')
else
+ url = '/help/shortcuts'
+ url = gon.relative_url_root + url if gon.relative_url_root?
$.ajax(
- url: '/help/shortcuts',
+ url: url,
dataType: 'script',
success: (e) ->
if location and location.length > 0
@@ -33,3 +39,14 @@ $(document).on 'click.more_help', '.js-more-help-button', (e) ->
$(@).remove()
$('.hidden-shortcut').show()
e.preventDefault()
+
+Mousetrap.stopCallback = (->
+ defaultStopCallback = Mousetrap.stopCallback
+
+ return (e, element, combo) ->
+ # allowed shortcuts if textarea, input, contenteditable are focused
+ if ['ctrl+shift+p', 'command+shift+p'].indexOf(combo) != -1
+ return false
+ else
+ return defaultStopCallback.apply(@, arguments)
+)()
diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee
index cefa1857d7f..bbf02f1db24 100644
--- a/app/assets/javascripts/shortcuts_issuable.coffee
+++ b/app/assets/javascripts/shortcuts_issuable.coffee
@@ -24,6 +24,10 @@ class @ShortcutsIssuable extends ShortcutsNavigation
@nextIssue()
return false
)
+ Mousetrap.bind('e', =>
+ @editIssue()
+ return false
+ )
if isMergeRequest
@@ -63,3 +67,7 @@ class @ShortcutsIssuable extends ShortcutsNavigation
# Focus the input field
replyField.focus()
+
+ editIssue: ->
+ $editBtn = $('.issuable-edit')
+ Turbolinks.visit($editBtn.attr('href'))
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index ae59480af9e..860d4f438d0 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -1,11 +1,26 @@
-$(document).on("click", '.toggle-nav-collapse', (e) ->
- e.preventDefault()
- collapsed = 'page-sidebar-collapsed'
- expanded = 'page-sidebar-expanded'
+collapsed = 'page-sidebar-collapsed'
+expanded = 'page-sidebar-expanded'
+toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
- $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
+
+ setTimeout ( ->
+ niceScrollBars = $('.nicescroll').niceScroll();
+ niceScrollBars.updateScrollBar();
+ ), 300
+
+$(document).on("click", '.toggle-nav-collapse', (e) ->
+ e.preventDefault()
+
+ toggleSidebar()
)
+
+$ ->
+ size = bp.getBreakpointSize()
+
+ if size is "xs" or size is "sm"
+ if $('.page-with-sidebar').hasClass(expanded)
+ toggleSidebar()
diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee
index f5584bcfe4b..31617c88b4a 100644
--- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee
+++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee
@@ -95,4 +95,4 @@ window.ContributorsStatGraphUtil =
if date_range is null || date_range[0] <= new Date(date) <= date_range[1]
true
else
- false \ No newline at end of file
+ false
diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee
index 7f41616d4e7..084f0e0dc65 100644
--- a/app/assets/javascripts/subscription.js.coffee
+++ b/app/assets/javascripts/subscription.js.coffee
@@ -1,17 +1,21 @@
class @Subscription
- constructor: (url) ->
- $(".subscribe-button").unbind("click").click (event)=>
- btn = $(event.currentTarget)
- action = btn.find("span").text()
- current_status = $(".subscription-status").attr("data-status")
- btn.prop("disabled", true)
-
- $.post url, =>
- btn.prop("disabled", false)
- status = if current_status == "subscribed" then "unsubscribed" else "subscribed"
- $(".subscription-status").attr("data-status", status)
- action = if status == "subscribed" then "Unsubscribe" else "Subscribe"
- btn.find("span").text(action)
- $(".subscription-status>div").toggleClass("hidden")
+ constructor: (container) ->
+ $container = $(container)
+ @url = $container.attr('data-url')
+ @subscribe_button = $container.find('.subscribe-button')
+ @subscription_status = $container.find('.subscription-status')
+ @subscribe_button.unbind('click').click(@toggleSubscription)
-
+ toggleSubscription: (event) =>
+ btn = $(event.currentTarget)
+ action = btn.find('span').text()
+ current_status = @subscription_status.attr('data-status')
+ btn.prop('disabled', true)
+
+ $.post @url, =>
+ btn.prop('disabled', false)
+ status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed'
+ @subscription_status.attr('data-status', status)
+ action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
+ btn.find('span').text(action)
+ @subscription_status.find('>div').toggleClass('hidden')
diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee
new file mode 100644
index 00000000000..b6b4bd90e6a
--- /dev/null
+++ b/app/assets/javascripts/todos.js.coffee
@@ -0,0 +1,56 @@
+class @Todos
+ constructor: (@name) ->
+ @clearListeners()
+ @initBtnListeners()
+
+ clearListeners: ->
+ $('.done-todo').off('click')
+ $('.js-todos-mark-all').off('click')
+
+ initBtnListeners: ->
+ $('.done-todo').on('click', @doneClicked)
+ $('.js-todos-mark-all').on('click', @allDoneClicked)
+
+ doneClicked: (e) =>
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ $this = $(e.currentTarget)
+ $this.disable()
+
+ $.ajax
+ type: 'POST'
+ url: $this.attr('href')
+ dataType: 'json'
+ data: '_method': 'delete'
+ success: (data) =>
+ @clearDone $this.closest('li')
+ @updateBadges data
+
+ allDoneClicked: (e) =>
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ $this = $(e.currentTarget)
+ $this.disable()
+
+ $.ajax
+ type: 'POST'
+ url: $this.attr('href')
+ dataType: 'json'
+ data: '_method': 'delete'
+ success: (data) =>
+ $this.remove()
+ $('.js-todos-list').remove()
+ @updateBadges data
+
+ clearDone: ($row) ->
+ $ul = $row.closest('ul')
+ $row.remove()
+
+ if not $ul.find('li').length
+ $ul.parents('.panel').remove()
+
+ updateBadges: (data) ->
+ $('.todos-pending .badge, .todos-pending-count').text data.count
+ $('.todos-done .badge').text data.done_count
diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee
index ec4271b092c..2882a90d118 100644
--- a/app/assets/javascripts/user.js.coffee
+++ b/app/assets/javascripts/user.js.coffee
@@ -1,10 +1,17 @@
class @User
- constructor: ->
+ constructor: (@opts) ->
$('.profile-groups-avatars').tooltip("placement": "top")
- new ProjectsList()
+
+ @initTabs()
$('.hide-project-limit-message').on 'click', (e) ->
path = '/'
$.cookie('hide_project_limit_message', 'false', { path: path })
$(@).parents('.project-limit-message').remove()
e.preventDefault()
+
+ initTabs: ->
+ new UserTabs(
+ parentEl: '.user-profile'
+ action: @opts.action
+ )
diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee
new file mode 100644
index 00000000000..09b7eec9104
--- /dev/null
+++ b/app/assets/javascripts/user_tabs.js.coffee
@@ -0,0 +1,146 @@
+# UserTabs
+#
+# Handles persisting and restoring the current tab selection and lazily-loading
+# content on the Users#show page.
+#
+# ### Example Markup
+#
+# <ul class="nav-links">
+# <li class="activity-tab active">
+# <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
+# Activity
+# </a>
+# </li>
+# <li class="groups-tab">
+# <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
+# Groups
+# </a>
+# </li>
+# <li class="contributed-tab">
+# <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
+# Contributed projects
+# </a>
+# </li>
+# <li class="projects-tab">
+# <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
+# Personal projects
+# </a>
+# </li>
+# </ul>
+#
+# <div class="tab-content">
+# <div class="tab-pane" id="activity">
+# Activity Content
+# </div>
+# <div class="tab-pane" id="groups">
+# Groups Content
+# </div>
+# <div class="tab-pane" id="contributed">
+# Contributed projects content
+# </div>
+# <div class="tab-pane" id="projects">
+# Projects content
+# </div>
+# </div>
+#
+# <div class="loading-status">
+# <div class="loading">
+# Loading Animation
+# </div>
+# </div>
+#
+class @UserTabs
+ constructor: (opts) ->
+ {
+ @action = 'activity'
+ @defaultAction = 'activity'
+ @parentEl = $(document)
+ } = opts
+
+ # Make jQuery object if selector is provided
+ @parentEl = $(@parentEl) if typeof @parentEl is 'string'
+
+ # Store the `location` object, allowing for easier stubbing in tests
+ @_location = location
+
+ # Set tab states
+ @loaded = {}
+ for item in @parentEl.find('.nav-links a')
+ @loaded[$(item).attr 'data-action'] = false
+
+ # Actions
+ @actions = Object.keys @loaded
+
+ @bindEvents()
+
+ # Set active tab
+ @action = @defaultAction if @action is 'show'
+ @activateTab(@action)
+
+ bindEvents: ->
+ # Toggle event listeners
+ @parentEl
+ .off 'shown.bs.tab', '.nav-links a[data-toggle="tab"]'
+ .on 'shown.bs.tab', '.nav-links a[data-toggle="tab"]', @tabShown
+
+ tabShown: (event) =>
+ $target = $(event.target)
+ action = $target.data('action')
+ source = $target.attr('href')
+
+ @setTab(source, action)
+ @setCurrentAction(action)
+
+ activateTab: (action) ->
+ @parentEl.find(".nav-links .#{action}-tab a").tab('show')
+
+ setTab: (source, action) ->
+ return if @loaded[action] is true
+
+ if action is 'activity'
+ @loadActivities(source)
+
+ if action in ['groups', 'contributed', 'projects']
+ @loadTab(source, action)
+
+ loadTab: (source, action) ->
+ $.ajax
+ beforeSend: => @toggleLoading(true)
+ complete: => @toggleLoading(false)
+ dataType: 'json'
+ type: 'GET'
+ url: "#{source}.json"
+ success: (data) =>
+ tabSelector = 'div#' + action
+ @parentEl.find(tabSelector).html(data.html)
+ @loaded[action] = true
+
+ loadActivities: (source) ->
+ return if @loaded['activity'] is true
+
+ $calendarWrap = @parentEl.find('.user-calendar')
+ $calendarWrap.load($calendarWrap.data('href'))
+
+ new Activities()
+ @loaded['activity'] = true
+
+ toggleLoading: (status) ->
+ @parentEl.find('.loading-status .loading').toggle(status)
+
+ setCurrentAction: (action) ->
+ # Remove possible actions from URL
+ regExp = new RegExp('\/(' + @actions.join('|') + ')(\.html)?\/?$')
+ new_state = @_location.pathname
+ new_state = new_state.replace(/\/+$/, "") # remove trailing slashes
+ new_state = new_state.replace(regExp, '')
+
+ # Append the new action if we're on a tab other than 'activity'
+ unless action == @defaultAction
+ new_state += "/#{action}"
+
+ # Ensure parameters and hash come along for the ride
+ new_state += @_location.search + @_location.hash
+
+ history.replaceState {turbolinks: true, url: new_state}, document.title, new_state
+
+ new_state
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 93c0c7adfee..3ffb18045c1 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -1,7 +1,191 @@
class @UsersSelect
- constructor: ->
+ constructor: (currentUser) ->
@usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json"
+ if currentUser?
+ @currentUser = JSON.parse(currentUser)
+
+ $('.js-user-search').each (i, dropdown) =>
+ $dropdown = $(dropdown)
+ @projectId = $dropdown.data('project-id')
+ @showCurrentUser = $dropdown.data('current-user')
+ showNullUser = $dropdown.data('null-user')
+ showAnyUser = $dropdown.data('any-user')
+ firstUser = $dropdown.data('first-user')
+ selectedId = $dropdown.data('selected')
+ defaultLabel = $dropdown.data('default-label')
+ issueURL = $dropdown.data('issueUpdate')
+ $selectbox = $dropdown.closest('.selectbox')
+ $block = $selectbox.closest('.block')
+ abilityName = $dropdown.data('ability-name')
+ $value = $block.find('.value')
+ $loading = $block.find('.block-loading').fadeOut()
+
+ $block.on('click', '.js-assign-yourself', (e) =>
+ e.preventDefault()
+ assignTo(@currentUser.id)
+ )
+
+ assignTo = (selected) ->
+ data = {}
+ data[abilityName] = {}
+ data[abilityName].assignee_id = selected
+ $loading
+ .fadeIn()
+ $.ajax(
+ type: 'PUT'
+ dataType: 'json'
+ url: issueURL
+ data: data
+ ).done (data) ->
+ $loading.fadeOut()
+ $selectbox.hide()
+
+ if data.assignee
+ user =
+ name: data.assignee.name
+ username: data.assignee.username
+ avatar: data.assignee.avatar_url
+ else
+ user =
+ name: 'Unassigned'
+ username: ''
+ avatar: ''
+
+ $value.html(noAssigneeTemplate(user))
+ $value.find('a').attr('href')
+
+ noAssigneeTemplate = _.template(
+ '<% if (username) { %>
+ <a class="author_link " href="/u/<%= username %>">
+ <% if( avatar ) { %>
+ <img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>">
+ <% } %>
+ <span class="author"><%= name %></span>
+ <span class="username">
+ @<%= username %>
+ </span>
+ </a>
+ <% } else { %>
+ <span class="assign-yourself">
+ No assignee -
+ <a href="#" class="js-assign-yourself">
+ assign yourself
+ </a>
+ </span>
+ <% } %>'
+ )
+
+ $dropdown.glDropdown(
+ data: (term, callback) =>
+ @users term, (users) =>
+ if term.length is 0
+ showDivider = 0
+
+ if firstUser
+ # Move current user to the front of the list
+ for obj, index in users
+ if obj.username == firstUser
+ users.splice(index, 1)
+ users.unshift(obj)
+ break
+
+ if showNullUser
+ showDivider += 1
+ users.unshift(
+ beforeDivider: true
+ name: 'Unassigned',
+ id: 0
+ )
+
+ if showAnyUser
+ showDivider += 1
+ name = showAnyUser
+ name = 'Any User' if name == true
+ anyUser = {
+ beforeDivider: true
+ name: name,
+ id: null
+ }
+ users.unshift(anyUser)
+
+ if showDivider
+ users.splice(showDivider, 0, "divider")
+
+ # Send the data back
+ callback users
+ filterable: true
+ filterRemote: true
+ search:
+ fields: ['name', 'username']
+ selectable: true
+ fieldName: $dropdown.data('field-name')
+
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.name
+ else
+ defaultLabel
+
+ inputId: 'issue_assignee_id'
+
+ hidden: (e) ->
+ $selectbox.hide()
+ $value.show()
+
+ clicked: ->
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+ if $dropdown.hasClass('js-filter-bulk-update')
+ return
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ else
+ selected = $dropdown
+ .closest('.selectbox')
+ .find("input[name='#{$dropdown.data('field-name')}']").val()
+ assignTo(selected)
+
+ renderRow: (user) ->
+ username = if user.username then "@#{user.username}" else ""
+ avatar = if user.avatar_url then user.avatar_url else false
+ selected = if user.id is selectedId then "is-active" else ""
+ img = ""
+
+ if user.beforeDivider?
+ "<li>
+ <a href='#' class='#{selected}'>
+ #{user.name}
+ </a>
+ </li>"
+ else
+ if avatar
+ img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
+
+ # split into three parts so we can remove the username section if nessesary
+ listWithName = "<li>
+ <a href='#' class='dropdown-menu-user-link #{selected}'>
+ #{img}
+ <strong class='dropdown-menu-user-full-name'>
+ #{user.name}
+ </strong>"
+
+ listWithUserName = "<span class='dropdown-menu-user-username'>
+ #{username}
+ </span>"
+ listClosingTags = "</a>
+ </li>"
+
+
+ if username is ''
+ listWithUserName = ''
+
+ listWithName + listWithUserName + listClosingTags
+ )
$('.ajax-users-select').each (i, select) =>
@projectId = $(select).data('project-id')
diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee
index 19420f42468..1ee827f1fa3 100644
--- a/app/assets/javascripts/wikis.js.coffee
+++ b/app/assets/javascripts/wikis.js.coffee
@@ -2,7 +2,7 @@
class @Wikis
constructor: ->
- $('.build-new-wiki').bind 'click', (e) =>
+ $('.new-wiki-page').on 'submit', (e) =>
$('[data-error~=slug]').addClass('hidden')
field = $('#new_wiki_path')
slug = @slugify(field.val())
@@ -10,6 +10,7 @@ class @Wikis
if (slug.length > 0)
path = field.attr('data-wikis-path')
location.href = path + '/' + slug
+ e.preventDefault()
dasherize: (value) ->
value.replace(/[_\s]+/g, '-')
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 0c0451fe4dd..69b3b6586de 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -9,6 +9,8 @@
*= require_self
*= require dropzone/basic
*= require cal-heatmap
+ *= require cropper.css
+ *= require animate
*/
/*
@@ -25,12 +27,6 @@
@import "framework";
/*
- * NProgress load bar css
- */
-@import 'nprogress';
-@import 'nprogress-bootstrap';
-
-/*
* Font icons
*/
@import "font-awesome";
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index fa7641b1676..c85ab9148d0 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -11,6 +11,7 @@
@import "framework/calendar.scss";
@import "framework/callout.scss";
@import "framework/common.scss";
+@import "framework/dropdowns.scss";
@import "framework/files.scss";
@import "framework/filters.scss";
@import "framework/flash.scss";
@@ -26,6 +27,7 @@
@import "framework/mobile.scss";
@import "framework/nav.scss";
@import "framework/pagination.scss";
+@import "framework/progress.scss";
@import "framework/panels.scss";
@import "framework/selects.scss";
@import "framework/sidebar.scss";
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index b7ffa3e6ffb..5aa425dab6c 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -16,7 +16,7 @@
}
&.group-avatar, &.project-avatar, &.avatar-tile {
- @include border-radius(0px);
+ @include border-radius(0);
}
&.s16 { width: 16px; height: 16px; margin-right: 6px; }
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index bd89cc7dc1d..62b2af0dbf7 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -23,7 +23,7 @@
margin-bottom: -$gl-padding;
background-color: $background-color;
padding: $gl-padding;
- margin-bottom: 0px;
+ margin-bottom: 0;
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
color: $gl-gray;
@@ -66,7 +66,7 @@
}
.oneline {
- line-height: 42px;
+ line-height: 35px;
}
> p:last-child {
@@ -107,15 +107,37 @@
margin: 0;
font-size: 23px;
font-weight: normal;
- margin: 16px 0 5px 0;
+ margin: 16px 0 5px;
color: #4c4e54;
font-size: 23px;
line-height: 1.1;
+
+ h1 {
+ color: #313236;
+ margin-bottom: 6px;
+ font-size: 23px;
+ }
+
+ .visibility-icon {
+ display: inline-block;
+ margin-left: 5px;
+ font-size: 18px;
+ color: $gray;
+ }
+
+ p {
+ padding: 0 $gl-padding;
+ color: #5c5d5e;
+ }
}
.cover-desc {
padding: 0 $gl-padding 3px;
color: $gl-text-color;
+
+ &.username:last-child {
+ padding-bottom: $gl-padding;
+ }
}
.cover-controls {
@@ -153,3 +175,7 @@
float: right;
}
}
+
+.content-block-small {
+ padding: 10px 0;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 5f193fa7434..657c5f033c7 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -7,7 +7,7 @@
&:focus,
&:active {
outline: none;
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+ @include box-shadow($gl-btn-active-background);
}
}
@@ -28,7 +28,7 @@
}
&:active {
- @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12));
+ @include box-shadow ($gl-btn-active-background);
background-color: $dark;
border-color: $border-dark;
@@ -37,23 +37,23 @@
}
@mixin btn-green {
- @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #FFFFFF);
+ @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #fff);
}
@mixin btn-blue {
- @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF);
+ @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #fff);
}
@mixin btn-blue-medium {
- @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #FFFFFF);
+ @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #fff);
}
@mixin btn-orange {
- @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF);
+ @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #fff);
}
@mixin btn-red {
- @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #FFFFFF);
+ @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #fff);
}
@mixin btn-gray {
@@ -68,6 +68,12 @@
@include btn-default;
@include btn-white;
+ color: $gl-text-color;
+
+ &:focus:active {
+ outline: 0;
+ }
+
&.btn-small,
&.btn-sm {
padding: 4px 10px;
@@ -121,7 +127,7 @@
margin-right: 7px;
float: left;
&:last-child {
- margin-right: 0px;
+ margin-right: 0;
}
&.btn-xs {
margin-right: 3px;
@@ -130,6 +136,23 @@
&.disabled {
pointer-events: auto !important;
}
+
+ .caret {
+ margin-left: 5px;
+ }
+}
+
+.btn-transparent {
+ color: $btn-transparent-color;
+ background-color: transparent;
+ border: 0;
+
+ &:hover,
+ &:active,
+ &:focus {
+ background-color: transparent;
+ box-shadow: none;
+ }
}
.btn-block {
@@ -146,7 +169,7 @@
margin-right: 7px;
float: left;
&:last-child {
- margin-right: 0px;
+ margin-right: 0;
}
}
}
@@ -179,9 +202,19 @@
}
.active {
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+ @include box-shadow($gl-btn-active-background);
border: 1px solid #c6cacf !important;
background-color: #e4e7ed !important;
}
}
+
+.btn-loading {
+ &:not(.disabled) .fa {
+ display: none;
+ }
+
+ .fa {
+ margin-right: 5px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 580012abd77..e3192823a1a 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -33,19 +33,19 @@
}
.q2 {
- fill: #ACD5F2 !important;
+ fill: #acd5f2 !important;
}
.q3 {
- fill: #7FA8D1 !important;
+ fill: #7fa8d1 !important;
}
.q4 {
- fill: #49729B !important;
+ fill: #49729b !important;
}
.q5 {
- fill: #254E77 !important;
+ fill: #254e77 !important;
}
.domain-background {
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index 20a9bfb9816..da7bab74a32 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -39,6 +39,6 @@
}
.bs-callout-success {
background-color: #dff0d8;
- border-color: #5cA64d;
+ border-color: #5ca64d;
color: #3c763d;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index ea56d9e12a0..9b676d759e0 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -1,21 +1,27 @@
/** COLORS **/
.cgray { color: $gl-gray; }
-.clgray { color: #BBB }
+.clgray { color: #bbb }
.cred { color: $gl-text-red; }
.cgreen { color: $gl-text-green; }
.cdark { color: #444 }
/** COMMON CLASSES **/
-.prepend-top-10 { margin-top:10px }
+.prepend-top-0 { margin-top: 0; }
+.prepend-top-5 { margin-top: 5px; }
+.prepend-top-10 { margin-top: 10px }
.prepend-top-default { margin-top: $gl-padding !important; }
-.prepend-top-20 { margin-top:20px }
-.prepend-left-10 { margin-left:10px }
-.prepend-left-20 { margin-left:20px }
-.append-right-10 { margin-right:10px }
-.append-right-20 { margin-right:20px }
-.append-bottom-10 { margin-bottom:10px }
-.append-bottom-15 { margin-bottom:15px }
-.append-bottom-20 { margin-bottom:20px }
+.prepend-top-20 { margin-top: 20px }
+.prepend-left-10 { margin-left: 10px }
+.prepend-left-default { margin-left: $gl-padding; }
+.prepend-left-20 { margin-left: 20px }
+.append-right-5 { margin-right: 5px }
+.append-right-10 { margin-right: 10px }
+.append-right-default { margin-right: $gl-padding; }
+.append-right-20 { margin-right: 20px }
+.append-bottom-0 { margin-bottom: 0 }
+.append-bottom-10 { margin-bottom: 10px }
+.append-bottom-15 { margin-bottom: 15px }
+.append-bottom-20 { margin-bottom: 20px }
.append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block }
.center { text-align: center }
@@ -45,7 +51,7 @@ pre {
}
&.well-pre {
- border: 1px solid #EEE;
+ border: 1px solid #eee;
background: #f9f9f9;
border-radius: 0;
color: #555;
@@ -56,21 +62,6 @@ hr {
margin: $gl-padding 0;
}
-.dropdown-menu > li > a {
- text-shadow: none;
-}
-
-.dropdown-menu-align-right {
- left: auto;
- right: 0px;
-}
-
-.dropdown-menu > li > a:hover,
-.dropdown-menu > li > a:focus {
- background: $gl-primary;
- color: #FFF;
-}
-
.str-truncated {
@include str-truncated;
}
@@ -112,7 +103,7 @@ span.update-author {
}
.user-mention {
- color: #2FA0BB;
+ color: #2fa0bb;
font-weight: bold;
}
@@ -143,10 +134,10 @@ p.time {
// Fix issue with notes & lists creating a bunch of bottom borders.
li.note {
- img { max-width:100% }
+ img { max-width: 100% }
.note-title {
li {
- border-bottom:none !important;
+ border-bottom: none !important;
}
}
}
@@ -196,9 +187,9 @@ li.note {
.error-message {
padding: 10px;
- background: #C67;
+ background: #c67;
margin: 0;
- color: #FFF;
+ color: #fff;
a {
color: #fff;
@@ -209,7 +200,7 @@ li.note {
.browser-alert {
padding: 10px;
text-align: center;
- background: #C67;
+ background: #c67;
color: #fff;
font-weight: bold;
a {
@@ -280,7 +271,7 @@ img.emoji {
table {
td.permission-x {
- background: #D9EDF7 !important;
+ background: #d9edf7 !important;
text-align: center;
}
}
@@ -289,7 +280,7 @@ table {
float: left;
text-align: center;
font-size: 32px;
- color: #AAA;
+ color: #aaa;
width: 60px;
}
@@ -301,8 +292,11 @@ table {
}
.btn-sign-in {
- margin-top: 8px;
text-shadow: none;
+
+ @media (min-width: $screen-sm-min) {
+ margin-top: 11px;
+ }
}
.side-filters {
@@ -356,7 +350,7 @@ table {
.profiler-button,
.profiler-controls {
- border-color: #EEE !important;
+ border-color: #eee !important;
}
}
@@ -384,7 +378,7 @@ table {
position: absolute;
top: 0;
right: 0;
- width: 250px !important;
+ min-width: 250px;
visibility: hidden;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
new file mode 100644
index 00000000000..6c870ed927e
--- /dev/null
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -0,0 +1,375 @@
+.caret {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin-left: 2px;
+ vertical-align: middle;
+ border-top: $caret-width-base dashed;
+ border-right: $caret-width-base solid transparent;
+ border-left: $caret-width-base solid transparent;
+}
+
+.btn-group {
+ .caret {
+ margin-left: 0;
+ }
+}
+
+.dropdown {
+ position: relative;
+}
+
+.open {
+ .dropdown-menu {
+ display: block;
+ }
+
+ .dropdown-menu-toggle {
+ border-color: $dropdown-toggle-hover-border-color;
+
+ .fa {
+ color: $dropdown-toggle-hover-icon-color;
+ }
+ }
+}
+
+.dropdown-menu-toggle {
+ position: relative;
+ width: 160px;
+ padding: 6px 20px 6px 10px;
+ background-color: $dropdown-toggle-bg;
+ color: $dropdown-toggle-color;
+ font-size: 15px;
+ text-align: left;
+ border: 1px solid $dropdown-toggle-border-color;
+ border-radius: 2px;
+ outline: 0;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+
+ .fa {
+ position: absolute;
+ top: 50%;
+ right: 6px;
+ margin-top: -4px;
+ color: $dropdown-toggle-icon-color;
+ font-size: 10px;
+ }
+
+ &:hover, {
+ border-color: $dropdown-toggle-hover-border-color;
+
+ .fa {
+ color: $dropdown-toggle-hover-icon-color;
+ }
+ }
+}
+
+.dropdown-menu {
+ display: none;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 9;
+ width: 240px;
+ margin-top: 2px;
+ margin-bottom: 0;
+ padding: 10px;
+ font-size: 14px;
+ font-weight: normal;
+ background-color: $dropdown-bg;
+ border: 1px solid $dropdown-border-color;
+ border-radius: $border-radius-base;
+ box-shadow: 0 2px 4px $dropdown-shadow-color;
+
+ &.is-loading {
+ .dropdown-content {
+ display: none;
+ }
+
+ .dropdown-loading {
+ display: block;
+ }
+ }
+
+ ul {
+ margin: 0;
+ padding: 0;
+ }
+
+ li {
+ text-align: left;
+ list-style: none;
+ }
+
+ .divider {
+ width: 100%;
+ height: 1px;
+ margin-top: 8px;
+ margin-bottom: 8px;
+ background-color: $dropdown-divider-color;
+ }
+
+ a {
+ display: block;
+ position: relative;
+ padding-left: 10px;
+ padding-right: 10px;
+ color: $dropdown-link-color;
+ line-height: 34px;
+ text-overflow: ellipsis;
+ border-radius: 2px;
+ white-space: nowrap;
+ overflow: hidden;
+
+ &:hover,
+ &:focus,
+ &.is-focused {
+ background-color: $dropdown-link-hover-bg;
+ text-decoration: none;
+ outline: 0;
+ }
+
+ &.dropdown-menu-empty-link {
+ &.is-focused {
+ background-color: $dropdown-empty-row-bg;
+ }
+ }
+
+ &.dropdown-menu-user-link {
+ line-height: 16px;
+ }
+ }
+}
+
+.dropdown-menu-paging {
+ .dropdown-page-two,
+ .dropdown-menu-back {
+ display: none;
+ }
+
+ &.is-page-two {
+ .dropdown-page-one {
+ display: none;
+ }
+
+ .dropdown-page-two,
+ .dropdown-menu-back {
+ display: block;
+ }
+ }
+}
+
+.dropdown-menu-user {
+ .avatar {
+ float: left;
+ width: 30px;
+ height: 30px;
+ margin: 0 10px 0 0;
+ }
+}
+
+.dropdown-menu-user-link {
+ padding-top: 10px;
+ padding-bottom: 7px;
+}
+
+.dropdown-menu-user-full-name {
+ display: block;
+ font-weight: 500;
+ line-height: 16px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.dropdown-menu-user-username {
+ display: block;
+ line-height: 16px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.dropdown-select {
+ width: 300px;
+}
+
+.dropdown-menu-align-right {
+ left: auto;
+ right: 0;
+}
+
+.dropdown-menu-selectable {
+ a {
+ padding-left: 25px;
+
+ &.is-active {
+ &::before {
+ content: "\f00c";
+ position: absolute;
+ left: 5px;
+ top: 50%;
+ margin-top: -7px;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ }
+ }
+}
+
+.dropdown-header {
+ padding-left: 5px;
+ padding-right: 5px;
+ color: $dropdown-header-color;
+ font-size: 13px;
+ line-height: 22px;
+}
+
+.dropdown-title {
+ position: relative;
+ margin-bottom: 10px;
+ padding-left: 30px;
+ padding-right: 30px;
+ padding-bottom: 10px;
+ font-weight: 600;
+ line-height: 1;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-bottom: 1px solid $dropdown-divider-color;
+ overflow: hidden;
+}
+
+.dropdown-title-button {
+ position: absolute;
+ top: 0;
+ padding: 0;
+ color: $dropdown-title-btn-color;
+ font-size: 14px;
+ border: 0;
+ background: none;
+ outline: 0;
+
+ &:hover {
+ color: darken($dropdown-title-btn-color, 15%);
+ }
+}
+
+.dropdown-menu-close {
+ right: 0;
+}
+
+.dropdown-menu-back {
+ left: 0;
+}
+
+.dropdown-input {
+ position: relative;
+ margin-bottom: 10px;
+
+ .fa {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ color: #c7c7c7;
+ font-size: 12px;
+ pointer-events: none;
+ }
+
+ .dropdown-input-clear {
+ display: none;
+ cursor: pointer;
+ pointer-events: all;
+ }
+
+ &.has-value {
+ .dropdown-input-clear {
+ display: block;
+ }
+
+ .dropdown-input-search {
+ display: none;
+ }
+ }
+}
+
+.dropdown-input-field {
+ width: 100%;
+ padding: 0 7px;
+ color: $dropdown-input-color;
+ line-height: 30px;
+ border: 1px solid $dropdown-divider-color;
+ border-radius: 2px;
+ outline: 0;
+
+ &:focus {
+ color: $dropdown-link-color;
+ border-color: $dropdown-input-focus-border;
+ box-shadow: 0 0 4px $dropdown-input-focus-shadow;
+
+ ~ .fa {
+ color: $dropdown-link-color;
+ }
+ }
+
+ &:hover {
+ ~ .fa {
+ color: $dropdown-link-color;
+ }
+ }
+}
+
+.dropdown-content {
+ max-height: 215px;
+ overflow-y: scroll;
+}
+
+.dropdown-footer {
+ padding-top: 10px;
+ margin-top: 10px;
+ font-size: 13px;
+ border-top: 1px solid $dropdown-divider-color;
+}
+
+.dropdown-footer-list {
+ font-size: 14px;
+
+ a {
+ padding-left: 10px;
+ }
+}
+
+.dropdown-loading {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: none;
+ z-index: 9;
+ background-color: $dropdown-loading-bg;
+ font-size: 28px;
+
+ .fa {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-top: -14px;
+ margin-left: -14px;
+ }
+}
+
+.dropdown-label-box {
+ position: relative;
+ top: 3px;
+ margin-right: 5px;
+ display: inline-block;
+ width: 15px;
+ height: 15px;
+ border-radius: $border-radius-base;
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index c7f3604850d..a26ace5cc19 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -3,12 +3,10 @@
*
*/
.file-holder {
- border: none;
border: 1px solid $border-color;
&.readme-holder {
- margin-top: 10px;
- border-bottom: 0;
+ margin: $gl-padding-top 0;
}
table {
@@ -30,7 +28,7 @@
right: 15px;
.btn {
- padding: 0px 10px;
+ padding: 0 10px;
font-size: 13px;
line-height: 28px;
}
@@ -50,6 +48,10 @@
}
}
+ a {
+ color: $gl-dark-link-color;
+ }
+
.left-options {
margin-top: -3px;
}
@@ -84,7 +86,7 @@
&.blob-no-preview {
background: #eee;
- text-shadow: 0 1px 2px #FFF;
+ text-shadow: 0 1px 2px #fff;
padding: 100px 0;
}
@@ -124,7 +126,7 @@
}
td.line-numbers {
float: none;
- border-left: 1px solid #DDD;
+ border-left: 1px solid #ddd;
}
td.lines {
padding: 0;
@@ -158,7 +160,7 @@
}
&:hover {
- background: $hover;
+ background: $row-hover;
}
}
}
@@ -169,6 +171,7 @@
*/
&.code {
padding: 0;
+ -webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
}
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index b7638c86bfa..b05c5df1bd8 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -1,30 +1,21 @@
.filter-item {
margin-right: 6px;
+ vertical-align: top;
}
-@media (min-width: 800px) {
+@media (min-width: $screen-sm-min) {
.issues-filters,
.issues_bulk_update {
- select, .select2-container {
- width: 120px !important;
- display: inline-block;
+ .dropdown-menu-toggle {
+ width: 132px;
}
}
}
-@media (min-width: 1200px) {
- .issues-filters,
- .issues_bulk_update {
- select, .select2-container {
- width: 150px !important;
- display: inline-block;
- }
+@media (max-width: $screen-xs-max) {
+ .filter-item {
+ display: block;
+ margin: 0 0 10px;
}
}
-.issues-filters,
-.issues_bulk_update {
- .select2-container .select2-choice {
- color: #444 !important;
- }
-}
diff --git a/app/assets/stylesheets/framework/fonts.scss b/app/assets/stylesheets/framework/fonts.scss
index 7a946109e3a..5f9685bc71a 100644
--- a/app/assets/stylesheets/framework/fonts.scss
+++ b/app/assets/stylesheets/framework/fonts.scss
@@ -1,3 +1,7 @@
+// Disabling "SpaceAfterPropertyColon" linter because the linter doesn't like
+// the way the `src` property is formatted in this file.
+// scss-lint:disable SpaceAfterPropertyColon
+
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index d097e4d32f7..4cb4129b71b 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -28,21 +28,21 @@ input[type='search'].search-input {
}
&.search-input:-moz-placeholder { /* Firefox 18- */
- text-align: center;
+ text-align: center;
}
&.search-input::-moz-placeholder { /* Firefox 19+ */
- text-align: center;
+ text-align: center;
}
- &.search-input:-ms-input-placeholder {
- text-align: center;
+ &.search-input:-ms-input-placeholder {
+ text-align: center;
}
}
input[type='text'].danger {
- background: #F2DEDE!important;
- border-color: #D66;
+ background: #f2dede!important;
+ border-color: #d66;
text-shadow: 0 1px 1px #fff
}
@@ -69,6 +69,10 @@ label {
&.inline-label {
margin: 0;
}
+
+ &.label-light {
+ font-weight: 600;
+ }
}
.inline-input-group {
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 12cef6f8ea1..c83cf881596 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -23,13 +23,13 @@
&:hover {
background-color: $color-darker;
a {
- color: #FFF;
+ color: #fff;
}
}
}
.collapse-nav a {
- color: #FFF;
+ color: #fff;
background: $color;
}
@@ -42,7 +42,7 @@
&:hover {
background-color: $color-dark;
- color: #FFF;
+ color: #fff;
text-decoration: none;
}
}
@@ -71,7 +71,7 @@
}
&.active a {
- color: #FFF;
+ color: #fff;
background: $color-dark;
&.no-highlight {
@@ -79,42 +79,42 @@
}
i {
- color: #FFF
+ color: #fff
}
}
}
}
}
-$theme-blue: #2980B9;
+$theme-blue: #2980b9;
$theme-charcoal: #333c47;
-$theme-graphite: #888888;
+$theme-graphite: #888;
$theme-gray: #373737;
$theme-green: #019875;
-$theme-violet: #554488;
+$theme-violet: #548;
body {
&.ui_blue {
- @include gitlab-theme(#BECDE9, $theme-blue, #1970A9, #096099);
+ @include gitlab-theme(#becde9, $theme-blue, #1970a9, #096099);
}
&.ui_charcoal {
- @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272D);
+ @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272d);
}
&.ui_graphite {
- @include gitlab-theme(#CCCCCC, $theme-graphite, #777777, #666666);
+ @include gitlab-theme(#ccc, $theme-graphite, #777, #666);
}
&.ui_gray {
- @include gitlab-theme(#979797, $theme-gray, #272727, #222222);
+ @include gitlab-theme(#979797, $theme-gray, #272727, #222);
}
&.ui_green {
- @include gitlab-theme(#AADDCC, $theme-green, #018865, #017855);
+ @include gitlab-theme(#adc, $theme-green, #018865, #017855);
}
&.ui_violet {
- @include gitlab-theme(#9988CC, $theme-violet, #443366, #332255);
+ @include gitlab-theme(#98c, $theme-violet, #436, #325);
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index a81e258573d..6a68bb5c115 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -7,8 +7,8 @@ header {
&.navbar-empty {
height: 58px;
- background: #FFF;
- border-bottom: 1px solid #EEE;
+ background: #fff;
+ border-bottom: 1px solid #eee;
.center-logo {
margin: 11px 0;
@@ -28,7 +28,7 @@ header {
min-height: $header-height;
background-color: #fff;
border: none;
- border-bottom: 1px solid #EEE;
+ border-bottom: 1px solid #eee;
.container-fluid {
width: 100% !important;
@@ -47,7 +47,7 @@ header {
text-align: center;
&:hover, &:focus, &:active {
- background-color: #FFF;
+ background-color: #fff;
}
}
@@ -59,7 +59,7 @@ header {
right: 2px;
&:hover {
- background-color: #EEE;
+ background-color: #eee;
}
&.active {
color: #7f8fa4;
@@ -70,6 +70,11 @@ header {
.header-content {
height: $header-height;
+ padding-right: 20px;
+
+ @media (min-width: $screen-sm-min) {
+ padding-right: 0;
+ }
.title {
margin: 0;
@@ -77,6 +82,7 @@ header {
line-height: $header-height;
font-weight: normal;
color: #4c4e54;
+ overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
@@ -140,18 +146,18 @@ header {
margin-left: $sidebar_collapsed_width;
}
-@media (max-width: $screen-md-max) {
- .header-collapsed, .header-expanded {
+.header-collapsed {
+ margin-left: $sidebar_collapsed_width;
+
+ @media (min-width: $screen-md-min) {
@include collapsed-header;
}
}
-@media(min-width: $screen-md-max) {
- .header-collapsed {
- @include collapsed-header;
- }
+.header-expanded {
+ margin-left: $sidebar_collapsed_width;
- .header-expanded {
+ @media (min-width: $screen-md-min) {
margin-left: $sidebar_width;
}
}
@@ -161,7 +167,7 @@ header {
font-size: 18px;
.navbar-nav {
- margin: 0px;
+ margin: 0;
float: none !important;
.visible-xs, .visable-sm {
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 9854df4c45c..7cf4d4fba42 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -1,8 +1,8 @@
.file-content.code {
border: none;
box-shadow: none;
- margin: 0px;
- padding: 0px;
+ margin: 0;
+ padding: 0;
table-layout: fixed;
pre {
@@ -44,8 +44,10 @@
white-space: nowrap;
i {
+ float: left;
+ margin-top: 3px;
+ margin-right: 5px;
visibility: hidden;
- @extend .pull-left;
}
&:hover i {
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 08dcb563dce..7f7b7c806e7 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -5,32 +5,38 @@
*/
.status-box {
- @include border-radius(3px);
+
+ /* Extra small devices (phones, less than 768px) */
+ /* No media query since this is the default in Bootstrap */
+ padding: 5px 11px;
+ margin-top: 4px;
+ /* Small devices (tablets, 768px and up) */
+ @media (min-width: $screen-sm-min) {
+ padding: 0 $gl-btn-padding;
+ margin-top: 5px;
+ }
+ @include border-radius(3px);
display: block;
float: left;
- padding: 0 $gl-btn-padding;
- font-weight: normal;
margin-right: 10px;
+ color: #fff;
font-size: $gl-font-size;
+ line-height: 25px;
&.status-box-closed {
background-color: $gl-danger;
- color: #FFF;
}
&.status-box-merged {
background-color: $gl-primary;
- color: #FFF;
}
&.status-box-open {
background-color: $green-light;
- color: #FFF;
}
&.status-box-expired {
background: #cea61b;
- color: #FFF;
}
}
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index d6cd78813c0..525ed81b059 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -3,13 +3,13 @@
font-size: $font-size-base;
&.ui-datepicker-inline {
- border: 1px solid #DDD;
+ border: 1px solid #ddd;
padding: 10px;
width: 270px;
.ui-datepicker-header {
- background: #FFF;
- border-color: #DDD;
+ background: #fff;
+ border-color: #ddd;
}
.ui-datepicker-calendar td a {
@@ -19,7 +19,7 @@
}
&.ui-autocomplete {
- border-color: #DDD;
+ border-color: #ddd;
padding: 0;
margin-top: 2px;
z-index: 1001;
@@ -30,26 +30,26 @@
}
.ui-state-default {
- border: 1px solid #FFF;
- background: #FFF;
+ border: 1px solid #fff;
+ background: #fff;
color: #777;
}
.ui-state-highlight {
- border: 1px solid #EEE;
- background: #EEE;
+ border: 1px solid #eee;
+ background: #eee;
}
.ui-state-active {
border: 1px solid $gl-primary;
background: $gl-primary;
- color: #FFF;
+ color: #fff;
}
.ui-state-hover,
.ui-state-focus {
- border: 1px solid $hover;
- background: $hover;
+ border: 1px solid $row-hover;
+ background: $row-hover;
color: #333;
}
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index e901c78d02f..8bb047db2dd 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -16,7 +16,7 @@ body {
}
.container .content {
- margin: 0 0;
+ margin: 0;
}
.navless-container {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 5c65383ec1a..b17c8bcbb1e 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -3,6 +3,7 @@
*
*/
.well-list {
+ position: relative;
margin: 0;
padding: 0;
list-style: none;
@@ -38,7 +39,7 @@
&.smoke { background-color: $background-color; }
&:hover {
- background: $hover;
+ background: $row-hover;
}
&:last-child {
@@ -110,7 +111,23 @@ ul.content-list {
> li {
border-color: $table-border-color;
- color: $gl-gray;
+ font-size: $list-font-size;
+ color: $list-text-color;
+
+ .title {
+ font-weight: 600;
+ }
+
+ a {
+ color: $gl-dark-link-color;
+ }
+
+ .description {
+ p {
+ @include str-truncated;
+ margin-bottom: 0;
+ }
+ }
.avatar {
margin-right: 15px;
@@ -127,11 +144,8 @@ ul.content-list {
}
}
-.panel > .content-list {
- li {
- margin: 0;
- padding: $gl-padding;
- }
+.panel > .content-list > li {
+ padding: $gl-padding-top $gl-padding;
}
ul.controls {
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 1d8611b04dc..8328aac4e7a 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -71,7 +71,7 @@
}
.md-preview-holder {
- background: #FFF;
+ background: #fff;
border: 1px solid #ddd;
min-height: 169px;
padding: 5px;
@@ -80,7 +80,7 @@
.markdown-area {
@include border-radius(0);
- background: #FFF;
+ background: #fff;
border: 1px solid #ddd;
min-height: 140px;
max-height: 500px;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 1d5000fe388..250d6309291 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -1,7 +1,7 @@
/**
* Generic mixins
*/
- @mixin box-shadow($shadow) {
+@mixin box-shadow($shadow) {
-webkit-box-shadow: $shadow;
-moz-box-shadow: $shadow;
-ms-box-shadow: $shadow;
@@ -67,17 +67,17 @@
* Base mixin for lists in GitLab
*/
@mixin basic-list {
- margin: 5px 0px;
- padding: 0px;
+ margin: 5px 0;
+ padding: 0;
list-style: none;
> li {
@include clearfix;
padding: 10px 0;
- border-bottom: 1px solid #EEE;
+ border-bottom: 1px solid #eee;
display: block;
- margin: 0px;
+ margin: 0;
&:last-child {
border-bottom: none;
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 3bfac2ad9b5..5ea4f9a49db 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -128,12 +128,12 @@
.show-aside {
display: none;
position: fixed;
- right: 0px;
+ right: 0;
top: 30%;
padding: 5px 15px;
- background: #EEE;
+ background: #eee;
font-size: 20px;
color: #777;
z-index: 100;
- @include box-shadow(0 1px 2px #DDD);
+ @include box-shadow(0 1px 2px #ddd);
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 252a586358c..95bdd6d1ea3 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -26,7 +26,7 @@
}
&.active a {
- color: #000000;
+ color: #000;
border-bottom: 2px solid #4688f1;
}
@@ -41,7 +41,7 @@
.top-area {
@include clearfix;
- border-bottom: 1px solid #EEE;
+ border-bottom: 1px solid #eee;
.nav-text {
padding-top: 16px;
@@ -59,11 +59,11 @@
.nav-links {
display: inline-block;
width: 50%;
- margin-bottom: 0px;
+ margin-bottom: 0;
border-bottom: none;
/* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-sm-max) {
width: 100%;
}
}
@@ -74,27 +74,44 @@
float: right;
text-align: right;
padding: 11px 0;
- margin-bottom: 0px;
+ margin-bottom: 0;
> .dropdown {
- margin-right: 10px;
+ margin-right: $gl-padding-top;
display: inline-block;
+
+ &:last-child {
+ margin-right: 0;
+ }
}
> .btn {
+ margin-right: $gl-padding-top;
display: inline-block;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ > .btn-grouped {
+ float: none;
}
> form {
display: inline-block;
}
+ .icon-label {
+ display: none;
+ }
+
input {
height: 34px;
display: inline-block;
position: relative;
top: 1px;
- margin-right: 10px;
+ margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */
@media (min-width: $screen-md-min) { width: 200px; }
@@ -111,9 +128,38 @@
}
}
- /* Hide on extra small devices (phones) */
@media (max-width: $screen-xs-max) {
- display: none;
+ padding-bottom: 0;
+
+ .btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
+ margin: 0 0 10px;
+ display: block;
+ width: 100%;
+ }
+
+ form {
+ display: block;
+ height: auto;
+
+ input {
+ width: 100%;
+ margin: 0 0 10px;
+ }
+ }
+
+ .input-short {
+ width: 100%;
+ }
+
+ .icon-label {
+ display: inline-block;
+ }
+
+ // Applies on /dashboard/issues
+ .project-item-select-holder {
+ display: block;
+ margin: 0;
+ }
}
/* Small devices (tablets, 768px and lower) */
diff --git a/app/assets/stylesheets/framework/progress.scss b/app/assets/stylesheets/framework/progress.scss
new file mode 100644
index 00000000000..e9800bd24b5
--- /dev/null
+++ b/app/assets/stylesheets/framework/progress.scss
@@ -0,0 +1,5 @@
+html.turbolinks-progress-bar::before {
+ background-color: $progress-color!important;
+ height: 2px!important;
+ box-shadow: 0 0 10px $progress-color, 0 0 5px $progress-color;
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 3ee3443e349..e82d052f45a 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -1,49 +1,54 @@
/** Select2 selectbox style override **/
+.select2-container {
+ width: 100% !important;
+}
+
.select2-container, .select2-container.select2-drop-above {
.select2-choice {
- background: #FFF;
- border-color: #DDD;
- height: 36px;
- padding: 6px $gl-padding;
+ background: #fff;
+ border-color: $input-border;
+ border-color: $border-white-light;
+ height: 35px;
+ padding: $gl-vert-padding $gl-btn-padding;
font-size: $gl-font-size;
line-height: 1.42857143;
- @include border-radius(2px);
+ @include border-radius($border-radius-default);
.select2-arrow {
- background: #FFF;
- border-left: none;
- padding-top: 5px;
+ background-image: none;
+ background-color: transparent;
+ border: none;
+ padding-top: 6px;
+ padding-right: 10px;
+
+ b {
+ @extend .caret;
+ color: $gray-darkest;
+ }
}
.select2-chosen {
- color: $gl-text-color;
+ margin-right: 15px;
}
- &.select2-default {
- .select2-chosen {
- color: #999;
- }
+ &:hover {
+ background-color: $gray-dark;
+ border-color: $border-white-normal;
+ color: $gl-text-color;
}
}
}
-.select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice{
- color: #7f8fa4;
- border: 1px solid #e7e9ed;
-}
-
-
.select2-drop {
- @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
- @include border-radius (0px);
-
- padding: 16px;
- border: none !important;
+ @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0);
+ @include border-radius ($border-radius-default);
+ border: none;
+ min-width: 175px;
}
.select2-results .select2-result-label {
- padding: 9px;
+ padding: 10px 15px;
}
.select2-drop{
@@ -56,15 +61,30 @@
.select2-results li.select2-result-with-children > .select2-result-label {
font-weight: 600;
- color: #313236;
+ color: $gl-text-color;
+}
+
+.select2-container-active {
+ .select2-choice, .select2-choices {
+ @include box-shadow(none);
+ }
+}
+
+.select2-dropdown-open {
+ .select2-choice {
+ border-color: $border-white-normal;
+ outline: 0;
+ background-image: none;
+ background-color: $white-dark;
+ @include box-shadow($gl-btn-active-gradient);
+ }
}
.select2-container-multi {
.select2-choices {
- @include border-radius(2px);
+ @include border-radius($border-radius-default);
border-color: $input-border;
- background: white;
- padding-left: $gl-padding / 2;
+ background: none;
.select2-search-field input {
padding: $gl-padding / 2;
@@ -76,14 +96,16 @@
.select2-search-choice {
margin: 8px 0 0 8px;
- background: white;
box-shadow: none;
border-color: $input-border;
color: $gl-text-color;
line-height: 15px;
+ background-color: $background-color;
+ background-image: none;
.select2-search-choice-close {
- top: 5px;
+ top: 4px;
+ left: 3px;
}
&.select2-search-choice-focus {
@@ -91,22 +113,25 @@
}
}
}
+
+ &.select2-container-active .select2-choices,
+ &.select2-dropdown-open .select2-choices {
+ border-color: $border-white-normal;
+ @include box-shadow($gl-btn-active-gradient);
+ }
+}
+
+.select2-container-multi .select2-choices .select2-search-choice {
}
.select2-drop-active {
- border: 1px solid #BBB !important;
- margin-top: 4px;
- font-size: 13px;
+ margin-top: 6px;
+ font-size: 14px;
&.select2-drop-above {
margin-bottom: 8px;
}
- .select2-search input {
- background: #fafafa;
- border-color: #DDD;
- }
-
.select2-results {
max-height: 350px;
.select2-highlighted {
@@ -115,8 +140,34 @@
}
}
-.select2-container {
- width: 100% !important;
+.select2-search {
+ padding: 15px 15px 5px;
+
+ .select2-drop-auto-width & {
+ padding: 15px 15px 5px;
+ }
+}
+
+.select2-search input {
+ padding: 2px 25px 2px 5px;
+ background: #fff image-url('select2.png');
+ background-repeat: no-repeat;
+ background-position: right 0 bottom 6px;
+ border: 1px solid $input-border;
+ @include border-radius($border-radius-default);
+ @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s);
+
+ &:focus {
+ border-color: $input-border-focus;
+ }
+}
+
+.select2-search input.select2-active {
+ background-color: #fff;
+ background-image: image-url('select2-spinner.gif') !important;
+ background-repeat: no-repeat;
+ background-position: right 5px center !important;
+ background-size: 16px 16px !important;
}
/** Branch/tag selector **/
@@ -124,10 +175,19 @@
width: 160px !important;
}
-.ajax-users-dropdown, .ajax-project-users-dropdown {
- .select2-search {
- padding-top: 2px;
- }
+.select2-results .select2-no-results,
+.select2-results .select2-searching,
+.select2-results .select2-ajax-error,
+.select2-results .select2-selection-limit {
+ background: $gray-light;
+ display: list-item;
+ padding: 10px 15px;
+}
+
+
+.select2-results {
+ margin: 0;
+ padding: 10px 0;
}
.ajax-users-select {
@@ -170,7 +230,7 @@
.namespace-result {
.namespace-kind {
- color: #AAA;
+ color: #aaa;
font-weight: normal;
}
.namespace-path {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index b141928f706..9d188317783 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,3 +1,10 @@
+#logo {
+ z-index: 2;
+ position: absolute;
+ width: 58px;
+ cursor: pointer;
+}
+
.page-with-sidebar {
padding-top: $header-height;
transition-duration: .3s;
@@ -12,10 +19,16 @@
height: 100%;
transition-duration: .3s;
}
+
+ .gitlab-text-container-link {
+ z-index: 1;
+ position: absolute;
+ left: 0;
+ }
}
.sidebar-wrapper {
- z-index: 99;
+ z-index: 1000;
background: $background-color;
}
@@ -23,7 +36,7 @@
width: 100%;
.container-fluid {
- background: #FFF;
+ background: #fff;
padding: 0 $gl-padding;
&.container-blank {
@@ -45,19 +58,6 @@
overflow: hidden;
transition-duration: .3s;
- .home {
- z-index: 1;
- position: absolute;
- left: 0px;
- }
-
- #logo {
- z-index: 2;
- position: absolute;
- width: 58px;
- cursor: pointer;
- }
-
a {
float: left;
height: $header-height;
@@ -92,7 +92,7 @@
}
&:hover {
- background-color: #EEE;
+ background-color: #eee;
}
}
@@ -132,7 +132,7 @@
overflow: hidden;
&.navbar-collapse {
- padding: 0px !important;
+ padding: 0 !important;
}
li {
@@ -171,7 +171,7 @@
.count {
float: right;
background: #eee;
- padding: 0px 8px;
+ padding: 0 8px;
@include border-radius(6px);
}
@@ -183,45 +183,33 @@
}
.sidebar-subnav {
- margin-left: 0px;
- padding-left: 0px;
+ margin-left: 0;
+ padding-left: 0;
li {
list-style: none;
}
}
-@mixin expanded-sidebar {
- padding-left: $sidebar_width;
-
- .sidebar-wrapper {
- width: $sidebar_width;
-
- .nav-sidebar {
- width: $sidebar_width;
- }
-
- .nav-sidebar li a{
- width: 230px;
+.collapse-nav a {
+ width: $sidebar_width;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ font-size: 13px;
+ background: transparent;
+ height: 40px;
+ text-align: center;
+ line-height: 40px;
+ transition-duration: .3s;
+ outline: none;
- &.back-link {
- i {
- opacity: 0;
- }
- }
- }
+ &:hover {
+ text-decoration: none;
}
}
-@mixin expanded-gutter {
- padding-right: $gutter_width;
-}
-
-@mixin collapsed-gutter {
- padding-right: $sidebar_collapsed_width;
-}
-
-@mixin collapsed-sidebar {
+.page-sidebar-collapsed {
padding-left: $sidebar_collapsed_width;
.sidebar-wrapper {
@@ -268,66 +256,48 @@
}
}
-.collapse-nav a {
- width: $sidebar_width;
- position: fixed;
- bottom: 0;
- left: 0;
- font-size: 13px;
- background: transparent;
- height: 40px;
- text-align: center;
- line-height: 40px;
- transition-duration: .3s;
- outline: none;
-}
-
-.collapse-nav a:hover {
- text-decoration: none;
- background: #f2f6f7;
-}
+.page-sidebar-expanded {
+ padding-left: $sidebar_collapsed_width;
-// page is small enough
-@media (max-width: $screen-md-max) {
- .page-sidebar-collapsed {
- @include collapsed-sidebar;
+ @media (min-width: $screen-md-min) {
+ padding-left: $sidebar_width;
}
- .page-sidebar-expanded {
- @include collapsed-sidebar;
- }
+ .sidebar-wrapper {
+ width: $sidebar_width;
- .page-gutter {
- &.right-sidebar-collapsed {
- @include collapsed-gutter;
- }
- &.right-sidebar-expanded {
- @include expanded-gutter;
+ .nav-sidebar {
+ width: $sidebar_width;
}
- }
- .collapse-nav {
- display: none;
+ .nav-sidebar li a {
+ width: 230px;
+
+ &.back-link {
+ i {
+ opacity: 0;
+ }
+ }
+ }
}
}
-// page is large enough
-@media(min-width: $screen-md-max) {
+.right-sidebar-collapsed {
+ padding-right: 0;
- .page-gutter {
- &.right-sidebar-collapsed {
- @include collapsed-gutter;
- }
- &.right-sidebar-expanded {
- @include expanded-gutter;
- }
+ @media (min-width: $screen-sm-min) {
+ padding-right: $sidebar_collapsed_width;
}
+}
- .page-sidebar-collapsed {
- @include collapsed-sidebar;
+.right-sidebar-expanded {
+ padding-right: 0;
+
+ @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ padding-right: $sidebar_collapsed_width;
}
- .page-sidebar-expanded {
- @include expanded-sidebar;
+ @media (min-width: $screen-md-min) {
+ padding-right: $gutter_width;
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 47b843e5e3d..aa244fe548d 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -5,13 +5,13 @@
padding: 0;
.timeline-entry {
- padding: $gl-padding 0;
+ padding: $gl-padding $gl-btn-padding;
border-color: $table-border-color;
color: $gl-gray;
border-bottom: 1px solid $border-white-light;
&:target {
- background: $hover;
+ background: $row-hover;
}
&:last-child {
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 3e709244879..dd42db1840f 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -22,7 +22,7 @@
// Components
@import "bootstrap/component-animations";
-@import "bootstrap/dropdowns";
+// @import "bootstrap/dropdowns";
@import "bootstrap/button-groups";
@import "bootstrap/input-groups";
@import "bootstrap/navs";
@@ -95,7 +95,7 @@
}
&.label-inverse {
- background-color: #333333;
+ background-color: #333;
}
}
@@ -138,7 +138,7 @@
}
.btn-clipboard {
- min-width: 0px;
+ min-width: 0;
}
}
@@ -167,12 +167,6 @@
}
}
-.alert-help {
- background-color: $background-color;
- border: 1px solid $border-color;
- color: $gl-gray;
-}
-
// Typography =================================================================
.text-primary,
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 33270388e64..f63ac033234 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -57,7 +57,7 @@ $component-active-bg: $brand-info;
$input-color: $text-color;
$input-border: #e7e9ed;
-$input-border-focus: #7F8FA4;
+$input-border-focus: #7f8fa4;
$legend-color: $text-color;
@@ -70,7 +70,7 @@ $pagination-bg: #fff;
$pagination-border: $border-color;
$pagination-hover-color: $gl-gray;
-$pagination-hover-bg: $hover;
+$pagination-hover-bg: $row-hover;
$pagination-hover-border: $border-color;
$pagination-active-color: $blue-dark;
@@ -125,8 +125,8 @@ $panel-inner-border: $border-color;
//
//##
-$well-bg: #F9F9F9;
-$well-border: #EEE;
+$well-bg: #f9f9f9;
+$well-border: #eee;
//== Code
//
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 8d8f41287da..b1886fbe67b 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -27,20 +27,20 @@
line-height: 10px;
color: #555;
vertical-align: middle;
- background-color: #FCFCFC;
+ background-color: #fcfcfc;
border-width: 1px;
border-style: solid;
- border-color: #CCC #CCC #BBB;
+ border-color: #ccc #ccc #bbb;
border-image: none;
border-radius: 3px;
- box-shadow: 0px -1px 0px #BBB inset;
+ box-shadow: 0 -1px 0 #bbb inset;
}
h1 {
font-size: 1.3em;
font-weight: 600;
- margin: 24px 0 12px 0;
- padding: 0 0 10px 0;
+ margin: 24px 0 12px;
+ padding: 0 0 10px;
border-bottom: 1px solid #e7e9ed;
color: #313236;
}
@@ -48,27 +48,27 @@
h2 {
font-size: 1.2em;
font-weight: 600;
- margin: 24px 0 12px 0;
+ margin: 24px 0 12px;
color: #313236;
}
h3 {
- margin: 24px 0 12px 0;
+ margin: 24px 0 12px;
font-size: 1.1em;
}
h4 {
- margin: 24px 0 12px 0;
+ margin: 24px 0 12px;
font-size: 0.98em;
}
h5 {
- margin: 24px 0 12px 0;
+ margin: 24px 0 12px;
font-size: 0.95em;
}
h6 {
- margin: 24px 0 12px 0;
+ margin: 24px 0 12px;
font-size: 0.90em;
}
@@ -76,7 +76,7 @@
color: #7f8fa4;
font-size: inherit;
padding: 8px 21px;
- margin: 12px 0 12px;
+ margin: 12px 0;
border-left: 3px solid #e7e9ed;
}
@@ -88,13 +88,13 @@
p {
color: #5c5d5e;
- margin: 6px 0 0 0;
+ margin: 6px 0 0;
}
table {
@extend .table;
@extend .table-bordered;
- margin: 12px 0 12px 0;
+ margin: 12px 0;
color: #5c5d5e;
th {
background: #f8fafc;
@@ -102,7 +102,7 @@
}
pre {
- margin: 12px 0 12px 0;
+ margin: 12px 0;
font-size: 13px;
line-height: 1.6em;
overflow-x: auto;
@@ -149,13 +149,13 @@
}
&:hover > a.anchor {
- $size: 16px;
+ $size: 14px;
position: absolute;
right: 100%;
top: 50%;
- margin-top: -$size/2;
- margin-right: 0px;
- padding-right: 20px;
+ margin-top: -11px;
+ margin-right: 0;
+ padding-right: 15px;
display: inline-block;
width: $size;
height: $size;
@@ -187,16 +187,16 @@ body {
}
.page-title-empty {
- margin-top: 0px;
+ margin-top: 0;
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
- margin: 12px 7px 12px 7px;
+ margin: 12px 7px;
}
h1, h2, h3, h4, h5, h6 {
color: $gl-header-color;
- font-weight: 500;
+ font-weight: 600;
}
/** CODE **/
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index b8386362637..61e0dd4d672 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,42 +1,83 @@
-$hover: #faf9f9;
-$gl-text-color: #54565B;
-$gl-text-green: #4A2;
-$gl-text-red: #D12F19;
-$gl-text-orange: #D90;
-$gl-header-color: #323232;
-$gl-link-color: #333c48;
-$md-text-color: #444;
-$md-link-color: #3084bb;
-$nprogress-color: #c0392b;
-$gl-font-size: 15px;
-$list-font-size: 15px;
+/*
+ * Layout
+ */
$sidebar_collapsed_width: 62px;
$sidebar_width: 230px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 258px;
-$avatar_radius: 50%;
+
+/*
+ * UI elements
+ */
+$border-color: #efeff1;
+$table-border-color: #eef0f2;
+$background-color: #faf9f9;
+
+/*
+ * Text
+ */
+$gl-font-size: 15px;
+$gl-title-color: #333;
+$gl-text-color: #555;
+$gl-text-green: #4a2;
+$gl-text-red: #d12f19;
+$gl-text-orange: #d90;
+$gl-link-color: #3084bb;
+$gl-dark-link-color: #333;
+$gl-placeholder-color: #8f8f8f;
+$gl-gray: $gl-text-color;
+$gl-header-color: $gl-title-color;
+
+/*
+ * Lists
+ */
+$list-font-size: $gl-font-size;
+$list-title-color: $gl-title-color;
+$list-text-color: $gl-text-color;
+
+/*
+ * Markdown
+ */
+$md-text-color: $gl-text-color;
+$md-link-color: $gl-link-color;
+
+/*
+ * Code
+ */
$code_font_size: 13px;
$code_line_height: 1.5;
-$border-color: #efeff1;
-$table-border-color: #eef0f2;
-$background-color: #faf9f9;
-$header-height: 58px;
-$fixed-layout-width: 1280px;
-$gl-gray: #5a5a5a;
+
+/*
+ * Padding
+ */
$gl-padding: 16px;
$gl-btn-padding: 10px;
$gl-vert-padding: 6px;
-$gl-padding-top:10px;
+$gl-padding-top: 10px;
+
+/*
+ * Misc
+ */
+$row-hover: #f4f8fe;
+$progress-color: #c0392b;
+$avatar_radius: 50%;
+$header-height: 58px;
+$fixed-layout-width: 1280px;
$gl-avatar-size: 40px;
-$secondary-text: #7f8fa4;
-$error-exclamation-point: #E62958;
+$error-exclamation-point: #e62958;
+$border-radius-default: 3px;
+$btn-transparent-color: #8f8f8f;
+$ssh-key-icon-color: #8f8f8f;
+$ssh-key-icon-size: 18px;
+$provider-btn-group-border: #e5e5e5;
+$provider-btn-not-active-color: #4688f1;
/*
* Color schema
*/
-$white-light: #FFFFFF;
+$white-light: #fff;
$white-normal: #ededed;
$white-dark: #ededed;
@@ -46,48 +87,55 @@ $gray-dark: #ededed;
$gray-darkest: #c9c9c9;
$green-light: #38ae67;
-$green-normal: #2FAA60;
-$green-dark: #2CA05B;
+$green-normal: #2faa60;
+$green-dark: #2ca05b;
-$blue-light: #2EA8E5;
-$blue-normal: #2D9FD8;
-$blue-dark: #2897CE;
+$blue-light: #2ea8e5;
+$blue-normal: #2d9fd8;
+$blue-dark: #2897ce;
-$blue-medium-light: #3498CB;
-$blue-medium: #2F8EBF;
-$blue-medium-dark: #2D86B4;
+$blue-medium-light: #3498cb;
+$blue-medium: #2f8ebf;
+$blue-medium-dark: #2d86b4;
$orange-light: rgba(252, 109, 38, 0.80);
-$orange-normal: #E75E40;
-$orange-dark: #CE5237;
+$orange-normal: #e75e40;
+$orange-dark: #ce5237;
-$red-light: #F43263;
-$red-normal: #E52C5A;
-$red-dark: #D22852;
+$red-light: #f06559;
+$red-normal: #e52c5a;
+$red-dark: #d22852;
-$border-white-light: #F1F2F4;
-$border-white-normal: #D6DAE2;
-$border-white-dark: #C6CACF;
+$border-white-light: #f1f2f4;
+$border-white-normal: #d6dae2;
+$border-white-dark: #c6cacf;
$border-gray-light: rgba(0, 0, 0, 0.06);
$border-gray-normal: rgba(0, 0, 0, 0.10);;
-$border-gray-dark: #C6CACF;
+$border-gray-dark: #c6cacf;
-$border-green-light: #2FAA60;
-$border-green-normal: #2CA05B;
+$border-green-light: #2faa60;
+$border-green-normal: #2ca05b;
$border-green-dark: #279654;
-$border-blue-light: #2D9FD8;
-$border-blue-normal: #2897CE;
-$border-blue-dark: #258DC1;
+$border-blue-light: #2d9fd8;
+$border-blue-normal: #2897ce;
+$border-blue-dark: #258dc1;
$border-orange-light: #fc6d26;
-$border-orange-normal: #CE5237;
-$border-orange-dark: #C14E35;
+$border-orange-normal: #ce5237;
+$border-orange-dark: #c14e35;
+
+$border-red-light: #f24f41;
+$border-red-normal: #d22852;
+$border-red-dark: #ca264f;
-$border-red-light: #E52C5A;
-$border-red-normal: #D22852;
-$border-red-dark: #CA264F;
+$help-well-bg: #fafafa;
+$help-well-border: #e5e5e5;
+
+$warning-message-bg: #fbf2d9;
+$warning-message-color: #9e8e60;
+$warning-message-border: #f0e2bb;
/* header */
$light-grey-header: #faf9f9;
@@ -100,6 +148,8 @@ $gl-success: $green-normal;
$gl-info: $blue-normal;
$gl-warning: $orange-normal;
$gl-danger: $red-normal;
+$gl-btn-active-background: rgba(0, 0, 0, 0.12);
+$gl-btn-active-gradient: inset 0 0 4px $gl-btn-active-background;
/*
* Commit Diff Colors
@@ -112,3 +162,34 @@ $deleted: #f77;
*/
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif;
+
+/*
+* Dropdowns
+*/
+$dropdown-bg: #fff;
+$dropdown-link-color: #555;
+$dropdown-link-hover-bg: $row-hover;
+$dropdown-empty-row-bg: rgba(#000, .04);
+$dropdown-border-color: rgba(#000, .1);
+$dropdown-shadow-color: rgba(#000, .1);
+$dropdown-divider-color: rgba(#000, .1);
+$dropdown-header-color: #959494;
+$dropdown-title-btn-color: #bfbfbf;
+$dropdown-input-color: #555;
+$dropdown-input-focus-border: rgb(58, 171, 240);
+$dropdown-input-focus-shadow: rgba(#000, .2);
+$dropdown-loading-bg: rgba(#fff, .6);
+
+$dropdown-toggle-bg: #fff;
+$dropdown-toggle-color: #626262;
+$dropdown-toggle-border-color: #eaeaea;
+$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
+$dropdown-toggle-icon-color: #c4c4c4;
+$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
+
+/*
+ * Award emoji
+ */
+$award-emoji-menu-bg: #fff;
+$award-emoji-menu-border: #f1f2f4;
+$award-emoji-new-btn-icon-color: #dcdcdc;
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index c3f27333fad..02e24ec7c4d 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -2,7 +2,7 @@
a.js-zen-enter {
color: $gl-gray;
position: absolute;
- top: 0px;
+ top: 0;
right: 4px;
line-height: 56px;
}
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index b794da2ce98..47673944896 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -43,12 +43,12 @@
// Search result highlight
span.highlight_word {
background-color: #ffe792 !important;
- color: #000000 !important;
+ color: #000 !important;
}
.hll { background-color: #373b41 }
.c { color: #969896 } /* Comment */
- .err { color: #cc6666 } /* Error */
+ .err { color: #c66 } /* Error */
.k { color: #b294bb } /* Keyword */
.l { color: #de935f } /* Literal */
.n { color: #c5c8c6 } /* Name */
@@ -58,7 +58,7 @@
.cp { color: #969896 } /* Comment.Preproc */
.c1 { color: #969896 } /* Comment.Single */
.cs { color: #969896 } /* Comment.Special */
- .gd { color: #cc6666 } /* Generic.Deleted */
+ .gd { color: #c66 } /* Generic.Deleted */
.ge { font-style: italic } /* Generic.Emph */
.gh { color: #c5c8c6; font-weight: bold } /* Generic.Heading */
.gi { color: #b5bd68 } /* Generic.Inserted */
@@ -77,17 +77,17 @@
.na { color: #81a2be } /* Name.Attribute */
.nb { color: #c5c8c6 } /* Name.Builtin */
.nc { color: #f0c674 } /* Name.Class */
- .no { color: #cc6666 } /* Name.Constant */
+ .no { color: #c66 } /* Name.Constant */
.nd { color: #8abeb7 } /* Name.Decorator */
.ni { color: #c5c8c6 } /* Name.Entity */
- .ne { color: #cc6666 } /* Name.Exception */
+ .ne { color: #c66 } /* Name.Exception */
.nf { color: #81a2be } /* Name.Function */
.nl { color: #c5c8c6 } /* Name.Label */
.nn { color: #f0c674 } /* Name.Namespace */
.nx { color: #81a2be } /* Name.Other */
.py { color: #c5c8c6 } /* Name.Property */
.nt { color: #8abeb7 } /* Name.Tag */
- .nv { color: #cc6666 } /* Name.Variable */
+ .nv { color: #c66 } /* Name.Variable */
.ow { color: #8abeb7 } /* Operator.Word */
.w { color: #c5c8c6 } /* Text.Whitespace */
.mf { color: #de935f } /* Literal.Number.Float */
@@ -106,8 +106,8 @@
.s1 { color: #b5bd68 } /* Literal.String.Single */
.ss { color: #b5bd68 } /* Literal.String.Symbol */
.bp { color: #c5c8c6 } /* Name.Builtin.Pseudo */
- .vc { color: #cc6666 } /* Name.Variable.Class */
- .vg { color: #cc6666 } /* Name.Variable.Global */
- .vi { color: #cc6666 } /* Name.Variable.Instance */
+ .vc { color: #c66 } /* Name.Variable.Class */
+ .vg { color: #c66 } /* Name.Variable.Global */
+ .vi { color: #c66 } /* Name.Variable.Instance */
.il { color: #de935f } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 9098e07adcd..806401c21ae 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -43,7 +43,7 @@
// Search result highlight
span.highlight_word {
background-color: #ffe792 !important;
- color: #000000 !important;
+ color: #000 !important;
}
.hll { background-color: #49483e }
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 8b1a2824f76..6a809d4dfd2 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -96,7 +96,7 @@
.m { color: #2aa198 } /* Literal.Number */
.s { color: #2aa198 } /* Literal.String */
.na { color: #93a1a1 } /* Name.Attribute */
- .nb { color: #B58900 } /* Name.Builtin */
+ .nb { color: #b58900 } /* Name.Builtin */
.nc { color: #268bd2 } /* Name.Class */
.no { color: #cb4b16 } /* Name.Constant */
.nd { color: #268bd2 } /* Name.Decorator */
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 7ad89dd2c7c..b90c95c62d1 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -96,7 +96,7 @@
.m { color: #2aa198 } /* Literal.Number */
.s { color: #2aa198 } /* Literal.String */
.na { color: #586e75 } /* Name.Attribute */
- .nb { color: #B58900 } /* Name.Builtin */
+ .nb { color: #b58900 } /* Name.Builtin */
.nc { color: #268bd2 } /* Name.Class */
.no { color: #cb4b16 } /* Name.Constant */
.nd { color: #268bd2 } /* Name.Decorator */
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 8a091028a6c..8c1b0cd84ec 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -23,7 +23,7 @@
.line_holder {
.diff-line-num {
&.old {
- background: #ffdddd;
+ background: #fdd;
border-color: #f1c0c0;
}
@@ -68,66 +68,66 @@
}
.hll { background-color: #f8f8f8 }
- .c { color: #999988; font-style: italic; }
+ .c { color: #998; font-style: italic; }
.err { color: #a61717; background-color: #e3d2d2; }
.k { font-weight: bold; }
.o { font-weight: bold; }
- .cm { color: #999988; font-style: italic; }
- .cp { color: #999999; font-weight: bold; }
- .c1 { color: #999988; font-style: italic; }
- .cs { color: #999999; font-weight: bold; font-style: italic; }
- .gd { color: #000000; background-color: #ffdddd; }
- .gd .x { color: #000000; background-color: #ffaaaa; }
+ .cm { color: #998; font-style: italic; }
+ .cp { color: #999; font-weight: bold; }
+ .c1 { color: #998; font-style: italic; }
+ .cs { color: #999; font-weight: bold; font-style: italic; }
+ .gd { color: #000; background-color: #fdd; }
+ .gd .x { color: #000; background-color: #faa; }
.ge { font-style: italic; }
- .gr { color: #aa0000; }
- .gh { color: #999999; }
- .gi { color: #000000; background-color: #ddffdd; }
- .gi .x { color: #000000; background-color: #aaffaa; }
- .go { color: #888888; }
- .gp { color: #555555; }
+ .gr { color: #a00; }
+ .gh { color: #999; }
+ .gi { color: #000; background-color: #dfd; }
+ .gi .x { color: #000; background-color: #afa; }
+ .go { color: #888; }
+ .gp { color: #555; }
.gs { font-weight: bold; }
.gu { color: #800080; font-weight: bold; }
- .gt { color: #aa0000; }
+ .gt { color: #a00; }
.kc { font-weight: bold; }
.kd { font-weight: bold; }
.kn { font-weight: bold; }
.kp { font-weight: bold; }
.kr { font-weight: bold; }
- .kt { color: #445588; font-weight: bold; }
- .m { color: #009999; }
- .s { color: #dd1144; }
- .n { color: #333333; }
+ .kt { color: #458; font-weight: bold; }
+ .m { color: #099; }
+ .s { color: #d14; }
+ .n { color: #333; }
.na { color: teal; }
.nb { color: #0086b3; }
- .nc { color: #445588; font-weight: bold; }
+ .nc { color: #458; font-weight: bold; }
.no { color: teal; }
.ni { color: purple; }
- .ne { color: #990000; font-weight: bold; }
- .nf { color: #990000; font-weight: bold; }
- .nn { color: #555555; }
+ .ne { color: #900; font-weight: bold; }
+ .nf { color: #900; font-weight: bold; }
+ .nn { color: #555; }
.nt { color: navy; }
.nv { color: teal; }
.ow { font-weight: bold; }
- .w { color: #bbbbbb; }
- .mf { color: #009999; }
- .mh { color: #009999; }
- .mi { color: #009999; }
- .mo { color: #009999; }
- .sb { color: #dd1144; }
- .sc { color: #dd1144; }
- .sd { color: #dd1144; }
- .s2 { color: #dd1144; }
- .se { color: #dd1144; }
- .sh { color: #dd1144; }
- .si { color: #dd1144; }
- .sx { color: #dd1144; }
+ .w { color: #bbb; }
+ .mf { color: #099; }
+ .mh { color: #099; }
+ .mi { color: #099; }
+ .mo { color: #099; }
+ .sb { color: #d14; }
+ .sc { color: #d14; }
+ .sd { color: #d14; }
+ .s2 { color: #d14; }
+ .se { color: #d14; }
+ .sh { color: #d14; }
+ .si { color: #d14; }
+ .sx { color: #d14; }
.sr { color: #009926; }
- .s1 { color: #dd1144; }
+ .s1 { color: #d14; }
.ss { color: #990073; }
- .bp { color: #999999; }
+ .bp { color: #999; }
.vc { color: teal; }
.vg { color: teal; }
.vi { color: teal; }
- .il { color: #009999; }
- .gc { color: #999; background-color: #EAF2F5; }
+ .il { color: #099; }
+ .gc { color: #999; background-color: #eaf2f5; }
}
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
new file mode 100644
index 00000000000..0a13a7e0b54
--- /dev/null
+++ b/app/assets/stylesheets/notify.scss
@@ -0,0 +1,24 @@
+img {
+ max-width: 100%;
+ height: auto;
+}
+p.details {
+ font-style: italic;
+ color: #777
+}
+.footer p {
+ font-size: small;
+ color: #777
+}
+pre.commit-message {
+ white-space: pre-wrap;
+}
+.file-stats a {
+ text-decoration: none;
+}
+.file-stats .new-file {
+ color: #090;
+}
+.file-stats .deleted-file {
+ color: #b00;
+}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index a61161810a3..e05f14e7496 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -34,9 +34,9 @@
background: #fff
}
- .visibility-levels {
- .controls {
- margin-bottom: 9px;
+ .visibility-levels {
+ .controls {
+ margin-bottom: 9px;
}
i {
diff --git a/app/assets/stylesheets/pages/appearances.scss b/app/assets/stylesheets/pages/appearances.scss
new file mode 100644
index 00000000000..878f44116ba
--- /dev/null
+++ b/app/assets/stylesheets/pages/appearances.scss
@@ -0,0 +1,11 @@
+.appearance-logo-preview {
+ max-width: 400px;
+ margin-bottom: 20px;
+}
+
+.appearance-light-logo-preview {
+ background-color: $background-color;
+ max-width: 72px;
+ padding: 10px;
+ margin-bottom: 10px;
+}
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 87dd30f4111..28994e60baa 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -1,125 +1,133 @@
.awards {
- @include clearfix;
line-height: 34px;
.emoji-icon {
width: 20px;
height: 20px;
- margin: 7px 0 0 5px;
}
+}
- .award {
- @include border-radius(5px);
-
- border: 1px solid;
- padding: 0px 10px;
- float: left;
- margin-right: 5px;
- border-color: $border-color;
- cursor: pointer;
+.emoji-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ margin-top: 3px;
+ z-index: 1000;
+ min-width: 160px;
+ font-size: 14px;
+ background-color: $award-emoji-menu-bg;
+ border: 1px solid $award-emoji-menu-border;
+ border-radius: $border-radius-base;
+ box-shadow: 0 6px 12px rgba(0,0,0,.175);
+ pointer-events: none;
+ opacity: 0;
+ transform: scale(.2);
+ transform-origin: 0 -45px;
+ transition: all .3s cubic-bezier(.87,-.41,.19,1.44);
+
+ &.is-visible {
+ pointer-events: all;
+ opacity: 1;
+ transform: scale(1);
+ }
- &:hover {
- background-color: #dce0e5;
+ .emoji-menu-content {
+ padding: $gl-padding;
+ width: 300px;
+ height: 300px;
+ overflow-y: scroll;
+
+ input.emoji-search{
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC");
+ background-repeat: no-repeat;
+ background-position: right 5px center;
+ background-size: 16px;
}
+ }
+}
- &.active {
- border-color: $border-gray-light;
- background-color: $gray-light;
-
- &:hover {
- background-color: #dce0e5;
- }
+.emoji-menu-list {
+ list-style: none;
+ padding-left: 0;
+ margin-bottom: 0;
+}
- .counter {
- font-weight: bold;
- }
- }
+.emoji-menu-list-item {
+ padding: 3px;
+ margin-left: 1px;
+ margin-right: 1px;
+}
- .icon {
- float: left;
- margin-right: 10px;
- }
+.emoji-menu-btn {
+ display: block;
+ cursor: pointer;
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ background: none;
+ border: 0;
+ border-radius: $border-radius-base;
+ transition: transform .15s cubic-bezier(.3, 0, .2, 2);
+
+ &:hover {
+ background-color: transparent;
+ outline: 0;
+ transform: scale(1.3);
+ }
- .counter {
- float: left;
- }
+ &:focus,
+ &:active {
+ outline: 0;
}
- .awards-controls {
+ .emoji-icon {
+ display: inline-block;
position: relative;
- margin-left: 10px;
- float: left;
+ top: 3px;
+ }
+}
- .add-award {
- font-size: 24px;
- color: $gl-gray;
- position: relative;
- top: 2px;
+.award-menu-holder {
+ display: inline-block;
+ position: relative;
+}
- &:hover,
- &:link {
- text-decoration: none;
- }
- }
+.award-control {
+ margin-right: 5px;
+ padding-left: 5px;
+ padding-right: 5px;
+ line-height: 20px;
+ outline: 0;
+
+ &.active,
+ &:active {
+ background-color: $white-dark;
+ box-shadow: none;
+ outline: 0;
+ }
- .emoji-menu{
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 1000;
+ &.is-loading {
+ .award-control-icon {
display: none;
- float: left;
- min-width: 160px;
- padding: 5px 0;
- margin: 2px 0 0;
- font-size: 14px;
- text-align: left;
- list-style: none;
- background-color: #fff;
- -webkit-background-clip: padding-box;
- background-clip: padding-box;
- border: 1px solid #ccc;
- border: 1px solid rgba(0,0,0,.15);
- border-radius: 4px;
- -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
- box-shadow: 0 6px 12px rgba(0,0,0,.175);
-
- .emoji-menu-content {
- padding: $gl-padding;
- width: 300px;
- height: 300px;
- overflow-y: scroll;
-
- h5 {
- clear: left;
- }
-
- ul {
- list-style-type: none;
- margin-left: -20px;
- margin-bottom: 20px;
- overflow: auto;
- }
-
- input.emoji-search{
- background: image-url("icon-search.png") 240px no-repeat;
- }
-
- li {
- cursor: pointer;
- width: 30px;
- height: 30px;
- text-align: center;
- float: left;
- margin: 3px;
- list-decorate: none;
- @include border-radius(5px);
-
- &:hover {
- background-color: #ccc;
- }
- }
- }
}
+
+ .award-control-icon-loading {
+ display: block;
+ }
+ }
+
+ .icon,
+ .award-control-icon {
+ float: left;
+ margin-right: 5px;
+ font-size: 20px;
+ }
+
+ .award-control-icon-loading {
+ display: none;
+ }
+
+ .award-control-icon {
+ color: $award-emoji-new-btn-icon-color;
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 3c2997c1d5a..201f3e5ca46 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,6 +1,6 @@
.build-page {
pre.trace {
- background: #111111;
+ background: #111;
color: #fff;
font-family: $monospace_font;
white-space: pre;
@@ -27,10 +27,25 @@
}
.scroll-controls {
- position: fixed;
- bottom: 10px;
- left: 250px;
- z-index: 100;
+ &.affix-top {
+ position: absolute;
+ top: 10px;
+ right: 25px;
+ }
+
+ &.affix-bottom {
+ position: absolute;
+ right: 25px;
+ }
+
+ &.affix {
+ right: 30px;
+ bottom: 15px;
+
+ @media (min-width: $screen-md-min) {
+ right: 26%;
+ }
+ }
a {
display: block;
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index e53d6fc6bdc..971656feb42 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -55,7 +55,7 @@
padding: 10px 0;
li {
- padding: 3px 0px;
+ padding: 3px 0;
line-height: 20px;
}
}
@@ -90,6 +90,7 @@
position: relative;
font-family: $monospace_font;
$left: 12px;
+ overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
.max-width-marker {
width: 72ch;
color: rgba(0, 0, 0, 0.0);
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 818fd03e2ae..b6011fe7679 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -9,7 +9,7 @@
.lists-separator {
margin: 10px 0;
- border-color: #DDD;
+ border-color: #ddd;
}
.commits-row {
@@ -55,7 +55,7 @@ li.commit {
}
.commit-row-message {
- color: $gl-link-color;
+ color: $gl-dark-link-color;
&:hover {
text-decoration: underline;
@@ -76,7 +76,7 @@ li.commit {
.commit-row-description {
font-size: 14px;
- border-left: 1px solid #EEE;
+ border-left: 1px solid #eee;
padding: 10px 15px;
margin: 5px 0 10px 5px;
background: #f9f9f9;
@@ -93,12 +93,15 @@ li.commit {
.commit-row-info {
color: $gl-gray;
line-height: 24px;
- font-size: 13px;
a {
color: $gl-gray;
}
+ .avatar {
+ margin-right: 8px;
+ }
+
.committed_ago {
display: inline-block;
}
@@ -152,7 +155,7 @@ li.commit {
.count {
padding-top: 6px;
- padding-bottom: 0px;
+ padding-bottom: 0;
font-size: 12px;
color: #333;
display: block;
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
index 88639399148..cf7567513ec 100644
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -11,15 +11,15 @@
}
.dashboard-search-filter {
- padding:5px;
+ padding: 5px;
.search-text-input {
- float:left;
+ float: left;
@extend .col-md-2;
}
.btn {
margin-left: 5px;
- float:left;
+ float: left;
}
}
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index d93b6ee6733..d3eda1a57e6 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -18,7 +18,8 @@
}
.issue-meta {
- margin-left: 65px
+ display: inline-block;
+ line-height: 20px;
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index a7925e79549..f1368d74b3b 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1,7 +1,7 @@
// Common
.diff-file {
border: 1px solid $border-color;
- border-top: none;
+ margin-bottom: $gl-padding;
.diff-header {
position: relative;
@@ -29,7 +29,7 @@
.diff-content {
overflow: auto;
overflow-y: hidden;
- background: #FFF;
+ background: #fff;
color: #333;
.unfold {
@@ -57,8 +57,8 @@
font-family: $monospace_font;
border: none;
border-collapse: separate;
- margin: 0px;
- padding: 0px;
+ margin: 0;
+ padding: 0;
.line_holder td {
line-height: $code_line_height;
font-size: $code_font_size;
@@ -76,10 +76,10 @@
}
.old_line, .new_line {
- margin: 0px;
- padding: 0px;
+ margin: 0;
+ padding: 0;
border: none;
- padding: 0px 5px;
+ padding: 0 5px;
border-right: 1px solid;
text-align: right;
min-width: 35px;
@@ -97,8 +97,8 @@
}
.line_content {
display: block;
- margin: 0px;
- padding: 0px 0.5em;
+ margin: 0;
+ padding: 0 0.5em;
border: none;
&.parallel {
display: table-cell;
@@ -118,7 +118,7 @@
background-color: #fff;
line-height: 0;
img {
- border: 1px solid #FFF;
+ border: 1px solid #fff;
background: image-url('trans_bg.gif');
max-width: 100%;
}
@@ -132,7 +132,7 @@
}
.image-info {
font-size: 12px;
- margin: 5px 0 0 0;
+ margin: 5px 0 0;
color: grey;
}
@@ -183,7 +183,7 @@
height: 14px;
width: 15px;
position: absolute;
- top: 0px;
+ top: 0;
background: image-url('swipemode_sprites.gif') 0 3px no-repeat;
}
.bottom-handle {
@@ -191,7 +191,7 @@
height: 14px;
width: 15px;
position: absolute;
- bottom: 0px;
+ bottom: 0;
background: image-url('swipemode_sprites.gif') 0 -11px no-repeat;
}
}
@@ -206,8 +206,8 @@
.frame.added, .frame.deleted {
position: absolute;
display: block;
- top: 0px;
- left: 0px;
+ top: 0;
+ left: 0;
}
.controls {
display: block;
@@ -215,7 +215,7 @@
width: 300px;
z-index: 100;
position: absolute;
- bottom: 0px;
+ bottom: 0;
left: 50%;
margin-left: -150px;
@@ -231,11 +231,11 @@
.dragger {
display: block;
position: absolute;
- left: 0px;
- top: 0px;
+ left: 0;
+ top: 0;
height: 14px;
width: 14px;
- background: image-url('onion_skin_sprites.gif') 0px -34px repeat-x;
+ background: image-url('onion_skin_sprites.gif') 0 -34px repeat-x;
cursor: pointer;
}
@@ -243,17 +243,17 @@
display: block;
position: absolute;
top: 2px;
- right: 0px;
+ right: 0;
height: 10px;
width: 10px;
- background: image-url('onion_skin_sprites.gif') -2px 0px no-repeat;
+ background: image-url('onion_skin_sprites.gif') -2px 0 no-repeat;
}
.opaque {
display: block;
position: absolute;
top: 2px;
- left: 0px;
+ left: 0;
height: 10px;
width: 10px;
background: image-url('onion_skin_sprites.gif') -2px -10px no-repeat;
@@ -265,7 +265,7 @@
.view-modes {
padding: 10px;
text-align: center;
- background: #EEE;
+ background: #eee;
ul, li {
list-style: none;
@@ -361,3 +361,11 @@
border-color: $border;
}
}
+
+.files {
+ margin-top: -1px;
+
+ .diff-file:last-child {
+ margin-bottom: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 39d916cd336..43be5e38ba8 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -14,9 +14,9 @@
}
.cancel-btn {
- color: #B94A48;
+ color: #b94a48;
&:hover {
- color: #B94A48;
+ color: #b94a48;
}
}
diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/pages/emojis.scss
index 6c721b514f8..b731abc7450 100644
--- a/app/assets/stylesheets/pages/emojis.scss
+++ b/app/assets/stylesheets/pages/emojis.scss
@@ -1,60 +1,60 @@
-.emoji-0023-20E3 { background-position: 0px 0px; }
-.emoji-002A-20E3 { background-position: -20px 0px; }
-.emoji-0030-20E3 { background-position: 0px -20px; }
+.emoji-0023-20E3 { background-position: 0 0; }
+.emoji-002A-20E3 { background-position: -20px 0; }
+.emoji-0030-20E3 { background-position: 0 -20px; }
.emoji-0031-20E3 { background-position: -20px -20px; }
-.emoji-0032-20E3 { background-position: -40px 0px; }
+.emoji-0032-20E3 { background-position: -40px 0; }
.emoji-0033-20E3 { background-position: -40px -20px; }
-.emoji-0034-20E3 { background-position: 0px -40px; }
+.emoji-0034-20E3 { background-position: 0 -40px; }
.emoji-0035-20E3 { background-position: -20px -40px; }
.emoji-0036-20E3 { background-position: -40px -40px; }
-.emoji-0037-20E3 { background-position: -60px 0px; }
+.emoji-0037-20E3 { background-position: -60px 0; }
.emoji-0038-20E3 { background-position: -60px -20px; }
.emoji-0039-20E3 { background-position: -60px -40px; }
-.emoji-00A9 { background-position: 0px -60px; }
+.emoji-00A9 { background-position: 0 -60px; }
.emoji-00AE { background-position: -20px -60px; }
.emoji-1F004 { background-position: -40px -60px; }
.emoji-1F0CF { background-position: -60px -60px; }
-.emoji-1F170 { background-position: -80px 0px; }
+.emoji-1F170 { background-position: -80px 0; }
.emoji-1F171 { background-position: -80px -20px; }
.emoji-1F17E { background-position: -80px -40px; }
.emoji-1F17F { background-position: -80px -60px; }
-.emoji-1F18E { background-position: 0px -80px; }
+.emoji-1F18E { background-position: 0 -80px; }
.emoji-1F191 { background-position: -20px -80px; }
.emoji-1F192 { background-position: -40px -80px; }
.emoji-1F193 { background-position: -60px -80px; }
.emoji-1F194 { background-position: -80px -80px; }
-.emoji-1F195 { background-position: -100px 0px; }
+.emoji-1F195 { background-position: -100px 0; }
.emoji-1F196 { background-position: -100px -20px; }
.emoji-1F197 { background-position: -100px -40px; }
.emoji-1F198 { background-position: -100px -60px; }
.emoji-1F199 { background-position: -100px -80px; }
-.emoji-1F19A { background-position: 0px -100px; }
+.emoji-1F19A { background-position: 0 -100px; }
.emoji-1F1E6-1F1E8 { background-position: -20px -100px; }
.emoji-1F1E6-1F1E9 { background-position: -40px -100px; }
.emoji-1F1E6-1F1EA { background-position: -60px -100px; }
.emoji-1F1E6-1F1EB { background-position: -80px -100px; }
.emoji-1F1E6-1F1EC { background-position: -100px -100px; }
-.emoji-1F1E6-1F1EE { background-position: -120px 0px; }
+.emoji-1F1E6-1F1EE { background-position: -120px 0; }
.emoji-1F1E6-1F1F1 { background-position: -120px -20px; }
.emoji-1F1E6-1F1F2 { background-position: -120px -40px; }
.emoji-1F1E6-1F1F4 { background-position: -120px -60px; }
.emoji-1F1E6-1F1F6 { background-position: -120px -80px; }
.emoji-1F1E6-1F1F7 { background-position: -120px -100px; }
-.emoji-1F1E6-1F1F8 { background-position: 0px -120px; }
+.emoji-1F1E6-1F1F8 { background-position: 0 -120px; }
.emoji-1F1E6-1F1F9 { background-position: -20px -120px; }
.emoji-1F1E6-1F1FA { background-position: -40px -120px; }
.emoji-1F1E6-1F1FC { background-position: -60px -120px; }
.emoji-1F1E6-1F1FD { background-position: -80px -120px; }
.emoji-1F1E6-1F1FF { background-position: -100px -120px; }
.emoji-1F1E7-1F1E6 { background-position: -120px -120px; }
-.emoji-1F1E7-1F1E7 { background-position: -140px 0px; }
+.emoji-1F1E7-1F1E7 { background-position: -140px 0; }
.emoji-1F1E7-1F1E9 { background-position: -140px -20px; }
.emoji-1F1E7-1F1EA { background-position: -140px -40px; }
.emoji-1F1E7-1F1EB { background-position: -140px -60px; }
.emoji-1F1E7-1F1EC { background-position: -140px -80px; }
.emoji-1F1E7-1F1ED { background-position: -140px -100px; }
.emoji-1F1E7-1F1EE { background-position: -140px -120px; }
-.emoji-1F1E7-1F1EF { background-position: 0px -140px; }
+.emoji-1F1E7-1F1EF { background-position: 0 -140px; }
.emoji-1F1E7-1F1F1 { background-position: -20px -140px; }
.emoji-1F1E7-1F1F2 { background-position: -40px -140px; }
.emoji-1F1E7-1F1F3 { background-position: -60px -140px; }
@@ -62,7 +62,7 @@
.emoji-1F1E7-1F1F6 { background-position: -100px -140px; }
.emoji-1F1E7-1F1F7 { background-position: -120px -140px; }
.emoji-1F1E7-1F1F8 { background-position: -140px -140px; }
-.emoji-1F1E7-1F1F9 { background-position: -160px 0px; }
+.emoji-1F1E7-1F1F9 { background-position: -160px 0; }
.emoji-1F1E7-1F1FB { background-position: -160px -20px; }
.emoji-1F1E7-1F1FC { background-position: -160px -40px; }
.emoji-1F1E7-1F1FE { background-position: -160px -60px; }
@@ -70,7 +70,7 @@
.emoji-1F1E8-1F1E6 { background-position: -160px -100px; }
.emoji-1F1E8-1F1E8 { background-position: -160px -120px; }
.emoji-1F1E8-1F1E9 { background-position: -160px -140px; }
-.emoji-1F1E8-1F1EB { background-position: 0px -160px; }
+.emoji-1F1E8-1F1EB { background-position: 0 -160px; }
.emoji-1F1E8-1F1EC { background-position: -20px -160px; }
.emoji-1F1E8-1F1ED { background-position: -40px -160px; }
.emoji-1F1E8-1F1EE { background-position: -60px -160px; }
@@ -79,7 +79,7 @@
.emoji-1F1E8-1F1F2 { background-position: -120px -160px; }
.emoji-1F1E8-1F1F3 { background-position: -140px -160px; }
.emoji-1F1E8-1F1F4 { background-position: -160px -160px; }
-.emoji-1F1E8-1F1F5 { background-position: -180px 0px; }
+.emoji-1F1E8-1F1F5 { background-position: -180px 0; }
.emoji-1F1E8-1F1F7 { background-position: -180px -20px; }
.emoji-1F1E8-1F1FA { background-position: -180px -40px; }
.emoji-1F1E8-1F1FB { background-position: -180px -60px; }
@@ -88,7 +88,7 @@
.emoji-1F1E8-1F1FE { background-position: -180px -120px; }
.emoji-1F1E8-1F1FF { background-position: -180px -140px; }
.emoji-1F1E9-1F1EA { background-position: -180px -160px; }
-.emoji-1F1E9-1F1EC { background-position: 0px -180px; }
+.emoji-1F1E9-1F1EC { background-position: 0 -180px; }
.emoji-1F1E9-1F1EF { background-position: -20px -180px; }
.emoji-1F1E9-1F1F0 { background-position: -40px -180px; }
.emoji-1F1E9-1F1F2 { background-position: -60px -180px; }
@@ -98,7 +98,7 @@
.emoji-1F1EA-1F1E8 { background-position: -140px -180px; }
.emoji-1F1EA-1F1EA { background-position: -160px -180px; }
.emoji-1F1EA-1F1EC { background-position: -180px -180px; }
-.emoji-1F1EA-1F1ED { background-position: -200px 0px; }
+.emoji-1F1EA-1F1ED { background-position: -200px 0; }
.emoji-1F1EA-1F1F7 { background-position: -200px -20px; }
.emoji-1F1EA-1F1F8 { background-position: -200px -40px; }
.emoji-1F1EA-1F1F9 { background-position: -200px -60px; }
@@ -108,7 +108,7 @@
.emoji-1F1EB-1F1F0 { background-position: -200px -140px; }
.emoji-1F1EB-1F1F2 { background-position: -200px -160px; }
.emoji-1F1EB-1F1F4 { background-position: -200px -180px; }
-.emoji-1F1EB-1F1F7 { background-position: 0px -200px; }
+.emoji-1F1EB-1F1F7 { background-position: 0 -200px; }
.emoji-1F1EC-1F1E6 { background-position: -20px -200px; }
.emoji-1F1EC-1F1E7 { background-position: -40px -200px; }
.emoji-1F1EC-1F1E9 { background-position: -60px -200px; }
@@ -119,7 +119,7 @@
.emoji-1F1EC-1F1EE { background-position: -160px -200px; }
.emoji-1F1EC-1F1F1 { background-position: -180px -200px; }
.emoji-1F1EC-1F1F2 { background-position: -200px -200px; }
-.emoji-1F1EC-1F1F3 { background-position: -220px 0px; }
+.emoji-1F1EC-1F1F3 { background-position: -220px 0; }
.emoji-1F1EC-1F1F5 { background-position: -220px -20px; }
.emoji-1F1EC-1F1F6 { background-position: -220px -40px; }
.emoji-1F1EC-1F1F7 { background-position: -220px -60px; }
@@ -130,7 +130,7 @@
.emoji-1F1EC-1F1FE { background-position: -220px -160px; }
.emoji-1F1ED-1F1F0 { background-position: -220px -180px; }
.emoji-1F1ED-1F1F2 { background-position: -220px -200px; }
-.emoji-1F1ED-1F1F3 { background-position: 0px -220px; }
+.emoji-1F1ED-1F1F3 { background-position: 0 -220px; }
.emoji-1F1ED-1F1F7 { background-position: -20px -220px; }
.emoji-1F1ED-1F1F9 { background-position: -40px -220px; }
.emoji-1F1ED-1F1FA { background-position: -60px -220px; }
@@ -142,7 +142,7 @@
.emoji-1F1EE-1F1F3 { background-position: -180px -220px; }
.emoji-1F1EE-1F1F4 { background-position: -200px -220px; }
.emoji-1F1EE-1F1F6 { background-position: -220px -220px; }
-.emoji-1F1EE-1F1F7 { background-position: -240px 0px; }
+.emoji-1F1EE-1F1F7 { background-position: -240px 0; }
.emoji-1F1EE-1F1F8 { background-position: -240px -20px; }
.emoji-1F1EE-1F1F9 { background-position: -240px -40px; }
.emoji-1F1EF-1F1EA { background-position: -240px -60px; }
@@ -154,7 +154,7 @@
.emoji-1F1F0-1F1ED { background-position: -240px -180px; }
.emoji-1F1F0-1F1EE { background-position: -240px -200px; }
.emoji-1F1F0-1F1F2 { background-position: -240px -220px; }
-.emoji-1F1F0-1F1F3 { background-position: 0px -240px; }
+.emoji-1F1F0-1F1F3 { background-position: 0 -240px; }
.emoji-1F1F0-1F1F5 { background-position: -20px -240px; }
.emoji-1F1F0-1F1F7 { background-position: -40px -240px; }
.emoji-1F1F0-1F1FC { background-position: -60px -240px; }
@@ -167,7 +167,7 @@
.emoji-1F1F1-1F1F0 { background-position: -200px -240px; }
.emoji-1F1F1-1F1F7 { background-position: -220px -240px; }
.emoji-1F1F1-1F1F8 { background-position: -240px -240px; }
-.emoji-1F1F1-1F1F9 { background-position: -260px 0px; }
+.emoji-1F1F1-1F1F9 { background-position: -260px 0; }
.emoji-1F1F1-1F1FA { background-position: -260px -20px; }
.emoji-1F1F1-1F1FB { background-position: -260px -40px; }
.emoji-1F1F1-1F1FE { background-position: -260px -60px; }
@@ -180,7 +180,7 @@
.emoji-1F1F2-1F1ED { background-position: -260px -200px; }
.emoji-1F1F2-1F1F0 { background-position: -260px -220px; }
.emoji-1F1F2-1F1F1 { background-position: -260px -240px; }
-.emoji-1F1F2-1F1F2 { background-position: 0px -260px; }
+.emoji-1F1F2-1F1F2 { background-position: 0 -260px; }
.emoji-1F1F2-1F1F3 { background-position: -20px -260px; }
.emoji-1F1F2-1F1F4 { background-position: -40px -260px; }
.emoji-1F1F2-1F1F5 { background-position: -60px -260px; }
@@ -194,7 +194,7 @@
.emoji-1F1F2-1F1FD { background-position: -220px -260px; }
.emoji-1F1F2-1F1FE { background-position: -240px -260px; }
.emoji-1F1F2-1F1FF { background-position: -260px -260px; }
-.emoji-1F1F3-1F1E6 { background-position: -280px 0px; }
+.emoji-1F1F3-1F1E6 { background-position: -280px 0; }
.emoji-1F1F3-1F1E8 { background-position: -280px -20px; }
.emoji-1F1F3-1F1EA { background-position: -280px -40px; }
.emoji-1F1F3-1F1EB { background-position: -280px -60px; }
@@ -208,7 +208,7 @@
.emoji-1F1F3-1F1FF { background-position: -280px -220px; }
.emoji-1F1F4-1F1F2 { background-position: -280px -240px; }
.emoji-1F1F5-1F1E6 { background-position: -280px -260px; }
-.emoji-1F1F5-1F1EA { background-position: 0px -280px; }
+.emoji-1F1F5-1F1EA { background-position: 0 -280px; }
.emoji-1F1F5-1F1EB { background-position: -20px -280px; }
.emoji-1F1F5-1F1EC { background-position: -40px -280px; }
.emoji-1F1F5-1F1ED { background-position: -60px -280px; }
@@ -223,7 +223,7 @@
.emoji-1F1F5-1F1FE { background-position: -240px -280px; }
.emoji-1F1F6-1F1E6 { background-position: -260px -280px; }
.emoji-1F1F7-1F1EA { background-position: -280px -280px; }
-.emoji-1F1F7-1F1F4 { background-position: -300px 0px; }
+.emoji-1F1F7-1F1F4 { background-position: -300px 0; }
.emoji-1F1F7-1F1F8 { background-position: -300px -20px; }
.emoji-1F1F7-1F1FA { background-position: -300px -40px; }
.emoji-1F1F7-1F1FC { background-position: -300px -60px; }
@@ -238,7 +238,7 @@
.emoji-1F1F8-1F1EF { background-position: -300px -240px; }
.emoji-1F1F8-1F1F0 { background-position: -300px -260px; }
.emoji-1F1F8-1F1F1 { background-position: -300px -280px; }
-.emoji-1F1F8-1F1F2 { background-position: 0px -300px; }
+.emoji-1F1F8-1F1F2 { background-position: 0 -300px; }
.emoji-1F1F8-1F1F3 { background-position: -20px -300px; }
.emoji-1F1F8-1F1F4 { background-position: -40px -300px; }
.emoji-1F1F8-1F1F7 { background-position: -60px -300px; }
@@ -254,7 +254,7 @@
.emoji-1F1F9-1F1EB { background-position: -260px -300px; }
.emoji-1F1F9-1F1EC { background-position: -280px -300px; }
.emoji-1F1F9-1F1ED { background-position: -300px -300px; }
-.emoji-1F1F9-1F1EF { background-position: -320px 0px; }
+.emoji-1F1F9-1F1EF { background-position: -320px 0; }
.emoji-1F1F9-1F1F0 { background-position: -320px -20px; }
.emoji-1F1F9-1F1F1 { background-position: -320px -40px; }
.emoji-1F1F9-1F1F2 { background-position: -320px -60px; }
@@ -270,7 +270,7 @@
.emoji-1F1FA-1F1F2 { background-position: -320px -260px; }
.emoji-1F1FA-1F1F8 { background-position: -320px -280px; }
.emoji-1F1FA-1F1FE { background-position: -320px -300px; }
-.emoji-1F1FA-1F1FF { background-position: 0px -320px; }
+.emoji-1F1FA-1F1FF { background-position: 0 -320px; }
.emoji-1F1FB-1F1E6 { background-position: -20px -320px; }
.emoji-1F1FB-1F1E8 { background-position: -40px -320px; }
.emoji-1F1FB-1F1EA { background-position: -60px -320px; }
@@ -287,7 +287,7 @@
.emoji-1F1FF-1F1F2 { background-position: -280px -320px; }
.emoji-1F1FF-1F1FC { background-position: -300px -320px; }
.emoji-1F201 { background-position: -320px -320px; }
-.emoji-1F202 { background-position: -340px 0px; }
+.emoji-1F202 { background-position: -340px 0; }
.emoji-1F21A { background-position: -340px -20px; }
.emoji-1F22F { background-position: -340px -40px; }
.emoji-1F232 { background-position: -340px -60px; }
@@ -304,7 +304,7 @@
.emoji-1F300 { background-position: -340px -280px; }
.emoji-1F301 { background-position: -340px -300px; }
.emoji-1F302 { background-position: -340px -320px; }
-.emoji-1F303 { background-position: 0px -340px; }
+.emoji-1F303 { background-position: 0 -340px; }
.emoji-1F304 { background-position: -20px -340px; }
.emoji-1F305 { background-position: -40px -340px; }
.emoji-1F306 { background-position: -60px -340px; }
@@ -322,7 +322,7 @@
.emoji-1F312 { background-position: -300px -340px; }
.emoji-1F313 { background-position: -320px -340px; }
.emoji-1F314 { background-position: -340px -340px; }
-.emoji-1F315 { background-position: -360px 0px; }
+.emoji-1F315 { background-position: -360px 0; }
.emoji-1F316 { background-position: -360px -20px; }
.emoji-1F317 { background-position: -360px -40px; }
.emoji-1F318 { background-position: -360px -60px; }
@@ -340,7 +340,7 @@
.emoji-1F326 { background-position: -360px -300px; }
.emoji-1F327 { background-position: -360px -320px; }
.emoji-1F328 { background-position: -360px -340px; }
-.emoji-1F329 { background-position: 0px -360px; }
+.emoji-1F329 { background-position: 0 -360px; }
.emoji-1F32A { background-position: -20px -360px; }
.emoji-1F32B { background-position: -40px -360px; }
.emoji-1F32C { background-position: -60px -360px; }
@@ -359,7 +359,7 @@
.emoji-1F339 { background-position: -320px -360px; }
.emoji-1F33A { background-position: -340px -360px; }
.emoji-1F33B { background-position: -360px -360px; }
-.emoji-1F33C { background-position: -380px 0px; }
+.emoji-1F33C { background-position: -380px 0; }
.emoji-1F33D { background-position: -380px -20px; }
.emoji-1F33E { background-position: -380px -40px; }
.emoji-1F33F { background-position: -380px -60px; }
@@ -378,7 +378,7 @@
.emoji-1F34C { background-position: -380px -320px; }
.emoji-1F34D { background-position: -380px -340px; }
.emoji-1F34E { background-position: -380px -360px; }
-.emoji-1F34F { background-position: 0px -380px; }
+.emoji-1F34F { background-position: 0 -380px; }
.emoji-1F350 { background-position: -20px -380px; }
.emoji-1F351 { background-position: -40px -380px; }
.emoji-1F352 { background-position: -60px -380px; }
@@ -398,7 +398,7 @@
.emoji-1F360 { background-position: -340px -380px; }
.emoji-1F361 { background-position: -360px -380px; }
.emoji-1F362 { background-position: -380px -380px; }
-.emoji-1F363 { background-position: -400px 0px; }
+.emoji-1F363 { background-position: -400px 0; }
.emoji-1F364 { background-position: -400px -20px; }
.emoji-1F365 { background-position: -400px -40px; }
.emoji-1F366 { background-position: -400px -60px; }
@@ -418,7 +418,7 @@
.emoji-1F374 { background-position: -400px -340px; }
.emoji-1F375 { background-position: -400px -360px; }
.emoji-1F376 { background-position: -400px -380px; }
-.emoji-1F377 { background-position: 0px -400px; }
+.emoji-1F377 { background-position: 0 -400px; }
.emoji-1F378 { background-position: -20px -400px; }
.emoji-1F379 { background-position: -40px -400px; }
.emoji-1F37A { background-position: -60px -400px; }
@@ -439,7 +439,7 @@
.emoji-1F385-1F3FE { background-position: -360px -400px; }
.emoji-1F385-1F3FF { background-position: -380px -400px; }
.emoji-1F386 { background-position: -400px -400px; }
-.emoji-1F387 { background-position: -420px 0px; }
+.emoji-1F387 { background-position: -420px 0; }
.emoji-1F388 { background-position: -420px -20px; }
.emoji-1F389 { background-position: -420px -40px; }
.emoji-1F38A { background-position: -420px -60px; }
@@ -460,7 +460,7 @@
.emoji-1F399 { background-position: -420px -360px; }
.emoji-1F39A { background-position: -420px -380px; }
.emoji-1F39B { background-position: -420px -400px; }
-.emoji-1F39C { background-position: 0px -420px; }
+.emoji-1F39C { background-position: 0 -420px; }
.emoji-1F39D { background-position: -20px -420px; }
.emoji-1F39E { background-position: -40px -420px; }
.emoji-1F39F { background-position: -60px -420px; }
@@ -482,7 +482,7 @@
.emoji-1F3AF { background-position: -380px -420px; }
.emoji-1F3B0 { background-position: -400px -420px; }
.emoji-1F3B1 { background-position: -420px -420px; }
-.emoji-1F3B2 { background-position: -440px 0px; }
+.emoji-1F3B2 { background-position: -440px 0; }
.emoji-1F3B3 { background-position: -440px -20px; }
.emoji-1F3B4 { background-position: -440px -40px; }
.emoji-1F3B5 { background-position: -440px -60px; }
@@ -504,7 +504,7 @@
.emoji-1F3C3-1F3FC { background-position: -440px -380px; }
.emoji-1F3C3-1F3FD { background-position: -440px -400px; }
.emoji-1F3C3-1F3FE { background-position: -440px -420px; }
-.emoji-1F3C3-1F3FF { background-position: 0px -440px; }
+.emoji-1F3C3-1F3FF { background-position: 0 -440px; }
.emoji-1F3C4 { background-position: -20px -440px; }
.emoji-1F3C4-1F3FB { background-position: -40px -440px; }
.emoji-1F3C4-1F3FC { background-position: -60px -440px; }
@@ -527,7 +527,7 @@
.emoji-1F3CA-1F3FD { background-position: -400px -440px; }
.emoji-1F3CA-1F3FE { background-position: -420px -440px; }
.emoji-1F3CA-1F3FF { background-position: -440px -440px; }
-.emoji-1F3CB { background-position: -460px 0px; }
+.emoji-1F3CB { background-position: -460px 0; }
.emoji-1F3CB-1F3FB { background-position: -460px -20px; }
.emoji-1F3CB-1F3FC { background-position: -460px -40px; }
.emoji-1F3CB-1F3FD { background-position: -460px -60px; }
@@ -550,7 +550,7 @@
.emoji-1F3DA { background-position: -460px -400px; }
.emoji-1F3DB { background-position: -460px -420px; }
.emoji-1F3DC { background-position: -460px -440px; }
-.emoji-1F3DD { background-position: 0px -460px; }
+.emoji-1F3DD { background-position: 0 -460px; }
.emoji-1F3DE { background-position: -20px -460px; }
.emoji-1F3DF { background-position: -40px -460px; }
.emoji-1F3E0 { background-position: -60px -460px; }
@@ -574,7 +574,7 @@
.emoji-1F3F2 { background-position: -420px -460px; }
.emoji-1F3F3 { background-position: -440px -460px; }
.emoji-1F3F4 { background-position: -460px -460px; }
-.emoji-1F3F5 { background-position: -480px 0px; }
+.emoji-1F3F5 { background-position: -480px 0; }
.emoji-1F3F6 { background-position: -480px -20px; }
.emoji-1F3F7 { background-position: -480px -40px; }
.emoji-1F3F8 { background-position: -480px -60px; }
@@ -598,7 +598,7 @@
.emoji-1F40A { background-position: -480px -420px; }
.emoji-1F40B { background-position: -480px -440px; }
.emoji-1F40C { background-position: -480px -460px; }
-.emoji-1F40D { background-position: 0px -480px; }
+.emoji-1F40D { background-position: 0 -480px; }
.emoji-1F40E { background-position: -20px -480px; }
.emoji-1F40F { background-position: -40px -480px; }
.emoji-1F410 { background-position: -60px -480px; }
@@ -623,7 +623,7 @@
.emoji-1F423 { background-position: -440px -480px; }
.emoji-1F424 { background-position: -460px -480px; }
.emoji-1F425 { background-position: -480px -480px; }
-.emoji-1F426 { background-position: -500px 0px; }
+.emoji-1F426 { background-position: -500px 0; }
.emoji-1F427 { background-position: -500px -20px; }
.emoji-1F428 { background-position: -500px -40px; }
.emoji-1F429 { background-position: -500px -60px; }
@@ -648,7 +648,7 @@
.emoji-1F43C { background-position: -500px -440px; }
.emoji-1F43D { background-position: -500px -460px; }
.emoji-1F43E { background-position: -500px -480px; }
-.emoji-1F43F { background-position: 0px -500px; }
+.emoji-1F43F { background-position: 0 -500px; }
.emoji-1F440 { background-position: -20px -500px; }
.emoji-1F441 { background-position: -40px -500px; }
.emoji-1F441-1F5E8 { background-position: -60px -500px; }
@@ -674,7 +674,7 @@
.emoji-1F446-1F3FF { background-position: -460px -500px; }
.emoji-1F447 { background-position: -480px -500px; }
.emoji-1F447-1F3FB { background-position: -500px -500px; }
-.emoji-1F447-1F3FC { background-position: -520px 0px; }
+.emoji-1F447-1F3FC { background-position: -520px 0; }
.emoji-1F447-1F3FD { background-position: -520px -20px; }
.emoji-1F447-1F3FE { background-position: -520px -40px; }
.emoji-1F447-1F3FF { background-position: -520px -60px; }
@@ -700,7 +700,7 @@
.emoji-1F44B-1F3FB { background-position: -520px -460px; }
.emoji-1F44B-1F3FC { background-position: -520px -480px; }
.emoji-1F44B-1F3FD { background-position: -520px -500px; }
-.emoji-1F44B-1F3FE { background-position: 0px -520px; }
+.emoji-1F44B-1F3FE { background-position: 0 -520px; }
.emoji-1F44B-1F3FF { background-position: -20px -520px; }
.emoji-1F44C { background-position: -40px -520px; }
.emoji-1F44C-1F3FB { background-position: -60px -520px; }
@@ -727,7 +727,7 @@
.emoji-1F44F-1F3FE { background-position: -480px -520px; }
.emoji-1F44F-1F3FF { background-position: -500px -520px; }
.emoji-1F450 { background-position: -520px -520px; }
-.emoji-1F450-1F3FB { background-position: -540px 0px; }
+.emoji-1F450-1F3FB { background-position: -540px 0; }
.emoji-1F450-1F3FC { background-position: -540px -20px; }
.emoji-1F450-1F3FD { background-position: -540px -40px; }
.emoji-1F450-1F3FE { background-position: -540px -60px; }
@@ -754,7 +754,7 @@
.emoji-1F464 { background-position: -540px -480px; }
.emoji-1F465 { background-position: -540px -500px; }
.emoji-1F466 { background-position: -540px -520px; }
-.emoji-1F466-1F3FB { background-position: 0px -540px; }
+.emoji-1F466-1F3FB { background-position: 0 -540px; }
.emoji-1F466-1F3FC { background-position: -20px -540px; }
.emoji-1F466-1F3FD { background-position: -40px -540px; }
.emoji-1F466-1F3FE { background-position: -60px -540px; }
@@ -782,7 +782,7 @@
.emoji-1F468-1F469-1F467-1F467 { background-position: -500px -540px; }
.emoji-1F468-2764-1F468 { background-position: -520px -540px; }
.emoji-1F468-2764-1F48B-1F468 { background-position: -540px -540px; }
-.emoji-1F469 { background-position: -560px 0px; }
+.emoji-1F469 { background-position: -560px 0; }
.emoji-1F469-1F3FB { background-position: -560px -20px; }
.emoji-1F469-1F3FC { background-position: -560px -40px; }
.emoji-1F469-1F3FD { background-position: -560px -60px; }
@@ -810,7 +810,7 @@
.emoji-1F470-1F3FB { background-position: -560px -500px; }
.emoji-1F470-1F3FC { background-position: -560px -520px; }
.emoji-1F470-1F3FD { background-position: -560px -540px; }
-.emoji-1F470-1F3FE { background-position: 0px -560px; }
+.emoji-1F470-1F3FE { background-position: 0 -560px; }
.emoji-1F470-1F3FF { background-position: -20px -560px; }
.emoji-1F471 { background-position: -40px -560px; }
.emoji-1F471-1F3FB { background-position: -60px -560px; }
@@ -839,7 +839,7 @@
.emoji-1F475 { background-position: -520px -560px; }
.emoji-1F475-1F3FB { background-position: -540px -560px; }
.emoji-1F475-1F3FC { background-position: -560px -560px; }
-.emoji-1F475-1F3FD { background-position: -580px 0px; }
+.emoji-1F475-1F3FD { background-position: -580px 0; }
.emoji-1F475-1F3FE { background-position: -580px -20px; }
.emoji-1F475-1F3FF { background-position: -580px -40px; }
.emoji-1F476 { background-position: -580px -60px; }
@@ -868,7 +868,7 @@
.emoji-1F47C-1F3FC { background-position: -580px -520px; }
.emoji-1F47C-1F3FD { background-position: -580px -540px; }
.emoji-1F47C-1F3FE { background-position: -580px -560px; }
-.emoji-1F47C-1F3FF { background-position: 0px -580px; }
+.emoji-1F47C-1F3FF { background-position: 0 -580px; }
.emoji-1F47D { background-position: -20px -580px; }
.emoji-1F47E { background-position: -40px -580px; }
.emoji-1F47F { background-position: -60px -580px; }
@@ -898,7 +898,7 @@
.emoji-1F485-1F3FD { background-position: -540px -580px; }
.emoji-1F485-1F3FE { background-position: -560px -580px; }
.emoji-1F485-1F3FF { background-position: -580px -580px; }
-.emoji-1F486 { background-position: -600px 0px; }
+.emoji-1F486 { background-position: -600px 0; }
.emoji-1F486-1F3FB { background-position: -600px -20px; }
.emoji-1F486-1F3FC { background-position: -600px -40px; }
.emoji-1F486-1F3FD { background-position: -600px -60px; }
@@ -928,7 +928,7 @@
.emoji-1F497 { background-position: -600px -540px; }
.emoji-1F498 { background-position: -600px -560px; }
.emoji-1F499 { background-position: -600px -580px; }
-.emoji-1F49A { background-position: 0px -600px; }
+.emoji-1F49A { background-position: 0 -600px; }
.emoji-1F49B { background-position: -20px -600px; }
.emoji-1F49C { background-position: -40px -600px; }
.emoji-1F49D { background-position: -60px -600px; }
@@ -959,7 +959,7 @@
.emoji-1F4B1 { background-position: -560px -600px; }
.emoji-1F4B2 { background-position: -580px -600px; }
.emoji-1F4B3 { background-position: -600px -600px; }
-.emoji-1F4B4 { background-position: -620px 0px; }
+.emoji-1F4B4 { background-position: -620px 0; }
.emoji-1F4B5 { background-position: -620px -20px; }
.emoji-1F4B6 { background-position: -620px -40px; }
.emoji-1F4B7 { background-position: -620px -60px; }
@@ -990,7 +990,7 @@
.emoji-1F4D0 { background-position: -620px -560px; }
.emoji-1F4D1 { background-position: -620px -580px; }
.emoji-1F4D2 { background-position: -620px -600px; }
-.emoji-1F4D3 { background-position: 0px -620px; }
+.emoji-1F4D3 { background-position: 0 -620px; }
.emoji-1F4D4 { background-position: -20px -620px; }
.emoji-1F4D5 { background-position: -40px -620px; }
.emoji-1F4D6 { background-position: -60px -620px; }
@@ -1022,7 +1022,7 @@
.emoji-1F4F0 { background-position: -580px -620px; }
.emoji-1F4F1 { background-position: -600px -620px; }
.emoji-1F4F2 { background-position: -620px -620px; }
-.emoji-1F4F3 { background-position: -640px 0px; }
+.emoji-1F4F3 { background-position: -640px 0; }
.emoji-1F4F4 { background-position: -640px -20px; }
.emoji-1F4F5 { background-position: -640px -40px; }
.emoji-1F4F6 { background-position: -640px -60px; }
@@ -1054,7 +1054,7 @@
.emoji-1F510 { background-position: -640px -580px; }
.emoji-1F511 { background-position: -640px -600px; }
.emoji-1F512 { background-position: -640px -620px; }
-.emoji-1F513 { background-position: 0px -640px; }
+.emoji-1F513 { background-position: 0 -640px; }
.emoji-1F514 { background-position: -20px -640px; }
.emoji-1F515 { background-position: -40px -640px; }
.emoji-1F516 { background-position: -60px -640px; }
@@ -1087,7 +1087,7 @@
.emoji-1F531 { background-position: -600px -640px; }
.emoji-1F532 { background-position: -620px -640px; }
.emoji-1F533 { background-position: -640px -640px; }
-.emoji-1F534 { background-position: -660px 0px; }
+.emoji-1F534 { background-position: -660px 0; }
.emoji-1F535 { background-position: -660px -20px; }
.emoji-1F536 { background-position: -660px -40px; }
.emoji-1F537 { background-position: -660px -60px; }
@@ -1120,7 +1120,7 @@
.emoji-1F55B { background-position: -660px -600px; }
.emoji-1F55C { background-position: -660px -620px; }
.emoji-1F55D { background-position: -660px -640px; }
-.emoji-1F55E { background-position: 0px -660px; }
+.emoji-1F55E { background-position: 0 -660px; }
.emoji-1F55F { background-position: -20px -660px; }
.emoji-1F560 { background-position: -40px -660px; }
.emoji-1F561 { background-position: -60px -660px; }
@@ -1154,7 +1154,7 @@
.emoji-1F578 { background-position: -620px -660px; }
.emoji-1F579 { background-position: -640px -660px; }
.emoji-1F57B { background-position: -660px -660px; }
-.emoji-1F57E { background-position: -680px 0px; }
+.emoji-1F57E { background-position: -680px 0; }
.emoji-1F57F { background-position: -680px -20px; }
.emoji-1F581 { background-position: -680px -40px; }
.emoji-1F582 { background-position: -680px -60px; }
@@ -1188,7 +1188,7 @@
.emoji-1F595-1F3FF { background-position: -680px -620px; }
.emoji-1F596 { background-position: -680px -640px; }
.emoji-1F596-1F3FB { background-position: -680px -660px; }
-.emoji-1F596-1F3FC { background-position: 0px -680px; }
+.emoji-1F596-1F3FC { background-position: 0 -680px; }
.emoji-1F596-1F3FD { background-position: -20px -680px; }
.emoji-1F596-1F3FE { background-position: -40px -680px; }
.emoji-1F596-1F3FF { background-position: -60px -680px; }
@@ -1223,7 +1223,7 @@
.emoji-1F5C4 { background-position: -640px -680px; }
.emoji-1F5C6 { background-position: -660px -680px; }
.emoji-1F5C7 { background-position: -680px -680px; }
-.emoji-1F5C9 { background-position: -700px 0px; }
+.emoji-1F5C9 { background-position: -700px 0; }
.emoji-1F5CA { background-position: -700px -20px; }
.emoji-1F5CE { background-position: -700px -40px; }
.emoji-1F5CF { background-position: -700px -60px; }
@@ -1258,7 +1258,7 @@
.emoji-1F5F8 { background-position: -700px -640px; }
.emoji-1F5F9 { background-position: -700px -660px; }
.emoji-1F5FA { background-position: -700px -680px; }
-.emoji-1F5FB { background-position: 0px -700px; }
+.emoji-1F5FB { background-position: 0 -700px; }
.emoji-1F5FC { background-position: -20px -700px; }
.emoji-1F5FD { background-position: -40px -700px; }
.emoji-1F5FE { background-position: -60px -700px; }
@@ -1294,7 +1294,7 @@
.emoji-1F61C { background-position: -660px -700px; }
.emoji-1F61D { background-position: -680px -700px; }
.emoji-1F61E { background-position: -700px -700px; }
-.emoji-1F61F { background-position: -720px 0px; }
+.emoji-1F61F { background-position: -720px 0; }
.emoji-1F620 { background-position: -720px -20px; }
.emoji-1F621 { background-position: -720px -40px; }
.emoji-1F622 { background-position: -720px -60px; }
@@ -1330,7 +1330,7 @@
.emoji-1F640 { background-position: -720px -660px; }
.emoji-1F641 { background-position: -720px -680px; }
.emoji-1F642 { background-position: -720px -700px; }
-.emoji-1F643 { background-position: 0px -720px; }
+.emoji-1F643 { background-position: 0 -720px; }
.emoji-1F644 { background-position: -20px -720px; }
.emoji-1F645 { background-position: -40px -720px; }
.emoji-1F645-1F3FB { background-position: -60px -720px; }
@@ -1367,7 +1367,7 @@
.emoji-1F64C-1F3FF { background-position: -680px -720px; }
.emoji-1F64D { background-position: -700px -720px; }
.emoji-1F64D-1F3FB { background-position: -720px -720px; }
-.emoji-1F64D-1F3FC { background-position: -740px 0px; }
+.emoji-1F64D-1F3FC { background-position: -740px 0; }
.emoji-1F64D-1F3FD { background-position: -740px -20px; }
.emoji-1F64D-1F3FE { background-position: -740px -40px; }
.emoji-1F64D-1F3FF { background-position: -740px -60px; }
@@ -1404,7 +1404,7 @@
.emoji-1F692 { background-position: -740px -680px; }
.emoji-1F693 { background-position: -740px -700px; }
.emoji-1F694 { background-position: -740px -720px; }
-.emoji-1F695 { background-position: 0px -740px; }
+.emoji-1F695 { background-position: 0 -740px; }
.emoji-1F696 { background-position: -20px -740px; }
.emoji-1F697 { background-position: -40px -740px; }
.emoji-1F698 { background-position: -60px -740px; }
@@ -1442,7 +1442,7 @@
.emoji-1F6B3 { background-position: -700px -740px; }
.emoji-1F6B4 { background-position: -720px -740px; }
.emoji-1F6B4-1F3FB { background-position: -740px -740px; }
-.emoji-1F6B4-1F3FC { background-position: -760px 0px; }
+.emoji-1F6B4-1F3FC { background-position: -760px 0; }
.emoji-1F6B4-1F3FD { background-position: -760px -20px; }
.emoji-1F6B4-1F3FE { background-position: -760px -40px; }
.emoji-1F6B4-1F3FF { background-position: -760px -60px; }
@@ -1480,7 +1480,7 @@
.emoji-1F6C5 { background-position: -760px -700px; }
.emoji-1F6C6 { background-position: -760px -720px; }
.emoji-1F6C7 { background-position: -760px -740px; }
-.emoji-1F6C8 { background-position: 0px -760px; }
+.emoji-1F6C8 { background-position: 0 -760px; }
.emoji-1F6C9 { background-position: -20px -760px; }
.emoji-1F6CA { background-position: -40px -760px; }
.emoji-1F6CB { background-position: -60px -760px; }
@@ -1519,7 +1519,7 @@
.emoji-1F918-1F3FC { background-position: -720px -760px; }
.emoji-1F918-1F3FD { background-position: -740px -760px; }
.emoji-1F918-1F3FE { background-position: -760px -760px; }
-.emoji-1F918-1F3FF { background-position: -780px 0px; }
+.emoji-1F918-1F3FF { background-position: -780px 0; }
.emoji-1F980 { background-position: -780px -20px; }
.emoji-1F981 { background-position: -780px -40px; }
.emoji-1F982 { background-position: -780px -60px; }
@@ -1558,7 +1558,7 @@
.emoji-24C2 { background-position: -780px -720px; }
.emoji-25AA { background-position: -780px -740px; }
.emoji-25AB { background-position: -780px -760px; }
-.emoji-25B6 { background-position: 0px -780px; }
+.emoji-25B6 { background-position: 0 -780px; }
.emoji-25C0 { background-position: -20px -780px; }
.emoji-25FB { background-position: -40px -780px; }
.emoji-25FC { background-position: -60px -780px; }
@@ -1598,7 +1598,7 @@
.emoji-264D { background-position: -740px -780px; }
.emoji-264E { background-position: -760px -780px; }
.emoji-264F { background-position: -780px -780px; }
-.emoji-2650 { background-position: -800px 0px; }
+.emoji-2650 { background-position: -800px 0; }
.emoji-2651 { background-position: -800px -20px; }
.emoji-2652 { background-position: -800px -40px; }
.emoji-2653 { background-position: -800px -60px; }
@@ -1638,7 +1638,7 @@
.emoji-26F0 { background-position: -800px -740px; }
.emoji-26F1 { background-position: -800px -760px; }
.emoji-26F2 { background-position: -800px -780px; }
-.emoji-26F3 { background-position: 0px -800px; }
+.emoji-26F3 { background-position: 0 -800px; }
.emoji-26F4 { background-position: -20px -800px; }
.emoji-26F5 { background-position: -40px -800px; }
.emoji-26F7 { background-position: -60px -800px; }
@@ -1679,7 +1679,7 @@
.emoji-270D-1F3FD { background-position: -760px -800px; }
.emoji-270D-1F3FE { background-position: -780px -800px; }
.emoji-270D-1F3FF { background-position: -800px -800px; }
-.emoji-270F { background-position: -820px 0px; }
+.emoji-270F { background-position: -820px 0; }
.emoji-2712 { background-position: -820px -20px; }
.emoji-2714 { background-position: -820px -40px; }
.emoji-2716 { background-position: -820px -60px; }
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 35df9a61c86..84eefd01cfe 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -6,7 +6,7 @@
font-size: $gl-font-size;
padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
border-bottom: 1px solid $table-border-color;
- color: #7f8fa4;
+ color: $list-text-color;
&.event-inline {
.avatar {
@@ -21,7 +21,7 @@
}
a {
- color: #4c4e54;
+ color: $gl-dark-link-color;
}
.avatar {
@@ -31,10 +31,7 @@
.event-title {
@include str-truncated(calc(100% - 174px));
font-weight: 600;
-
- .author_name {
- color: #333;
- }
+ color: $list-text-color;
}
.event-body {
@@ -63,7 +60,7 @@
.note-image-attach {
margin-top: 4px;
- margin-left: 0px;
+ margin-left: 0;
max-width: 200px;
float: none;
}
@@ -83,10 +80,10 @@
.event_icon {
position: relative;
float: right;
- border: 1px solid #EEE;
+ border: 1px solid #eee;
padding: 5px;
@include border-radius(5px);
- background: #F9F9F9;
+ background: #f9f9f9;
margin-left: 10px;
top: -6px;
img {
@@ -94,7 +91,7 @@
}
}
- &:last-child { border:none }
+ &:last-child { border: none }
.event_commits {
li {
@@ -138,7 +135,7 @@
@include str-truncated(100%);
padding: 5px 0;
font-size: 13px;
- float:left;
+ float: left;
margin-right: -150px;
padding-right: 150px;
line-height: 20px;
@@ -160,7 +157,7 @@
.event-body {
margin: 0;
- border-left: 2px solid #DDD;
+ border-left: 2px solid #ddd;
padding-left: 10px;
}
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index c3b10d144e1..4e5c4ed84b6 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -6,11 +6,11 @@
font-size: 14px;
padding: 5px;
border-bottom: 1px solid $border-color;
- background: #EEE;
+ background: #eee;
}
.network-graph {
- background: #FFF;
+ background: #fff;
height: 500px;
overflow-y: scroll;
overflow-x: hidden;
diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss
index 3df4bb84bd2..6a99cd9cb94 100644
--- a/app/assets/stylesheets/pages/import.scss
+++ b/app/assets/stylesheets/pages/import.scss
@@ -1,6 +1,6 @@
i.icon-gitorious {
display: inline-block;
- background-position: 0px 0px;
+ background-position: 0 0;
background-size: contain;
background-repeat: no-repeat;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index ef62f069dc2..88c1b614c74 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,34 +1,3 @@
-@media (max-width: $screen-sm-max) {
- .issuable-affix {
- margin-top: 20px;
- }
-}
-
-@media (max-width: $screen-md-max) {
- .issuable-affix {
- position: static;
- }
-}
-
-@media (min-width: $screen-md-max) {
- .issuable-affix {
- &.affix-top {
- position: static;
- }
-
- &.affix {
- position: fixed;
- top: 70px;
- margin-right: 35px;
-
- &.no-affix {
- position: relative;
- top: 0;
- }
- }
- }
-}
-
.issuable-details {
section {
.issuable-discussion {
@@ -54,21 +23,29 @@
padding: 6px 10px;
}
}
+
+ &.has-labels {
+ margin-bottom: -5px;
+ }
}
.issuable-sidebar {
+ a {
+ color: inherit;
+ }
+
.block {
@include clearfix;
- padding: $gl-padding 0;
+ padding: $gl-padding 0;
border-bottom: 1px solid $border-gray-light;
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
width: $gutter_inner_width;
- overflow-x: hidden;
// --
- &:first-child {
- padding-top: 5px;
+ &.issuable-sidebar-header {
+ padding-top: 0;
+ padding-bottom: 10px;
}
&:last-child {
@@ -76,7 +53,6 @@
}
span {
- margin-top: 7px;
display: inline-block;
}
@@ -85,12 +61,11 @@
}
.issuable-count {
-
+ margin-top: 7px;
}
.gutter-toggle {
margin-left: 20px;
- border-left: 1px solid $border-gray-light;
padding-left: 10px;
&:hover {
@@ -101,24 +76,24 @@
.title {
color: $gl-text-color;
- margin-bottom: 8px;
+ margin-bottom: 10px;
+ line-height: 1;
.avatar {
margin-left: 0;
}
- label {
- font-weight: normal;
- margin-right: 4px;
- }
-
.edit-link {
color: $gl-gray;
+
+ &:hover {
+ color: $md-link-color;
+ }
}
}
.cross-project-reference {
- color: $gl-link-color;
+ color: inherit;
span {
white-space: nowrap;
@@ -146,30 +121,56 @@
.btn-clipboard {
color: $gl-gray;
}
-
- .participants .avatar {
- margin-top: 6px;
- margin-right: 2px;
- }
}
-
.right-sidebar {
position: fixed;
top: 58px;
+ bottom: 0;
right: 0;
- height: 100%;
- transition-duration: .3s;
+ transition: width .3s;
background: $gray-light;
- overflow: scroll;
padding: 10px 20px;
&.right-sidebar-expanded {
width: $gutter_width;
- hr {
+ .value {
+ line-height: 1;
+
+ .assign-yourself {
+ margin-top: 10px;
+ font-weight: normal;
+ display: block;
+ }
+ }
+
+ .bold {
+ font-weight: 600;
+ }
+
+ .sidebar-collapsed-icon {
display: none;
}
+
+ .gutter-toggle {
+ margin-top: 7px;
+ border-left: 1px solid $border-gray-light;
+ }
+
+ .assignee .avatar {
+ float: left;
+ margin-right: 10px;
+ margin-bottom: 0;
+ margin-left: 0;
+ }
+
+ .username {
+ display: block;
+ margin-top: 4px;
+ font-size: 13px;
+ font-weight: normal;
+ }
}
.subscribe-button {
@@ -179,63 +180,54 @@
}
&.right-sidebar-collapsed {
+ /* Extra small devices (phones, less than 768px) */
+ display: none;
+ /* Small devices (tablets, 768px and up) */
+ @media (min-width: $screen-sm-min) {
+ display: block
+ }
+
width: $sidebar_collapsed_width;
padding-top: 0;
- overflow-x: hidden;
-
- hr {
- margin: 0;
- color: $gray-normal;
- border-color: $gray-normal;
- width: 62px;
- margin-left: -20px
- }
.block {
+ width: $sidebar_collapsed_width - 1px;
+ margin-left: -19px;
+ padding: 15px 0 0;
border-bottom: none;
- padding: 15px 0 0 0;
+ overflow: hidden;
}
- }
- .btn {
- background: $gray-normal;
- border: 1px solid $border-gray-normal;
- &:hover {
- background: $gray-dark;
- border: 1px solid $border-gray-dark;
+ .participants {
+ border-bottom: 1px solid $border-gray-light;
}
- }
- &.right-sidebar-collapsed {
- .issuable-count,
- .issuable-nav,
- .assignee > *,
- .milestone > *,
- .labels > *,
- .participants > *,
- .light > *,
- .project-reference > * {
+ .hide-collapsed {
display: none;
}
.gutter-toggle {
- margin-left: -$gutter_inner_width + 4;
+ width: 100%;
+ margin-left: 0;
+ padding-left: 25px;
}
.sidebar-collapsed-icon {
display: block;
- float: left;
- width: 62px;
+ width: 100%;
text-align: center;
- margin-left: -19px;
padding-bottom: 10px;
- color: #999999;
+ color: #999;
span {
display: block;
margin-top: 0;
}
+ .author {
+ display: none;
+ }
+
.btn-clipboard {
border: none;
@@ -244,23 +236,83 @@
}
i {
- color: #999999;
+ color: #999;
}
}
+ }
+ .sidebar-collapsed-user {
+ padding-bottom: 0;
+ margin-bottom: 10px;
}
+ }
+ .btn {
+ background: $gray-normal;
+ border: 1px solid $border-gray-normal;
+ &:hover {
+ background: $gray-dark;
+ border: 1px solid $border-gray-dark;
+ }
}
- &.right-sidebar-expanded {
- .sidebar-collapsed-icon {
- display: none;
+ a:not(.btn) {
+ &:hover {
+ color: $md-link-color;
+ text-decoration: none;
}
}
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ padding-top: 6px;
+ }
+
+ .open .dropdown-menu {
+ width: 100%;
+ }
+}
+
+.btn-default.gutter-toggle {
+ margin-top: 4px;
}
.detail-page-description {
small {
color: $gray-darkest;
}
-} \ No newline at end of file
+}
+
+.edited-text {
+ color: $gray-darkest;
+
+ .author_link {
+ color: $gray-darkest;
+ }
+}
+
+.participants-list {
+ margin: -5px;
+}
+
+.participants-author {
+ display: inline-block;
+ padding: 5px;
+
+ .author_link {
+ display: block;
+ }
+
+ .avatar.avatar-inline {
+ margin: 0;
+ }
+}
+
+.participants-more {
+ margin-top: 5px;
+ margin-left: 5px;
+
+ a {
+ color: #8c8c8c;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 1cc853dd4f5..6a1d28590c2 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -3,14 +3,8 @@
padding: 10px $gl-padding;
position: relative;
- .issue-title {
- margin-bottom: 5px;
- font-size: $list-font-size;
- font-weight: 600;
- }
-
- .issue-info {
- color: $gl-gray;
+ .title {
+ margin-bottom: 2px;
}
.issue-check {
@@ -24,7 +18,7 @@
display: inline-block;
}
- .issue-no-comments, .issue-no-votes {
+ .issue-no-comments {
opacity: 0.5;
}
}
@@ -55,7 +49,7 @@ form.edit-issue {
margin: 0;
}
-.merge-requests-title {
+.merge-requests-title, .related-branches-title {
font-size: 16px;
font-weight: 600;
}
@@ -74,18 +68,18 @@ form.edit-issue {
.merge-request,
.issue {
&.today {
- background: #EFE;
- border-color: #CEC;
+ background: #efe;
+ border-color: #cec;
}
&.closed {
- background: #F9F9F9;
- border-color: #E5E5E5;
+ background: #f9f9f9;
+ border-color: #e5e5e5;
}
&.merged {
- background: #F9F9F9;
- border-color: #E5E5E5;
+ background: #f9f9f9;
+ border-color: #e5e5e5;
}
}
@@ -105,18 +99,17 @@ form.edit-issue {
.btn {
width: 100%;
- margin-top: -1px;
&:first-child:not(:last-child) {
- border-radius: 4px 4px 0 0;
+
}
&:not(:first-child):not(:last-child) {
- border-radius: 0;
+ margin-top: 10px;
}
&:last-child:not(:first-child) {
- border-radius: 0 0 4px 4px;
+ margin-top: 10px;
}
}
}
@@ -137,6 +130,14 @@ form.edit-issue {
}
.issue-closed-by-widget {
- color: $secondary-text;
+ color: $gl-text-color;
margin-left: 52px;
}
+
+.editor-details {
+ display: block;
+
+ @media (min-width: $screen-sm-min) {
+ display: inline-block;
+ }
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 1c78aafdb87..4e02ec4e891 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -7,6 +7,45 @@
display: inline-block;
margin-right: 10px;
}
+
+ &.suggest-colors-dropdown {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ border-radius: $border-radius-base;
+ overflow: hidden;
+
+ a {
+ @include border-radius(0);
+ width: (100% / 7);
+ margin-right: 0;
+ margin-bottom: -5px;
+ }
+ }
+}
+
+.dropdown-new-label {
+ .dropdown-content {
+ max-height: 260px;
+ }
+}
+
+.dropdown-label-color-input {
+ position: relative;
+ margin-bottom: 10px;
+
+ &.is-active {
+ padding-left: 32px;
+ }
+}
+
+.dropdown-label-color-preview {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 32px;
+ height: 32px;
+ border-top-left-radius: $border-radius-base;
+ border-bottom-left-radius: $border-radius-base;
}
.label-row {
@@ -19,3 +58,14 @@
.color-label {
padding: 3px 4px;
}
+
+.label-subscription {
+ display: inline-block;
+}
+
+.dropdown-labels-error {
+ padding: 5px 10px;
+ margin-bottom: 10px;
+ background-color: $gl-danger;
+ color: $white-light;
+}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index f9c6f1b39f9..777bcbca5c3 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -8,6 +8,10 @@
max-width: none;
}
+ .flash-container {
+ margin-bottom: $gl-padding;
+ }
+
.brand-holder {
font-size: 18px;
line-height: 1.5;
@@ -24,7 +28,7 @@
img {
max-width: 100%;
- margin-bottom: 30px;
+ margin-bottom: 30px;
}
a {
@@ -35,13 +39,13 @@
.login-box{
background: #fafafa;
border-radius: 10px;
- box-shadow: 0 0px 2px #CCC;
+ box-shadow: 0 0 2px #ccc;
padding: 15px;
.login-heading h3 {
font-weight: 300;
line-height: 1.5;
- margin: 0 0 10px 0;
+ margin: 0 0 10px;
}
.login-footer {
@@ -70,7 +74,7 @@
&.top {
@include border-radius(5px 5px 0 0);
- margin-bottom: 0px;
+ margin-bottom: 0;
}
&.bottom {
@@ -81,12 +85,12 @@
&.middle {
border-top: 0;
- margin-bottom:0px;
+ margin-bottom: 0;
@include border-radius(0);
}
&:active, &:focus {
- background-color: #FFF;
+ background-color: #fff;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 12fb030760c..7ff63ca20b6 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -113,7 +113,7 @@
}
.mr-widget-footer {
- border-top: 1px solid #EEE;
+ border-top: 1px solid #eee;
}
.ci-coverage {
@@ -148,22 +148,15 @@
position: relative;
.merge-request-title {
- margin-bottom: 5px;
- font-size: $list-font-size;
- font-weight: 600;
- }
-
- .merge-request-info {
- color: $gl-gray;
+ margin-bottom: 2px;
}
-
}
.merge-request-labels {
display: inline-block;
}
- .merge-request-no-comments, .merge-request-no-votes {
+ .merge-request-no-comments {
opacity: 0.5;
}
}
@@ -229,7 +222,7 @@
margin-bottom: 20px;
span {
- color: #B2B2B2;
+ color: #b2b2b2;
a {
color: $md-link-color;
@@ -238,4 +231,8 @@
}
}
-
+.builds {
+ .table-holder {
+ overflow-x: scroll;
+ }
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 9144a83647d..d0e72a4422c 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -19,10 +19,11 @@ li.milestone {
width: 105px;
}
- .issue-row {
+ .issuable-row {
.color-label {
border-radius: 2px;
padding: 3px !important;
+ margin-right: 7px;
}
// Issue title
@@ -39,25 +40,20 @@ li.milestone {
margin-right: 10px;
}
- .time-elapsed {
+ .remaining-days {
color: $orange-light;
}
}
-.issues-sortable-list {
- .issue-detail {
+.issues-sortable-list, .merge_requests-sortable-list {
+ .issuable-detail {
display: block;
+ margin-top: 7px;
- .issue-number{
+ .issuable-number {
color: rgba(0,0,0,0.44);
margin-right: 5px;
}
- .color-label {
- padding: 6px 10px;
- margin-right: 7px;
- margin-top: 10px;
- }
-
.avatar {
float: none;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 158c2a47862..655f88b0c2c 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -26,7 +26,7 @@
display: none;
}
-.new_note, .edit_note {
+.new_note, .note-edit-form {
.note-form-actions {
margin-top: $gl-padding;
}
@@ -71,8 +71,6 @@
}
.note-form-actions {
- background: #fff;
-
.note-form-option {
margin-top: 8px;
margin-left: 30px;
@@ -156,7 +154,7 @@
.comment-hints {
color: #999;
- background: #FFF;
+ background: #fff;
padding: 7px;
margin-top: -7px;
border: 1px solid $border-color;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 19ead07c06a..4bd2016bdcf 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -3,22 +3,34 @@
*/
@-webkit-keyframes targe3-note {
- from { background:#fffff0; }
- 50% { background:#ffffd3; }
- to { background:#fffff0; }
+ from { background: #fffff0; }
+ 50% { background: #ffffd3; }
+ to { background: #fffff0; }
}
ul.notes {
display: block;
list-style: none;
- margin: 0px;
- padding: 0px;
+ margin: 0;
+ padding: 0;
+
+ .timeline-icon {
+ float: left;
+ }
+
+ .timeline-content {
+ margin-left: 55px;
+ }
+
+ .note_created_ago, .note-updated-at {
+ white-space: nowrap;
+ }
.system-note {
font-size: 14px;
padding-top: 10px;
padding-bottom: 10px;
- background: #FDFDFD;
+ background: #fdfdfd;
.timeline-icon {
.avatar {
@@ -81,12 +93,24 @@ ul.notes {
.discussion {
overflow: hidden;
display: block;
- position:relative;
+ position: relative;
}
.note {
display: block;
- position:relative;
+ position: relative;
+
+ &.is-editting {
+ .note-header,
+ .note-text,
+ .edited-text {
+ display: none;
+ }
+
+ .note-edit-form {
+ display: block;
+ }
+ }
.note-body {
overflow: auto;
@@ -96,6 +120,13 @@ ul.notes {
word-wrap: break-word;
@include md-typography;
+ // On diffs code should wrap nicely and not overflow
+ pre {
+ code {
+ white-space: pre-wrap;
+ }
+ }
+
// Reset ul style types since we're nested inside a ul already
& > ul {
list-style-type: disc;
@@ -117,7 +148,7 @@ ul.notes {
hr {
// Darken 'whitesmoke' a bit to make it more visible in note bodies
- border-color: darken(#F5F5F5, 8%);
+ border-color: darken(#f5f5f5, 8%);
margin: 10px 0;
}
}
@@ -151,9 +182,10 @@ ul.notes {
border-left: none;
&.notes_line {
+ vertical-align: middle;
text-align: center;
padding: 10px 0;
- background: #FFF;
+ background: #fff;
color: $text-color;
}
&.notes_line2 {
@@ -219,7 +251,7 @@ ul.notes {
.add-diff-note {
margin-top: -4px;
@include border-radius(40px);
- background: #FFF;
+ background: #fff;
padding: 4px;
font-size: 16px;
color: $gl-link-color;
@@ -236,7 +268,7 @@ ul.notes {
&:hover {
background: $gl-info;
- color: #FFF;
+ color: #fff;
@include show-add-diff-note;
}
}
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index cc273f55222..94fbbef3c77 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -1,16 +1,18 @@
-.global-notifications-form .level-title {
- font-size: 15px;
- color: #333;
- font-weight: bold;
+.notification-list-item {
+ line-height: 34px;
}
-.notification-icon-holder {
- width: 20px;
- float: left;
+.notification {
+ position: relative;
+ top: 1px;
+
+ > .fa {
+ font-size: 18px;
+ }
}
.ns-part {
- color: $gl-primary;
+ color: $gl-text-green;
}
.ns-watch {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 95fc26a608a..a9656e5cae7 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -1,16 +1,27 @@
-.account-page {
- fieldset {
- margin-bottom: 15px;
- padding-bottom: 15px;
- }
-}
-
.profile-avatar-form-option {
hr {
margin: 10px 0;
}
}
+.avatar-image {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ margin-bottom: 0;
+ }
+}
+
+.avatar-file-name {
+ position: relative;
+ top: 2px;
+ display: inline-block;
+}
+
+.account-btn-link,
+.profile-settings-sidebar a {
+ color: $md-link-color;
+}
+
.oauth-buttons {
.btn-group {
margin-right: 10px;
@@ -19,7 +30,7 @@
.btn {
line-height: 40px;
height: 42px;
- padding: 0px 12px;
+ padding: 0 12px;
img {
width: 32px;
@@ -42,6 +53,18 @@
}
}
+.account-well {
+ padding: 10px;
+ background-color: $help-well-bg;
+ border: 1px solid $help-well-border;
+ border-radius: $border-radius-base;
+
+ ul {
+ padding-left: 20px;
+ margin-bottom: 0;
+ }
+}
+
.calendar-hint {
margin-top: -12px;
float: right;
@@ -51,9 +74,17 @@
.profile-link-holder {
display: inline;
+ a {
+ color: $blue-dark;
+ text-decoration: none;
+ }
+}
+
+// Middle dot divider between each element in a list of items.
+.middle-dot-divider {
&:after {
- content: "\00B7";
- padding: 0px 6px;
+ content: "\00B7"; // Middle Dot
+ padding: 0 6px;
font-weight: bold;
}
@@ -63,9 +94,127 @@
padding: 0;
}
}
+}
+
+.profile-user-bio {
+ // Limits the width of the user bio for readability.
+ max-width: 750px;
+ margin: auto;
+}
+
+.user-avatar-button {
+ .file-name {
+ display: inline-block;
+ padding-left: 10px;
+ }
+}
+
+.key-list-item {
+ .key-list-item-info {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ }
+ }
+
+ .description {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+}
+
+.key-icon {
+ color: $ssh-key-icon-color;
+ font-size: $ssh-key-icon-size;
+ line-height: 42px;
+}
+
+.key-created-at {
+ line-height: 42px;
+}
+.profile-settings-content {
a {
- color: $blue-dark;
- text-decoration: none;
+ color: $md-link-color;
+ }
+}
+
+.change-username-title {
+ color: $gl-warning;
+}
+
+.remove-account-title {
+ color: $gl-danger;
+}
+
+.provider-btn-group {
+ display: inline-block;
+ margin-right: 10px;
+ border: 1px solid $provider-btn-group-border;
+ border-radius: 3px;
+
+ &:last-child {
+ margin-right: 0;
+ }
+}
+
+.provider-btn-image {
+ display: inline-block;
+ padding: 5px 10px;
+ border-right: 1px solid $provider-btn-group-border;
+
+ > img {
+ width: 20px;
+ }
+}
+
+.provider-btn {
+ display: inline-block;
+ padding: 5px 10px;
+ margin-left: -3px;
+ line-height: 22px;
+ background-color: $gray-light;
+
+ &.not-active {
+ color: $provider-btn-not-active-color;
+ }
+}
+
+.profile-settings-message {
+ line-height: 32px;
+ color: $warning-message-color;
+ background-color: $warning-message-bg;
+ border: 1px solid $warning-message-border;
+ border-radius: $border-radius-base;
+}
+
+.oauth-applications {
+ form {
+ display: inline-block;
+ }
+
+ .last-heading {
+ width: 105px;
+ }
+}
+
+.modal-profile-crop {
+ .modal-dialog {
+ width: 380px;
+
+ @media (max-width: $screen-sm-min) {
+ width: auto;
+ }
+
+ }
+
+ .profile-crop-image-container {
+ height: 300px;
+ margin: 0 auto;
+ }
+
+ .crop-controls {
+ padding: 10px 0 0;
+ text-align: center;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 542ac896f6b..4e6aa8cd1a6 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -32,6 +32,14 @@
.cover-controls {
.project-settings-dropdown {
margin-left: 10px;
+ display: inline-block;
+
+ .dropdown-menu {
+ left: auto;
+ width: auto;
+ right: 0;
+ max-width: 240px;
+ }
}
}
@@ -48,10 +56,6 @@
}
}
- .project-home-dropdown {
- margin: 13px 0px 0;
- }
-
.notifications-btn {
margin-top: -28px;
@@ -64,31 +68,9 @@
}
}
- .project-home-desc {
- h1 {
- color: #313236;
- margin: 0;
- margin-bottom: 6px;
- font-size: 23px;
- font-weight: normal;
- }
-
- .visibility-icon {
- display: inline-block;
- margin-left: 5px;
- font-size: 18px;
- color: $gray;
- }
-
- p {
- padding: 0 $gl-padding;
- color: #5c5d5e;
- }
- }
-
.project-repo-buttons {
margin-top: 20px;
- margin-bottom: 0px;
+ margin-bottom: 0;
.count-buttons {
display: block;
@@ -143,7 +125,7 @@
left: 1px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
- border-right-color: #FFF;
+ border-right-color: #fff;
}
}
.count {
@@ -165,10 +147,10 @@
cursor: pointer;
background-image: none;
white-space: nowrap;
- margin: 0 11px 0px 4px;
+ margin: 0 11px 0 4px;
&:hover {
- background: #FFF;
+ background: #fff;
}
}
}
@@ -180,30 +162,7 @@
margin-right: 12px;
a {
- margin: -1px !important;
- }
-}
-
-.dropdown-menu {
- @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
- @include border-radius (0px);
-
- border: none;
- padding: 16px 0;
- font-size: 14px;
- font-weight: 100;
-
- li a {
- color: #5f697a;
- line-height: 30px;
-
- &:hover {
- background-color: #3084bb !important;
- }
- }
-
- i {
- margin-right: 8px;
+ margin: -1px;
}
}
@@ -236,7 +195,7 @@
}
.project_member_row form {
- margin: 0px;
+ margin: 0;
}
.transfer-project .select2-container {
@@ -263,13 +222,17 @@
padding: 0;
background: transparent;
border: none;
- line-height: 42px;
+ line-height: 36px;
margin: 0;
> li + li:before {
padding: 0 3px;
color: #999;
}
+
+ a {
+ color: $gl-dark-link-color;
+ }
}
.last-push-widget {
@@ -312,11 +275,11 @@ table.table.protected-branches-list tr.no-border {
padding-bottom: 4px;
ul.nav {
- display:inline-block;
+ display: inline-block;
}
.nav li {
- display:inline;
+ display: inline;
}
.nav > li > a {
@@ -329,11 +292,11 @@ table.table.protected-branches-list tr.no-border {
}
li {
- display:inline;
+ display: inline;
}
a {
- float:left;
+ float: left;
font-size: 17px;
}
@@ -352,7 +315,7 @@ pre.light-well {
}
.git-empty {
- margin: 0 7px 0 7px;
+ margin: 0 7px;
h5 {
color: #5c5d5e;
@@ -396,15 +359,10 @@ pre.light-well {
.project-full-name {
@include str-truncated;
- font-weight: 600;
- color: #4c4e54;
}
- .project-controls {
- float: right;
- color: $gl-gray;
+ .controls {
line-height: 40px;
- color: #7f8fa4;
a:hover {
text-decoration: none;
@@ -414,16 +372,6 @@ pre.light-well {
margin-left: 10px;
}
}
-
- .project-description {
- color: #7f8fa4;
-
- p {
- @include str-truncated;
- margin-bottom: 0;
- color: #7f8fa4;
- }
- }
}
.bottom {
@@ -503,7 +451,7 @@ pre.light-well {
.form-control {
@extend .monospace;
- background: #FFF;
+ background: #fff;
font-size: 14px;
margin-left: -1px;
cursor: auto;
@@ -513,16 +461,16 @@ pre.light-well {
.cannot-be-merged,
.cannot-be-merged:hover {
- color: #E62958;
+ color: #e62958;
margin-top: 2px;
}
.private-forks-notice .private-fork-icon {
i:nth-child(1) {
- color: #2AA056;
+ color: #2aa056;
}
i:nth-child(2) {
- color: #FFFFFF;
+ color: #fff;
}
}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index a9111a7388f..eec22c5dc96 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -1,7 +1,7 @@
.runner-state {
padding: 6px 12px;
margin-right: 10px;
- color: #FFF;
+ color: #fff;
&.runner-state-shared {
background: #32b186;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 3aaa96da609..b6e45024644 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -1,8 +1,12 @@
.search-results {
.search-result-row {
- border-bottom: 1px solid #DDD;
- padding-bottom: 15px;
- margin-bottom: 15px;
+ border-bottom: 1px solid $border-color;
+ padding-bottom: $gl-padding;
+ margin-bottom: $gl-padding;
+
+ &:last-child {
+ border-bottom: none;
+ }
}
}
@@ -12,7 +16,7 @@
margin-bottom: 20px;
input {
- border-color: #BBB;
+ border-color: #bbb;
font-weight: bold;
}
}
diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss
index 92d84d9640f..bed6470dbd3 100644
--- a/app/assets/stylesheets/pages/sherlock.scss
+++ b/app/assets/stylesheets/pages/sherlock.scss
@@ -13,13 +13,13 @@ table .sherlock-code {
}
.sherlock-line-samples-table {
- margin-bottom: 0px !important;
+ margin-bottom: 0 !important;
thead tr th,
tbody tr td {
font-size: 13px !important;
text-align: right;
- padding: 0px 10px !important;
+ padding: 0 10px !important;
}
}
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 1430d01859d..639d639d5b0 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -2,30 +2,6 @@
padding: 2px;
}
-
-.snippet-row {
- .snippet-title {
- font-size: 15px;
- font-weight: bold;
- line-height: 20px;
- margin-bottom: 2px;
-
- .monospace {
- font-weight: normal;
- }
- }
-
- .snippet-info {
- color: #888;
- font-size: 13px;
- line-height: 24px;
-
- a {
- color: #888;
- }
- }
-}
-
.snippet-holder {
margin-bottom: -$gl-padding;
@@ -50,5 +26,13 @@
margin-right: 10px;
font-size: $gl-font-size;
border: 1px solid;
- line-height: 40px;
+ line-height: 32px;
+}
+
+.markdown-snippet-copy {
+ position: fixed;
+ top: -10px;
+ left: -10px;
+ max-height: 0;
+ max-width: 0;
}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index b9be47e7700..85a0304196c 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -16,7 +16,7 @@
#contributors {
.contributors-list {
- margin: 0 0 10px 0;
+ margin: 0 0 10px;
list-style: none;
padding: 0;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 4b6ef035673..5e5e38a0ba6 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,54 +1,58 @@
-.ci-status {
- padding: 2px 7px;
- margin-right: 5px;
- border: 1px solid #EEE;
- white-space: nowrap;
- @include border-radius(4px);
+.container-fluid .content {
+ .ci-status {
+ padding: 2px 7px;
+ margin-right: 5px;
+ border: 1px solid #eee;
+ white-space: nowrap;
+ @include border-radius(4px);
- &:hover {
- text-decoration: none;
- }
+ &:hover {
+ text-decoration: none;
+ }
- &.ci-failed {
- color: $gl-danger;
- border-color: $gl-danger;
- }
+ &.ci-failed {
+ color: $gl-danger;
+ border-color: $gl-danger;
+ }
- &.ci-success {
- color: $gl-success;
- border-color: $gl-success;
- }
+ &.ci-success {
+ color: $gl-success;
+ border-color: $gl-success;
+ }
- &.ci-info {
- color: $gl-info;
- border-color: $gl-info;
- }
+ &.ci-info {
+ color: $gl-info;
+ border-color: $gl-info;
+ }
- &.ci-disabled {
- color: $gl-gray;
- border-color: $gl-gray;
+ &.ci-canceled,
+ &.ci-skipped,
+ &.ci-disabled {
+ color: $gl-gray;
+ border-color: $gl-gray;
+ }
+
+ &.ci-pending,
+ &.ci-running {
+ color: $gl-warning;
+ border-color: $gl-warning;
+ }
}
- &.ci-pending,
- &.ci-running {
+ .ci-status-icon-success {
+ color: $gl-success;
+ }
+ .ci-status-icon-failed {
+ color: $gl-danger;
+ }
+ .ci-status-icon-running,
+ .ci-status-icon-pending {
color: $gl-warning;
- border-color: $gl-warning;
}
-}
-
-.ci-status-icon-success {
- @extend .cgreen;
-}
-.ci-status-icon-failed {
- @extend .cred;
-}
-.ci-status-icon-running,
-.ci-status-icon-pending {
- // These are standard text color
-}
-.ci-status-icon-canceled,
-.ci-status-icon-disabled,
-.ci-status-icon-not-found,
-.ci-status-icon-skipped {
- @extend .cgray;
+ .ci-status-icon-canceled,
+ .ci-status-icon-disabled,
+ .ci-status-icon-not-found,
+ .ci-status-icon-skipped {
+ color: $gl-gray;
+ }
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 2f57f21963d..f983e9829e6 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -8,49 +8,14 @@
.badge.todos-pending-count {
background-color: #7f8fa4;
margin-top: -5px;
+ font-weight: normal;
}
}
}
-.todos {
- .panel {
- border-top: none;
- margin-bottom: 0;
- }
-}
-
.todo-item {
- font-size: $gl-font-size;
- padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
- border-bottom: 1px solid $table-border-color;
- color: #7f8fa4;
-
- &.todo-inline {
- .avatar {
- position: relative;
- top: -2px;
- }
-
- .todo-title {
- line-height: 40px;
- }
- }
-
- a {
- color: #4c4e54;
- }
-
- .avatar {
- margin-left: -($gl-avatar-size + $gl-padding-top);
- }
-
.todo-title {
@include str-truncated(calc(100% - 174px));
- font-weight: 600;
-
- .author_name {
- color: #333;
- }
}
.todo-body {
@@ -79,7 +44,7 @@
.note-image-attach {
margin-top: 4px;
- margin-left: 0px;
+ margin-left: 0;
max-width: 200px;
float: none;
}
@@ -88,17 +53,7 @@
margin-bottom: 0;
}
}
-
- .todo-note-icon {
- color: #777;
- float: left;
- font-size: $gl-font-size;
- line-height: 16px;
- margin-right: 5px;
- }
}
-
- &:last-child { border:none }
}
@media (max-width: $screen-xs-max) {
@@ -117,7 +72,7 @@
.todo-body {
margin: 0;
- border-left: 2px solid #DDD;
+ border-left: 2px solid #ddd;
padding-left: 10px;
}
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index c7411617cb3..25b5e95583e 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -21,7 +21,7 @@
&:hover {
td {
- background: $hover;
+ background: $row-hover;
}
cursor: pointer;
}
@@ -41,12 +41,12 @@
vertical-align: middle;
i, a {
- color: $gl-link-color;
+ color: $gl-dark-link-color;
}
img {
position: relative;
- top:-1px;
+ top: -1px;
}
}
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index 185f3622e64..587bd6a1e8a 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -3,4 +3,15 @@
margin: 35px 0 20px;
font-weight: bold;
}
+
+ .example {
+ &:before {
+ content: "Example";
+ color: #bbb;
+ }
+
+ padding: 15px;
+ border: 1px dashed #ddd;
+ margin-bottom: 15px;
+ }
}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index cdf514197cb..dfaeba41cf6 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -4,8 +4,3 @@
margin-right: auto;
padding-right: 7px;
}
-
-.wiki-last-edit-by {
- font-size: 80%;
- font-weight: normal;
-}
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 9a50096c0d0..3f28e402929 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -2,38 +2,38 @@
// color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg
// see also: https://gist.github.com/jasonm23/2868981
- $black: #000000;
+ $black: #000;
$red: #cd0000;
$green: #00cd00;
$yellow: #cdcd00;
- $blue: #0000ee; // according to wikipedia, this is the xterm standard
+ $blue: #00e; // according to wikipedia, this is the xterm standard
//$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile)
$magenta: #cd00cd;
$cyan: #00cdcd;
$white: #e5e5e5;
$l-black: #7f7f7f;
- $l-red: #ff0000;
- $l-green: #00ff00;
- $l-yellow: #ffff00;
+ $l-red: #f00;
+ $l-green: #0f0;
+ $l-yellow: #ff0;
$l-blue: #5c5cff;
- $l-magenta: #ff00ff;
- $l-cyan: #00ffff;
- $l-white: #ffffff;
+ $l-magenta: #f0f;
+ $l-cyan: #0ff;
+ $l-white: #fff;
.term-bold {
- font-weight: bold;
+ font-weight: bold;
}
.term-italic {
- font-style: italic;
+ font-style: italic;
}
.term-conceal {
- visibility: hidden;
+ visibility: hidden;
}
.term-underline {
- text-decoration: underline;
+ text-decoration: underline;
}
.term-cross {
- text-decoration: line-through;
+ text-decoration: line-through;
}
.term-fg-black {
@@ -136,7 +136,7 @@
.xterm-fg-0 {
- color: #000000;
+ color: #000;
}
.xterm-fg-1 {
color: #800000;
@@ -163,28 +163,28 @@
color: #808080;
}
.xterm-fg-9 {
- color: #ff0000;
+ color: #f00;
}
.xterm-fg-10 {
- color: #00ff00;
+ color: #0f0;
}
.xterm-fg-11 {
- color: #ffff00;
+ color: #ff0;
}
.xterm-fg-12 {
- color: #0000ff;
+ color: #00f;
}
.xterm-fg-13 {
- color: #ff00ff;
+ color: #f0f;
}
.xterm-fg-14 {
- color: #00ffff;
+ color: #0ff;
}
.xterm-fg-15 {
- color: #ffffff;
+ color: #fff;
}
.xterm-fg-16 {
- color: #000000;
+ color: #000;
}
.xterm-fg-17 {
color: #00005f;
@@ -199,7 +199,7 @@
color: #0000d7;
}
.xterm-fg-21 {
- color: #0000ff;
+ color: #00f;
}
.xterm-fg-22 {
color: #005f00;
@@ -274,7 +274,7 @@
color: #00d7ff;
}
.xterm-fg-46 {
- color: #00ff00;
+ color: #0f0;
}
.xterm-fg-47 {
color: #00ff5f;
@@ -289,7 +289,7 @@
color: #00ffd7;
}
.xterm-fg-51 {
- color: #00ffff;
+ color: #0ff;
}
.xterm-fg-52 {
color: #5f0000;
@@ -724,7 +724,7 @@
color: #d7ffff;
}
.xterm-fg-196 {
- color: #ff0000;
+ color: #f00;
}
.xterm-fg-197 {
color: #ff005f;
@@ -739,7 +739,7 @@
color: #ff00d7;
}
.xterm-fg-201 {
- color: #ff00ff;
+ color: #f0f;
}
.xterm-fg-202 {
color: #ff5f00;
@@ -814,7 +814,7 @@
color: #ffd7ff;
}
.xterm-fg-226 {
- color: #ffff00;
+ color: #ff0;
}
.xterm-fg-227 {
color: #ffff5f;
@@ -829,7 +829,7 @@
color: #ffffd7;
}
.xterm-fg-231 {
- color: #ffffff;
+ color: #fff;
}
.xterm-fg-232 {
color: #080808;
@@ -850,7 +850,7 @@
color: #3a3a3a;
}
.xterm-fg-238 {
- color: #444444;
+ color: #444;
}
.xterm-fg-239 {
color: #4e4e4e;
@@ -901,6 +901,6 @@
color: #e4e4e4;
}
.xterm-fg-255 {
- color: #eeeeee;
+ color: #eee;
}
}
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index 2463cfa87be..e9b0972bdd8 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -6,7 +6,7 @@ class Admin::AbuseReportsController < Admin::ApplicationController
def destroy
abuse_report = AbuseReport.find(params[:id])
- abuse_report.remove_user if params[:remove_user]
+ abuse_report.remove_user(deleted_by: current_user) if params[:remove_user]
abuse_report.destroy
render nothing: true
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
new file mode 100644
index 00000000000..26cf74e4849
--- /dev/null
+++ b/app/controllers/admin/appearances_controller.rb
@@ -0,0 +1,57 @@
+class Admin::AppearancesController < Admin::ApplicationController
+ before_action :set_appearance, except: :create
+
+ def show
+ end
+
+ def preview
+ end
+
+ def create
+ @appearance = Appearance.new(appearance_params)
+
+ if @appearance.save
+ redirect_to admin_appearances_path, notice: 'Appearance was successfully created.'
+ else
+ render action: 'show'
+ end
+ end
+
+ def update
+ if @appearance.update(appearance_params)
+ redirect_to admin_appearances_path, notice: 'Appearance was successfully updated.'
+ else
+ render action: 'show'
+ end
+ end
+
+ def logo
+ @appearance.remove_logo!
+
+ @appearance.save
+
+ redirect_to admin_appearances_path, notice: 'Logo was succesfully removed.'
+ end
+
+ def header_logos
+ @appearance.remove_header_logo!
+ @appearance.save
+
+ redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.'
+ end
+
+ private
+
+ # Use callbacks to share common setup or constraints between actions.
+ def set_appearance
+ @appearance = Appearance.last || Appearance.new
+ end
+
+ # Only allow a trusted parameter "white list" through.
+ def appearance_params
+ params.require(:appearance).permit(
+ :title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache,
+ :updated_by
+ )
+ end
+end
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 04a99d8c84a..ed9f6031389 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -61,6 +61,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:session_expire_delay,
:default_project_visibility,
:default_snippet_visibility,
+ :default_group_visibility,
:restricted_signup_domains_raw,
:version_check_enabled,
:admin_notification_email,
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 4d3e48f7f81..a6db4690df0 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -5,12 +5,12 @@ class Admin::GroupsController < Admin::ApplicationController
@groups = Group.all
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present?
- @groups = @groups.page(params[:page]).per(PER_PAGE)
+ @groups = @groups.page(params[:page])
end
def show
- @members = @group.members.order("access_level DESC").page(params[:members_page]).per(PER_PAGE)
- @projects = @group.projects.page(params[:projects_page]).per(PER_PAGE)
+ @members = @group.members.order("access_level DESC").page(params[:members_page])
+ @projects = @group.projects.page(params[:projects_page])
end
def new
@@ -55,10 +55,10 @@ class Admin::GroupsController < Admin::ApplicationController
private
def group
- @group = Group.find_by(path: params[:id])
+ @group ||= Group.find_by(path: params[:id])
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar)
+ params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level)
end
end
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index d79ce2b10fe..d496f08a598 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -2,7 +2,7 @@ class Admin::LabelsController < Admin::ApplicationController
before_action :set_label, only: [:show, :edit, :update, :destroy]
def index
- @labels = Label.templates.page(params[:page]).per(PER_PAGE)
+ @labels = Label.templates.page(params[:page])
end
def show
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index ae1de06b983..4089091d569 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -1,7 +1,6 @@
class Admin::ProjectsController < Admin::ApplicationController
before_action :project, only: [:show, :transfer]
before_action :group, only: [:show, :transfer]
- before_action :repository, only: [:show, :transfer]
def index
@projects = Project.all
@@ -12,15 +11,15 @@ class Admin::ProjectsController < Admin::ApplicationController
@projects = @projects.non_archived unless params[:with_archived].present?
@projects = @projects.search(params[:name]) if params[:name].present?
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(PER_PAGE)
+ @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
end
def show
if @group
- @group_members = @group.members.order("access_level DESC").page(params[:group_members_page]).per(PER_PAGE)
+ @group_members = @group.members.order("access_level DESC").page(params[:group_members_page])
end
- @project_members = @project.project_members.page(params[:project_members_page]).per(PER_PAGE)
+ @project_members = @project.project_members.page(params[:project_members_page])
end
def transfer
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 87f4fb455b8..9abf08d0e19 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -119,10 +119,10 @@ class Admin::UsersController < Admin::ApplicationController
end
def destroy
- DeleteUserService.new(current_user).execute(user)
+ DeleteUserWorker.perform_async(current_user.id, user.id)
respond_to do |format|
- format.html { redirect_to admin_users_path }
+ format.html { redirect_to admin_users_path, notice: "The user is being deleted." }
format.json { head :ok }
end
end
@@ -150,7 +150,7 @@ class Admin::UsersController < Admin::ApplicationController
:email, :remember_me, :bio, :name, :username,
:skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password,
:extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password,
- :projects_limit, :can_create_group, :admin, :key_id
+ :projects_limit, :can_create_group, :admin, :key_id, :external
)
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index fb74919ea23..c81cb85dc1b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -6,8 +6,6 @@ class ApplicationController < ActionController::Base
include GitlabRoutingHelper
include PageLayoutHelper
- PER_PAGE = 20
-
before_action :authenticate_user_from_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
@@ -25,7 +23,6 @@ class ApplicationController < ActionController::Base
helper_method :abilities, :can?, :current_application_settings
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?
- helper_method :repository, :can_collaborate_with_project?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -118,47 +115,6 @@ class ApplicationController < ActionController::Base
abilities.allowed?(object, action, subject)
end
- def project
- unless @project
- namespace = params[:namespace_id]
- id = params[:project_id] || params[:id]
-
- # Redirect from
- # localhost/group/project.git
- # to
- # localhost/group/project
- #
- if id =~ /\.git\Z/
- redirect_to request.original_url.gsub(/\.git\/?\Z/, '') and return
- end
-
- project_path = "#{namespace}/#{id}"
- @project = Project.find_with_namespace(project_path)
-
- if @project and can?(current_user, :read_project, @project)
- if @project.path_with_namespace != project_path
- redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) and return
- end
- @project
- elsif current_user.nil?
- @project = nil
- authenticate_user!
- else
- @project = nil
- render_404 and return
- end
- end
- @project
- end
-
- def repository
- @repository ||= project.repository
- end
-
- def authorize_project!(action)
- return access_denied! unless can?(current_user, action, project)
- end
-
def access_denied!
render "errors/access_denied", layout: "errors", status: 404
end
@@ -167,14 +123,6 @@ class ApplicationController < ActionController::Base
render "errors/git_not_found.html", layout: "errors", status: 404
end
- def method_missing(method_sym, *arguments, &block)
- if method_sym.to_s =~ /\Aauthorize_(.*)!\z/
- authorize_project!($1.to_sym)
- else
- super
- end
- end
-
def render_403
head :forbidden
end
@@ -183,10 +131,6 @@ class ApplicationController < ActionController::Base
render file: Rails.root.join("public", "404"), layout: false, status: "404"
end
- def require_non_empty_project
- redirect_to @project if @project.empty_repo?
- end
-
def no_cache_headers
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
response.headers["Pragma"] = "no-cache"
@@ -246,6 +190,8 @@ class ApplicationController < ActionController::Base
def ldap_security_check
if current_user && current_user.requires_ldap_check?
+ return unless current_user.try_obtain_ldap_lease
+
unless Gitlab::LDAP::Access.allowed?(current_user)
sign_out current_user
flash[:alert] = "Access denied for your LDAP account."
@@ -410,13 +356,6 @@ class ApplicationController < ActionController::Base
current_user.nil? && root_path == request.path
end
- def can_collaborate_with_project?(project = nil)
- project ||= @project
-
- can?(current_user, :push_code, project) ||
- (current_user && current_user.already_forked?(project))
- end
-
private
def set_default_sort
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 1e4fc612a3c..e595b233e30 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -7,7 +7,7 @@ class AutocompleteController < ApplicationController
@users = @users.search(params[:search]) if params[:search].present?
@users = @users.active
@users = @users.reorder(:name)
- @users = @users.page(params[:page]).per(PER_PAGE)
+ @users = @users.page(params[:page])
if params[:search].blank?
# Include current user if available to filter by "Me"
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
index d1824b481d7..8bf71a1adbb 100644
--- a/app/controllers/ci/projects_controller.rb
+++ b/app/controllers/ci/projects_controller.rb
@@ -1,10 +1,15 @@
module Ci
class ProjectsController < Ci::ApplicationController
before_action :project
- before_action :authorize_read_project!, except: [:badge]
before_action :no_cache, only: [:badge]
+ before_action :authorize_read_project!, except: [:badge, :index]
+ skip_before_action :authenticate_user!, only: [:badge]
protect_from_forgery
+ def index
+ redirect_to root_path
+ end
+
def show
# Temporary compatibility with CI badges pointing to CI project page
redirect_to namespace_project_path(project.namespace, project)
@@ -18,6 +23,7 @@ module Ci
#
def badge
return render_404 unless @project
+
image = Ci::ImageForBuildService.new.execute(@project, params)
send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml"
end
@@ -33,5 +39,9 @@ module Ci
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
end
+
+ def authorize_read_project!
+ return access_denied! unless can?(current_user, :read_project, project)
+ end
end
end
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
new file mode 100644
index 00000000000..0a995c45bdf
--- /dev/null
+++ b/app/controllers/concerns/continue_params.rb
@@ -0,0 +1,13 @@
+module ContinueParams
+ extend ActiveSupport::Concern
+
+ def continue_params
+ continue_params = params[:continue]
+ return nil unless continue_params
+
+ continue_params = continue_params.permit(:to, :notice, :notice_now)
+ return unless continue_params[:to] && continue_params[:to].start_with?('/')
+
+ continue_params
+ end
+end
diff --git a/app/controllers/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb
new file mode 100644
index 00000000000..f63b703d101
--- /dev/null
+++ b/app/controllers/concerns/filter_projects.rb
@@ -0,0 +1,15 @@
+# == FilterProjects
+#
+# Controller concern to handle projects filtering
+# * by name
+# * by archived state
+#
+module FilterProjects
+ extend ActiveSupport::Concern
+
+ def filter_projects(projects)
+ projects = projects.search(params[:filter_projects]) if params[:filter_projects].present?
+ projects = projects.non_archived if params[:archived].blank?
+ projects
+ end
+end
diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb
index 3e4c0e63601..5c503c5b698 100644
--- a/app/controllers/concerns/global_milestones.rb
+++ b/app/controllers/concerns/global_milestones.rb
@@ -6,7 +6,6 @@ module GlobalMilestones
@milestones = MilestonesFinder.new.execute(@projects, params)
@milestones = GlobalMilestone.build_collection(@milestones)
@milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
- @milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE)
end
def milestone
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
new file mode 100644
index 00000000000..f40b62446e5
--- /dev/null
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -0,0 +1,23 @@
+module IssuableActions
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :authorize_destroy_issuable!, only: :destroy
+ end
+
+ def destroy
+ issuable.destroy
+
+ name = issuable.class.name.titleize.downcase
+ flash[:notice] = "The #{name} was successfully deleted."
+ redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
+ end
+
+ private
+
+ def authorize_destroy_issuable!
+ unless current_user.can?(:"destroy_#{issuable.to_ability_name}", issuable)
+ return access_denied!
+ end
+ end
+end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index 5b098628557..4feabc32b1c 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -2,8 +2,8 @@ module IssuesAction
extend ActiveSupport::Concern
def issues
- @issues = get_issues_collection
- @issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE)
+ @issues = get_issues_collection.non_archived
+ @issues = @issues.page(params[:page])
@issues = @issues.preload(:author, :project)
@label = @issuable_finder.labels.first
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index f6de696e84d..06a6b065e7e 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -2,8 +2,8 @@ module MergeRequestsAction
extend ActiveSupport::Concern
def merge_requests
- @merge_requests = get_merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE)
+ @merge_requests = get_merge_requests_collection.non_archived
+ @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:author, :target_project)
@label = @issuable_finder.labels.first
diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb
new file mode 100644
index 00000000000..8a43c0b93c4
--- /dev/null
+++ b/app/controllers/concerns/toggle_subscription_action.rb
@@ -0,0 +1,17 @@
+module ToggleSubscriptionAction
+ extend ActiveSupport::Concern
+
+ def toggle_subscription
+ return unless current_user
+
+ subscribable_resource.toggle_subscription(current_user)
+
+ render nothing: true
+ end
+
+ private
+
+ def subscribable_resource
+ raise NotImplementedError
+ end
+end
diff --git a/app/controllers/dashboard/application_controller.rb b/app/controllers/dashboard/application_controller.rb
index 962ea38d6c9..9d3d1c23c28 100644
--- a/app/controllers/dashboard/application_controller.rb
+++ b/app/controllers/dashboard/application_controller.rb
@@ -1,3 +1,9 @@
class Dashboard::ApplicationController < ApplicationController
layout 'dashboard'
+
+ private
+
+ def projects
+ @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
+ end
end
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 3bc94ff2187..71ba6153021 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,5 +1,5 @@
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
- @group_members = current_user.group_members.page(params[:page]).per(PER_PAGE)
+ @group_members = current_user.group_members.page(params[:page])
end
end
diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb
new file mode 100644
index 00000000000..23a4ef21ea2
--- /dev/null
+++ b/app/controllers/dashboard/labels_controller.rb
@@ -0,0 +1,9 @@
+class Dashboard::LabelsController < Dashboard::ApplicationController
+ def index
+ labels = Label.where(project_id: projects).select(:title, :color).uniq(:title)
+
+ respond_to do |format|
+ format.json { render json: labels }
+ end
+ end
+end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 2bdce0f8a00..fa9c6c054f0 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -2,18 +2,19 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
include GlobalMilestones
before_action :projects
- before_action :milestones, only: [:index]
before_action :milestone, only: [:show]
def index
+ respond_to do |format|
+ format.html do
+ @milestones = Kaminari.paginate_array(milestones).page(params[:page])
+ end
+ format.json do
+ render json: milestones
+ end
+ end
end
def show
end
-
- private
-
- def projects
- @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
- end
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 2df6924b13d..71acc244a91 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -1,18 +1,15 @@
class Dashboard::ProjectsController < Dashboard::ApplicationController
+ include FilterProjects
+
before_action :event_filter
def index
- @projects = current_user.authorized_projects.sorted_by_activity.non_archived
- @projects = @projects.sort(@sort = params[:sort])
+ @projects = current_user.authorized_projects.sorted_by_activity
+ @projects = filter_projects(@projects)
@projects = @projects.includes(:namespace)
+ @projects = @projects.sort(@sort = params[:sort])
+ @projects = @projects.page(params[:page])
- terms = params['filter_projects']
-
- if terms.present?
- @projects = @projects.search(terms)
- end
-
- @projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank?
@last_push = current_user.recent_push
respond_to do |format|
@@ -31,17 +28,12 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def starred
- @projects = current_user.starred_projects
+ @projects = current_user.starred_projects.sorted_by_activity
+ @projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
+ @projects = @projects.page(params[:page])
- terms = params['filter_projects']
-
- if terms.present?
- @projects = @projects.search(terms)
- end
-
- @projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank?
@last_push = current_user.recent_push
@groups = []
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
index b3594d82530..bcfdbe14be9 100644
--- a/app/controllers/dashboard/snippets_controller.rb
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -6,6 +6,6 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController
user: current_user,
scope: params[:scope]
)
- @snippets = @snippets.page(params[:page]).per(PER_PAGE)
+ @snippets = @snippets.page(params[:page])
end
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 9ee9039f004..5abf97342c3 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -1,16 +1,21 @@
class Dashboard::TodosController < Dashboard::ApplicationController
- before_action :find_todos, only: [:index, :destroy_all]
+ before_action :find_todos, only: [:index, :destroy, :destroy_all]
def index
- @todos = @todos.page(params[:page]).per(PER_PAGE)
+ @todos = @todos.page(params[:page])
end
def destroy
- todo.done!
+ todo.done
+
+ todo_notice = 'Todo was successfully marked as done.'
respond_to do |format|
- format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
+ format.html { redirect_to dashboard_todos_path, notice: todo_notice }
format.js { render nothing: true }
+ format.json do
+ render json: { count: @todos.size, done_count: current_user.todos.done.count }
+ end
end
end
@@ -20,6 +25,10 @@ class Dashboard::TodosController < Dashboard::ApplicationController
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { render nothing: true }
+ format.json do
+ find_todos
+ render json: { count: @todos.size, done_count: current_user.todos.done.count }
+ end
end
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 139e40db180..1dce4a21729 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -34,8 +34,4 @@ class DashboardController < Dashboard::ApplicationController
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
-
- def projects
- @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
- end
end
diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb
new file mode 100644
index 00000000000..1bec5a7d27f
--- /dev/null
+++ b/app/controllers/emojis_controller.rb
@@ -0,0 +1,6 @@
+class EmojisController < ApplicationController
+ layout false
+
+ def index
+ end
+end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index a9bf4321f73..a962f9a0937 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,8 +1,8 @@
class Explore::GroupsController < Explore::ApplicationController
def index
- @groups = Group.order_id_desc
+ @groups = GroupsFinder.new.execute(current_user)
@groups = @groups.search(params[:search]) if params[:search].present?
@groups = @groups.sort(@sort = params[:sort])
- @groups = @groups.page(params[:page]).per(PER_PAGE)
+ @groups = @groups.page(params[:page])
end
end
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index a384f3004db..88a0c18180b 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -1,14 +1,14 @@
class Explore::ProjectsController < Explore::ApplicationController
+ include FilterProjects
+
def index
@projects = ProjectsFinder.new.execute(current_user)
@tags = @projects.tags_on(:tags)
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
- @projects = @projects.non_archived
- @projects = @projects.search(params[:search]) if params[:search].present?
- @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
+ @projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+ @projects = @projects.includes(:namespace).page(params[:page])
respond_to do |format|
format.html
@@ -22,9 +22,8 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending
@projects = TrendingProjectsFinder.new.execute(current_user)
- @projects = @projects.non_archived
- @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
- @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+ @projects = filter_projects(@projects)
+ @projects = @projects.page(params[:page])
respond_to do |format|
format.html
@@ -38,9 +37,9 @@ class Explore::ProjectsController < Explore::ApplicationController
def starred
@projects = ProjectsFinder.new.execute(current_user)
- @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
+ @projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
- @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+ @projects = @projects.page(params[:page])
respond_to do |format|
format.html
diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb
index b70ac51d06e..28760c3f84b 100644
--- a/app/controllers/explore/snippets_controller.rb
+++ b/app/controllers/explore/snippets_controller.rb
@@ -1,6 +1,6 @@
class Explore::SnippetsController < Explore::ApplicationController
def index
@snippets = SnippetsFinder.new.execute(current_user, filter: :all)
- @snippets = @snippets.page(params[:page]).per(PER_PAGE)
+ @snippets = @snippets.page(params[:page])
end
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index be801858eaf..949b4a6c25a 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -1,21 +1,32 @@
class Groups::ApplicationController < ApplicationController
layout 'group'
+
+ skip_before_action :authenticate_user!
before_action :group
private
def group
- @group ||= Group.find_by(path: params[:group_id])
- end
+ unless @group
+ id = params[:group_id] || params[:id]
+ @group = Group.find_by(path: id)
+
+ unless @group && can?(current_user, :read_group, @group)
+ @group = nil
- def authorize_read_group!
- unless @group and can?(current_user, :read_group, @group)
- if current_user.nil?
- return authenticate_user!
- else
- return render_404
+ if current_user.nil?
+ authenticate_user!
+ else
+ render_404
+ end
end
end
+
+ @group
+ end
+
+ def group_projects
+ @projects ||= GroupProjectsFinder.new(group).execute(current_user)
end
def authorize_admin_group!
diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb
index 76c87366baa..ad2c20b42db 100644
--- a/app/controllers/groups/avatars_controller.rb
+++ b/app/controllers/groups/avatars_controller.rb
@@ -1,4 +1,6 @@
class Groups::AvatarsController < Groups::ApplicationController
+ before_action :authorize_admin_group!
+
def destroy
@group.remove_avatar!
@group.save
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 0e902c4bb43..d5ef33888c6 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,8 +1,5 @@
class Groups::GroupMembersController < Groups::ApplicationController
- skip_before_action :authenticate_user!, only: [:index]
-
# Authorize
- before_action :authorize_read_group!
before_action :authorize_admin_group_member!, except: [:index, :leave]
def index
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 0c2a350bc39..b23c3022fb5 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -1,12 +1,16 @@
class Groups::MilestonesController < Groups::ApplicationController
include GlobalMilestones
- before_action :projects
- before_action :milestones, only: [:index]
+ before_action :group_projects
before_action :milestone, only: [:show, :update]
- before_action :authorize_group_milestone!, only: [:create, :update]
+ before_action :authorize_admin_milestones!, only: [:new, :create, :update]
def index
+ respond_to do |format|
+ format.html do
+ @milestones = Kaminari.paginate_array(milestones).page(params[:page])
+ end
+ end
end
def new
@@ -17,7 +21,7 @@ class Groups::MilestonesController < Groups::ApplicationController
project_ids = params[:milestone][:project_ids]
title = milestone_params[:title]
- @group.projects.where(id: project_ids).each do |project|
+ @projects.where(id: project_ids).each do |project|
Milestones::CreateService.new(project, current_user, milestone_params).execute
end
@@ -37,7 +41,7 @@ class Groups::MilestonesController < Groups::ApplicationController
private
- def authorize_group_milestone!
+ def authorize_admin_milestones!
return render_404 unless can?(current_user, :admin_milestones, group)
end
@@ -48,8 +52,4 @@ class Groups::MilestonesController < Groups::ApplicationController
def milestone_path(title)
group_milestone_path(@group, title.to_slug.to_s, title: title)
end
-
- def projects
- @projects ||= @group.projects
- end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index ca5ce1e2046..c1adc999567 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -1,20 +1,20 @@
class GroupsController < Groups::ApplicationController
+ include FilterProjects
include IssuesAction
include MergeRequestsAction
respond_to :html
- skip_before_action :authenticate_user!, only: [:index, :show, :issues, :merge_requests]
+ before_action :authenticate_user!, only: [:new, :create]
before_action :group, except: [:index, :new, :create]
# Authorize
- before_action :authorize_read_group!, except: [:index, :show, :new, :create, :autocomplete]
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
before_action :authorize_create_group!, only: [:new, :create]
# Load group projects
- before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete]
- before_action :event_filter, only: [:show, :events]
+ before_action :group_projects, only: [:show, :projects, :activity, :issues, :merge_requests]
+ before_action :event_filter, only: [:activity]
layout :determine_layout
@@ -27,11 +27,9 @@ class GroupsController < Groups::ApplicationController
end
def create
- @group = Group.new(group_params)
- @group.name = @group.path.dup unless @group.name
+ @group = Groups::CreateService.new(current_user, group_params).execute
- if @group.save
- @group.add_owner(current_user)
+ if @group.persisted?
redirect_to @group, notice: "Group '#{@group.name}' was successfully created."
else
render action: "new"
@@ -40,9 +38,13 @@ class GroupsController < Groups::ApplicationController
def show
@last_push = current_user.recent_push if current_user
+
@projects = @projects.includes(:namespace)
- @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
- @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+ @projects = filter_projects(@projects)
+ @projects = @projects.sort(@sort = params[:sort])
+ @projects = @projects.page(params[:page]) if params[:filter_projects].blank?
+
+ @shared_projects = GroupProjectsFinder.new(group, only_shared: true).execute(current_user)
respond_to do |format|
format.html
@@ -60,8 +62,10 @@ class GroupsController < Groups::ApplicationController
end
end
- def events
+ def activity
respond_to do |format|
+ format.html
+
format.json do
load_events
pager_json("events/_events", @events.count)
@@ -77,7 +81,7 @@ class GroupsController < Groups::ApplicationController
end
def update
- if @group.update_attributes(group_params)
+ if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else
render action: "edit"
@@ -92,26 +96,6 @@ class GroupsController < Groups::ApplicationController
protected
- def group
- @group ||= Group.find_by(path: params[:id])
- @group || render_404
- end
-
- def load_projects
- @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity.non_archived
- end
-
- # Dont allow unauthorized access to group
- def authorize_read_group!
- unless @group and (@projects.present? or can?(current_user, :read_group, @group))
- if current_user.nil?
- return authenticate_user!
- else
- return render_404
- end
- end
- end
-
def authorize_create_group!
unless can?(current_user, :create_group, nil)
return render_404
@@ -129,7 +113,7 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar, :public)
+ params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock)
end
def load_events
diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb
index 282012c60a1..5a94dcb0dbd 100644
--- a/app/controllers/namespaces_controller.rb
+++ b/app/controllers/namespaces_controller.rb
@@ -14,7 +14,7 @@ class NamespacesController < ApplicationController
if user
redirect_to user_path(user)
- elsif group
+ elsif group && can?(current_user, :read_group, namespace)
redirect_to group_path(group)
elsif current_user.nil?
authenticate_user!
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index dc22101cd5e..d1e4ac10f6c 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -8,7 +8,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
layout 'profile'
def index
- head :forbidden and return
+ set_index_vars
end
def create
@@ -20,18 +20,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to oauth_application_url(@application)
else
- render :new
+ set_index_vars
+ render :index
end
end
- def destroy
- if @application.destroy
- flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :destroy])
- end
-
- redirect_to applications_profile_url
- end
-
private
def verify_user_oauth_applications_enabled
@@ -40,6 +33,17 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
redirect_to applications_profile_url
end
+ def set_index_vars
+ @applications = current_user.oauth_applications
+ @authorized_tokens = current_user.oauth_authorized_tokens
+ @authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
+ @authorized_apps = @authorized_tokens.map(&:application).uniq.reject(&:nil?)
+
+ # Don't overwrite a value possibly set by `create`
+ @application ||= Doorkeeper::Application.new
+ end
+
+ # Override Doorkeeper to scope to the current user
def set_application
@application = current_user.oauth_applications.find(params[:id])
end
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index 24025d8c723..c721dca58d9 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -7,6 +7,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
if pre_auth.authorizable?
if skip_authorization? || matching_token?
auth = authorization.authorize
+ session.delete(:user_return_to)
redirect_to auth.redirect_uri
else
render "doorkeeper/authorizations/new"
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index f74daff3bd0..a8575e037e4 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -23,6 +23,14 @@ class PasswordsController < Devise::PasswordsController
end
end
+ def update
+ super do |resource|
+ if resource.valid? && resource.require_password?
+ resource.update_attribute(:password_automatically_set, false)
+ end
+ end
+ end
+
protected
def resource_from_email
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index f3224148fda..b88c080352b 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -3,23 +3,21 @@ class Profiles::KeysController < Profiles::ApplicationController
def index
@keys = current_user.keys
+ @key = Key.new
end
def show
@key = current_user.keys.find(params[:id])
end
- def new
- @key = current_user.keys.new
- end
-
def create
@key = current_user.keys.new(key_params)
if @key.save
redirect_to profile_key_path(@key)
else
- render 'new'
+ @keys = current_user.keys.select(&:persisted?)
+ render :index
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index f3bfede4354..8f83fdd02bc 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -12,11 +12,13 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.save! if current_user.changed?
- if two_factor_grace_period_expired?
- flash.now[:alert] = 'You must enable Two-factor Authentication for your account.'
- else
- grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
- flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}."
+ if two_factor_authentication_required?
+ if two_factor_grace_period_expired?
+ flash.now[:alert] = 'You must enable Two-factor Authentication for your account.'
+ else
+ grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
+ flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}."
+ end
end
@qr_code = build_qr_code
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 28803164fcf..c5fa756d02b 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -8,25 +8,19 @@ class ProfilesController < Profiles::ApplicationController
def show
end
- def applications
- @applications = current_user.oauth_applications
- @authorized_tokens = current_user.oauth_authorized_tokens
- @authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
- @authorized_apps = @authorized_tokens.map(&:application).uniq - [nil]
- end
-
def update
user_params.except!(:email) if @user.ldap_user?
- if @user.update_attributes(user_params)
- flash[:notice] = "Profile was successfully updated"
- else
- messages = @user.errors.full_messages.uniq.join('. ')
- flash[:alert] = "Failed to update profile. #{messages}"
- end
-
respond_to do |format|
- format.html { redirect_back_or_default(default: { action: 'show' }) }
+ if @user.update_attributes(user_params)
+ message = "Profile was successfully updated"
+ format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
+ format.json { render json: { message: message } }
+ else
+ message = @user.errors.full_messages.uniq.join('. ')
+ format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: "Failed to update profile. #{message}" }) }
+ format.json { render json: { message: message }, status: :unprocessable_entity }
+ end
end
end
@@ -41,8 +35,7 @@ class ProfilesController < Profiles::ApplicationController
def audit_log
@events = AuditEvent.where(entity_type: "User", entity_id: current_user.id).
order("created_at DESC").
- page(params[:page]).
- per(PER_PAGE)
+ page(params[:page])
end
def update_username
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index a326bc58215..657ee94cfd7 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -1,20 +1,74 @@
class Projects::ApplicationController < ApplicationController
+ skip_before_action :authenticate_user!
before_action :project
before_action :repository
layout 'project'
- def authenticate_user!
- # Restrict access to Projects area only
- # for non-signed users
- if !current_user
+ helper_method :repository, :can_collaborate_with_project?
+
+ private
+
+ def project
+ unless @project
+ namespace = params[:namespace_id]
id = params[:project_id] || params[:id]
- project_with_namespace = "#{params[:namespace_id]}/#{id}"
- @project = Project.find_with_namespace(project_with_namespace)
- return if @project && @project.public?
+ # Redirect from
+ # localhost/group/project.git
+ # to
+ # localhost/group/project
+ #
+ if id =~ /\.git\Z/
+ redirect_to request.original_url.gsub(/\.git\/?\Z/, '')
+ return
+ end
+
+ project_path = "#{namespace}/#{id}"
+ @project = Project.find_with_namespace(project_path)
+
+ if @project && can?(current_user, :read_project, @project)
+ if @project.path_with_namespace != project_path
+ redirect_to request.original_url.gsub(project_path, @project.path_with_namespace)
+ end
+ else
+ @project = nil
+
+ if current_user.nil?
+ authenticate_user!
+ else
+ render_404
+ end
+ end
+ end
+
+ @project
+ end
+
+ def repository
+ @repository ||= project.repository
+ end
+
+ def can_collaborate_with_project?(project = nil)
+ project ||= @project
+
+ can?(current_user, :push_code, project) ||
+ (current_user && current_user.already_forked?(project))
+ end
+
+ def authorize_project!(action)
+ return access_denied! unless can?(current_user, action, project)
+ end
+
+ def method_missing(method_sym, *arguments, &block)
+ if method_sym.to_s =~ /\Aauthorize_(.*)!\z/
+ authorize_project!($1.to_sym)
+ else
+ super
end
+ end
- super
+ def require_non_empty_project
+ redirect_to namespace_project_path(@project.namespace, @project) if @project.empty_repo?
end
def require_branch_head
@@ -26,8 +80,6 @@ class Projects::ApplicationController < ApplicationController
end
end
- private
-
def apply_diff_view_cookie!
view = params[:view] || cookies[:diff_view]
cookies.permanent[:diff_view] = params[:view] = view if view
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index f7e6bb34443..72921b3aa14 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -1,13 +1,18 @@
class Projects::AvatarsController < Projects::ApplicationController
- before_action :project
+ include BlobHelper
+
+ before_action :authorize_admin_project!, only: [:destroy]
def show
@blob = @repository.blob_at_branch('master', @project.avatar_in_git)
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
+
+ return if cached_blob?
+
headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
headers['Content-Disposition'] = 'inline'
- headers['Content-Type'] = @blob.content_type
+ headers['Content-Type'] = safe_content_type(@blob)
head :ok # 'render nothing: true' messes up the Content-Type
else
render_404
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index a4dd94b941c..6ff47c4033a 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -1,4 +1,6 @@
class Projects::BadgesController < Projects::ApplicationController
+ before_action :no_cache_headers
+
def build
respond_to do |format|
format.html { render_404 }
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 4db3b3bf23d..c0a53734921 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -8,8 +8,8 @@ class Projects::BranchesController < Projects::ApplicationController
def index
@sort = params[:sort] || 'name'
@branches = @repository.branches_sorted_by(@sort)
- @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE)
-
+ @branches = Kaminari.paginate_array(@branches).page(params[:page])
+
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
@@ -23,11 +23,15 @@ class Projects::BranchesController < Projects::ApplicationController
def create
branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name)
- ref = sanitize(strip_tags(params[:ref]))
- ref = Addressable::URI.unescape(ref)
+
result = CreateBranchService.new(project, current_user).
execute(branch_name, ref)
+ if params[:issue_iid]
+ issue = @project.issues.find_by(iid: params[:issue_iid])
+ SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
+ end
+
if result[:status] == :success
@branch = result[:branch]
redirect_to namespace_project_tree_path(@project.namespace, @project,
@@ -49,4 +53,15 @@ class Projects::BranchesController < Projects::ApplicationController
format.js { render status: status[:return_code] }
end
end
+
+ private
+
+ def ref
+ if params[:ref]
+ ref_escaped = sanitize(strip_tags(params[:ref]))
+ Addressable::URI.unescape(ref_escaped)
+ else
+ @project.default_branch
+ end
+ end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 97d31a4229a..576fa3cedb2 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -3,6 +3,7 @@
# Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController
include CreatesCommit
+ include DiffHelper
# Authorize
before_action :require_non_empty_project
@@ -100,12 +101,10 @@ class Projects::CommitController < Projects::ApplicationController
def define_show_vars
return git_not_found! unless commit
- if params[:w].to_i == 1
- @diffs = commit.diffs({ ignore_whitespace_change: true })
- else
- @diffs = commit.diffs
- end
+ opts = diff_options
+ opts[:ignore_whitespace_change] = true if params[:format] == 'diff'
+ @diffs = commit.diffs(opts)
@diff_refs = [commit.parent || commit, commit]
@notes_count = commit.notes.count
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index dc5d217f3e4..671d5c23024 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -1,6 +1,8 @@
require 'addressable/uri'
class Projects::CompareController < Projects::ApplicationController
+ include DiffHelper
+
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
@@ -11,16 +13,14 @@ class Projects::CompareController < Projects::ApplicationController
end
def show
- diff_options = { ignore_whitespace_change: true } if params[:w] == '1'
-
- compare_result = CompareService.new.
+ compare = CompareService.new.
execute(@project, @head_ref, @project, @base_ref, diff_options)
- if compare_result
- @commits = Commit.decorate(compare_result.commits, @project)
- @diffs = compare_result.diffs
+ if compare
+ @commits = Commit.decorate(compare.commits, @project)
@commit = @project.commit(@head_ref)
@base_commit = @project.merge_base_commit(@base_ref, @head_ref)
+ @diffs = compare.diffs(diff_options)
@diff_refs = [@base_commit, @commit]
@line_notes = []
end
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 0c551501ca4..ade01c706a7 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -1,14 +1,30 @@
class Projects::ForksController < Projects::ApplicationController
+ include ContinueParams
+
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
def index
- @sort = params[:sort] || 'id_desc'
- @all_forks = project.forks.includes(:creator).order_by(@sort)
+ base_query = project.forks.includes(:creator)
+
+ @forks = base_query.merge(ProjectsFinder.new.execute(current_user))
+ @total_forks_count = base_query.size
+ @private_forks_count = @total_forks_count - @forks.size
+ @public_forks_count = @total_forks_count - @private_forks_count
+
+ @sort = params[:sort] || 'id_desc'
+ @forks = @forks.search(params[:filter_projects]) if params[:filter_projects].present?
+ @forks = @forks.order_by(@sort).page(params[:page])
- @public_forks, @protected_forks = @all_forks.partition do |project|
- can?(current_user, :read_project, project)
+ respond_to do |format|
+ format.html
+
+ format.json do
+ render json: {
+ html: view_to_html_string("projects/forks/_projects", projects: @forks)
+ }
+ end
end
end
@@ -39,15 +55,4 @@ class Projects::ForksController < Projects::ApplicationController
render :error
end
end
-
- private
-
- def continue_params
- continue_params = params[:continue]
- if continue_params
- continue_params.permit(:to, :notice, :notice_now)
- else
- nil
- end
- end
end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
new file mode 100644
index 00000000000..4159e53bfa9
--- /dev/null
+++ b/app/controllers/projects/group_links_controller.rb
@@ -0,0 +1,23 @@
+class Projects::GroupLinksController < Projects::ApplicationController
+ layout 'project_settings'
+ before_action :authorize_admin_project!
+
+ def index
+ @group_links = project.project_group_links.all
+ end
+
+ def create
+ link = project.project_group_links.new
+ link.group_id = params[:link_group_id]
+ link.group_access = params[:link_group_access]
+ link.save
+
+ redirect_to namespace_project_group_links_path(project.namespace, project)
+ end
+
+ def destroy
+ project.project_group_links.find(params[:id]).destroy
+
+ redirect_to namespace_project_group_links_path(project.namespace, project)
+ end
+end
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 196996f1752..7756f0f0ed3 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -1,4 +1,6 @@
class Projects::ImportsController < Projects::ApplicationController
+ include ContinueParams
+
# Authorize
before_action :authorize_admin_project!
before_action :require_no_repo, only: [:new, :create]
@@ -44,16 +46,6 @@ class Projects::ImportsController < Projects::ApplicationController
private
- def continue_params
- continue_params = params[:continue]
-
- if continue_params
- continue_params.permit(:to, :notice, :notice_now)
- else
- nil
- end
- end
-
def finished_notice
if @project.forked?
'The project was successfully forked.'
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 8aa85e448c4..b9ce0d46dab 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -1,9 +1,12 @@
class Projects::IssuesController < Projects::ApplicationController
+ include ToggleSubscriptionAction
+ include IssuableActions
+
before_action :module_enabled
- before_action :issue, only: [:edit, :update, :show, :toggle_subscription]
+ before_action :issue, only: [:edit, :update, :show]
# Allow read any issue
- before_action :authorize_read_issue!
+ before_action :authorize_read_issue!, only: [:show]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
@@ -31,7 +34,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- @issues = @issues.page(params[:page]).per(PER_PAGE)
+ @issues = @issues.page(params[:page])
@label = @project.labels.find_by(title: params[:label_name])
respond_to do |format|
@@ -63,8 +66,15 @@ class Projects::IssuesController < Projects::ApplicationController
@notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
@merge_requests = @issue.referenced_merge_requests(current_user)
+ @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @issue.to_json(include: [:milestone, :labels])
+ end
+ end
- respond_with(@issue)
end
def create
@@ -87,6 +97,12 @@ class Projects::IssuesController < Projects::ApplicationController
def update
@issue = Issues::UpdateService.new(project, current_user, issue_params).execute(issue)
+ if params[:move_to_project_id].to_i > 0
+ new_project = Project.find(params[:move_to_project_id])
+ move_service = Issues::MoveService.new(project, current_user)
+ @issue = move_service.execute(@issue, new_project)
+ end
+
respond_to do |format|
format.js
format.html do
@@ -97,10 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
format.json do
- render json: {
- saved: @issue.valid?,
- assignee_avatar_url: @issue.assignee.try(:avatar_url)
- }
+ render json: @issue.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }])
end
end
end
@@ -110,12 +123,6 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
end
- def toggle_subscription
- @issue.toggle_subscription(current_user)
-
- render nothing: true
- end
-
def closed_by_merge_requests
@closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
end
@@ -129,6 +136,12 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_old
end
end
+ alias_method :subscribable_resource, :issue
+ alias_method :issuable, :issue
+
+ def authorize_read_issue!
+ return render_404 unless can?(current_user, :read_issue, @issue)
+ end
def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue)
@@ -160,7 +173,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
- :title, :assignee_id, :position, :description,
+ :title, :assignee_id, :position, :description, :confidential,
:milestone_id, :state_event, :task_num, label_ids: []
)
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index ecac3c395ec..ff771ea6d9c 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -1,13 +1,24 @@
class Projects::LabelsController < Projects::ApplicationController
+ include ToggleSubscriptionAction
+
before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_read_label!
- before_action :authorize_admin_labels!, except: [:index]
+ before_action :authorize_admin_labels!, only: [
+ :new, :create, :edit, :update, :generate, :destroy
+ ]
respond_to :js, :html
def index
- @labels = @project.labels.page(params[:page]).per(PER_PAGE)
+ @labels = @project.labels.page(params[:page])
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @project.labels
+ end
+ end
end
def new
@@ -73,8 +84,9 @@ class Projects::LabelsController < Projects::ApplicationController
end
def label
- @label = @project.labels.find(params[:id])
+ @label ||= @project.labels.find(params[:id])
end
+ alias_method :subscribable_resource, :label
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 5fe21694605..6189de09f27 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -1,8 +1,12 @@
class Projects::MergeRequestsController < Projects::ApplicationController
+ include ToggleSubscriptionAction
+ include DiffHelper
+ include IssuableActions
+
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
- :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds
+ :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip
]
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
@@ -17,7 +21,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_create_merge_request!, only: [:new, :create]
# Allow modify merge_request
- before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :sort]
+ before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
def index
terms = params['issue_search']
@@ -31,7 +35,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
- @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
+ @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project)
@label = @project.labels.find_by(title: params[:label_name])
@@ -111,7 +115,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.last_commit
@base_commit = @merge_request.diff_base_commit
- @diffs = @merge_request.compare_diffs
+ @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare
@ci_commit = @merge_request.ci_commit
@statuses = @ci_commit.statuses if @ci_commit
@@ -150,10 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.target_project, @merge_request])
end
format.json do
- render json: {
- saved: @merge_request.valid?,
- assignee_avatar_url: @merge_request.assignee.try(:avatar_url)
- }
+ render json: @merge_request.to_json(include: [:milestone, :labels, :assignee])
end
end
else
@@ -161,6 +162,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ def remove_wip
+ MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request)
+
+ redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
+ notice: "The merge request can now be merged."
+ end
+
def merge_check
@merge_request.check_if_can_be_merged
@@ -231,12 +239,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response
end
- def toggle_subscription
- @merge_request.toggle_subscription(current_user)
-
- render nothing: true
- end
-
protected
def selected_target_project
@@ -250,6 +252,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_request
@merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end
+ alias_method :subscribable_resource, :merge_request
+ alias_method :issuable, :merge_request
def closes_issues
@closes_issues ||= @merge_request.closes_issues
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 21f30f278c8..5b0a63a933c 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -19,7 +19,14 @@ class Projects::MilestonesController < Projects::ApplicationController
end
@milestones = @milestones.includes(:project)
- @milestones = @milestones.page(params[:page]).per(PER_PAGE)
+ respond_to do |format|
+ format.html do
+ @milestones = @milestones.page(params[:page])
+ end
+ format.json do
+ render json: @milestones
+ end
+ end
end
def new
@@ -32,10 +39,6 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def show
- @issues = @milestone.issues
- @users = @milestone.participants.uniq
- @merge_requests = @milestone.merge_requests
- @labels = @milestone.labels
end
def create
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 8364fc293b7..e7bddc4a6f1 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -27,6 +27,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
@project_member = @project.project_members.new
+ @project_group_links = @project.project_group_links
end
def create
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 87b4d08da0e..10de0e60530 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -1,6 +1,7 @@
# Controller for viewing a file's raw
class Projects::RawController < Projects::ApplicationController
include ExtractsPath
+ include BlobHelper
before_action :require_non_empty_project
before_action :assign_ref_vars
@@ -12,12 +13,14 @@ class Projects::RawController < Projects::ApplicationController
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
+ return if cached_blob?
+
if @blob.lfs_pointer?
send_lfs_object
else
headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
headers['Content-Disposition'] = 'inline'
- headers['Content-Type'] = get_blob_type
+ headers['Content-Type'] = safe_content_type(@blob)
head :ok # 'render nothing: true' messes up the Content-Type
end
else
@@ -27,16 +30,6 @@ class Projects::RawController < Projects::ApplicationController
private
- def get_blob_type
- if @blob.text?
- 'text/plain; charset=utf-8'
- elsif @blob.image?
- @blob.content_type
- else
- 'application/octet-stream'
- end
- end
-
def send_lfs_object
lfs_object = find_lfs_object
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index a8f091819ca..00df1c9c965 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -64,9 +64,9 @@ class Projects::RefsController < Projects::ApplicationController
}
end
- if @logs.present?
- @log_url = namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/'))
- @more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit))
+ offset = (@offset + @limit)
+ if contents.size > offset
+ @more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: offset)
end
respond_to do |format|
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 92b0caa2efb..6d2901a24a4 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -3,7 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read any snippet
- before_action :authorize_read_project_snippet!
+ before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
# Allow write(create) snippet
before_action :authorize_create_project_snippet!, only: [:new, :create]
@@ -21,7 +21,7 @@ class Projects::SnippetsController < Projects::ApplicationController
filter: :by_project,
project: @project
})
- @snippets = @snippets.page(params[:page]).per(PER_PAGE)
+ @snippets = @snippets.page(params[:page])
end
def new
@@ -81,6 +81,10 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet ||= @project.snippets.find(params[:id])
end
+ def authorize_read_project_snippet!
+ return render_404 unless can?(current_user, :read_project_snippet, @snippet)
+ end
+
def authorize_update_project_snippet!
return render_404 unless can?(current_user, :update_project_snippet, @snippet)
end
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 280fe12cc7c..46b242aa5ff 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -7,7 +7,7 @@ class Projects::TagsController < Projects::ApplicationController
def index
sorted = VersionSorter.rsort(@repository.tag_names)
- @tags = Kaminari.paginate_array(sorted).page(params[:page]).per(PER_PAGE)
+ @tags = Kaminari.paginate_array(sorted).page(params[:page])
@releases = project.releases.where(tag: @tags)
end
@@ -34,6 +34,11 @@ class Projects::TagsController < Projects::ApplicationController
def destroy
DeleteTagService.new(project, current_user).execute(params[:id])
- redirect_to namespace_project_tags_path(@project.namespace, @project)
+ respond_to do |format|
+ format.html do
+ redirect_to namespace_project_tags_path(@project.namespace, @project)
+ end
+ format.js
+ end
end
end
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index e1fe7ea2114..caed064dfbc 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -1,7 +1,9 @@
class Projects::UploadsController < Projects::ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked!, :project,
+ skip_before_action :reject_blocked!, :project,
:repository, if: -> { action_name == 'show' && image? }
+ before_action :authorize_upload_file!, only: [:create]
+
def create
link_to_file = ::Projects::UploadService.new(project, params[:file]).
execute
@@ -26,6 +28,8 @@ class Projects::UploadsController < Projects::ApplicationController
send_file uploader.file.path, disposition: disposition
end
+ private
+
def uploader
return @uploader if defined?(@uploader)
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 88fccfed509..02ceb8f4334 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -7,7 +7,7 @@ class Projects::WikisController < Projects::ApplicationController
before_action :load_project_wiki
def pages
- @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(PER_PAGE)
+ @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page])
end
def show
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index aea08ecce3e..928817ba811 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,8 +1,7 @@
-class ProjectsController < ApplicationController
+class ProjectsController < Projects::ApplicationController
include ExtractsPath
- prepend_before_action :render_go_import, only: [:show]
- skip_before_action :authenticate_user!, only: [:show, :activity]
+ before_action :authenticate_user!, except: [:show, :activity]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
@@ -135,7 +134,7 @@ class ProjectsController < ApplicationController
def autocomplete_sources
note_type = params['type']
note_id = params['type_id']
- autocomplete = ::Projects::AutocompleteService.new(@project)
+ autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = {
@@ -173,10 +172,15 @@ class ProjectsController < ApplicationController
def housekeeping
::Projects::HousekeepingService.new(@project).execute
- respond_to do |format|
- flash[:notice] = "Housekeeping successfully started."
- format.html { redirect_to project_path(@project) }
- end
+ redirect_to(
+ project_path(@project),
+ notice: "Housekeeping successfully started"
+ )
+ rescue ::Projects::HousekeepingService::LeaseTaken => ex
+ redirect_to(
+ edit_project_path(@project),
+ alert: ex.to_s
+ )
end
def toggle_star
@@ -242,16 +246,6 @@ class ProjectsController < ApplicationController
end
end
- def render_go_import
- return unless params["go-get"] == "1"
-
- @namespace = params[:namespace_id]
- @id = params[:project_id] || params[:id]
- @id = @id.gsub(/\.git\Z/, "")
-
- render "go_import", layout: false
- end
-
def repo_exists?
project.repository_exists? && !project.empty_repo?
end
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index ad04c646e1b..627be74a38f 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -26,6 +26,10 @@ class RootController < Dashboard::ProjectsController
redirect_to activity_dashboard_path
when 'starred_project_activity'
redirect_to activity_dashboard_path(filter: 'starred')
+ when 'groups'
+ redirect_to dashboard_groups_path
+ when 'todos'
+ redirect_to dashboard_todos_path
else
return
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 9bb42ec86b3..e42d2d73947 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -1,4 +1,6 @@
class SearchController < ApplicationController
+ skip_before_action :authenticate_user!, :reject_blocked
+
include SearchHelper
layout 'search'
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 44eb58e418b..65677a3dd3c 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -4,8 +4,10 @@ class SessionsController < Devise::SessionsController
skip_before_action :check_2fa_requirement, only: [:destroy]
+ prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor, only: [:create]
prepend_before_action :store_redirect_path, only: [:new]
+
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
@@ -33,6 +35,22 @@ class SessionsController < Devise::SessionsController
private
+ # Handle an "initial setup" state, where there's only one user, it's an admin,
+ # and they require a password change.
+ def check_initial_setup
+ return unless User.count == 1
+
+ user = User.admins.last
+
+ return unless user && user.require_password?
+
+ token = user.generate_reset_token
+ user.save
+
+ redirect_to edit_user_password_path(reset_password_token: token),
+ notice: "Please create a password for your new account."
+ end
+
def user_params
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt)
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index c72df73af46..2daceed039b 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -25,7 +25,7 @@ class SnippetsController < ApplicationController
filter: :by_user,
user: @user,
scope: params[:scope] }).
- page(params[:page]).per(PER_PAGE)
+ page(params[:page])
render 'index'
else
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 868b05929d7..509f4f412ca 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -55,14 +55,15 @@ class UploadsController < ApplicationController
"user" => User,
"project" => Project,
"note" => Note,
- "group" => Group
+ "group" => Group,
+ "appearance" => Appearance
}
upload_models[params[:model]]
end
def upload_mount
- upload_mounts = %w(avatar attachment file)
+ upload_mounts = %w(avatar attachment file logo header_logo)
if upload_mounts.include?(params[:mounted_as])
params[:mounted_as]
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 6055b606086..8e7956da48f 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -3,13 +3,6 @@ class UsersController < ApplicationController
before_action :set_user
def show
- @contributed_projects = contributed_projects.joined(@user).reject(&:forked?)
-
- @projects = PersonalProjectsFinder.new(@user).execute(current_user)
- @projects = @projects.page(params[:page]).per(PER_PAGE)
-
- @groups = @user.groups.order_id_desc
-
respond_to do |format|
format.html
@@ -25,6 +18,45 @@ class UsersController < ApplicationController
end
end
+ def groups
+ load_groups
+
+ respond_to do |format|
+ format.html { render 'show' }
+ format.json do
+ render json: {
+ html: view_to_html_string("shared/groups/_list", groups: @groups)
+ }
+ end
+ end
+ end
+
+ def projects
+ load_projects
+
+ respond_to do |format|
+ format.html { render 'show' }
+ format.json do
+ render json: {
+ html: view_to_html_string("shared/projects/_list", projects: @projects, remote: true)
+ }
+ end
+ end
+ end
+
+ def contributed
+ load_contributed_projects
+
+ respond_to do |format|
+ format.html { render 'show' }
+ format.json do
+ render json: {
+ html: view_to_html_string("shared/projects/_list", projects: @contributed_projects)
+ }
+ end
+ end
+ end
+
def calendar
calendar = contributions_calendar
@timestamps = calendar.timestamps
@@ -35,12 +67,8 @@ class UsersController < ApplicationController
end
def calendar_activities
- @calendar_date = Date.parse(params[:date]) rescue nil
- @events = []
-
- if @calendar_date
- @events = contributions_calendar.events_by_date(@calendar_date)
- end
+ @calendar_date = Date.parse(params[:date]) rescue Date.today
+ @events = contributions_calendar.events_by_date(@calendar_date)
render 'calendar_activities', layout: false
end
@@ -57,7 +85,7 @@ class UsersController < ApplicationController
def contributions_calendar
@contributions_calendar ||= Gitlab::ContributionsCalendar.
- new(contributed_projects.reject(&:forked?), @user)
+ new(contributed_projects, @user)
end
def load_events
@@ -69,6 +97,20 @@ class UsersController < ApplicationController
limit_recent(20, params[:offset])
end
+ def load_projects
+ @projects =
+ PersonalProjectsFinder.new(@user).execute(current_user)
+ .page(params[:page])
+ end
+
+ def load_contributed_projects
+ @contributed_projects = contributed_projects.joined(@user)
+ end
+
+ def load_groups
+ @groups = JoinedGroupsFinder.new(@user).execute(current_user)
+ end
+
def projects_for_current_user
ProjectsFinder.new.execute(current_user)
end
diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb
index 0209649b017..a685719555c 100644
--- a/app/finders/contributed_projects_finder.rb
+++ b/app/finders/contributed_projects_finder.rb
@@ -1,4 +1,4 @@
-class ContributedProjectsFinder
+class ContributedProjectsFinder < UnionFinder
def initialize(user)
@user = user
end
@@ -11,27 +11,19 @@ class ContributedProjectsFinder
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
- if current_user
- relation = projects_visible_to_user(current_user)
- else
- relation = public_projects
- end
+ segments = all_projects(current_user)
- relation.includes(:namespace).order_id_desc
+ find_union(segments, Project).includes(:namespace).order_id_desc
end
private
- def projects_visible_to_user(current_user)
- authorized = @user.contributed_projects.visible_to_user(current_user)
+ def all_projects(current_user)
+ projects = []
- union = Gitlab::SQL::Union.
- new([authorized.select(:id), public_projects.select(:id)])
+ projects << @user.contributed_projects.visible_to_user(current_user) if current_user
+ projects << @user.contributed_projects.public_to_user(current_user)
- Project.where("projects.id IN (#{union.to_sql})")
- end
-
- def public_projects
- @user.contributed_projects.public_only
+ projects
end
end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
new file mode 100644
index 00000000000..3b9a421b118
--- /dev/null
+++ b/app/finders/group_projects_finder.rb
@@ -0,0 +1,42 @@
+class GroupProjectsFinder < UnionFinder
+ def initialize(group, options = {})
+ @group = group
+ @options = options
+ end
+
+ def execute(current_user = nil)
+ segments = group_projects(current_user)
+ find_union(segments, Project)
+ end
+
+ private
+
+ def group_projects(current_user)
+ only_owned = @options.fetch(:only_owned, false)
+ only_shared = @options.fetch(:only_shared, false)
+
+ projects = []
+
+ if current_user
+ if @group.users.include?(current_user)
+ projects << @group.projects unless only_shared
+ projects << @group.shared_projects unless only_owned
+ else
+ unless only_shared
+ projects << @group.projects.visible_to_user(current_user)
+ projects << @group.projects.public_to_user(current_user)
+ end
+
+ unless only_owned
+ projects << @group.shared_projects.visible_to_user(current_user)
+ projects << @group.shared_projects.public_to_user(current_user)
+ end
+ end
+ else
+ projects << @group.projects.public_only unless only_shared
+ projects << @group.shared_projects.public_only unless only_owned
+ end
+
+ projects
+ end
+end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
new file mode 100644
index 00000000000..4e43f42e9e1
--- /dev/null
+++ b/app/finders/groups_finder.rb
@@ -0,0 +1,18 @@
+class GroupsFinder < UnionFinder
+ def execute(current_user = nil)
+ segments = all_groups(current_user)
+
+ find_union(segments, Group).order_id_desc
+ end
+
+ private
+
+ def all_groups(current_user)
+ groups = []
+
+ groups << current_user.authorized_groups if current_user
+ groups << Group.unscoped.public_to_user(current_user)
+
+ groups
+ end
+end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index f7240edd618..046286dd9e1 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -80,9 +80,10 @@ class IssuableFinder
@projects = project
elsif current_user && params[:authorized_only].presence && !current_user_related?
@projects = current_user.authorized_projects.reorder(nil)
+ elsif group
+ @projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil)
else
- @projects = ProjectsFinder.new.execute(current_user, group: group).
- reorder(nil)
+ @projects = ProjectsFinder.new.execute(current_user).reorder(nil)
end
end
@@ -171,14 +172,12 @@ class IssuableFinder
def by_scope(items)
case params[:scope]
- when 'created-by-me', 'authored' then
+ when 'created-by-me', 'authored'
items.where(author_id: current_user.id)
- when 'all' then
- items
- when 'assigned-to-me' then
+ when 'assigned-to-me'
items.where(assignee_id: current_user.id)
else
- raise 'You must specify default scope'
+ items
end
end
@@ -198,8 +197,7 @@ class IssuableFinder
end
def by_group(items)
- items = items.of_group(group) if group
-
+ # Selection by group is already covered by `by_project` and `projects`
items
end
@@ -244,10 +242,17 @@ class IssuableFinder
items
end
+ def filter_by_upcoming_milestone?
+ params[:milestone_title] == '#upcoming'
+ end
+
def by_milestone(items)
if milestones?
if filter_by_no_milestone?
items = items.where(milestone_id: [-1, nil])
+ elsif filter_by_upcoming_milestone?
+ upcoming = Milestone.where(project_id: projects).upcoming
+ items = items.joins(:milestone).where(milestones: { title: upcoming.title })
else
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
@@ -263,11 +268,9 @@ class IssuableFinder
def by_label(items)
if labels?
if filter_by_no_label?
- items = items.
- joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id").
- where(label_links: { id: nil })
+ items = items.without_label
else
- items = items.joins(:labels).where(labels: { title: label_names })
+ items = items.with_label(label_names)
if projects
items = items.where(labels: { project_id: projects })
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 20a2b0ce8f0..c2befa5a5b3 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -19,4 +19,10 @@ class IssuesFinder < IssuableFinder
def klass
Issue
end
+
+ private
+
+ def init_collection
+ Issue.visible_to_user(current_user)
+ end
end
diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb
new file mode 100644
index 00000000000..47174980258
--- /dev/null
+++ b/app/finders/joined_groups_finder.rb
@@ -0,0 +1,24 @@
+class JoinedGroupsFinder < UnionFinder
+ def initialize(user)
+ @user = user
+ end
+
+ # Finds the groups of the source user, optionally limited to those visible to
+ # the current user.
+ def execute(current_user = nil)
+ segments = all_groups(current_user)
+
+ find_union(segments, Group).order_id_desc
+ end
+
+ private
+
+ def all_groups(current_user)
+ groups = []
+
+ groups << @user.authorized_groups.visible_to_user(current_user) if current_user
+ groups << @user.authorized_groups.public_to_user(current_user)
+
+ groups
+ end
+end
diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb
index a61ffa22990..3ad4bd5f066 100644
--- a/app/finders/personal_projects_finder.rb
+++ b/app/finders/personal_projects_finder.rb
@@ -1,4 +1,4 @@
-class PersonalProjectsFinder
+class PersonalProjectsFinder < UnionFinder
def initialize(user)
@user = user
end
@@ -11,31 +11,19 @@ class PersonalProjectsFinder
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
- if current_user
- relation = projects_visible_to_user(current_user)
- else
- relation = public_projects
- end
+ segments = all_projects(current_user)
- relation.includes(:namespace).order_id_desc
+ find_union(segments, Project).includes(:namespace).order_id_desc
end
private
- def projects_visible_to_user(current_user)
- authorized = @user.personal_projects.visible_to_user(current_user)
+ def all_projects(current_user)
+ projects = []
- union = Gitlab::SQL::Union.
- new([authorized.select(:id), public_and_internal_projects.select(:id)])
+ projects << @user.personal_projects.visible_to_user(current_user) if current_user
+ projects << @user.personal_projects.public_to_user(current_user)
- Project.where("projects.id IN (#{union.to_sql})")
- end
-
- def public_projects
- @user.personal_projects.public_only
- end
-
- def public_and_internal_projects
- @user.personal_projects.public_and_internal_only
+ projects
end
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 3b4e0362e04..2f0a9659d15 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -1,76 +1,18 @@
-class ProjectsFinder
- # Returns all projects, optionally including group projects a user has access
- # to.
- #
- # ## Examples
- #
- # Retrieving all public projects:
- #
- # ProjectsFinder.new.execute
- #
- # Retrieving all public/internal projects and those the given user has access
- # to:
- #
- # ProjectsFinder.new.execute(some_user)
- #
- # Retrieving all public/internal projects as well as the group's projects the
- # user has access to:
- #
- # ProjectsFinder.new.execute(some_user, group: some_group)
- #
- # Returns an ActiveRecord::Relation.
+class ProjectsFinder < UnionFinder
def execute(current_user = nil, options = {})
- group = options[:group]
+ segments = all_projects(current_user)
- if group
- segments = group_projects(current_user, group)
- else
- segments = all_projects(current_user)
- end
-
- if segments.length > 1
- union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) })
-
- Project.where("projects.id IN (#{union.to_sql})")
- else
- segments.first
- end
+ find_union(segments, Project)
end
private
- def group_projects(current_user, group)
- if current_user
- [
- group_projects_for_user(current_user, group),
- group.projects.public_and_internal_only
- ]
- else
- [group.projects.public_only]
- end
- end
-
def all_projects(current_user)
- if current_user
- [current_user.authorized_projects, public_and_internal_projects]
- else
- [Project.public_only]
- end
- end
-
- def group_projects_for_user(current_user, group)
- if group.users.include?(current_user)
- group.projects
- else
- group.projects.visible_to_user(current_user)
- end
- end
+ projects = []
- def public_projects
- Project.unscoped.public_only
- end
+ projects << current_user.authorized_projects if current_user
+ projects << Project.unscoped.public_to_user(current_user)
- def public_and_internal_projects
- Project.unscoped.public_and_internal_only
+ projects
end
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 07b5759443b..a41172816b8 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -4,7 +4,7 @@ class SnippetsFinder
case filter
when :all then
- snippets(current_user).fresh.non_expired
+ snippets(current_user).fresh
when :by_user then
by_user(current_user, params[:user], params[:scope])
when :by_project
@@ -27,7 +27,7 @@ class SnippetsFinder
end
def by_user(current_user, user, scope)
- snippets = user.snippets.fresh.non_expired
+ snippets = user.snippets.fresh
return snippets.are_public unless current_user
@@ -48,7 +48,7 @@ class SnippetsFinder
end
def by_project(current_user, project)
- snippets = project.snippets.fresh.non_expired
+ snippets = project.snippets.fresh
if current_user
if project.team.member?(current_user.id)
diff --git a/app/finders/union_finder.rb b/app/finders/union_finder.rb
new file mode 100644
index 00000000000..33cd1a491f3
--- /dev/null
+++ b/app/finders/union_finder.rb
@@ -0,0 +1,11 @@
+class UnionFinder
+ def find_union(segments, klass)
+ if segments.length > 1
+ union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) })
+
+ klass.where("#{klass.table_name}.id IN (#{union.to_sql})")
+ else
+ segments.first
+ end
+ end
+end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index c5820bf4c50..e0abc3a2869 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -1,21 +1,33 @@
module AppearancesHelper
- def brand_item
- nil
- end
-
def brand_title
- 'GitLab Community Edition'
+ if brand_item && brand_item.title
+ brand_item.title
+ else
+ 'GitLab Community Edition'
+ end
end
def brand_image
- nil
+ if brand_item.logo?
+ image_tag brand_item.logo
+ else
+ nil
+ end
end
def brand_text
- nil
+ markdown(brand_item.description)
+ end
+
+ def brand_item
+ @appearance ||= Appearance.first
end
def brand_header_logo
- render 'shared/logo.svg'
+ if brand_item && brand_item.header_logo?
+ image_tag brand_item.header_logo
+ else
+ render 'shared/logo.svg'
+ end
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index f0aa2b57121..e6ceb213532 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User)
user = user_or_email
else
- user = User.find_by(email: user_or_email.downcase)
+ user = User.find_by_any_email(user_or_email.try(:downcase))
end
if user
@@ -182,7 +182,7 @@ module ApplicationHelper
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
element = content_tag :time, time.to_s,
- class: "#{html_class} js-timeago js-timeago-pending",
+ class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}",
datetime: time.to_time.getutc.iso8601,
title: time.in_time_zone.to_s(:medium),
data: { toggle: 'tooltip', placement: placement, container: 'body' }
@@ -196,6 +196,22 @@ module ApplicationHelper
element
end
+ def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', include_author: false)
+ return if object.updated_at == object.created_at
+
+ content_tag :small, class: "edited-text" do
+ output = content_tag(:span, "Edited ")
+ output << time_ago_with_tooltip(object.updated_at, placement: placement, html_class: html_class)
+
+ if include_author && object.updated_by && object.updated_by != object.author
+ output << content_tag(:span, " by ")
+ output << link_to_member(object.project, object.updated_by, avatar: false, author_class: nil)
+ end
+
+ output
+ end
+ end
+
def render_markup(file_name, file_content)
if gitlab_markdown?(file_name)
Haml::Helpers.preserve(markdown(file_content))
@@ -285,7 +301,7 @@ module ApplicationHelper
if project.nil?
nil
elsif current_controller?(:issues)
- project.issues.send(entity).count
+ project.issues.visible_to_user(current_user).send(entity).count
elsif current_controller?(:merge_requests)
project.merge_requests.send(entity).count
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index de669e529a7..b4f80fd9b3e 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -6,6 +6,10 @@ module AuthHelper
Gitlab.config.ldap.enabled
end
+ def omniauth_enabled?
+ Gitlab.config.omniauth.enabled
+ end
+
def provider_has_icon?(name)
PROVIDERS_WITH_ICONS.include?(name.to_s)
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 7143a744869..820d69c230b 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -27,7 +27,7 @@ module BlobHelper
link_opts)
if !on_top_of_branch?(project, ref)
- button_tag "Edit", class: "btn btn-default disabled has_tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
+ button_tag "Edit", class: "btn btn-default disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
link_to "Edit", edit_path, class: 'btn'
elsif can?(current_user, :fork_project, project)
@@ -50,9 +50,9 @@ module BlobHelper
return unless blob
if !on_top_of_branch?(project, ref)
- button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
+ button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.lfs_pointer?
- button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
+ button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
@@ -134,4 +134,43 @@ module BlobHelper
blob.data = Loofah.scrub_fragment(blob.data, :strip).to_xml
blob
end
+
+ # If we blindly set the 'real' content type when serving a Git blob we
+ # are enabling XSS attacks. An attacker could upload e.g. a Javascript
+ # file to a Git repository, trick the browser of a victim into
+ # downloading the blob, and then the 'application/javascript' content
+ # type would tell the browser to execute the attacker's Javascript. By
+ # overriding the content type and setting it to 'text/plain' (in the
+ # example of Javascript) we tell the browser of the victim not to
+ # execute untrusted data.
+ def safe_content_type(blob)
+ if blob.text?
+ 'text/plain; charset=utf-8'
+ elsif blob.image?
+ blob.content_type
+ else
+ 'application/octet-stream'
+ end
+ end
+
+ def cached_blob?
+ stale = stale?(etag: @blob.id) # The #stale? method sets cache headers.
+
+ # Because we are opionated we set the cache headers ourselves.
+ response.cache_control[:public] = @project.public?
+
+ if @ref && @commit && @ref == @commit.id
+ # This is a link to a commit by its commit SHA. That means that the blob
+ # is immutable. The only reason to invalidate the cache is if the commit
+ # was deleted or if the user lost access to the repository.
+ response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE
+ else
+ # A branch or tag points at this blob. That means that the expected blob
+ # value may change over time.
+ response.cache_control[:max_age] = Blob::CACHE_TIME
+ end
+
+ response.etag = @blob.id
+ !stale
+ end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index d6c05843743..a9047ede8c5 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -23,36 +23,34 @@ module ButtonHelper
end
def http_clone_button(project)
- klass = 'btn js-protocol-switch'
- klass << ' active' if default_clone_protocol == 'http'
- klass << ' has_tooltip' if current_user.try(:require_password?)
+ klass = 'http-selector'
+ klass << ' has-tooltip' if current_user.try(:require_password?)
protocol = gitlab_config.protocol.upcase
- content_tag :button, protocol,
+ content_tag :a, protocol,
class: klass,
+ href: @project.http_url_to_repo,
data: {
- clone: project.http_url_to_repo,
+ html: true,
+ placement: 'right',
container: 'body',
- html: 'true',
title: "Set a password on your account<br>to pull or push via #{protocol}"
- },
- type: :button
+ }
end
def ssh_clone_button(project)
- klass = 'btn js-protocol-switch'
- klass << ' active' if default_clone_protocol == 'ssh'
- klass << ' has_tooltip' if current_user.try(:require_ssh_key?)
+ klass = 'ssh-selector'
+ klass << ' has-tooltip' if current_user.try(:require_ssh_key?)
- content_tag :button, 'SSH',
+ content_tag :a, 'SSH',
class: klass,
+ href: project.ssh_url_to_repo,
data: {
- clone: project.ssh_url_to_repo,
+ html: true,
+ placement: 'right',
container: 'body',
- html: 'true',
title: 'Add an SSH key to your profile<br>to pull or push via SSH.'
- },
- type: :button
+ }
end
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index d8bee21c82e..8b1575d5e0c 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -12,9 +12,13 @@ module CiStatusHelper
ci_label_for_status(ci_commit.status)
end
- def ci_status_with_icon(status)
- content_tag :span, class: "ci-status ci-#{status}" do
- ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
+ def ci_status_with_icon(status, target = nil)
+ content = ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
+ klass = "ci-status ci-#{status}"
+ if target
+ link_to content, target, class: klass
+ else
+ content_tag :span, content, class: klass
end
end
@@ -42,12 +46,12 @@ module CiStatusHelper
icon(icon_name + ' fw')
end
- def render_ci_status(ci_commit)
+ def render_ci_status(ci_commit, tooltip_placement: 'auto left')
link_to ci_status_icon(ci_commit),
ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
title: "Build #{ci_status_label(ci_commit)}",
- data: { toggle: 'tooltip', placement: 'left' }
+ data: { toggle: 'tooltip', placement: tooltip_placement }
end
def no_runners_for_project?(project)
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 7ff539118d3..bde0799f3de 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -130,7 +130,7 @@ module CommitsHelper
if can_collaborate_with_project?
content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do
- link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}"
+ link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}"
end
elsif can?(current_user, :fork_project, @project)
continue_params = {
@@ -142,7 +142,7 @@ module CommitsHelper
namespace_key: current_user.namespace.id,
continue: continue_params)
- link_to 'Revert', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', title: tooltip
+ link_to 'Revert', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip
end
end
@@ -182,7 +182,7 @@ module CommitsHelper
end
options = {
- class: "commit-#{options[:source]}-link has_tooltip",
+ class: "commit-#{options[:source]}-link has-tooltip",
data: { 'original-title'.to_sym => sanitize(source_email) }
}
@@ -211,4 +211,15 @@ module CommitsHelper
def clean(string)
Sanitize.clean(string, remove_contents: true)
end
+
+ def limited_commits(commits)
+ if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
+ [
+ commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE),
+ commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE
+ ]
+ else
+ [commits, 0]
+ end
+ end
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 6a3ab3ea40a..ff32e834499 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -12,40 +12,20 @@ module DiffHelper
params[:view] == 'parallel' ? 'parallel' : 'inline'
end
- def allowed_diff_size
- if diff_hard_limit_enabled?
- Commit::DIFF_HARD_LIMIT_FILES
- else
- Commit::DIFF_SAFE_FILES
- end
+ def diff_hard_limit_enabled?
+ params[:force_show_diff].present?
end
- def allowed_diff_lines
+ def diff_options
+ options = { ignore_whitespace_change: params[:w] == '1' }
if diff_hard_limit_enabled?
- Commit::DIFF_HARD_LIMIT_LINES
- else
- Commit::DIFF_SAFE_LINES
+ options.merge!(Commit.max_diff_options)
end
+ options
end
def safe_diff_files(diffs, diff_refs)
- lines = 0
- safe_files = []
- diffs.first(allowed_diff_size).each do |diff|
- lines += diff.diff.lines.count
- break if lines > allowed_diff_lines
- safe_files << Gitlab::Diff::File.new(diff, diff_refs)
- end
- safe_files
- end
-
- def diff_hard_limit_enabled?
- # Enabling hard limit allows user to see more diff information
- if params[:force_show_diff].present?
- true
- else
- false
- end
+ diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs) }
end
def generate_line_code(file_path, line)
@@ -137,7 +117,7 @@ module DiffHelper
# Always use HTML to handle case where JSON diff rendered this button
params_copy.delete(:format)
- link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn') do
+ link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn'), data: { view_type: name } do
title
end
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
new file mode 100644
index 00000000000..14697f774cc
--- /dev/null
+++ b/app/helpers/dropdowns_helper.rb
@@ -0,0 +1,101 @@
+module DropdownsHelper
+ def dropdown_tag(toggle_text, options: {}, &block)
+ content_tag :div, class: "dropdown" do
+ data_attr = { toggle: "dropdown" }
+
+ if options.has_key?(:data)
+ data_attr = options[:data].merge(data_attr)
+ end
+
+ dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
+
+ dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do
+ output = ""
+
+ if options.has_key?(:title)
+ output << dropdown_title(options[:title])
+ end
+
+ if options.has_key?(:filter)
+ output << dropdown_filter(options[:placeholder])
+ end
+
+ output << content_tag(:div, class: "dropdown-content") do
+ capture(&block) if block && !options.has_key?(:footer_content)
+ end
+
+ if block && options[:footer_content]
+ output << content_tag(:div, class: "dropdown-footer") do
+ capture(&block)
+ end
+ end
+
+ output << dropdown_loading
+
+ output.html_safe
+ end
+
+ dropdown_output.html_safe
+ end
+ end
+
+ def dropdown_toggle(toggle_text, data_attr, options)
+ content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
+ output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
+ output << icon('chevron-down')
+ output.html_safe
+ end
+ end
+
+ def dropdown_title(title, back: false)
+ content_tag :div, class: "dropdown-title" do
+ title_output = ""
+
+ if back
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
+ icon('arrow-left')
+ end
+ end
+
+ title_output << content_tag(:span, title)
+
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
+ icon('times', class: 'dropdown-menu-close-icon')
+ end
+
+ title_output.html_safe
+ end
+ end
+
+ def dropdown_filter(placeholder)
+ content_tag :div, class: "dropdown-input" do
+ filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
+ filter_output << icon('search', class: "dropdown-input-search")
+ filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
+
+ filter_output.html_safe
+ end
+ end
+
+ def dropdown_content(&block)
+ content_tag(:div, class: "dropdown-content") do
+ if block
+ capture(&block)
+ end
+ end
+ end
+
+ def dropdown_footer(&block)
+ content_tag(:div, class: "dropdown-footer") do
+ if block
+ capture(&block)
+ end
+ end
+ end
+
+ def dropdown_loading
+ content_tag :div, class: "dropdown-loading" do
+ icon('spinner spin')
+ end
+ end
+end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 31bf45baeb7..d3e5e3aa8b9 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -3,7 +3,7 @@ module EventsHelper
author = event.author
if author
- link_to author.name, user_path(author.username)
+ link_to author.name, user_path(author.username), title: h(author.name)
else
event.author_name
end
@@ -159,7 +159,7 @@ module EventsHelper
link_to(
namespace_project_commit_path(event.project.namespace, event.project,
event.note_commit_id,
- anchor: dom_id(event.target)),
+ anchor: dom_id(event.target), title: h(event.target_title)),
class: "commit_short_id"
) do
"#{event.note_target_type} #{event.note_short_commit_id}"
@@ -167,12 +167,12 @@ module EventsHelper
elsif event.note_project_snippet?
link_to(namespace_project_snippet_path(event.project.namespace,
event.project,
- event.note_target)) do
- "#{event.note_target_type} ##{truncate event.note_target_id}"
+ event.note_target), title: h(event.project.name)) do
+ "#{event.note_target_type} #{truncate event.note_target.to_reference}"
end
else
link_to event_note_target_path(event) do
- "#{event.note_target_type} ##{truncate event.note_target_iid}"
+ "#{event.note_target_type} #{truncate event.note_target.to_reference}"
end
end
else
@@ -194,7 +194,7 @@ module EventsHelper
end
def event_to_atom(xml, event)
- if event.proper?
+ if event.visible_to_user?(current_user)
xml.entry do
event_link = event_feed_url(event)
event_title = event_feed_title(event)
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
index 3648757428b..337b0aacbb5 100644
--- a/app/helpers/explore_helper.rb
+++ b/app/helpers/explore_helper.rb
@@ -1,5 +1,5 @@
module ExploreHelper
- def explore_projects_filter_path(options={})
+ def filter_projects_path(options={})
exist_opts = {
sort: params[:sort],
scope: params[:scope],
@@ -9,15 +9,7 @@ module ExploreHelper
}
options = exist_opts.merge(options)
-
- path = if explore_controller?
- explore_projects_path
- elsif current_action?(:starred)
- starred_dashboard_projects_path
- else
- dashboard_projects_path
- end
-
+ path = request.path
path << "?#{options.to_param}"
path
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 89d2a648494..2f760af02fd 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -50,6 +50,8 @@ module GitlabMarkdownHelper
context[:project] ||= @project
+ text = Banzai.pre_process(text, context)
+
html = Banzai.render(text, context)
context.merge!(
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 1d36969cd62..b1f0a765bb9 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -19,6 +19,10 @@ module GroupsHelper
end
end
+ def can_change_group_visibility_level?(group)
+ can?(current_user, :change_visibility_level, group)
+ end
+
def group_icon(group)
if group.is_a?(String)
group = Group.find_by(path: group)
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 84c6d0883b0..ab3ef454e1c 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -10,6 +10,15 @@ module IconsHelper
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
end
+ def audit_icon(names, options = {})
+ case names
+ when "standard"
+ names = "key"
+ end
+
+ options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
+ end
+
def spinner(text = nil, visible = false)
css_class = 'loading'
css_class << ' hide' unless visible
@@ -37,7 +46,7 @@ module IconsHelper
else # Gitlab::VisibilityLevel::PUBLIC
'globe'
end
-
+
name << " fw" if fw
icon(name)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 91a3aa371ef..62050691a39 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -16,10 +16,37 @@ module IssuablesHelper
base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
end
+ def issuable_json_path(issuable)
+ project = issuable.project
+
+ if issuable.kind_of?(MergeRequest)
+ namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json)
+ else
+ namespace_project_issue_path(project.namespace, project, issuable.iid, :json)
+ end
+ end
+
def prev_issuable_for(issuable)
base_issuable_scope(issuable).where('iid < ?', issuable.iid).first
end
+ def user_dropdown_label(user_id, default_label)
+ return "Unassigned" if user_id == "0"
+
+ if @project
+ member = @project.team.find_member(user_id)
+ user = member.user if member
+ else
+ user = User.find_by(id: user_id)
+ end
+
+ if user
+ user.name
+ else
+ default_label
+ end
+ end
+
private
def sidebar_gutter_collapsed?
@@ -31,7 +58,11 @@ module IssuablesHelper
end
def issuable_state_scope(issuable)
- issuable.open? ? :opened : :closed
+ if issuable.respond_to?(:merged?) && issuable.merged?
+ :merged
+ else
+ issuable.open? ? :opened : :closed
+ end
end
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index ae4ebc0854a..24b90fef4fe 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -57,6 +57,19 @@ module IssuesHelper
options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id)
end
+ def project_options(issuable, current_user, ability: :read_project)
+ projects = current_user.authorized_projects
+ projects = projects.select do |project|
+ current_user.can?(ability, project)
+ end
+
+ no_project = OpenStruct.new(id: 0, name_with_namespace: 'No project')
+ projects.unshift(no_project)
+ projects.delete(issuable.project)
+
+ options_from_collection_for_select(projects, :id, :name_with_namespace)
+ end
+
def status_box_class(item)
if item.respond_to?(:expired?) && item.expired?
'status-box-expired'
@@ -98,6 +111,10 @@ module IssuesHelper
end.sort.to_sentence(last_word_connector: ', or ')
end
+ def confidential_icon(issue)
+ icon('eye-slash') if issue.confidential?
+ end
+
def emoji_icon(name, unicode = nil, aliases = [])
unicode ||= Emoji.emoji_filename(name) rescue ""
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 1c7fcc13b42..3dded7c2f23 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -32,7 +32,7 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" }
#
# Returns a String
- def link_to_label(label, project: nil, type: :issue, &block)
+ def link_to_label(label, project: nil, type: :issue, tooltip: true, &block)
project ||= @project || label.project
link = send("namespace_project_#{type.to_s.pluralize}_path",
project.namespace,
@@ -42,7 +42,7 @@ module LabelsHelper
if block_given?
link_to link, &block
else
- link_to render_colored_label(label), link
+ link_to render_colored_label(label, tooltip: tooltip), link
end
end
@@ -50,19 +50,26 @@ module LabelsHelper
@project.labels.pluck(:title)
end
- def render_colored_label(label)
+ def render_colored_label(label, label_suffix = '', tooltip: true)
label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color)
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
- span = %(<span class="label color-label") +
- %( style="background-color: #{label_color}; color: #{text_color}">) +
- escape_once(label.name) + '</span>'
+ span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) +
+ %(style="background-color: #{label_color}; color: #{text_color}" ) +
+ %(title="#{escape_once(label.description)}" data-container="body">) +
+ %(#{escape_once(label.name)}#{label_suffix}</span>)
span.html_safe
end
+ def render_colored_cross_project_label(label, tooltip: true)
+ label_suffix = label.project.name_with_namespace
+ label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
+ render_colored_label(label, label_suffix, tooltip: tooltip)
+ end
+
def suggested_colors
[
'#0033CC',
@@ -103,21 +110,23 @@ module LabelsHelper
end
end
- def projects_labels_options
- labels =
- if @project
- @project.labels
- else
- Label.where(project_id: @projects)
- end
+ def labels_filter_path
+ if @project
+ namespace_project_labels_path(@project.namespace, @project, :json)
+ else
+ dashboard_labels_path(:json)
+ end
+ end
- grouped_labels = GlobalLabel.build_collection(labels)
- grouped_labels.unshift(Label::None)
- grouped_labels.unshift(Label::Any)
+ def label_subscription_status(label)
+ label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
+ end
- options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
+ def label_subscription_toggle_button_text(label)
+ label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
end
# Required for Banzai::Filter::LabelReferenceFilter
- module_function :render_colored_label, :text_color_for_bg, :escape_once
+ module_function :render_colored_label, :render_colored_cross_project_label,
+ :text_color_for_bg, :escape_once
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index a42cbcff182..87fc2db6901 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -9,10 +9,36 @@ module MilestonesHelper
end
end
+ def milestones_label_path(opts = {})
+ if @project
+ namespace_project_issues_path(@project.namespace, @project, opts)
+ elsif @group
+ issues_group_path(@group, opts)
+ else
+ issues_dashboard_path(opts)
+ end
+ end
+
+ def milestones_browse_issuables_path(milestone, type:)
+ opts = { milestone_title: milestone.title }
+
+ if @project
+ polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts)
+ elsif @group
+ polymorphic_url([type, @group], opts)
+ else
+ polymorphic_url([type, :dashboard], opts)
+ end
+ end
+
+ def milestone_issues_by_label_count(milestone, label, state:)
+ milestone.issues.with_label(label.title).send(state).size
+ end
+
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
- style: "width: #{milestone.percent_complete}%;"
+ style: "width: #{milestone.percent_complete(current_user)}%;"
}
content_tag :div, class: 'progress' do
@@ -20,20 +46,21 @@ module MilestonesHelper
end
end
- def projects_milestones_options
- milestones =
- if @project
- @project.milestones
- else
- Milestone.where(project_id: @projects)
- end.active
-
- epoch = DateTime.parse('1970-01-01')
- grouped_milestones = GlobalMilestone.build_collection(milestones)
- grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
- grouped_milestones.unshift(Milestone::None)
- grouped_milestones.unshift(Milestone::Any)
-
- options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
+ def milestones_filter_dropdown_path
+ if @project
+ namespace_project_milestones_path(@project.namespace, @project, :json)
+ else
+ dashboard_milestones_path(:json)
+ end
+ end
+
+ def milestone_remaining_days(milestone)
+ if milestone.expired?
+ content_tag(:strong, 'expired')
+ elsif milestone.due_date
+ days = milestone.remaining_days
+ content = content_tag(:strong, days)
+ content << " #{'day'.pluralize(days)} remaining"
+ end
end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 29cb753e62c..5d86bd490a8 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -23,6 +23,7 @@ module NavHelper
if current_path?('merge_requests#show') ||
current_path?('merge_requests#diffs') ||
current_path?('merge_requests#commits') ||
+ current_path?('merge_requests#builds') ||
current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index c73cb3028ee..c3832cf5d65 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -12,7 +12,9 @@ module PreferencesHelper
projects: 'Your Projects (default)',
stars: 'Starred Projects',
project_activity: "Your Projects' Activity",
- starred_project_activity: "Starred Projects' Activity"
+ starred_project_activity: "Starred Projects' Activity",
+ groups: "Your Groups",
+ todos: "Your Todos"
}.with_indifferent_access.freeze
# Returns an Array usable by a select field for more user-friendly option text
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d6fb629b0c2..4e4c6e301d5 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -8,7 +8,7 @@ module ProjectsHelper
end
def link_to_project(project)
- link_to [project.namespace.becomes(Namespace), project] do
+ link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
if project.namespace
@@ -26,7 +26,7 @@ module ProjectsHelper
image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
end
- def link_to_member(project, author, opts = {})
+ def link_to_member(project, author, opts = {}, &block)
default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
opts = default_opts.merge(opts)
@@ -38,15 +38,21 @@ module ProjectsHelper
author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
# Build name span tag
- author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
+ if opts[:by_username]
+ author_html << content_tag(:span, sanitize("@#{author.username}"), class: opts[:author_class]) if opts[:name]
+ else
+ author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
+ end
+
+ author_html << capture(&block) if block
author_html = author_html.html_safe
if opts[:name]
- link_to(author_html, user_path(author), class: "author_link").html_safe
+ link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else
title = opts[:title].sub(":name", sanitize(author.name))
- link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe
+ link_to(author_html, user_path(author), class: "author_link has-tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe
end
end
@@ -203,7 +209,7 @@ module ProjectsHelper
def default_clone_protocol
if !current_user || current_user.require_ssh_key?
- "http"
+ gitlab_config.protocol
else
"ssh"
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 1eb790b1796..494dad0b41e 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -40,7 +40,7 @@ module SearchHelper
{ label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") },
{ label: "help: SSH Keys Help", url: help_page_path("ssh", "README") },
{ label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
- { label: "help: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") },
+ { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
{ label: "help: Workflow Help", url: help_page_path("workflow", "README") },
]
end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 41ae4048992..0a5a8eb5aee 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -1,14 +1,4 @@
module SnippetsHelper
- def lifetime_select_options
- options = [
- ['forever', nil],
- ['1 day', "#{Date.current + 1.day}"],
- ['1 week', "#{Date.current + 1.week}"],
- ['1 month', "#{Date.current + 1.month}"]
- ]
- options_for_select(options)
- end
-
def reliable_snippet_path(snippet)
if snippet.project_id?
namespace_project_snippet_path(snippet.project.namespace,
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index f9026b887da..2f2d2721d6d 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -16,6 +16,16 @@ module SortingHelper
}
end
+ def projects_sort_options_hash
+ {
+ sort_value_name => sort_title_name,
+ sort_value_recently_updated => sort_title_recently_updated,
+ sort_value_oldest_updated => sort_title_oldest_updated,
+ sort_value_recently_created => sort_title_recently_created,
+ sort_value_oldest_created => sort_title_oldest_created,
+ }
+ end
+
def sort_title_oldest_updated
'Oldest updated'
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 4b745a5b969..edc5686cf08 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -16,14 +16,19 @@ module TodosHelper
def todo_target_link(todo)
target = todo.target_type.titleize.downcase
- link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo)
+ link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title }
end
def todo_target_path(todo)
anchor = dom_id(todo.note) if todo.note.present?
- polymorphic_path([todo.project.namespace.becomes(Namespace),
- todo.project, todo.target], anchor: anchor)
+ if todo.for_commit?
+ namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
+ todo.target, anchor: anchor)
+ else
+ polymorphic_path([todo.project.namespace.becomes(Namespace),
+ todo.project, todo.target], anchor: anchor)
+ end
end
def todos_filter_params
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 71d33b445c2..3a83ae15dd8 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -19,6 +19,8 @@ module VisibilityLevelHelper
case form_model
when Project
project_visibility_level_description(level)
+ when Group
+ group_visibility_level_description(level)
when Snippet
snippet_visibility_level_description(level, form_model)
end
@@ -35,6 +37,17 @@ module VisibilityLevelHelper
end
end
+ def group_visibility_level_description(level)
+ case level
+ when Gitlab::VisibilityLevel::PRIVATE
+ "The group and its projects can only be viewed by members."
+ when Gitlab::VisibilityLevel::INTERNAL
+ "The group and any internal projects can be viewed by any logged in user."
+ when Gitlab::VisibilityLevel::PUBLIC
+ "The group and any public projects can be viewed without any authentication."
+ end
+ end
+
def snippet_visibility_level_description(level, snippet = nil)
case level
when Gitlab::VisibilityLevel::PRIVATE
@@ -50,6 +63,23 @@ module VisibilityLevelHelper
end
end
+ def visibility_icon_description(form_model)
+ case form_model
+ when Project
+ project_visibility_icon_description(form_model.visibility_level)
+ when Group
+ group_visibility_icon_description(form_model.visibility_level)
+ end
+ end
+
+ def group_visibility_icon_description(level)
+ "#{visibility_level_label(level)} - #{group_visibility_level_description(level)}"
+ end
+
+ def project_visibility_icon_description(level)
+ "#{visibility_level_label(level)} - #{project_visibility_level_description(level)}"
+ end
+
def visibility_level_label(level)
Project.visibility_levels.key(level)
end
@@ -67,8 +97,11 @@ module VisibilityLevelHelper
current_application_settings.default_snippet_visibility
end
+ def default_group_visibility
+ current_application_settings.default_group_visibility
+ end
+
def skip_level?(form_model, level)
- form_model.is_a?(Project) &&
- !form_model.visibility_level_allowed?(level)
+ form_model.is_a?(Project) && !form_model.visibility_level_allowed?(level)
end
end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 4a88cb61132..6f54c42146c 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -16,7 +16,15 @@ module Emails
def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
setup_issue_mail(issue_id, recipient_id)
- @updated_by = User.find updated_by_user_id
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+ end
+
+ def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id)
+ setup_issue_mail(issue_id, recipient_id)
+
+ @label_names = label_names
+ @labels_url = namespace_project_labels_url(@project.namespace, @project)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
@@ -24,20 +32,20 @@ module Emails
setup_issue_mail(issue_id, recipient_id)
@issue_status = status
- @updated_by = User.find updated_by_user_id
+ @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
- private
+ def issue_moved_email(recipient, issue, new_issue, updated_by_user)
+ setup_issue_mail(issue.id, recipient.id)
- def issue_thread_options(sender_id, recipient_id)
- {
- from: sender(sender_id),
- to: recipient(recipient_id),
- subject: subject("#{@issue.title} (##{@issue.iid})")
- }
+ @new_issue = new_issue
+ @new_project = new_issue.project
+ mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id))
end
+ private
+
def setup_issue_mail(issue_id, recipient_id)
@issue = Issue.find(issue_id)
@project = @issue.project
@@ -45,5 +53,13 @@ module Emails
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end
+
+ def issue_thread_options(sender_id, recipient_id)
+ {
+ from: sender(sender_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@issue.title} (##{@issue.iid})")
+ }
+ end
end
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 325996e2e16..55bb4f65270 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -3,50 +3,43 @@ module Emails
def new_merge_request_email(recipient_id, merge_request_id)
setup_merge_request_mail(merge_request_id, recipient_id)
- mail_new_thread(@merge_request,
- from: sender(@merge_request.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
+ end
+
+ def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
+ @label_names = label_names
+ @labels_url = namespace_project_labels_url(@project.namespace, @project)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
- @updated_by = User.find updated_by_user_id
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
@mr_status = status
- @updated_by = User.find updated_by_user_id
- mail_answer_thread(@merge_request,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ @updated_by = User.find(updated_by_user_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
private
@@ -54,11 +47,17 @@ module Emails
def setup_merge_request_mail(merge_request_id, recipient_id)
@merge_request = MergeRequest.find(merge_request_id)
@project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
+ @target_url = namespace_project_merge_request_url(@project.namespace, @project, @merge_request)
@sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
end
+
+ def merge_request_thread_options(sender_id, recipient_id)
+ {
+ from: sender(sender_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@merge_request.title} (##{@merge_request.iid})")
+ }
+ end
end
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 3a83b083109..256cbcd73a1 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -14,7 +14,10 @@ module Emails
end
def new_ssh_key_email(key_id)
- @key = Key.find(key_id)
+ @key = Key.find_by_id(key_id)
+
+ return unless @key
+
@current_user = @user = @key.user
@target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
diff --git a/app/models/ability.rb b/app/models/ability.rb
index a866eadeebb..c0bf6def7c5 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -9,6 +9,7 @@ class Ability
when CommitStatus then commit_status_abilities(user, subject)
when Project then project_abilities(user, subject)
when Issue then issue_abilities(user, subject)
+ when ExternalIssue then external_issue_abilities(user, subject)
when Note then note_abilities(user, subject)
when ProjectSnippet then project_snippet_abilities(user, subject)
when PersonalSnippet then personal_snippet_abilities(user, subject)
@@ -26,6 +27,8 @@ class Ability
case true
when subject.is_a?(PersonalSnippet)
anonymous_personal_snippet_abilities(subject)
+ when subject.is_a?(ProjectSnippet)
+ anonymous_project_snippet_abilities(subject)
when subject.is_a?(CommitStatus)
anonymous_commit_status_abilities(subject)
when subject.is_a?(Project) || subject.respond_to?(:project)
@@ -48,7 +51,6 @@ class Ability
rules = [
:read_project,
:read_wiki,
- :read_issue,
:read_label,
:read_milestone,
:read_project_snippet,
@@ -62,6 +64,9 @@ class Ability
# Allow to read builds by anonymous user if guests are allowed
rules << :read_build if project.public_builds?
+ # Allow to read issues by anonymous user if issue is not confidential
+ rules << :read_issue unless subject.is_a?(Issue) && subject.confidential?
+
rules - project_disabled_features_rules(project)
else
[]
@@ -82,7 +87,7 @@ class Ability
subject.group
end
- if group && group.projects.public_only.any?
+ if group && group.public?
[:read_group]
else
[]
@@ -97,6 +102,14 @@ class Ability
end
end
+ def anonymous_project_snippet_abilities(snippet)
+ if snippet.public?
+ [:read_project_snippet]
+ else
+ []
+ end
+ end
+
def global_abilities(user)
rules = []
rules << :create_group if user.can_create_group
@@ -108,37 +121,23 @@ class Ability
key = "/user/#{user.id}/project/#{project.id}"
RequestStore.store[key] ||= begin
- team = project.team
-
- # Rules based on role in project
- if team.master?(user)
- rules.push(*project_master_rules)
+ # Push abilities on the users team role
+ rules.push(*project_team_rules(project.team, user))
- elsif team.developer?(user)
- rules.push(*project_dev_rules)
+ if project.owner == user ||
+ (project.group && project.group.has_owner?(user)) ||
+ user.admin?
- elsif team.reporter?(user)
- rules.push(*project_report_rules)
-
- elsif team.guest?(user)
- rules.push(*project_guest_rules)
+ rules.push(*project_owner_rules)
end
- if project.public? || project.internal?
+ if project.public? || (project.internal? && !user.external?)
rules.push(*public_project_rules)
# Allow to read builds for internal projects
rules << :read_build if project.public_builds?
end
- if project.owner == user || user.admin?
- rules.push(*project_admin_rules)
- end
-
- if project.group && project.group.has_owner?(user)
- rules.push(*project_admin_rules)
- end
-
if project.archived?
rules -= project_archived_rules
end
@@ -147,6 +146,19 @@ class Ability
end
end
+ def project_team_rules(team, user)
+ # Rules based on role in project
+ if team.master?(user)
+ project_master_rules
+ elsif team.developer?(user)
+ project_dev_rules
+ elsif team.reporter?(user)
+ project_report_rules
+ elsif team.guest?(user)
+ project_guest_rules
+ end
+ end
+
def public_project_rules
@public_project_rules ||= project_guest_rules + [
:download_code,
@@ -168,7 +180,8 @@ class Ability
:read_note,
:create_project,
:create_issue,
- :create_note
+ :create_note,
+ :upload_file
]
end
@@ -188,6 +201,7 @@ class Ability
def project_dev_rules
@project_dev_rules ||= project_report_rules + [
:admin_merge_request,
+ :update_merge_request,
:create_commit_status,
:update_commit_status,
:create_build,
@@ -212,7 +226,6 @@ class Ability
@project_master_rules ||= project_dev_rules + [
:push_code_to_protected_branches,
:update_project_snippet,
- :update_merge_request,
:admin_milestone,
:admin_project_snippet,
:admin_project_member,
@@ -225,14 +238,16 @@ class Ability
]
end
- def project_admin_rules
- @project_admin_rules ||= project_master_rules + [
+ def project_owner_rules
+ @project_owner_rules ||= project_master_rules + [
:change_namespace,
:change_visibility_level,
:rename_project,
:remove_project,
:archive_project,
- :remove_fork_project
+ :remove_fork_project,
+ :destroy_merge_request,
+ :destroy_issue
]
end
@@ -270,11 +285,9 @@ class Ability
def group_abilities(user, group)
rules = []
- if user.admin? || group.users.include?(user) || ProjectsFinder.new.execute(user, group: group).any?
- rules << :read_group
- end
+ rules << :read_group if can_read_group?(user, group)
- # Only group masters and group owners can create new projects in group
+ # Only group masters and group owners can create new projects
if group.has_master?(user) || group.has_owner?(user) || user.admin?
rules += [
:create_projects,
@@ -287,13 +300,23 @@ class Ability
rules += [
:admin_group,
:admin_namespace,
- :admin_group_member
+ :admin_group_member,
+ :change_visibility_level
]
end
rules.flatten
end
+ def can_read_group?(user, group)
+ return true if user.admin?
+ return true if group.public?
+ return true if group.internal? && !user.external?
+ return true if group.users.include?(user)
+
+ GroupProjectsFinder.new(group).execute(user).any?
+ end
+
def namespace_abilities(user, namespace)
rules = []
@@ -320,28 +343,27 @@ class Ability
end
rules += project_abilities(user, subject.project)
+ rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue)
rules
end
end
- [:note, :project_snippet].each do |name|
- define_method "#{name}_abilities" do |user, subject|
- rules = []
-
- if subject.author == user
- rules += [
- :"read_#{name}",
- :"update_#{name}",
- :"admin_#{name}"
- ]
- end
+ def note_abilities(user, note)
+ rules = []
- if subject.respond_to?(:project) && subject.project
- rules += project_abilities(user, subject.project)
- end
+ if note.author == user
+ rules += [
+ :read_note,
+ :update_note,
+ :admin_note
+ ]
+ end
- rules
+ if note.respond_to?(:project) && note.project
+ rules += project_abilities(user, note.project)
end
+
+ rules
end
def personal_snippet_abilities(user, snippet)
@@ -355,13 +377,31 @@ class Ability
]
end
- if snippet.public? || snippet.internal?
+ if snippet.public? || (snippet.internal? && !user.external?)
rules << :read_personal_snippet
end
rules
end
+ def project_snippet_abilities(user, snippet)
+ rules = []
+
+ if snippet.author == user || user.admin?
+ rules += [
+ :read_project_snippet,
+ :update_project_snippet,
+ :admin_project_snippet
+ ]
+ end
+
+ if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user))
+ rules << :read_project_snippet
+ end
+
+ rules
+ end
+
def group_member_abilities(user, subject)
rules = []
target_user = subject.user
@@ -424,6 +464,10 @@ class Ability
end
end
+ def external_issue_abilities(user, subject)
+ project_abilities(user, subject.project)
+ end
+
private
def named_abilities(name)
@@ -434,5 +478,17 @@ class Ability
:"admin_#{name}"
]
end
+
+ def filter_confidential_issues_abilities(user, issue, rules)
+ return rules if user.admin? || !issue.confidential?
+
+ unless issue.author == user || issue.assignee == user || issue.project.team.member?(user.id)
+ rules.delete(:admin_issue)
+ rules.delete(:read_issue)
+ rules.delete(:update_issue)
+ end
+
+ rules
+ end
end
end
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index cc59aa4e911..b61f5123127 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -19,9 +19,9 @@ class AbuseReport < ActiveRecord::Base
validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' }
- def remove_user
+ def remove_user(deleted_by:)
user.block
- user.destroy
+ DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
end
def notify
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
new file mode 100644
index 00000000000..4cf8dd9a8ce
--- /dev/null
+++ b/app/models/appearance.rb
@@ -0,0 +1,9 @@
+class Appearance < ActiveRecord::Base
+ validates :title, presence: true
+ validates :description, presence: true
+ validates :logo, file_size: { maximum: 1.megabyte }
+ validates :header_logo, file_size: { maximum: 1.megabyte }
+
+ mount_uploader :logo, AttachmentUploader
+ mount_uploader :header_logo, AttachmentUploader
+end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 269056e0e77..c4879598c4e 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -18,6 +18,7 @@
# max_attachment_size :integer default(10), not null
# default_project_visibility :integer
# default_snippet_visibility :integer
+# default_group_visibility :integer
# restricted_signup_domains :text
# user_oauth_applications :boolean default(TRUE)
# after_sign_out_path :string(255)
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 8ee9f3006b2..72e6c5fa3fd 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -1,5 +1,8 @@
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
class Blob < SimpleDelegator
+ CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
+ CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
+
# Wrap a Gitlab::Git::Blob object, or return nil when given nil
#
# This method prevents the decorated object from evaluating to "truthy" when
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 1227458e525..7d33838044b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -37,8 +37,6 @@
module Ci
class Build < CommitStatus
- include Gitlab::Application.routes.url_helpers
-
LAZY_ATTRIBUTES = ['trace']
belongs_to :runner, class_name: 'Ci::Runner'
@@ -128,7 +126,7 @@ module Ci
end
def retried?
- !self.commit.latest_builds_for_ref(self.ref).include?(self)
+ !self.commit.latest_statuses_for_ref(self.ref).include?(self)
end
def depends_on_builds
@@ -309,22 +307,6 @@ module Ci
project.valid_runners_token? token
end
- def target_url
- namespace_project_build_url(project.namespace, project, self)
- end
-
- def cancel_url
- if active?
- cancel_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
- def retry_url
- if retryable?
- retry_namespace_project_build_path(project.namespace, project, self)
- end
- end
-
def can_be_served?(runner)
(tag_list - runner.tag_list).empty?
end
@@ -333,7 +315,7 @@ module Ci
project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
end
- def show_warning?
+ def stuck?
pending? && !any_runners_online?
end
@@ -348,18 +330,6 @@ module Ci
artifacts_file.exists?
end
- def artifacts_download_url
- if artifacts?
- download_namespace_project_build_artifacts_path(project.namespace, project, self)
- end
- end
-
- def artifacts_browse_url
- if artifacts_metadata?
- browse_namespace_project_build_artifacts_path(project.namespace, project, self)
- end
- end
-
def artifacts_metadata?
artifacts? && artifacts_metadata.exists?
end
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
index ecbd2078b1d..f4cf7034b14 100644
--- a/app/models/ci/commit.rb
+++ b/app/models/ci/commit.rb
@@ -25,8 +25,6 @@ module Ci
has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
- scope :ordered, -> { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }
-
validates_presence_of :sha
validate :valid_commit_sha
@@ -42,16 +40,6 @@ module Ci
project.id
end
- def last_build
- builds.order(:id).last
- end
-
- def retry
- latest_builds.each do |build|
- Ci::Build.retry(build)
- end
- end
-
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
@@ -121,12 +109,14 @@ module Ci
@latest_statuses ||= statuses.latest.to_a
end
- def latest_builds
- @latest_builds ||= builds.latest.to_a
+ def latest_statuses_for_ref(ref)
+ latest_statuses.select { |status| status.ref == ref }
end
- def latest_builds_for_ref(ref)
- latest_builds.select { |build| build.ref == ref }
+ def matrix_builds(build = nil)
+ matrix_builds = builds.latest.ordered
+ matrix_builds = matrix_builds.similar(build) if build
+ matrix_builds.to_a
end
def retried
@@ -170,7 +160,7 @@ module Ci
end
def duration
- duration_array = latest_statuses.map(&:duration).compact
+ duration_array = statuses.map(&:duration).compact
duration_array.reduce(:+).to_i
end
@@ -183,16 +173,12 @@ module Ci
end
def coverage
- coverage_array = latest_builds.map(&:coverage).compact
+ coverage_array = latest_statuses.map(&:coverage).compact
if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
end
- def matrix_for_ref?(ref)
- latest_builds_for_ref(ref).size > 1
- end
-
def config_processor
return nil unless ci_yaml_file
@config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
@@ -218,10 +204,6 @@ module Ci
git_commit_message =~ /(\[ci skip\])/ if git_commit_message
end
- def update_committed!
- update!(committed_at: DateTime.now)
- end
-
private
def save_yaml_error(error)
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index e725a6d468c..90349a07594 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -23,7 +23,7 @@ module Ci
LAST_CONTACT_TIME = 5.minutes.ago
AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online']
-
+
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id
@@ -46,9 +46,23 @@ module Ci
acts_as_taggable
+ # Searches for runners matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # This method performs a *partial* match on tokens, thus a query for "a"
+ # will match any runner where the token contains the letter "a". As a result
+ # you should *not* use this method for non-admin purposes as otherwise users
+ # might be able to query a list of all runners.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def self.search(query)
- where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query',
- query: "%#{query.try(:downcase)}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
end
def set_default_values
diff --git a/app/models/commit.rb b/app/models/commit.rb
index b99abb540ea..d0dbe009d0d 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -12,12 +12,7 @@ class Commit
attr_accessor :project
- # Safe amount of changes (files and lines) in one commit to render
- # Used to prevent 500 error on huge commits by suppressing diff
- #
- # User can force display of diff above this size
- DIFF_SAFE_FILES = 100 unless defined?(DIFF_SAFE_FILES)
- DIFF_SAFE_LINES = 5000 unless defined?(DIFF_SAFE_LINES)
+ DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
# Commits above this size will not be rendered in HTML
DIFF_HARD_LIMIT_FILES = 1000 unless defined?(DIFF_HARD_LIMIT_FILES)
@@ -36,13 +31,20 @@ class Commit
# Calculate number of lines to render for diffs
def diff_line_count(diffs)
- diffs.reduce(0) { |sum, d| sum + d.diff.lines.count }
+ diffs.reduce(0) { |sum, d| sum + Gitlab::Git::Util.count_lines(d.diff) }
end
# Truncate sha to 8 characters
def truncate_sha(sha)
sha[0..7]
end
+
+ def max_diff_options
+ {
+ max_files: DIFF_HARD_LIMIT_FILES,
+ max_lines: DIFF_HARD_LIMIT_LINES,
+ }
+ end
end
attr_accessor :raw
@@ -228,11 +230,11 @@ class Commit
end
def revert_message
- %Q{Revert "#{title}"\n\n#{revert_description}}
+ %Q{Revert "#{title.strip}"\n\n#{revert_description}}
end
def reverts_commit?(commit)
- description.include?(commit.revert_description)
+ description? && description.include?(commit.revert_description)
end
def merge_commit?
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 7ef50836322..3377a85a55a 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -114,7 +114,7 @@ class CommitStatus < ActiveRecord::Base
end
def ignored?
- failed? && allow_failure?
+ allow_failure? && (failed? || canceled?)
end
def duration
@@ -125,23 +125,7 @@ class CommitStatus < ActiveRecord::Base
end
end
- def cancel_url
- nil
- end
-
- def retry_url
- nil
- end
-
- def show_warning?
+ def stuck?
false
end
-
- def artifacts_download_url
- nil
- end
-
- def artifacts_browse_url
- nil
- end
end
diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb
index 821ed54fb98..51288094ef1 100644
--- a/app/models/concerns/internal_id.rb
+++ b/app/models/concerns/internal_id.rb
@@ -7,7 +7,10 @@ module InternalId
end
def set_iid
- max_iid = project.send(self.class.name.tableize).maximum(:iid)
+ records = project.send(self.class.name.tableize)
+ records = records.with_deleted if self.paranoid?
+ max_iid = records.maximum(:iid)
+
self.iid = max_iid.to_i + 1
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index e5f089fb8a0..cf5b2c71675 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -8,6 +8,7 @@ module Issuable
extend ActiveSupport::Concern
include Participable
include Mentionable
+ include Subscribable
include StripAttribute
included do
@@ -18,7 +19,6 @@ module Issuable
has_many :notes, as: :noteable, dependent: :destroy
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
- has_many :subscriptions, dependent: :destroy, as: :subscribable
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
@@ -29,15 +29,19 @@ module Issuable
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) }
+ scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :opened, -> { with_state(:opened, :reopened) }
scope :only_opened, -> { with_state(:opened) }
scope :only_reopened, -> { with_state(:reopened) }
scope :closed, -> { with_state(:closed) }
scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
+ scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) }
+ scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) }
+ scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) }
delegate :name,
:email,
@@ -54,15 +58,34 @@ module Issuable
attr_mentionable :description, cache: true
participant :author, :assignee, :notes_with_associations
strip_attributes :title
+
+ acts_as_paranoid
end
module ClassMethods
+ # Searches for records with a matching title.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("LOWER(title) like :query", query: "%#{query.downcase}%")
+ where(arel_table[:title].matches("%#{query}%"))
end
+ # Searches for records with a matching title or description.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def full_search(query)
- where("LOWER(title) like :query OR LOWER(description) like :query", query: "%#{query.downcase}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
def sort(method)
@@ -128,28 +151,10 @@ module Issuable
notes.awards.where(note: "thumbsup").count
end
- def subscribed?(user)
- subscription = subscriptions.find_by_user_id(user.id)
-
- if subscription
- return subscription.subscribed
- end
-
+ def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
- def toggle_subscription(user)
- subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: !subscribed?(user))
- end
-
- def unsubscribe(user)
- subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: false)
- end
-
def to_hook_data(user)
hook_data = {
object_kind: self.class.name.underscore,
@@ -206,4 +211,13 @@ module Issuable
Taskable.get_updated_tasks(old_content: previous_changes['description'].first,
new_content: description)
end
+
+ ##
+ # Method that checks if issuable can be moved to another project.
+ #
+ # Should be overridden if issuable can be moved.
+ #
+ def can_move?(*)
+ false
+ end
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
new file mode 100644
index 00000000000..5b8e3f654ea
--- /dev/null
+++ b/app/models/concerns/milestoneish.rb
@@ -0,0 +1,29 @@
+module Milestoneish
+ def closed_items_count(user = nil)
+ issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
+ end
+
+ def total_items_count(user = nil)
+ issues_visible_to_user(user).size + merge_requests.size
+ end
+
+ def complete?(user = nil)
+ total_items_count(user) == closed_items_count(user)
+ end
+
+ def percent_complete(user = nil)
+ ((closed_items_count(user) * 100) / total_items_count(user)).abs
+ rescue ZeroDivisionError
+ 0
+ end
+
+ def remaining_days
+ return 0 if !due_date || expired?
+
+ (due_date - Date.today).to_i
+ end
+
+ def issues_visible_to_user(user = nil)
+ issues.visible_to_user(user)
+ end
+end
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
new file mode 100644
index 00000000000..d5a881b2445
--- /dev/null
+++ b/app/models/concerns/subscribable.rb
@@ -0,0 +1,44 @@
+# == Subscribable concern
+#
+# Users can subscribe to these models.
+#
+# Used by Issue, MergeRequest, Label
+#
+
+module Subscribable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :subscriptions, dependent: :destroy, as: :subscribable
+ end
+
+ def subscribed?(user)
+ if subscription = subscriptions.find_by_user_id(user.id)
+ subscription.subscribed
+ else
+ subscribed_without_subscriptions?(user)
+ end
+ end
+
+ # Override this method to define custom logic to consider a subscribable as
+ # subscribed without an explicit subscription record.
+ def subscribed_without_subscriptions?(user)
+ false
+ end
+
+ def subscribers
+ subscriptions.where(subscribed: true).map(&:user)
+ end
+
+ def toggle_subscription(user)
+ subscriptions.
+ find_or_initialize_by(user_id: user.id).
+ update(subscribed: !subscribed?(user))
+ end
+
+ def unsubscribe(user)
+ subscriptions.
+ find_or_initialize_by(user_id: user.id).
+ update(subscribed: false)
+ end
+end
diff --git a/app/models/diff_line.rb b/app/models/diff_line.rb
deleted file mode 100644
index ad37945874a..00000000000
--- a/app/models/diff_line.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-class DiffLine
- attr_accessor :type, :content, :num, :code
-end
diff --git a/app/models/event.rb b/app/models/event.rb
index 9a0bbf50f8b..12183524b79 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -73,15 +73,17 @@ class Event < ActiveRecord::Base
end
end
- def proper?
+ def visible_to_user?(user = nil)
if push?
true
elsif membership_changed?
true
elsif created_project?
true
+ elsif issue? || issue_note?
+ Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target)
else
- ((issue? || merge_request? || note?) && target) || milestone?
+ ((merge_request? || note?) && target) || milestone?
end
end
@@ -296,6 +298,10 @@ class Event < ActiveRecord::Base
target.noteable_type == "Commit"
end
+ def issue_note?
+ note? && target && target.noteable_type == "Issue"
+ end
+
def note_project_snippet?
target.noteable_type == "Snippet"
end
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
index 0171f7d54b7..ddd4bad5c21 100644
--- a/app/models/global_label.rb
+++ b/app/models/global_label.rb
@@ -2,16 +2,19 @@ class GlobalLabel
attr_accessor :title, :labels
alias_attribute :name, :title
+ delegate :color, :description, to: :@first_label
+
def self.build_collection(labels)
labels = labels.group_by(&:title)
- labels.map do |title, label|
- new(title, label)
+ labels.map do |title, labels|
+ new(title, labels)
end
end
def initialize(title, labels)
@title = title
@labels = labels
+ @first_label = labels.find { |lbl| lbl.description.present? } || labels.first
end
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index 7ee276255a0..97bd79af083 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -1,4 +1,6 @@
class GlobalMilestone
+ include Milestoneish
+
attr_accessor :title, :milestones
alias_attribute :name, :title
@@ -28,33 +30,7 @@ class GlobalMilestone
end
def projects
- milestones.map { |milestone| milestone.project }
- end
-
- def issue_count
- milestones.map { |milestone| milestone.issues.count }.sum
- end
-
- def merge_requests_count
- milestones.map { |milestone| milestone.merge_requests.count }.sum
- end
-
- def open_items_count
- milestones.map { |milestone| milestone.open_items_count }.sum
- end
-
- def closed_items_count
- milestones.map { |milestone| milestone.closed_items_count }.sum
- end
-
- def total_items_count
- milestones.map { |milestone| milestone.total_items_count }.sum
- end
-
- def percent_complete
- ((closed_items_count * 100) / total_items_count).abs
- rescue ZeroDivisionError
- 0
+ @projects ||= Project.for_milestones(milestones.map(&:id))
end
def state
@@ -76,35 +52,20 @@ class GlobalMilestone
end
def issues
- @issues ||= milestones.map(&:issues).flatten.group_by(&:state)
+ @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project)
end
def merge_requests
- @merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
+ @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project)
end
def participants
@participants ||= milestones.map(&:participants).flatten.compact.uniq
end
- def opened_issues
- issues.values_at("opened", "reopened").compact.flatten
- end
-
- def closed_issues
- issues['closed']
- end
-
- def opened_merge_requests
- merge_requests.values_at("opened", "reopened").compact.flatten
- end
-
- def closed_merge_requests
- merge_requests.values_at("closed", "merged", "locked").compact.flatten
- end
-
- def complete?
- total_items_count == closed_items_count
+ def labels
+ @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten)
+ .sort_by!(&:title)
end
def due_date
diff --git a/app/models/group.rb b/app/models/group.rb
index 76042b3e3fd..b332601c59b 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -2,15 +2,16 @@
#
# Table name: namespaces
#
-# id :integer not null, primary key
-# name :string(255) not null
-# path :string(255) not null
-# owner_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255)
-# description :string(255) default(""), not null
-# avatar :string(255)
+# id :integer not null, primary key
+# name :string(255) not null
+# path :string(255) not null
+# owner_id :integer
+# visibility_level :integer default(20), not null
+# created_at :datetime
+# updated_at :datetime
+# type :string(255)
+# description :string(255) default(""), not null
+# avatar :string(255)
#
require 'carrierwave/orm/activerecord'
@@ -18,13 +19,18 @@ require 'file_size_validator'
class Group < Namespace
include Gitlab::ConfigHelper
+ include Gitlab::VisibilityLevel
include Referable
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, through: :group_members
+ has_many :project_group_links, dependent: :destroy
+ has_many :shared_projects, through: :project_group_links, source: :project
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
+ validate :visibility_level_allowed_by_projects
+
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
mount_uploader :avatar, AvatarUploader
@@ -33,8 +39,18 @@ class Group < Namespace
after_destroy :post_destroy_hook
class << self
+ # Searches for groups matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%")
+ table = Namespace.arel_table
+ pattern = "%#{query}%"
+
+ where(table[:name].matches(pattern).or(table[:path].matches(pattern)))
end
def sort(method)
@@ -62,6 +78,21 @@ class Group < Namespace
name
end
+ def visibility_level_field
+ visibility_level
+ end
+
+ def visibility_level_allowed_by_projects
+ allowed_by_projects = self.projects.where('visibility_level > ?', self.visibility_level).none?
+
+ unless allowed_by_projects
+ level_name = Gitlab::VisibilityLevel.level_name(visibility_level).downcase
+ self.errors.add(:visibility_level, "#{level_name} is not allowed since there are projects with higher visibility.")
+ end
+
+ allowed_by_projects
+ end
+
def avatar_url(size = nil)
if avatar.present?
[gitlab_config.url, avatar.url].join
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 5f58c0508fd..ed960cb39f4 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -16,6 +16,7 @@
# state :string(255)
# iid :integer
# updated_by_id :integer
+# moved_to_id :integer
#
require 'carrierwave/orm/activerecord'
@@ -31,10 +32,9 @@ class Issue < ActiveRecord::Base
ActsAsTaggableOn.strict_case_match = true
belongs_to :project
- validates :project, presence: true
+ belongs_to :moved_to, class_name: 'Issue'
- scope :of_group,
- ->(group) { where(project_id: group.projects.select(:id).reorder(nil)) }
+ validates :project, presence: true
scope :cared, ->(user) { where(assignee_id: user) }
scope :open_for, ->(user) { opened.assigned_to(user) }
@@ -58,6 +58,13 @@ class Issue < ActiveRecord::Base
attributes
end
+ def self.visible_to_user(user)
+ return where(confidential: false) if user.blank?
+ return all if user.admin?
+
+ where('issues.confidential = false OR (issues.confidential = true AND (issues.author_id = :user_id OR issues.assignee_id = :user_id OR issues.project_id IN(:project_ids)))', user_id: user.id, project_ids: user.authorized_projects.select(:id))
+ end
+
def self.reference_prefix
'#'
end
@@ -87,11 +94,20 @@ class Issue < ActiveRecord::Base
end
def referenced_merge_requests(current_user = nil)
- Gitlab::ReferenceExtractor.lazily do
- [self, *notes].flat_map do |note|
- note.all_references(current_user).merge_requests
- end
- end.sort_by(&:iid)
+ @referenced_merge_requests ||= {}
+ @referenced_merge_requests[current_user] ||= begin
+ Gitlab::ReferenceExtractor.lazily do
+ [self, *notes].flat_map do |note|
+ note.all_references(current_user).merge_requests
+ end
+ end.sort_by(&:iid).uniq
+ end
+ end
+
+ def related_branches
+ project.repository.branch_names.select do |branch|
+ branch.end_with?("-#{iid}")
+ end
end
# Reset issue events cache
@@ -120,4 +136,28 @@ class Issue < ActiveRecord::Base
note.all_references(current_user).merge_requests
end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
end
+
+ def moved?
+ !moved_to.nil?
+ end
+
+ def can_move?(user, to_project = nil)
+ if to_project
+ return false unless user.can?(:admin_issue, to_project)
+ end
+
+ !moved? && persisted? &&
+ user.can?(:admin_issue, self.project)
+ end
+
+ def to_branch_name
+ "#{title.parameterize}-#{iid}"
+ end
+
+ def can_be_worked_on?(current_user)
+ !self.closed? &&
+ !self.project.forked? &&
+ self.related_branches.empty? &&
+ self.closed_by_merge_requests(current_user).empty?
+ end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index 406a1257b5d..0282ad18139 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -16,6 +16,7 @@
require 'digest/md5'
class Key < ActiveRecord::Base
+ include AfterCommitQueue
include Sortable
belongs_to :user
@@ -62,7 +63,7 @@ class Key < ActiveRecord::Base
end
def notify_user
- NotificationService.new.new_key(self)
+ run_after_commit { NotificationService.new.new_key(self) }
end
def post_create_hook
diff --git a/app/models/label.rb b/app/models/label.rb
index 07a1db4abe5..500d5a35521 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -14,6 +14,8 @@
class Label < ActiveRecord::Base
include Referable
+ include Subscribable
+
# Represents a "No Label" state used for filtering Issues and Merge
# Requests that have no label assigned.
LabelStruct = Struct.new(:title, :name)
@@ -27,6 +29,7 @@ class Label < ActiveRecord::Base
belongs_to :project
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
+ has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? }
@@ -47,10 +50,15 @@ class Label < ActiveRecord::Base
'~'
end
+ ##
# Pattern used to extract label references from text
+ #
+ # This pattern supports cross-project references.
+ #
def self.reference_pattern
%r{
- #{reference_prefix}
+ (#{Project.reference_pattern})?
+ #{Regexp.escape(reference_prefix)}
(?:
(?<label_id>\d+) | # Integer-based label ID, or
(?<label_name>
@@ -61,36 +69,59 @@ class Label < ActiveRecord::Base
}x
end
+ def self.link_reference_pattern
+ nil
+ end
+
+ ##
# Returns the String necessary to reference this Label in Markdown
#
# format - Symbol format to use (default: :id, optional: :name)
#
- # Note that its argument differs from other objects implementing Referable. If
- # a non-Symbol argument is given (such as a Project), it will default to :id.
- #
# Examples:
#
- # Label.first.to_reference # => "~1"
- # Label.first.to_reference(:name) # => "~\"bug\""
+ # Label.first.to_reference # => "~1"
+ # Label.first.to_reference(format: :name) # => "~\"bug\""
+ # Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1"
#
# Returns a String
- def to_reference(format = :id)
- if format == :name && !name.include?('"')
- %(#{self.class.reference_prefix}"#{name}")
+ #
+ def to_reference(from_project = nil, format: :id)
+ format_reference = label_format_reference(format)
+ reference = "#{self.class.reference_prefix}#{format_reference}"
+
+ if cross_project_reference?(from_project)
+ project.to_reference + reference
else
- "#{self.class.reference_prefix}#{id}"
+ reference
end
end
- def open_issues_count
- issues.opened.count
+ def open_issues_count(user = nil)
+ issues.visible_to_user(user).opened.count
end
- def closed_issues_count
- issues.closed.count
+ def closed_issues_count(user = nil)
+ issues.visible_to_user(user).closed.count
+ end
+
+ def open_merge_requests_count
+ merge_requests.opened.count
end
def template?
template
end
+
+ private
+
+ def label_format_reference(format = :id)
+ raise StandardError, 'Unknown format' unless [:id, :name].include?(format)
+
+ if format == :name && !name.include?('"')
+ %("#{name}")
+ else
+ id
+ end
+ end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 1543ef311d7..ef48207f956 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -48,7 +48,7 @@ class MergeRequest < ActiveRecord::Base
after_create :create_merge_request_diff
after_update :update_merge_request_diff
- delegate :commits, :diffs, :diffs_no_whitespace, to: :merge_request_diff, prefix: nil
+ delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -56,8 +56,7 @@ class MergeRequest < ActiveRecord::Base
# Temporary fields to store compare vars
# when creating new merge request
- attr_accessor :can_be_created, :compare_failed,
- :compare_commits, :compare_diffs
+ attr_accessor :can_be_created, :compare_commits, :compare
state_machine :state, initial: :opened do
event :close do
@@ -132,15 +131,11 @@ class MergeRequest < ActiveRecord::Base
validate :validate_branches
validate :validate_fork
- scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.projects.select(:id).reorder(nil)) }
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
- scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
- scope :opened, -> { with_states(:opened, :reopened) }
scope :merged, -> { with_state(:merged) }
- scope :closed, -> { with_state(:closed) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :join_project, -> { joins(:target_project) }
@@ -164,6 +159,24 @@ class MergeRequest < ActiveRecord::Base
super("merge_requests", /(?<merge_request>\d+)/)
end
+ # Returns all the merge requests from an ActiveRecord:Relation.
+ #
+ # This method uses a UNION as it usually operates on the result of
+ # ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries
+ # using multiple sub-queries especially when combined with an OR statement.
+ # UNIONs on the other hand perform much better in these cases.
+ #
+ # relation - An ActiveRecord::Relation that returns a list of Projects.
+ #
+ # Returns an ActiveRecord::Relation.
+ def self.in_projects(relation)
+ source = where(source_project_id: relation).select(:id)
+ target = where(target_project_id: relation).select(:id)
+ union = Gitlab::SQL::Union.new([source, target])
+
+ where("merge_requests.id IN (#{union.to_sql})")
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -182,6 +195,10 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end
+ def diff_size
+ merge_request_diff.size
+ end
+
def diff_base_commit
if merge_request_diff
merge_request_diff.base_commit
@@ -259,8 +276,14 @@ class MergeRequest < ActiveRecord::Base
self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
end
+ WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
+
def work_in_progress?
- !!(title =~ /\A\[?WIP(\]|:| )/i)
+ title =~ WIP_REGEX
+ end
+
+ def wipless_title
+ self.title.sub(WIP_REGEX, "")
end
def mergeable?
@@ -487,12 +510,26 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def state_icon_name
+ if merged?
+ "check"
+ elsif closed?
+ "times"
+ else
+ "circle-o"
+ end
+ end
+
def target_sha
- @target_sha ||= target_project.repository.commit(target_branch).sha
+ @target_sha ||= target_project.repository.commit(target_branch).try(:sha)
end
def source_sha
- last_commit.try(:sha)
+ last_commit.try(:sha) || source_tip.try(:sha)
+ end
+
+ def source_tip
+ source_branch && source_project.repository.commit(source_branch)
end
def fetch_ref
@@ -524,6 +561,32 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def diverged_commits_count
+ cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
+
+ if cache.blank? || cache[:source_sha] != source_sha || cache[:target_sha] != target_sha
+ cache = {
+ source_sha: source_sha,
+ target_sha: target_sha,
+ diverged_commits_count: compute_diverged_commits_count
+ }
+ Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache)
+ end
+
+ cache[:diverged_commits_count]
+ end
+
+ def compute_diverged_commits_count
+ return 0 unless source_sha && target_sha
+
+ Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size
+ end
+ private :compute_diverged_commits_count
+
+ def diverged_from_target_branch?
+ diverged_commits_count > 0
+ end
+
def ci_commit
@ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index c95179d6046..33884118595 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -17,9 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Sortable
# Prevent store of diff if commits amount more then 500
- COMMITS_SAFE_SIZE = 500
-
- attr_reader :commits, :diffs, :diffs_no_whitespace
+ COMMITS_SAFE_SIZE = 100
belongs_to :merge_request
@@ -27,6 +25,9 @@ class MergeRequestDiff < ActiveRecord::Base
state_machine :state, initial: :empty do
state :collected
+ state :overflow
+ # Deprecated states: these are no longer used but these values may still occur
+ # in the database.
state :timeout
state :overflow_commits_safe_size
state :overflow_diff_files_limit
@@ -43,19 +44,23 @@ class MergeRequestDiff < ActiveRecord::Base
reload_diffs
end
- def diffs
- @diffs ||= (load_diffs(st_diffs) || [])
+ def size
+ real_size.presence || diffs.size
end
- def diffs_no_whitespace
- compare_result = Gitlab::CompareResult.new(
- Gitlab::Git::Compare.new(
- self.repository.raw_repository,
- self.target_branch,
- self.source_sha,
- ), { ignore_whitespace_change: true }
- )
- @diffs_no_whitespace ||= load_diffs(dump_commits(compare_result.diffs))
+ def diffs(options={})
+ if options[:ignore_whitespace_change]
+ @diffs_no_whitespace ||= begin
+ compare = Gitlab::Git::Compare.new(
+ self.repository.raw_repository,
+ self.target_branch,
+ self.source_sha,
+ )
+ compare.diffs(options)
+ end
+ else
+ @diffs ||= load_diffs(st_diffs, options)
+ end
end
def commits
@@ -94,16 +99,18 @@ class MergeRequestDiff < ActiveRecord::Base
end
end
- def load_diffs(raw)
- if raw.respond_to?(:map)
- raw.map { |hash| Gitlab::Git::Diff.new(hash) }
+ def load_diffs(raw, options)
+ if raw.respond_to?(:each)
+ Gitlab::Git::DiffCollection.new(raw, options)
+ else
+ Gitlab::Git::DiffCollection.new([])
end
end
# Collect array of Git::Commit objects
# between target and source branches
def unmerged_commits
- commits = compare_result.commits
+ commits = compare.commits
if commits.present?
commits = Commit.decorate(commits, merge_request.source_project).
@@ -133,27 +140,21 @@ class MergeRequestDiff < ActiveRecord::Base
if commits.size.zero?
self.state = :empty
- elsif commits.size > COMMITS_SAFE_SIZE
- self.state = :overflow_commits_safe_size
else
- new_diffs = unmerged_diffs
- end
+ diff_collection = unmerged_diffs
- if new_diffs.any?
- if new_diffs.size > Commit::DIFF_HARD_LIMIT_FILES
- self.state = :overflow_diff_files_limit
- new_diffs = new_diffs.first(Commit::DIFF_HARD_LIMIT_LINES)
+ if diff_collection.overflow?
+ # Set our state to 'overflow' to make the #empty? and #collected?
+ # methods (generated by StateMachine) return false.
+ self.state = :overflow
end
- if new_diffs.sum { |diff| diff.diff.lines.count } > Commit::DIFF_HARD_LIMIT_LINES
- self.state = :overflow_diff_lines_limit
- new_diffs = new_diffs.first(Commit::DIFF_HARD_LIMIT_LINES)
- end
- end
+ self.real_size = diff_collection.real_size
- if new_diffs.present?
- new_diffs = dump_commits(new_diffs)
- self.state = :collected
+ if diff_collection.any?
+ new_diffs = dump_diffs(diff_collection)
+ self.state = :collected
+ end
end
self.st_diffs = new_diffs
@@ -166,10 +167,7 @@ class MergeRequestDiff < ActiveRecord::Base
# Collect array of Git::Diff objects
# between target and source branches
def unmerged_diffs
- compare_result.diffs || []
- rescue Gitlab::Git::Diff::TimeoutError
- self.state = :timeout
- []
+ compare.diffs(Commit.max_diff_options)
end
def repository
@@ -181,18 +179,16 @@ class MergeRequestDiff < ActiveRecord::Base
source_commit.try(:sha)
end
- def compare_result
- @compare_result ||=
+ def compare
+ @compare ||=
begin
# Update ref for merge request
merge_request.fetch_ref
- Gitlab::CompareResult.new(
- Gitlab::Git::Compare.new(
- self.repository.raw_repository,
- self.target_branch,
- self.source_sha
- )
+ Gitlab::Git::Compare.new(
+ self.repository.raw_repository,
+ self.target_branch,
+ self.source_sha
)
end
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index cbe65d70997..bbd59eab9ae 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -19,17 +19,19 @@ class Milestone < ActiveRecord::Base
MilestoneStruct = Struct.new(:title, :name, :id)
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1)
+ Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include InternalId
include Sortable
include Referable
include StripAttribute
+ include Milestoneish
belongs_to :project
has_many :issues
- has_many :labels, through: :issues
+ has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
- has_many :participants, through: :issues, source: :assignee
+ has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee
scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) }
@@ -57,9 +59,18 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title
class << self
+ # Searches for milestones matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- query = "%#{query}%"
- where("title like ? or description like ?", query, query)
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
end
@@ -71,6 +82,10 @@ class Milestone < ActiveRecord::Base
super("milestones", /(?<milestone>\d+)/)
end
+ def self.upcoming
+ self.where('due_date > ?', Time.now).reorder(due_date: :asc).first
+ end
+
def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]")
@@ -92,37 +107,6 @@ class Milestone < ActiveRecord::Base
end
end
- def open_items_count
- self.issues.opened.count + self.merge_requests.opened.count
- end
-
- def closed_items_count
- self.issues.closed.count + self.merge_requests.closed_and_merged.count
- end
-
- def total_items_count
- self.issues.count + self.merge_requests.count
- end
-
- def percent_complete
- ((closed_items_count * 100) / total_items_count).abs
- rescue ZeroDivisionError
- 0
- end
-
- # Returns the elapsed time (in percent) since the Milestone creation date until today.
- # If the Milestone doesn't have a due_date then returns 0 since we can't calculate the elapsed time.
- # If the Milestone is overdue then it returns 100%.
- def percent_time_used
- return 0 unless due_date
- return 100 if expired?
-
- duration = ((created_at - due_date.to_datetime) / 1.day)
- days_elapsed = ((created_at - Time.now) / 1.day)
-
- ((days_elapsed.to_f / duration) * 100).floor
- end
-
def expires_at
if due_date
if due_date.past?
@@ -137,8 +121,8 @@ class Milestone < ActiveRecord::Base
active? && issues.opened.count.zero?
end
- def is_empty?
- total_items_count.zero?
+ def is_empty?(user = nil)
+ total_items_count(user).zero?
end
def author_id
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index bdb33f37495..55842df1e2d 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
end
+ # Searches for namespaces matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation
def search(query)
- where("name LIKE :query OR path LIKE :query", query: "%#{query}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
end
def clean_path(path)
diff --git a/app/models/note.rb b/app/models/note.rb
index d287e0f3c6d..b0c33f2eec5 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -39,10 +39,12 @@ class Note < ActiveRecord::Base
has_many :todos, dependent: :destroy
+ delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true
delegate :name, :email, to: :author, prefix: true
before_validation :set_award!
+ before_validation :clear_blank_line_code!
validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
@@ -62,7 +64,7 @@ class Note < ActiveRecord::Base
scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") }
- scope :not_inline, ->{ where(line_code: [nil, '']) }
+ scope :not_inline, ->{ where(line_code: nil) }
scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) }
@@ -87,7 +89,7 @@ class Note < ActiveRecord::Base
next if discussion_ids.include?(note.discussion_id)
# don't group notes for the main target
- if !note.for_diff_line? && note.noteable_type == "MergeRequest"
+ if !note.for_diff_line? && note.for_merge_request?
discussions << [note]
else
discussions << notes.select do |other_note|
@@ -104,8 +106,18 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym
end
+ # Searches for notes matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String.
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("LOWER(note) like :query", query: "%#{query.downcase}%")
+ table = arel_table
+ pattern = "%#{query}%"
+
+ where(table[:note].matches(pattern))
end
def grouped_awards
@@ -131,9 +143,11 @@ class Note < ActiveRecord::Base
end
def find_diff
- return nil unless noteable && noteable.diffs.present?
+ return nil unless noteable
+ return @diff if defined?(@diff)
- @diff ||= noteable.diffs.find do |d|
+ # Don't use ||= because nil is a valid value for @diff
+ @diff = noteable.diffs(Commit.max_diff_options).find do |d|
Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path
end
end
@@ -159,30 +173,29 @@ class Note < ActiveRecord::Base
Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff)
end
- # Check if such line of code exists in merge request diff
- # If exists - its active discussion
- # If not - its outdated diff
+ # Check if this note is part of an "active" discussion
+ #
+ # This will always return true for anything except MergeRequest noteables,
+ # which have special logic.
+ #
+ # If the note's current diff cannot be matched in the MergeRequest's current
+ # diff, it's considered inactive.
def active?
return true unless self.diff
return false unless noteable
+ return @active if defined?(@active)
- noteable.diffs.each do |mr_diff|
- next unless mr_diff.new_path == self.diff.new_path
+ noteable_diff = find_noteable_diff
- lines = Gitlab::Diff::Parser.new.parse(mr_diff.diff.lines.to_a)
+ if noteable_diff
+ parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line)
- lines.each do |line|
- if line.text == diff_line
- return true
- end
- end
+ @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
+ else
+ @active = false
end
- false
- end
-
- def outdated?
- !active?
+ @active
end
def diff_file_index
@@ -263,7 +276,7 @@ class Note < ActiveRecord::Base
end
def diff_lines
- @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.lines)
+ @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
end
def highlighted_diff_lines
@@ -315,20 +328,6 @@ class Note < ActiveRecord::Base
nil
end
- # Mentionable override.
- def gfm_reference(from_project = nil)
- noteable.gfm_reference(from_project)
- end
-
- # Mentionable override.
- def local_reference
- noteable
- end
-
- def noteable_type_name
- noteable_type.downcase if noteable_type.present?
- end
-
# FIXME: Hack for polymorphic associations with STI
# For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
def noteable_type=(noteable_type)
@@ -348,10 +347,6 @@ class Note < ActiveRecord::Base
Event.reset_event_cache_for(self)
end
- def system?
- read_attribute(:system)
- end
-
def downvote?
is_award && note == "thumbsdown"
end
@@ -384,8 +379,18 @@ class Note < ActiveRecord::Base
private
+ def clear_blank_line_code!
+ self.line_code = nil if self.line_code.blank?
+ end
+
+ # Find the diff on noteable that matches our own
+ def find_noteable_diff
+ diffs = noteable.diffs(Commit.max_diff_options)
+ diffs.find { |d| d.new_path == self.diff.new_path }
+ end
+
def awards_supported?
- (noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)) && !for_diff_line?
+ (for_issue? || for_merge_request?) && !for_diff_line?
end
def contains_emoji_only?
diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb
index 9cee3b70cb3..452f3913eef 100644
--- a/app/models/personal_snippet.rb
+++ b/app/models/personal_snippet.rb
@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
-# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
diff --git a/app/models/project.rb b/app/models/project.rb
index 95ad88c76ae..2285063ab50 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -73,7 +73,7 @@ class Project < ActiveRecord::Base
update_column(:last_activity_at, self.created_at)
end
- # update visibility_levet of forks
+ # update visibility_level of forks
after_update :update_forks_visibility_level
def update_forks_visibility_level
return unless visibility_level < visibility_level_was
@@ -151,6 +151,9 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects
+ has_many :project_group_links, dependent: :destroy
+ has_many :invited_groups, through: :project_group_links, source: :group
+ has_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
@@ -194,6 +197,8 @@ class Project < ActiveRecord::Base
validate :avatar_type,
if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
+ validate :visibility_level_allowed_by_group
+ validate :visibility_level_allowed_as_fork
add_authentication_token_field :runners_token
before_save :ensure_runners_token
@@ -212,9 +217,8 @@ class Project < ActiveRecord::Base
scope :in_group_namespace, -> { joins(:group) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
- scope :public_only, -> { where(visibility_level: Project::PUBLIC) }
- scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) }
scope :non_archived, -> { where(archived: false) }
+ scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
state_machine :import_status, initial: :none do
event :import_start do
@@ -242,20 +246,10 @@ class Project < ActiveRecord::Base
end
class << self
- def public_and_internal_levels
- [Project::PUBLIC, Project::INTERNAL]
- end
-
def abandoned
where('projects.last_activity_at < ?', 6.months.ago)
end
- def publicish(user)
- visibility_levels = [Project::PUBLIC]
- visibility_levels << Project::INTERNAL if user
- where(visibility_level: visibility_levels)
- end
-
def with_push
joins(:events).where('events.action = ?', Event::PUSHED)
end
@@ -264,13 +258,38 @@ class Project < ActiveRecord::Base
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end
+ # Searches for a list of projects based on the query given in `query`.
+ #
+ # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
+ # search. On MySQL a regular "LIKE" is used as it's already
+ # case-insensitive.
+ #
+ # query - The search query as a String.
def search(query)
- joins(:namespace).
- where('LOWER(projects.name) LIKE :query OR
- LOWER(projects.path) LIKE :query OR
- LOWER(namespaces.name) LIKE :query OR
- LOWER(projects.description) LIKE :query',
- query: "%#{query.try(:downcase)}%")
+ ptable = arel_table
+ ntable = Namespace.arel_table
+ pattern = "%#{query}%"
+
+ projects = select(:id).where(
+ ptable[:path].matches(pattern).
+ or(ptable[:name].matches(pattern)).
+ or(ptable[:description].matches(pattern))
+ )
+
+ # We explicitly remove any eager loading clauses as they're:
+ #
+ # 1. Not needed by this query
+ # 2. Combined with .joins(:namespace) lead to all columns from the
+ # projects & namespaces tables being selected, leading to a SQL error
+ # due to the columns of all UNION'd queries no longer being the same.
+ namespaces = select(:id).
+ except(:includes).
+ joins(:namespace).
+ where(ntable[:name].matches(pattern))
+
+ union = Gitlab::SQL::Union.new([projects, namespaces])
+
+ where("projects.id IN (#{union.to_sql})")
end
def search_by_visibility(level)
@@ -278,11 +297,14 @@ class Project < ActiveRecord::Base
end
def search_by_title(query)
- where('projects.archived = ?', false).where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%")
+ pattern = "%#{query}%"
+ table = Project.arel_table
+
+ non_archived.where(table[:name].matches(pattern))
end
def find_with_namespace(id)
- namespace_path, project_path = id.split('/')
+ namespace_path, project_path = id.split('/', 2)
return nil if !namespace_path || !project_path
@@ -409,6 +431,7 @@ class Project < ActiveRecord::Base
def safe_import_url
result = URI.parse(self.import_url)
result.password = '*****' unless result.password.nil?
+ result.user = '*****' unless result.user.nil? || result.user == "git" #tokens or other data may be saved as user
result.to_s
rescue
self.import_url
@@ -416,10 +439,25 @@ class Project < ActiveRecord::Base
def check_limit
unless creator.can_create_project? or namespace.kind == 'group'
- errors[:limit_reached] << ("Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it")
+ self.errors.add(:limit_reached, "Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it")
end
rescue
- errors[:base] << ("Can't check your ability to create project")
+ self.errors.add(:base, "Can't check your ability to create project")
+ end
+
+ def visibility_level_allowed_by_group
+ return if visibility_level_allowed_by_group?
+
+ level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
+ group_level_name = Gitlab::VisibilityLevel.level_name(self.group.visibility_level).downcase
+ self.errors.add(:visibility_level, "#{level_name} is not allowed in a #{group_level_name} group.")
+ end
+
+ def visibility_level_allowed_as_fork
+ return if visibility_level_allowed_as_fork?
+
+ level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
+ self.errors.add(:visibility_level, "#{level_name} is not allowed since the fork source project has lower visibility.")
end
def to_param
@@ -483,6 +521,7 @@ class Project < ActiveRecord::Base
end
def external_issue_tracker
+ return @external_issue_tracker if defined?(@external_issue_tracker)
@external_issue_tracker ||=
services.issue_trackers.active.without_defaults.first
end
@@ -526,11 +565,11 @@ class Project < ActiveRecord::Base
end
def ci_services
- services.select { |service| service.category == :ci }
+ services.where(category: :ci)
end
def ci_service
- @ci_service ||= ci_services.find(&:activated?)
+ @ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
def jira_tracker?
@@ -544,10 +583,7 @@ class Project < ActiveRecord::Base
end
def avatar_in_git
- @avatar_file ||= 'logo.png' if repository.blob_at_branch('master', 'logo.png')
- @avatar_file ||= 'logo.jpg' if repository.blob_at_branch('master', 'logo.jpg')
- @avatar_file ||= 'logo.gif' if repository.blob_at_branch('master', 'logo.gif')
- @avatar_file
+ repository.avatar
end
def avatar_url
@@ -711,6 +747,8 @@ class Project < ActiveRecord::Base
old_path_with_namespace = File.join(namespace_dir, path_was)
new_path_with_namespace = File.join(namespace_dir, path)
+ expire_caches_before_rename(old_path_with_namespace)
+
if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace)
# If repository moved successfully we need to send update instructions to users.
# However we cannot allow rollback since we moved repository
@@ -739,6 +777,22 @@ class Project < ActiveRecord::Base
Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path)
end
+ # Expires various caches before a project is renamed.
+ def expire_caches_before_rename(old_path)
+ repo = Repository.new(old_path, self)
+ wiki = Repository.new("#{old_path}.wiki", self)
+
+ if repo.exists?
+ repo.expire_cache
+ repo.expire_emptiness_caches
+ end
+
+ if wiki.exists?
+ wiki.expire_cache
+ wiki.expire_emptiness_caches
+ end
+ end
+
def hook_attrs
{
name: name,
@@ -801,10 +855,7 @@ class Project < ActiveRecord::Base
end
def change_head(branch)
- # Cached divergent commit counts are based on repository head
- repository.expire_branch_cache
- repository.expire_root_ref_cache
-
+ repository.before_change_head
gitlab_shell.update_repository_head(self.path_with_namespace, branch)
reload_default_branch
end
@@ -837,6 +888,7 @@ class Project < ActiveRecord::Base
# Forked import is handled asynchronously
unless forked?
if gitlab_shell.add_repository(path_with_namespace)
+ repository.after_create
true
else
errors.add(:base, 'Failed to create repository via gitlab-shell')
@@ -861,6 +913,10 @@ class Project < ActiveRecord::Base
jira_tracker? && jira_service.active
end
+ def allowed_to_share_with_group?
+ !namespace.share_with_group_lock
+ end
+
def ci_commit(sha)
ci_commits.find_by(sha: sha)
end
@@ -892,13 +948,13 @@ class Project < ActiveRecord::Base
end
def valid_runners_token? token
- self.runners_token && self.runners_token == token
+ self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
# TODO (ayufan): For now we use runners_token (backward compatibility)
# In 8.4 every build will have its own individual token valid for time of build
def valid_build_token? token
- self.builds_enabled? && self.runners_token && self.runners_token == token
+ self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
def build_coverage_enabled?
@@ -917,9 +973,25 @@ class Project < ActiveRecord::Base
issues.opened.count
end
- def visibility_level_allowed?(level)
+ def visibility_level_allowed_as_fork?(level = self.visibility_level)
return true unless forked?
- Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i)
+
+ # self.forked_from_project will be nil before the project is saved, so
+ # we need to go through the relation
+ original_project = forked_project_link.forked_from_project
+ return true unless original_project
+
+ level <= original_project.visibility_level
+ end
+
+ def visibility_level_allowed_by_group?(level = self.visibility_level)
+ return true unless group
+
+ level <= group.visibility_level
+ end
+
+ def visibility_level_allowed?(level = self.visibility_level)
+ visibility_level_allowed_as_fork?(level) && visibility_level_allowed_by_group?(level)
end
def runners_token
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
new file mode 100644
index 00000000000..e52a6bd7c84
--- /dev/null
+++ b/app/models/project_group_link.rb
@@ -0,0 +1,36 @@
+class ProjectGroupLink < ActiveRecord::Base
+ GUEST = 10
+ REPORTER = 20
+ DEVELOPER = 30
+ MASTER = 40
+
+ belongs_to :project
+ belongs_to :group
+
+ validates :project_id, presence: true
+ validates :group_id, presence: true
+ validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
+ validates :group_access, presence: true
+ validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
+ validate :different_group
+
+ def self.access_options
+ Gitlab::Access.options
+ end
+
+ def self.default_access
+ DEVELOPER
+ end
+
+ def human_access
+ self.class.access_options.key(self.group_access)
+ end
+
+ private
+
+ def different_group
+ if self.group && self.project && self.project.group == self.group
+ errors.add(:base, "Project cannot be shared with the project it is in.")
+ end
+ end
+end
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index e10b5529b42..d9f0849d147 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -26,7 +26,7 @@ class CiService < Service
default_value_for :category, 'ci'
def valid_token?(token)
- self.respond_to?(:token) && self.token.present? && self.token == token
+ self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
def supported_events
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index f6571fc063e..aba37921c09 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -108,7 +108,8 @@ class JiraService < IssueTrackerService
},
entity: {
name: noteable_name.humanize.downcase,
- url: entity_url
+ url: entity_url,
+ title: noteable.title
}
}
@@ -196,10 +197,11 @@ class JiraService < IssueTrackerService
user_url = data[:user][:url]
entity_name = data[:entity][:name]
entity_url = data[:entity][:url]
+ entity_title = data[:entity][:title]
project_name = data[:project][:name]
message = {
- body: "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]."
+ body: %Q{[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'}
}
unless existing_comment?(issue_name, message[:body])
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index 9e2c1b0e18e..1f7d85a5f3d 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
-# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
@@ -23,6 +22,4 @@ class ProjectSnippet < Snippet
# Scopes
scope :fresh, -> { order("created_at DESC") }
- scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
- scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 9629c7e1bb9..70a8bbaba65 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -160,7 +160,27 @@ class ProjectTeam
end
end
- access.max
+ if project.invited_groups.any? && project.allowed_to_share_with_group?
+ access << max_invited_level(user_id)
+ end
+
+ access.compact.max
+ end
+
+
+ def max_invited_level(user_id)
+ project.project_group_links.map do |group_link|
+ invited_group = group_link.group
+ access = invited_group.group_members.find_by(user_id: user_id).try(:access_field)
+
+ # If group member has higher access level we should restrict it
+ # to max allowed access level
+ if access && access > group_link.group_access
+ access = group_link.group_access
+ end
+
+ access
+ end.compact.max
end
private
@@ -168,6 +188,35 @@ class ProjectTeam
def fetch_members(level = nil)
project_members = project.project_members
group_members = group ? group.group_members : []
+ invited_members = []
+
+ if project.invited_groups.any? && project.allowed_to_share_with_group?
+ project.project_group_links.each do |group_link|
+ invited_group = group_link.group
+ im = invited_group.group_members
+
+ if level
+ int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
+
+ # Skip group members if we ask for masters
+ # but max group access is developers
+ next if int_level > group_link.group_access
+
+ # If we ask for developers and max
+ # group access is developers we need to provide
+ # both group master, developers as devs
+ if int_level == group_link.group_access
+ im.where("access_level >= ?)", group_link.group_access)
+ else
+ im.send(level)
+ end
+ end
+
+ invited_members << im
+ end
+
+ invited_members = invited_members.flatten.compact
+ end
if level
project_members = project_members.send(level)
@@ -175,6 +224,7 @@ class ProjectTeam
end
user_ids = project_members.pluck(:user_id)
+ user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
user_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids)
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index c96e6f0b8ea..7c1a61bb0bf 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -2,7 +2,7 @@ class ProjectWiki
include Gitlab::ShellAdapter
MARKUPS = {
- 'Markdown' => :md,
+ 'Markdown' => :markdown,
'RDoc' => :rdoc,
'AsciiDoc' => :asciidoc
} unless defined?(MARKUPS)
@@ -47,7 +47,7 @@ class ProjectWiki
def wiki
@wiki ||= begin
Gollum::Wiki.new(path_to_repo)
- rescue Gollum::NoSuchPathError
+ rescue Rugged::OSError
create_repo!
end
end
@@ -90,7 +90,7 @@ class ProjectWiki
def create_page(title, content, format = :markdown, message = nil)
commit = commit_details(:created, message, title)
- wiki.write_page(title, format, content, commit)
+ wiki.write_page(title, format.to_sym, content, commit)
update_project_activity
rescue Gollum::DuplicatePageError => e
@@ -101,7 +101,7 @@ class ProjectWiki
def update_page(page, content, format = :markdown, message = nil)
commit = commit_details(:updated, message, page.title)
- wiki.update_page(page, page.name, format, content, commit)
+ wiki.update_page(page, page.name, format.to_sym, content, commit)
update_project_activity
end
@@ -123,23 +123,27 @@ class ProjectWiki
end
def repository
- Repository.new(path_with_namespace, @project)
+ @repository ||= Repository.new(path_with_namespace, @project)
end
def default_branch
wiki.class.default_ref
end
- private
-
def create_repo!
if init_repo(path_with_namespace)
- Gollum::Wiki.new(path_to_repo)
+ wiki = Gollum::Wiki.new(path_to_repo)
else
raise CouldNotCreateWikiError
end
+
+ repository.after_create
+
+ wiki
end
+ private
+
def init_repo(path_with_namespace)
gitlab_shell.add_repository(path_with_namespace)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e050bd45254..c07e8072043 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -3,6 +3,10 @@ require 'securerandom'
class Repository
class CommitError < StandardError; end
+ # Files to use as a project avatar in case no avatar was uploaded via the web
+ # UI.
+ AVATAR_FILES = %w{logo.png logo.jpg logo.gif}
+
include Gitlab::ShellAdapter
attr_accessor :path_with_namespace, :project
@@ -38,12 +42,15 @@ class Repository
end
def exists?
- return false unless raw_repository
+ return @exists unless @exists.nil?
- raw_repository.rugged
- true
- rescue Gitlab::Git::Repository::NoRepository
- false
+ @exists = cache.fetch(:exists?) do
+ begin
+ raw_repository && raw_repository.rugged ? true : false
+ rescue Gitlab::Git::Repository::NoRepository
+ false
+ end
+ end
end
def empty?
@@ -133,18 +140,18 @@ class Repository
rugged.branches.create(branch_name, target)
end
- expire_branches_cache
+ after_create_branch
find_branch(branch_name)
end
def add_tag(tag_name, ref, message = nil)
- expire_tags_cache
+ before_push_tag
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end
def rm_branch(user, branch_name)
- expire_branches_cache
+ before_remove_branch
branch = find_branch(branch_name)
oldrev = branch.try(:target)
@@ -155,12 +162,12 @@ class Repository
rugged.branches.delete(branch_name)
end
- expire_branches_cache
+ after_remove_branch
true
end
def rm_tag(tag_name)
- expire_tags_cache
+ before_remove_tag
gitlab_shell.rm_tag(path_with_namespace, tag_name)
end
@@ -183,6 +190,14 @@ class Repository
end
end
+ def branch_count
+ @branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count }
+ end
+
+ def tag_count
+ @tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count }
+ end
+
# Return repo size in megabytes
# Cached in redis
def size
@@ -215,12 +230,6 @@ class Repository
send(key)
end
end
-
- branches.each do |branch|
- unless cache.exist?(:"diverging_commit_counts_#{branch.name}")
- send(:diverging_commit_counts, branch)
- end
- end
end
def expire_tags_cache
@@ -233,27 +242,19 @@ class Repository
@branches = nil
end
- def expire_cache(branch_name = nil)
+ def expire_cache(branch_name = nil, revision = nil)
cache_keys.each do |key|
cache.expire(key)
end
expire_branch_cache(branch_name)
+ expire_avatar_cache(branch_name, revision)
# This ensures this particular cache is flushed after the first commit to a
# new repository.
expire_emptiness_caches if empty?
end
- # Expires _all_ caches, including those that would normally only be expired
- # under specific conditions.
- def expire_all_caches!
- expire_cache
- expire_root_ref_cache
- expire_emptiness_caches
- expire_has_visible_content_cache
- end
-
def expire_branch_cache(branch_name = nil)
# When we push to the root branch we have to flush the cache for all other
# branches as their statistics are based on the commits relative to the
@@ -287,16 +288,14 @@ class Repository
@has_visible_content = nil
end
- def rebuild_cache
- cache_keys.each do |key|
- cache.expire(key)
- send(key)
- end
+ def expire_branch_count_cache
+ cache.expire(:branch_count)
+ @branch_count = nil
+ end
- branches.each do |branch|
- cache.expire(:"diverging_commit_counts_#{branch.name}")
- diverging_commit_counts(branch)
- end
+ def expire_tag_count_cache
+ cache.expire(:tag_count)
+ @tag_count = nil
end
def lookup_cache
@@ -307,6 +306,92 @@ class Repository
cache.expire(:branch_names)
end
+ def expire_avatar_cache(branch_name = nil, revision = nil)
+ # Avatars are pulled from the default branch, thus if somebody pushes to a
+ # different branch there's no need to expire anything.
+ return if branch_name && branch_name != root_ref
+
+ # We don't want to flush the cache if the commit didn't actually make any
+ # changes to any of the possible avatar files.
+ if revision && commit = self.commit(revision)
+ return unless commit.diffs.
+ any? { |diff| AVATAR_FILES.include?(diff.new_path) }
+ end
+
+ cache.expire(:avatar)
+
+ @avatar = nil
+ end
+
+ def expire_exists_cache
+ cache.expire(:exists?)
+ @exists = nil
+ end
+
+ # Runs code after a repository has been created.
+ def after_create
+ expire_exists_cache
+ end
+
+ # Runs code just before a repository is deleted.
+ def before_delete
+ expire_cache if exists?
+
+ expire_root_ref_cache
+ expire_emptiness_caches
+ expire_exists_cache
+ end
+
+ # Runs code just before the HEAD of a repository is changed.
+ def before_change_head
+ # Cached divergent commit counts are based on repository head
+ expire_branch_cache
+ expire_root_ref_cache
+ end
+
+ # Runs code before pushing (= creating or removing) a tag.
+ def before_push_tag
+ expire_cache
+ expire_tags_cache
+ expire_tag_count_cache
+ end
+
+ # Runs code before removing a tag.
+ def before_remove_tag
+ expire_tags_cache
+ expire_tag_count_cache
+ end
+
+ # Runs code after a repository has been forked/imported.
+ def after_import
+ expire_emptiness_caches
+ expire_exists_cache
+ end
+
+ # Runs code after a new commit has been pushed.
+ def after_push_commit(branch_name, revision)
+ expire_cache(branch_name, revision)
+ end
+
+ # Runs code after a new branch has been created.
+ def after_create_branch
+ expire_branches_cache
+ expire_has_visible_content_cache
+ expire_branch_count_cache
+ end
+
+ # Runs code before removing an existing branch.
+ def before_remove_branch
+ expire_branches_cache
+ end
+
+ # Runs code after an existing branch has been removed.
+ def after_remove_branch
+ expire_has_visible_content_cache
+ expire_branch_count_cache
+ expire_branches_cache
+ end
+
def method_missing(m, *args, &block)
if m == :lookup && !block_given?
lookup_cache[m] ||= {}
@@ -382,6 +467,18 @@ class Repository
end
end
+ def gitlab_ci_yml
+ return nil if !exists? || empty?
+
+ @gitlab_ci_yml ||= tree(:head).blobs.find do |file|
+ file.name == '.gitlab-ci.yml'
+ end
+ rescue Rugged::ReferenceError
+ # For unknow reason spinach scenario "Scenario: I change project path"
+ # lead to "Reference 'HEAD' not found" exception from Repository#empty?
+ nil
+ end
+
def head_commit
@head_commit ||= commit(self.root_ref)
end
@@ -623,30 +720,38 @@ class Repository
end
end
- def revert(user, commit, base_branch, target_branch = nil)
- source_sha = find_branch(base_branch).target
- target_branch ||= base_branch
- args = [commit.id, source_sha]
- args << { mainline: 1 } if commit.merge_commit?
+ def revert(user, commit, base_branch, revert_tree_id = nil)
+ source_sha = find_branch(base_branch).target
+ revert_tree_id ||= check_revert_content(commit, base_branch)
- revert_index = rugged.revert_commit(*args)
- return false if revert_index.conflicts?
-
- tree_id = revert_index.write_tree(rugged)
- return false unless diff_exists?(source_sha, tree_id)
+ return false unless revert_tree_id
- commit_with_hooks(user, target_branch) do |ref|
+ commit_with_hooks(user, base_branch) do |ref|
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
message: commit.revert_message,
author: committer,
committer: committer,
- tree: tree_id,
+ tree: revert_tree_id,
parents: [rugged.lookup(source_sha)],
update_ref: ref)
end
end
+ def check_revert_content(commit, base_branch)
+ source_sha = find_branch(base_branch).target
+ args = [commit.id, source_sha]
+ args << { mainline: 1 } if commit.merge_commit?
+
+ revert_index = rugged.revert_commit(*args)
+ return false if revert_index.conflicts?
+
+ tree_id = revert_index.write_tree(rugged)
+ return false unless diff_exists?(source_sha, tree_id)
+
+ tree_id
+ end
+
def diff_exists?(sha1, sha2)
rugged.diff(sha1, sha2).size > 0
end
@@ -684,12 +789,15 @@ class Repository
def parse_search_result(result)
ref = nil
filename = nil
+ basename = nil
startline = 0
result.each_line.each_with_index do |line, index|
if line =~ /^.*:.*:\d+:/
ref, filename, startline = line.split(':')
startline = startline.to_i - index
+ extname = File.extname(filename)
+ basename = filename.sub(/#{extname}$/, '')
break
end
end
@@ -702,6 +810,7 @@ class Repository
OpenStruct.new(
filename: filename,
+ basename: basename,
ref: ref,
startline: startline,
data: data
@@ -773,6 +882,22 @@ class Repository
raw_repository.ls_files(actual_ref)
end
+ def main_language
+ unless empty?
+ Linguist::Repository.new(rugged, rugged.head.target_id).language
+ end
+ end
+
+ def avatar
+ return nil unless exists?
+
+ @avatar ||= cache.fetch(:avatar) do
+ AVATAR_FILES.find do |file|
+ blob_at_branch('master', file)
+ end
+ end
+ end
+
private
def cache
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index f876be7a4c8..b9e835a4486 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
-# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
@@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") }
- scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
- scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
participant :author, :notes
@@ -111,21 +108,37 @@ class Snippet < ActiveRecord::Base
nil
end
- def expired?
- expires_at && expires_at < Time.current
- end
-
def visibility_level_field
visibility_level
end
class << self
+ # Searches for snippets with a matching title or file name.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String.
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%")
+ t = arel_table
+ pattern = "%#{query}%"
+
+ where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
end
+ # Searches for snippets with matching content.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String.
+ #
+ # Returns an ActiveRecord::Relation.
def search_code(query)
- where('(content LIKE :query)', query: "%#{query}%")
+ table = Snippet.arel_table
+ pattern = "%#{query}%"
+
+ where(table[:content].matches(pattern))
end
def accessible_to(user)
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index dd75d3ab8ba..dd800ce110f 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -15,7 +15,7 @@ class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :subscribable, polymorphic: true
- validates :user_id,
+ validates :user_id,
uniqueness: { scope: [:subscribable_id, :subscribable_type] },
presence: true
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 34d71c1b0d3..d85f7bfdf57 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -5,14 +5,15 @@
# id :integer not null, primary key
# user_id :integer not null
# project_id :integer not null
-# target_id :integer not null
+# target_id :integer
# target_type :string not null
# author_id :integer
-# note_id :integer
# action :integer not null
# state :string not null
# created_at :datetime
# updated_at :datetime
+# note_id :integer
+# commit_id :string
#
class Todo < ActiveRecord::Base
@@ -27,7 +28,9 @@ class Todo < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true, allow_nil: true
- validates :action, :project, :target, :user, presence: true
+ validates :action, :project, :target_type, :user, presence: true
+ validates :target_id, presence: true, unless: :for_commit?
+ validates :commit_id, presence: true, if: :for_commit?
default_scope { reorder(id: :desc) }
@@ -36,7 +39,7 @@ class Todo < ActiveRecord::Base
state_machine :state, initial: :pending do
event :done do
- transition pending: :done
+ transition [:pending] => :done
end
state :pending
@@ -50,4 +53,25 @@ class Todo < ActiveRecord::Base
target.title
end
end
+
+ def for_commit?
+ target_type == "Commit"
+ end
+
+ # override to return commits, which are not active record
+ def target
+ if for_commit?
+ project.commit(commit_id) rescue nil
+ else
+ super
+ end
+ end
+
+ def target_reference
+ if for_commit?
+ target.short_id
+ else
+ target.to_reference
+ end
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 02ff2456f2b..128ddc2a694 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -59,6 +59,7 @@
# hide_project_limit :boolean default(FALSE)
# unlock_token :string
# otp_grace_period_started_at :datetime
+# external :boolean default(FALSE)
#
require 'carrierwave/orm/activerecord'
@@ -77,6 +78,7 @@ class User < ActiveRecord::Base
add_authentication_token_field :authentication_token
default_value_for :admin, false
+ default_value_for :external, false
default_value_for :can_create_group, gitlab_config.default_can_create_group
default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false
@@ -171,6 +173,7 @@ class User < ActiveRecord::Base
after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
before_save :ensure_authentication_token
+ before_save :ensure_external_user_rights
after_save :ensure_namespace_correct
after_initialize :set_projects_limit
after_create :post_create_hook
@@ -181,7 +184,7 @@ class User < ActiveRecord::Base
# User's Dashboard preference
# Note: When adding an option, it MUST go on the end of the array.
- enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity]
+ enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]
# User's Project preference
# Note: When adding an option, it MUST go on the end of the array.
@@ -218,6 +221,7 @@ class User < ActiveRecord::Base
# Scopes
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
+ scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
@@ -273,13 +277,29 @@ class User < ActiveRecord::Base
self.with_two_factor
when 'wop'
self.without_projects
+ when 'external'
+ self.external
else
self.active
end
end
+ # Searches users matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
def search(query)
- where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%")
+ table = arel_table
+ pattern = "%#{query}%"
+
+ where(
+ table[:name].matches(pattern).
+ or(table[:email].matches(pattern)).
+ or(table[:username].matches(pattern))
+ )
end
def by_login(login)
@@ -354,17 +374,19 @@ class User < ActiveRecord::Base
def disable_two_factor!
update_attributes(
- two_factor_enabled: false,
- encrypted_otp_secret: nil,
- encrypted_otp_secret_iv: nil,
- encrypted_otp_secret_salt: nil,
- otp_backup_codes: nil
+ two_factor_enabled: false,
+ encrypted_otp_secret: nil,
+ encrypted_otp_secret_iv: nil,
+ encrypted_otp_secret_salt: nil,
+ otp_grace_period_started_at: nil,
+ otp_backup_codes: nil
)
end
def namespace_uniq
# Return early if username already failed the first uniqueness validation
- return if self.errors[:username].include?('has already been taken')
+ return if self.errors.key?(:username) &&
+ self.errors[:username].include?('has already been taken')
namespace_name = self.username
existing_namespace = Namespace.by_path(namespace_name)
@@ -413,7 +435,7 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
- # Returns the groups a user is authorized to access.
+ # Returns projects user is authorized to access.
def authorized_projects
Project.where("projects.id IN (#{projects_union.to_sql})")
end
@@ -602,6 +624,13 @@ class User < ActiveRecord::Base
end
end
+ def try_obtain_ldap_lease
+ # After obtaining this lease LDAP checks will be blocked for 600 seconds
+ # (10 minutes) for this user.
+ lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600)
+ lease.try_obtain
+ end
+
def solo_owned_groups
@solo_owned_groups ||= owned_groups.select do |group|
group.owners == [self]
@@ -801,7 +830,8 @@ class User < ActiveRecord::Base
def projects_union
Gitlab::SQL::Union.new([personal_projects.select(:id),
groups_projects.select(:id),
- projects.select(:id)])
+ projects.select(:id),
+ groups.joins(:shared_projects).select(:project_id)])
end
def ci_projects_union
@@ -817,4 +847,11 @@ class User < ActiveRecord::Base
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
end
+
+ def ensure_external_user_rights
+ return unless self.external?
+
+ self.can_create_group = false
+ self.projects_limit = 0
+ end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index dbd70dc5a44..526760779a4 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -62,7 +62,7 @@ class WikiPage
# The raw content of this page.
def content
@attributes[:content] ||= if @page
- @page.raw_data
+ @page.text_data
end
end
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 8563633816c..0d55ba5a981 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -43,12 +43,9 @@ class BaseService
def deny_visibility_level(model, denied_visibility_level = nil)
denied_visibility_level ||= model.visibility_level
- level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level)
+ level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level).downcase
- model.errors.add(
- :visibility_level,
- "#{level_name} visibility has been restricted by your GitLab administrator"
- )
+ model.errors.add(:visibility_level, "#{level_name} has been restricted by your GitLab administrator")
end
private
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
index 002f7ba1278..2cd51a7610f 100644
--- a/app/services/ci/create_builds_service.rb
+++ b/app/services/ci/create_builds_service.rb
@@ -1,7 +1,7 @@
module Ci
class CreateBuildsService
def execute(commit, stage, ref, tag, user, trigger_request, status)
- builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag)
+ builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag, trigger_request)
# check when to create next build
builds_attrs = builds_attrs.select do |build_attrs|
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index 005a5c4661c..50c95ced8a7 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -3,7 +3,7 @@ module Ci
def execute(project, opts)
sha = opts[:sha] || ref_sha(project, opts[:ref])
- commit = project.ci_commits.ordered.find_by(sha: sha)
+ commit = project.ci_commits.find_by(sha: sha)
image_name = image_for_commit(commit)
image_path = Rails.root.join('public/ci', image_name)
diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb
index 43d1c766e35..a3c950ede1f 100644
--- a/app/services/commits/revert_service.rb
+++ b/app/services/commits/revert_service.rb
@@ -9,7 +9,8 @@ module Commits
@commit = params[:commit]
@create_merge_request = params[:create_merge_request].present?
- validate and commit
+ check_push_permissions unless @create_merge_request
+ commit
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
ValidationError, ReversionError => ex
error(ex.message)
@@ -17,39 +18,39 @@ module Commits
def commit
revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch
+ revert_tree_id = repository.check_revert_content(@commit, @target_branch)
- if @create_merge_request
- # Temporary branch exists and contains the revert commit
- return success if repository.find_branch(revert_into)
+ if revert_tree_id
+ create_target_branch(revert_into) if @create_merge_request
- create_target_branch
- end
-
- unless repository.revert(current_user, @commit, revert_into)
+ repository.revert(current_user, @commit, revert_into, revert_tree_id)
+ success
+ else
error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically.
It may have already been reverted, or a more recent commit may have updated some of its content."
raise ReversionError, error_msg
end
-
- success
end
private
- def create_target_branch
+ def create_target_branch(new_branch)
+ # Temporary branch exists and contains the revert commit
+ return success if repository.find_branch(new_branch)
+
result = CreateBranchService.new(@project, current_user)
- .execute(@commit.revert_branch_name, @target_branch, source_project: @source_project)
+ .execute(new_branch, @target_branch, source_project: @source_project)
if result[:status] == :error
raise ReversionError, "There was an error creating the source branch: #{result[:message]}"
end
end
- def validate
+ def check_push_permissions
allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
unless allowed
- raise_error('You are not allowed to push into this branch')
+ raise ValidationError.new('You are not allowed to push into this branch')
end
true
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index ec581658fc1..e2bccbdbcc3 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -1,7 +1,7 @@
require 'securerandom'
# Compare 2 branches for one repo or between repositories
-# and return Gitlab::CompareResult object that responds to commits and diffs
+# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
def execute(source_project, source_branch, target_project, target_branch, diff_options = {})
source_commit = source_project.commit(source_branch)
@@ -20,12 +20,10 @@ class CompareService
)
end
- Gitlab::CompareResult.new(
- Gitlab::Git::Compare.new(
- target_project.repository.raw_repository,
- target_branch,
- source_sha,
- ), diff_options
+ Gitlab::Git::Compare.new(
+ target_project.repository.raw_repository,
+ target_branch,
+ source_sha,
)
end
end
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
index 31b407efeb1..69d5c42a877 100644
--- a/app/services/create_commit_builds_service.rb
+++ b/app/services/create_commit_builds_service.rb
@@ -33,7 +33,6 @@ class CreateCommitBuildsService
unless commit.skip_ci?
# Create builds for commit
tag = Gitlab::Git.tag_ref?(origin_ref)
- commit.update_committed!
commit.create_builds(ref, tag, user)
end
diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb
index 101a3df5eee..9884cb96661 100644
--- a/app/services/create_snippet_service.rb
+++ b/app/services/create_snippet_service.rb
@@ -6,8 +6,7 @@ class CreateSnippetService < BaseService
snippet = project.snippets.build(params)
end
- unless Gitlab::VisibilityLevel.allowed_for?(current_user,
- params[:visibility_level])
+ unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
deny_visibility_level(snippet)
return snippet
end
diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb
index 173e50c9206..ce79287e35a 100644
--- a/app/services/delete_user_service.rb
+++ b/app/services/delete_user_service.rb
@@ -5,18 +5,22 @@ class DeleteUserService
@current_user = current_user
end
- def execute(user)
- if user.solo_owned_groups.present?
+ def execute(user, options = {})
+ if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
- user
- else
- user.personal_projects.each do |project|
- # Skip repository removal because we remove directory with namespace
- # that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
- end
+ return user
+ end
+
+ user.solo_owned_groups.each do |group|
+ DestroyGroupService.new(group, current_user).execute
+ end
- user.destroy
+ user.personal_projects.each do |project|
+ # Skip repository removal because we remove directory with namespace
+ # that contain all this repositories
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
end
+
+ user.destroy
end
end
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
index 9189de390a2..3c42ac61be4 100644
--- a/app/services/destroy_group_service.rb
+++ b/app/services/destroy_group_service.rb
@@ -6,12 +6,12 @@ class DestroyGroupService
end
def execute
- @group.projects.each do |project|
+ group.projects.each do |project|
# Skip repository removal because we remove directory with namespace
# that contain all this repositories
::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
end
- @group.destroy
+ group.destroy
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index a1711d234ff..c007d648dd6 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -12,17 +12,18 @@ class GitPushService < BaseService
# 1. Creates the push event
# 2. Updates merge requests
# 3. Recognizes cross-references from commit messages
- # 4. Executes the project's web hooks
+ # 4. Executes the project's webhooks
# 5. Executes the project's services
+ # 6. Checks if the project's main language has changed
#
def execute
- @project.repository.expire_cache(branch_name)
+ @project.repository.after_push_commit(branch_name, params[:newrev])
if push_remove_branch?
- @project.repository.expire_has_visible_content_cache
+ @project.repository.after_remove_branch
@push_commits = []
elsif push_to_new_branch?
- @project.repository.expire_has_visible_content_cache
+ @project.repository.after_create_branch
# Re-find the pushed commits.
if is_default_branch?
@@ -42,9 +43,24 @@ class GitPushService < BaseService
@push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev])
process_commit_messages
end
+ # Checks if the main language has changed in the project and if so
+ # it updates it accordingly
+ update_main_language
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.
update_merge_requests
+
+ perform_housekeeping
+ end
+
+ def update_main_language
+ current_language = @project.repository.main_language
+
+ unless current_language == @project.main_language
+ return @project.update_attributes(main_language: current_language)
+ end
+
+ true
end
protected
@@ -59,6 +75,13 @@ class GitPushService < BaseService
ProjectCacheWorker.perform_async(@project.id)
end
+ def perform_housekeeping
+ housekeeping = Projects::HousekeepingService.new(@project)
+ housekeeping.increment!
+ housekeeping.execute if housekeeping.needed?
+ rescue Projects::HousekeepingService::LeaseTaken
+ end
+
def process_default_branch
@push_commits = project.repository.commits(params[:newrev])
@@ -66,7 +89,7 @@ class GitPushService < BaseService
project.change_head(branch_name)
# Set protection on the default branch if configured
- if (current_application_settings.default_branch_protection != PROTECTION_NONE)
+ if current_application_settings.default_branch_protection != PROTECTION_NONE
developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
@project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push })
end
@@ -96,7 +119,9 @@ class GitPushService < BaseService
# a different branch.
closed_issues = commit.closes_issues(current_user)
closed_issues.each do |issue|
- Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
+ if can?(current_user, :update_issue, issue)
+ Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit: commit)
+ end
end
end
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index 4144c7111d0..c88c7672805 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -2,7 +2,7 @@ class GitTagPushService
attr_accessor :project, :user, :push_data
def execute(project, user, oldrev, newrev, ref)
- project.repository.expire_cache
+ project.repository.before_push_tag
@project, @user = project, user
@push_data = build_push_data(oldrev, newrev, ref)
diff --git a/app/services/groups/base_service.rb b/app/services/groups/base_service.rb
new file mode 100644
index 00000000000..a8fa098246a
--- /dev/null
+++ b/app/services/groups/base_service.rb
@@ -0,0 +1,9 @@
+module Groups
+ class BaseService < ::BaseService
+ attr_accessor :group, :current_user, :params
+
+ def initialize(group, user, params = {})
+ @group, @current_user, @params = group, user, params.dup
+ end
+ end
+end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
new file mode 100644
index 00000000000..2bccd584dde
--- /dev/null
+++ b/app/services/groups/create_service.rb
@@ -0,0 +1,21 @@
+module Groups
+ class CreateService < Groups::BaseService
+ def initialize(user, params = {})
+ @current_user, @params = user, params.dup
+ end
+
+ def execute
+ @group = Group.new(params)
+
+ unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
+ deny_visibility_level(@group)
+ return @group
+ end
+
+ @group.name ||= @group.path.dup
+ @group.save
+ @group.add_owner(current_user)
+ @group
+ end
+ end
+end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
new file mode 100644
index 00000000000..99ad12b1003
--- /dev/null
+++ b/app/services/groups/update_service.rb
@@ -0,0 +1,20 @@
+module Groups
+ class UpdateService < Groups::BaseService
+ def execute
+ # check that user is allowed to set specified visibility_level
+ new_visibility = params[:visibility_level]
+ if new_visibility && new_visibility.to_i != group.visibility_level
+ unless can?(current_user, :change_visibility_level, group) &&
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+
+ deny_visibility_level(group, new_visibility)
+ return group
+ end
+ end
+
+ group.assign_attributes(params)
+
+ group.save
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index ca87dca4a70..18f76d3f650 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -11,7 +11,10 @@ class IssuableBaseService < BaseService
issuable, issuable.project, current_user, issuable.milestone)
end
- def create_labels_note(issuable, added_labels, removed_labels)
+ def create_labels_note(issuable, old_labels)
+ added_labels = issuable.labels - old_labels
+ removed_labels = old_labels - issuable.labels
+
SystemNoteService.change_label(
issuable, issuable.project, current_user, added_labels, removed_labels)
end
@@ -71,20 +74,19 @@ class IssuableBaseService < BaseService
end
end
- def has_changes?(issuable, options = {})
+ def has_changes?(issuable, old_labels: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
attrs_changed = valid_attrs.any? do |attr|
issuable.previous_changes.include?(attr.to_s)
end
- old_labels = options[:old_labels]
- labels_changed = old_labels && issuable.labels != old_labels
+ labels_changed = issuable.labels != old_labels
attrs_changed || labels_changed
end
- def handle_common_system_notes(issuable, options = {})
+ def handle_common_system_notes(issuable, old_labels: [])
if issuable.previous_changes.include?('title')
create_title_change_note(issuable, issuable.previous_changes['title'].first)
end
@@ -93,9 +95,6 @@ class IssuableBaseService < BaseService
create_task_status_note(issuable)
end
- old_labels = options[:old_labels]
- if old_labels && (issuable.labels != old_labels)
- create_labels_note(issuable, issuable.labels - old_labels, old_labels - issuable.labels)
- end
+ create_labels_note(issuable, old_labels) if issuable.labels != old_labels
end
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 78254b49af3..859c934ea3b 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -1,6 +1,6 @@
module Issues
class CloseService < Issues::BaseService
- def execute(issue, commit = nil)
+ def execute(issue, commit: nil, notifications: true, system_note: true)
if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue)
todo_service.close_issue(issue, current_user)
@@ -9,8 +9,8 @@ module Issues
if project.default_issues_tracker? && issue.close
event_service.close_issue(issue, current_user)
- create_note(issue, commit)
- notification_service.close_issue(issue, current_user)
+ create_note(issue, commit) if system_note
+ notification_service.close_issue(issue, current_user) if notifications
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 10787e8873c..e63e1af8766 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -4,7 +4,7 @@ module Issues
filter_params
label_params = params[:label_ids]
issue = project.issues.new(params.except(:label_ids))
- issue.author = current_user
+ issue.author = params[:author] || current_user
if issue.save
issue.update_attributes(label_ids: label_params)
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
new file mode 100644
index 00000000000..a5efb21fab6
--- /dev/null
+++ b/app/services/issues/move_service.rb
@@ -0,0 +1,97 @@
+module Issues
+ class MoveService < Issues::BaseService
+ class MoveError < StandardError; end
+
+ def execute(issue, new_project)
+ @old_issue = issue
+ @old_project = @project
+ @new_project = new_project
+
+ unless issue.can_move?(current_user, new_project)
+ raise MoveError, 'Cannot move issue due to insufficient permissions!'
+ end
+
+ if @project == new_project
+ raise MoveError, 'Cannot move issue to project it originates from!'
+ end
+
+ # Using transaction because of a high resources footprint
+ # on rewriting notes (unfolding references)
+ #
+ ActiveRecord::Base.transaction do
+ # New issue tasks
+ #
+ @new_issue = create_new_issue
+
+ rewrite_notes
+ add_note_moved_from
+
+ # Old issue tasks
+ #
+ add_note_moved_to
+ close_issue
+ mark_as_moved
+ end
+
+ notify_participants
+
+ @new_issue
+ end
+
+ private
+
+ def create_new_issue
+ new_params = { id: nil, iid: nil, label_ids: [], milestone: nil,
+ project: @new_project, author: @old_issue.author,
+ description: unfold_references(@old_issue.description) }
+
+ new_params = @old_issue.serializable_hash.merge(new_params)
+ CreateService.new(@new_project, @current_user, new_params).execute
+ end
+
+ def rewrite_notes
+ @old_issue.notes.find_each do |note|
+ new_note = note.dup
+ new_params = { project: @new_project, noteable: @new_issue,
+ note: unfold_references(new_note.note),
+ created_at: note.created_at,
+ updated_at: note.updated_at }
+
+ new_note.update(new_params)
+ end
+ end
+
+ def close_issue
+ close_service = CloseService.new(@old_project, @current_user)
+ close_service.execute(@old_issue, notifications: false, system_note: false)
+ end
+
+ def add_note_moved_from
+ SystemNoteService.noteable_moved(@new_issue, @new_project,
+ @old_issue, @current_user,
+ direction: :from)
+ end
+
+ def add_note_moved_to
+ SystemNoteService.noteable_moved(@old_issue, @old_project,
+ @new_issue, @current_user,
+ direction: :to)
+ end
+
+ def unfold_references(content)
+ return unless content
+
+ rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project,
+ @current_user)
+ rewriter.rewrite(@new_project)
+ end
+
+ def notify_participants
+ notification_service.issue_moved(@old_issue, @new_issue, @current_user)
+ end
+
+ def mark_as_moved
+ @old_issue.update(moved_to: @new_issue)
+ end
+ end
+end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 51ef9dfe610..3563cbaa997 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -4,8 +4,8 @@ module Issues
update(issue)
end
- def handle_changes(issue, options = {})
- if has_changes?(issue, options)
+ def handle_changes(issue, old_labels: [])
+ if has_changes?(issue, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(issue, current_user)
end
@@ -23,6 +23,11 @@ module Issues
notification_service.reassigned_issue(issue, current_user)
todo_service.reassigned_issue(issue, current_user)
end
+
+ added_labels = issue.labels - old_labels
+ if added_labels.present?
+ notification_service.relabeled_issue(issue, added_labels, current_user)
+ end
end
def reopen_service
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 7b306a8a531..ac5b58db862 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -5,6 +5,19 @@ module MergeRequests
SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
end
+ def create_title_change_note(issuable, old_title)
+ removed_wip = old_title =~ MergeRequest::WIP_REGEX && !issuable.work_in_progress?
+ added_wip = old_title !~ MergeRequest::WIP_REGEX && issuable.work_in_progress?
+
+ if removed_wip
+ SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user)
+ elsif added_wip
+ SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user)
+ else
+ super
+ end
+ end
+
def hook_data(merge_request, action)
hook_data = merge_request.to_hook_data(current_user)
merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index c0700d953dd..6e9152e444e 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -5,9 +5,7 @@ module MergeRequests
# Set MR attributes
merge_request.can_be_created = false
- merge_request.compare_failed = false
merge_request.compare_commits = []
- merge_request.compare_diffs = []
merge_request.source_project = project unless merge_request.source_project
merge_request.target_project ||= (project.forked_from_project || project)
merge_request.target_branch ||= merge_request.target_project.default_branch
@@ -21,35 +19,23 @@ module MergeRequests
return build_failed(merge_request, message)
end
- compare_result = CompareService.new.execute(
+ compare = CompareService.new.execute(
merge_request.source_project,
merge_request.source_branch,
merge_request.target_project,
merge_request.target_branch,
)
- commits = compare_result.commits
+ commits = compare.commits
# At this point we decide if merge request can be created
# If we have at least one commit to merge -> creation allowed
if commits.present?
merge_request.compare_commits = Commit.decorate(commits, merge_request.source_project)
merge_request.can_be_created = true
- merge_request.compare_failed = false
-
- # Try to collect diff for merge request.
- diffs = compare_result.diffs
-
- if diffs.present?
- merge_request.compare_diffs = diffs
-
- elsif diffs == false
- merge_request.can_be_created = false
- merge_request.compare_failed = true
- end
+ merge_request.compare = compare
else
merge_request.can_be_created = false
- merge_request.compare_failed = false
end
commits = merge_request.compare_commits
@@ -61,6 +47,21 @@ module MergeRequests
merge_request.title = merge_request.source_branch.titleize.humanize
end
+ # When your branch name starts with an iid followed by a dash this pattern will
+ # be interpreted as the use wants to close that issue on this project
+ # Pattern example: 112-fix-mep-mep
+ # Will lead to appending `Closes #112` to the description
+ if match = merge_request.source_branch.match(/-(\d+)\z/)
+ iid = match[1]
+ closes_issue = "Closes ##{iid}"
+
+ if merge_request.description.present?
+ merge_request.description << closes_issue.prepend("\n")
+ else
+ merge_request.description = closes_issue
+ end
+ end
+
merge_request
end
diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb
index 531bbc9b067..d6af12f9739 100644
--- a/app/services/merge_requests/merge_when_build_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb
@@ -24,10 +24,14 @@ module MergeRequests
merge_requests.each do |merge_request|
next unless merge_request.merge_when_build_succeeds?
+ next unless merge_request.mergeable?
- if merge_request.ci_commit && merge_request.ci_commit.success? && merge_request.mergeable?
- MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
- end
+ ci_commit = merge_request.ci_commit
+ next unless ci_commit
+ next unless ci_commit.sha == commit_status.sha
+ next unless ci_commit.success?
+
+ MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
end
@@ -51,6 +55,8 @@ module MergeRequests
# This is for ref-less builds
branches ||= @project.repository.branch_names_contains(commit_status.sha)
+ return [] if branches.blank?
+
merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index 8f25c5e2496..064910f81f7 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -21,7 +21,9 @@ module MergeRequests
closed_issues = merge_request.closes_issues(current_user)
closed_issues.each do |issue|
- Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
+ if can?(current_user, :update_issue, issue)
+ Issues::CloseService.new(project, current_user, {}).execute(issue, commit: merge_request)
+ end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 6319ad805b6..477c64e7377 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -14,8 +14,8 @@ module MergeRequests
update(merge_request)
end
- def handle_changes(merge_request, options = {})
- if has_changes?(merge_request, options)
+ def handle_changes(merge_request, old_labels: [])
+ if has_changes?(merge_request, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
end
@@ -44,6 +44,15 @@ module MergeRequests
merge_request.previous_changes.include?('source_branch')
merge_request.mark_as_unchecked
end
+
+ added_labels = merge_request.labels - old_labels
+ if added_labels.present?
+ notification_service.relabeled_merge_request(
+ merge_request,
+ added_labels,
+ current_user
+ )
+ end
end
def reopen_service
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index b970439b921..2bb312bb252 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -13,6 +13,5 @@ module Notes
note
end
-
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ca8a41d93b8..eff0d96f93d 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -24,16 +24,17 @@ class NotificationService
end
end
- # When create an issue we should send next emails:
+ # When create an issue we should send an email to:
#
# * issue assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
+ # * watchers of the issue's labels
#
def new_issue(issue, current_user)
new_resource_email(issue, issue.project, 'new_issue_email')
end
- # When we close an issue we should send next emails:
+ # When we close an issue we should send an email to:
#
# * issue author if their notification level is not Disabled
# * issue assignee if their notification level is not Disabled
@@ -43,7 +44,7 @@ class NotificationService
close_resource_email(issue, issue.project, current_user, 'closed_issue_email')
end
- # When we reassign an issue we should send next emails:
+ # When we reassign an issue we should send an email to:
#
# * issue old assignee if their notification level is not Disabled
# * issue new assignee if their notification level is not Disabled
@@ -52,16 +53,25 @@ class NotificationService
reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email')
end
+ # When we add labels to an issue we should send an email to:
+ #
+ # * watchers of the issue's labels
+ #
+ def relabeled_issue(issue, added_labels, current_user)
+ relabeled_resource_email(issue, added_labels, current_user, 'relabeled_issue_email')
+ end
- # When create a merge request we should send next emails:
+ # When create a merge request we should send an email to:
#
# * mr assignee if their notification level is not Disabled
+ # * project team members with notification level higher then Participating
+ # * watchers of the mr's labels
#
def new_merge_request(merge_request, current_user)
new_resource_email(merge_request, merge_request.target_project, 'new_merge_request_email')
end
- # When we reassign a merge_request we should send next emails:
+ # When we reassign a merge_request we should send an email to:
#
# * merge_request old assignee if their notification level is not Disabled
# * merge_request assignee if their notification level is not Disabled
@@ -70,6 +80,14 @@ class NotificationService
reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email')
end
+ # When we add labels to a merge request we should send an email to:
+ #
+ # * watchers of the mr's labels
+ #
+ def relabeled_merge_request(merge_request, added_labels, current_user)
+ relabeled_resource_email(merge_request, added_labels, current_user, 'relabeled_merge_request_email')
+ end
+
def close_mr(merge_request, current_user)
close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email')
end
@@ -91,7 +109,8 @@ class NotificationService
reopen_resource_email(
merge_request,
merge_request.target_project,
- current_user, 'merge_request_status_email',
+ current_user,
+ 'merge_request_status_email',
'reopened'
)
end
@@ -143,6 +162,7 @@ class NotificationService
recipients = add_subscribed_users(recipients, note.noteable)
recipients = reject_unsubscribed_users(recipients, note.noteable)
+ recipients = reject_users_without_access(recipients, note.noteable)
recipients.delete(note.author)
recipients = recipients.uniq
@@ -217,6 +237,16 @@ class NotificationService
end
end
+ def issue_moved(issue, new_issue, current_user)
+ recipients = build_recipients(issue, issue.project, current_user)
+
+ recipients.map do |recipient|
+ email = mailer.issue_moved_email(recipient, issue, new_issue, current_user)
+ email.deliver_later
+ email
+ end
+ end
+
protected
# Get project users with WATCH notification level
@@ -347,20 +377,32 @@ class NotificationService
end
end
+ def reject_users_without_access(recipients, target)
+ return recipients unless target.is_a?(Issue)
+
+ recipients.select do |user|
+ user.can?(:read_issue, target)
+ end
+ end
+
def add_subscribed_users(recipients, target)
- return recipients unless target.respond_to? :subscriptions
+ return recipients unless target.respond_to? :subscribers
- subscriptions = target.subscriptions
+ recipients + target.subscribers
+ end
- if subscriptions.any?
- recipients + subscriptions.where(subscribed: true).map(&:user)
- else
- recipients
+ def add_labels_subscribers(recipients, target, labels: nil)
+ return recipients unless target.respond_to? :labels
+
+ (labels || target.labels).each do |label|
+ recipients += label.subscribers
end
+
+ recipients
end
def new_resource_email(target, project, method)
- recipients = build_recipients(target, project, target.author)
+ recipients = build_recipients(target, project, target.author, action: :new)
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later
@@ -392,6 +434,15 @@ class NotificationService
end
end
+ def relabeled_resource_email(target, labels, current_user, method)
+ recipients = build_relabeled_recipients(target, current_user, labels: labels)
+ label_names = labels.map(&:name)
+
+ recipients.each do |recipient|
+ mailer.send(method, recipient.id, target.id, label_names, current_user.id).deliver_later
+ end
+ end
+
def reopen_resource_email(target, project, current_user, method, status)
recipients = build_recipients(target, project, current_user)
@@ -416,10 +467,23 @@ class NotificationService
recipients = reject_muted_users(recipients, project)
recipients = add_subscribed_users(recipients, target)
+
+ if action == :new
+ recipients = add_labels_subscribers(recipients, target)
+ end
+
recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user)
+ recipients.uniq
+ end
+ def build_relabeled_recipients(target, current_user, labels:)
+ recipients = add_labels_subscribers([], target, labels: labels)
+ recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
+ recipients.delete(current_user)
recipients.uniq
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 7408e09ed1e..ba50305dbd5 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -1,11 +1,7 @@
module Projects
class AutocompleteService < BaseService
- def initialize(project)
- @project = project
- end
-
def issues
- @project.issues.opened.select([:iid, :title])
+ @project.issues.visible_to_user(current_user).opened.select([:iid, :title])
end
def merge_requests
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index a6820183bee..501e58c1407 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -9,10 +9,8 @@ module Projects
@project = Project.new(params)
- # Make sure that the user is allowed to use the specified visibility
- # level
- unless Gitlab::VisibilityLevel.allowed_for?(current_user,
- params[:visibility_level])
+ # Make sure that the user is allowed to use the specified visibility level
+ unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
deny_visibility_level(@project)
return @project
end
@@ -55,9 +53,7 @@ module Projects
@project.save
if @project.persisted? && !@project.import?
- unless @project.create_repository
- raise 'Failed to create repository'
- end
+ raise 'Failed to create repository' unless @project.create_repository
end
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index f4dcb142850..df5054f08d7 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -76,11 +76,9 @@ module Projects
end
def flush_caches(project, wiki_path)
- project.repository.expire_all_caches! if project.repository.exists?
+ project.repository.before_delete
- wiki_repo = Repository.new(wiki_path, project)
-
- wiki_repo.expire_all_caches! if wiki_repo.exists?
+ Repository.new(wiki_path, project).before_delete
end
end
end
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index 0db85ac2142..a0973c5d260 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -9,12 +9,39 @@ module Projects
class HousekeepingService < BaseService
include Gitlab::ShellAdapter
+ LEASE_TIMEOUT = 3600
+
+ class LeaseTaken < StandardError
+ def to_s
+ "Somebody already triggered housekeeping for this project in the past #{LEASE_TIMEOUT / 60} minutes"
+ end
+ end
+
def initialize(project)
@project = project
end
def execute
- GitlabShellWorker.perform_async(:gc, @project.path_with_namespace)
+ raise LeaseTaken if !try_obtain_lease
+
+ GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace)
+ ensure
+ @project.update_column(:pushes_since_gc, 0)
+ end
+
+ def needed?
+ @project.pushes_since_gc >= 10
+ end
+
+ def increment!
+ @project.increment!(:pushes_since_gc)
+ end
+
+ private
+
+ def try_obtain_lease
+ lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
+ lease.try_obtain
end
end
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 895e089bea3..941df08995c 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -3,16 +3,13 @@ module Projects
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
- if new_visibility
- if new_visibility.to_i != project.visibility_level
- unless can?(current_user, :change_visibility_level, project) &&
- Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
- deny_visibility_level(project, new_visibility)
- return project
- end
+ if new_visibility && new_visibility.to_i != project.visibility_level
+ unless can?(current_user, :change_visibility_level, project) &&
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+
+ deny_visibility_level(project, new_visibility)
+ return project
end
-
- return false unless visibility_level_allowed?(new_visibility)
end
new_branch = params[:default_branch]
@@ -27,19 +24,5 @@ module Projects
end
end
end
-
- private
-
- def visibility_level_allowed?(level)
- return true if project.visibility_level_allowed?(level)
-
- level_name = Gitlab::VisibilityLevel.level_name(level)
- project.errors.add(
- :visibility_level,
- "#{level_name} could not be set as visibility level of this project - parent project settings are more restrictive"
- )
-
- false
- end
end
end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index e904cb6c6fc..aa9837038a6 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -10,9 +10,8 @@ module Search
group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group
- project_ids = projects.pluck(:id)
- Gitlab::SearchResults.new(project_ids, params[:search])
+ Gitlab::SearchResults.new(current_user, projects, params[:search])
end
end
end
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index f630c0a3790..4b500914cfb 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -7,7 +7,8 @@ module Search
end
def execute
- Gitlab::ProjectSearchResults.new(project.id,
+ Gitlab::ProjectSearchResults.new(current_user,
+ project,
params[:search],
params[:repository_ref])
end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 8ca0877321d..0b3e713e220 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -7,8 +7,9 @@ module Search
end
def execute
- snippet_ids = Snippet.accessible_to(current_user).pluck(:id)
- Gitlab::SnippetSearchResults.new(snippet_ids, params[:search])
+ snippets = Snippet.accessible_to(current_user)
+
+ Gitlab::SnippetSearchResults.new(snippets, params[:search])
end
end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index edced010811..e022a046c48 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -66,7 +66,7 @@ class SystemNoteService
def self.change_label(noteable, project, author, added_labels, removed_labels)
labels_count = added_labels.count + removed_labels.count
- references = ->(label) { label.to_reference(:id) }
+ references = ->(label) { label.to_reference(format: :id) }
added_labels = added_labels.map(&references).join(' ')
removed_labels = removed_labels.map(&references).join(' ')
@@ -144,6 +144,18 @@ class SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ def self.remove_merge_request_wip(noteable, project, author)
+ body = 'Unmarked this merge request as a Work In Progress'
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
+ def self.add_merge_request_wip(noteable, project, author)
+ body = 'Marked this merge request as a **Work In Progress**'
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
# Called when the title of a Noteable is changed
#
# noteable - Noteable object that responds to `title`
@@ -207,6 +219,18 @@ class SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ # Called when a branch is created from the 'new branch' button on a issue
+ # Example note text:
+ #
+ # "Started branch `issue-branch-button-201`"
+ def self.new_issue_branch(issue, project, author, branch)
+ h = Gitlab::Application.routes.url_helpers
+ link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
+
+ body = "Started branch [`#{branch}`](#{link})"
+ create_note(noteable: issue, project: project, author: author, note: body)
+ end
+
# Called when a Mentionable references a Noteable
#
# noteable - Noteable object being referenced
@@ -387,4 +411,26 @@ class SystemNoteService
body = "Marked the task **#{new_task.source}** as #{status_label}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
+
+ # Called when noteable has been moved to another project
+ #
+ # direction - symbol, :to or :from
+ # noteable - Noteable object
+ # noteable_ref - Referenced noteable
+ # author - User performing the move
+ #
+ # Example Note text:
+ #
+ # "Moved to some_namespace/project_new#11"
+ #
+ # Returns the created Note object
+ def self.noteable_moved(noteable, project, noteable_ref, author, direction:)
+ unless [:to, :from].include?(direction)
+ raise ArgumentError, "Invalid direction `#{direction}`"
+ end
+
+ cross_reference = noteable_ref.to_reference(project)
+ body = "Moved #{direction} #{cross_reference}"
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index dc270602ebc..f2662922e90 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -103,24 +103,16 @@ class TodoService
# * mark all pending todos related to the target for the current user as done
#
def mark_pending_todos_as_done(target, user)
- pending_todos(user, target.project, target).update_all(state: :done)
+ attributes = attributes_for_target(target)
+ pending_todos(user, attributes).update_all(state: :done)
end
private
- def create_todos(project, target, author, users, action, note = nil)
+ def create_todos(users, attributes)
Array(users).each do |user|
- next if pending_todos(user, project, target).exists?
-
- Todo.create(
- project: project,
- user_id: user.id,
- author_id: author.id,
- target_id: target.id,
- target_type: target.class.name,
- action: action,
- note: note
- )
+ next if pending_todos(user, attributes).exists?
+ Todo.create(attributes.merge(user_id: user.id))
end
end
@@ -130,8 +122,8 @@ class TodoService
end
def handle_note(note, author)
- # Skip system notes, like status changes and cross-references
- return if note.system
+ # Skip system notes, and notes on project snippet
+ return if note.system? || note.for_project_snippet?
project = note.project
target = note.noteable
@@ -142,13 +134,39 @@ class TodoService
def create_assignment_todo(issuable, author)
if issuable.assignee && issuable.assignee != author
- create_todos(issuable.project, issuable, author, issuable.assignee, Todo::ASSIGNED)
+ attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
+ create_todos(issuable.assignee, attributes)
end
end
- def create_mention_todos(project, issuable, author, note = nil)
- mentioned_users = filter_mentioned_users(project, note || issuable, author)
- create_todos(project, issuable, author, mentioned_users, Todo::MENTIONED, note)
+ def create_mention_todos(project, target, author, note = nil)
+ mentioned_users = filter_mentioned_users(project, note || target, author)
+ attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
+ create_todos(mentioned_users, attributes)
+ end
+
+ def attributes_for_target(target)
+ attributes = {
+ project_id: target.project.id,
+ target_id: target.id,
+ target_type: target.class.name,
+ commit_id: nil
+ }
+
+ if target.is_a?(Commit)
+ attributes.merge!(target_id: nil, commit_id: target.id)
+ end
+
+ attributes
+ end
+
+ def attributes_for_todo(project, target, author, action, note = nil)
+ attributes_for_target(target).merge!(
+ project_id: project.id,
+ author_id: author.id,
+ action: action,
+ note: note
+ )
end
def filter_mentioned_users(project, target, author)
@@ -160,11 +178,8 @@ class TodoService
mentioned_users.uniq
end
- def pending_todos(user, project, target)
- user.todos.pending.where(
- project_id: project.id,
- target_id: target.id,
- target_type: target.class.name
- )
+ def pending_todos(user, criteria = {})
+ valid_keys = [:project_id, :target_id, :target_type, :commit_id]
+ user.todos.pending.where(criteria.slice(*valid_keys))
end
end
diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb
index e9328bb7323..93af8f21972 100644
--- a/app/services/update_snippet_service.rb
+++ b/app/services/update_snippet_service.rb
@@ -9,7 +9,6 @@ class UpdateSnippetService < BaseService
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
-
if new_visibility && new_visibility.to_i != snippet.visibility_level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(snippet, new_visibility)
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
index 2848b9cd33d..a77beb2683d 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -29,8 +29,11 @@ class UrlValidator < ActiveModel::EachValidator
end
def valid_url?(value)
+ return false if value.nil?
+
options = default_options.merge(self.options)
+ value.strip!
value =~ /\A#{URI.regexp(options[:protocols])}\z/
end
end
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index f125ecf7be5..3bc1b24b5e2 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -2,7 +2,7 @@
%h3.page-title Report abuse
%p Please use this form to report users who create spam issues, comments or behave inappropriately.
%hr
-= form_for @abuse_report, html: { class: 'form-horizontal js-requires-input'} do |f|
+= form_for @abuse_report, html: { class: 'form-horizontal js-quick-submit js-requires-input'} do |f|
= f.hidden_field :user_id
- if @abuse_report.errors.any?
.alert.alert-danger
@@ -16,7 +16,7 @@
.form-group
= f.label :message, class: 'control-label'
.col-sm-10
- = f.text_area :message, class: "form-control js-quick-submit", rows: 2, required: true, value: sanitize(@ref_url)
+ = f.text_area :message, class: "form-control", rows: 2, required: true, value: sanitize(@ref_url)
.help-block
Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment.
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
new file mode 100644
index 00000000000..6f325914d14
--- /dev/null
+++ b/app/views/admin/appearances/_form.html.haml
@@ -0,0 +1,58 @@
+= form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f|
+ - if @appearance.errors.any?
+ .alert.alert-danger
+ - @appearance.errors.full_messages.each do |msg|
+ %p= msg
+
+ %fieldset.sign-in
+ %legend
+ Sign in/Sign up pages:
+ .form-group
+ = f.label :title, class: 'control-label'
+ .col-sm-10
+ = f.text_field :title, class: "form-control"
+ .form-group
+ = f.label :description, class: 'control-label'
+ .col-sm-10
+ = f.text_area :description, class: "form-control", rows: 10
+ .hint
+ Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown', 'markdown'), target: '_blank'}.
+ .form-group
+ = f.label :logo, class: 'control-label'
+ .col-sm-10
+ - if @appearance.logo?
+ = image_tag @appearance.logo_url, class: 'appearance-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo"
+ %hr
+ = f.hidden_field :logo_cache
+ = f.file_field :logo, class: ""
+ .hint
+ Maximum file size is 1MB. Pages are optimized for a 640x360 px logo.
+
+ %fieldset.app_logo
+ %legend
+ Navigation bar:
+ .form-group
+ = f.label :header_logo, 'Header logo', class: 'control-label'
+ .col-sm-10
+ - if @appearance.header_logo?
+ = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo"
+ %hr
+ = f.hidden_field :header_logo_cache
+ = f.file_field :header_logo, class: ""
+ .hint
+ Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo
+
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save'
+ - if @appearance.persisted?
+ = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank'
+
+ - if @appearance.updated_at
+ %span.pull-right
+ Last edit #{time_ago_with_tooltip(@appearance.updated_at)}
diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml
new file mode 100644
index 00000000000..dd4a64e80bc
--- /dev/null
+++ b/app/views/admin/appearances/preview.html.haml
@@ -0,0 +1,29 @@
+- page_title "Preview | Appearance"
+%h3.page-title
+ Appearance settings - Preview
+%hr
+
+.ui-box
+ .title
+ Sign-in page
+ %div
+ .login-page
+ .container
+ .content
+ .login-title
+ %h1= brand_title
+ %hr
+ .container
+ .content
+ .row
+ .col-sm-7
+ .brand-image
+ = brand_image
+ .brand_text
+ = brand_text
+ .col-sm-4
+ .login-box
+ %h3.page-title Sign in
+ = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email"
+ = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password"
+ = button_tag "Sign in", class: "btn-create btn"
diff --git a/app/views/admin/appearances/show.html.haml b/app/views/admin/appearances/show.html.haml
new file mode 100644
index 00000000000..089e8e4cb7a
--- /dev/null
+++ b/app/views/admin/appearances/show.html.haml
@@ -0,0 +1,7 @@
+- page_title "Appearance"
+%h3.page-title
+ Appearance settings
+%p.light
+ You can modify the look and feel of GitLab here
+
+= render 'form'
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index b30dfd109ea..0350995d03d 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -19,6 +19,10 @@
= f.label :default_snippet_visibility, class: 'control-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
+ .form-group.group-visibility-level-holder
+ = f.label :default_group_visibility, class: 'control-label col-sm-2'
+ .col-sm-10
+ = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
.form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-10
diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml
index 3147cbd659f..042971e1eed 100644
--- a/app/views/admin/applications/_delete_form.html.haml
+++ b/app/views/admin/applications/_delete_form.html.haml
@@ -1,4 +1,4 @@
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag admin_application_path(application) do
%input{:name => "_method", :type => "hidden", :value => "delete"}/
- = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css \ No newline at end of file
+ = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 5c9403fa0c2..b748460a9f7 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -3,7 +3,7 @@
.js-broadcast-message-preview
= render_broadcast_message(@broadcast_message.message.presence || "Your message here")
-= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-requires-input'} do |f|
+= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
-if @broadcast_message.errors.any?
.alert.alert-danger
- @broadcast_message.errors.full_messages.each do |msg|
@@ -11,7 +11,7 @@
.form-group
= f.label :message, class: 'control-label'
.col-sm-10
- = f.text_area :message, class: "form-control js-quick-submit js-autosize",
+ = f.text_area :message, class: "form-control js-autosize",
required: true,
data: { preview_path: preview_admin_broadcast_messages_path }
.form-group.js-toggle-colors-container
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 34d955568f2..588ad767426 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -4,13 +4,13 @@
= ci_status_with_icon(build.status)
%td.build-link
- - if can?(current_user, :read_build, project) && build.target_url
- = link_to build.target_url do
+ - if can?(current_user, :read_build, build.project)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
%strong Build ##{build.id}
- else
%strong Build ##{build.id}
- - if build.show_warning?
+ - if build.stuck?
%i.fa.fa-warning.text-warning
%td
@@ -18,11 +18,11 @@
= link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace"
%td
- = link_to build.short_sha, namespace_project_commit_path(project.namespace, project, build.sha), class: "monospace"
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
%td
- if build.ref
- = link_to build.ref, namespace_project_commits_path(project.namespace, project, build.ref)
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
- else
.light none
@@ -61,13 +61,12 @@
%td
.pull-right
- if can?(current_user, :read_build, project) && build.artifacts?
- = link_to build.artifacts_download_url, title: 'Download artifacts' do
+ = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
%i.fa.fa-download
- if can?(current_user, :update_build, build.project)
- if build.active?
- - if build.cancel_url
- = link_to build.cancel_url, method: :post, title: 'Cancel' do
- %i.fa.fa-remove.cred
- - elsif defined?(allow_retry) && allow_retry && build.retry_url
- = link_to build.retry_url, method: :post, title: 'Retry' do
+ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
+ %i.fa.fa-remove.cred
+ - elsif defined?(allow_retry) && allow_retry && build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
%i.fa.fa-repeat
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 198026a1f75..7f2b1cd235d 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -10,6 +10,8 @@
.col-sm-10
= render 'shared/choose_group_avatar_button', f: f
+ = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
+
- if @group.new_record?
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 118d3cfea07..6bdc885a312 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -46,6 +46,9 @@
%h4
= link_to [:admin, group] do
+ %span{ class: visibility_level_color(group.visibility_level) }
+ = visibility_level_icon(group.visibility_level)
+
%i.fa.fa-folder
= group.name
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index f7fd156b84a..f309e80a39a 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -28,6 +28,11 @@
= @group.description
%li
+ %span.light Visibility level:
+ %strong
+ = visibility_level_label(@group.visibility_level)
+
+ %li
%span.light Created on:
%strong
= @group.created_at.to_s(:medium)
@@ -50,6 +55,22 @@
.panel-footer
= paginate @projects, param_name: 'projects_page', theme: 'gitlab'
+ - if @group.shared_projects.any?
+ .panel.panel-default
+ .panel-heading
+ Projects shared with #{@group.name}
+ %span.badge
+ #{@group.shared_projects.count}
+ %ul.well-list
+ - @group.shared_projects.sort_by(&:name).each do |project|
+ %li
+ %strong
+ = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ %span.label.label-gray
+ = repository_size(project)
+ %span.pull-right.light
+ %span.monospace= project.path_with_namespace + ".git"
+
.col-md-6
- if can?(current_user, :admin_group_member, @group)
.panel.panel-default
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index 5736a301910..f417b2e44a4 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -1,6 +1,6 @@
%li{id: dom_id(label)}
.label-row
- = render_colored_label(label)
+ = render_colored_label(label, tooltip: false)
= markdown(label.description, pipeline: :single_line)
.pull-right
= link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index d734e60682a..c638c32a654 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -52,7 +52,7 @@
%li
%span.light fs:
%strong
- = @repository.path_to_repo
+ = @project.repository.path_to_repo
%li
%span.light Size
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index e18dd9bc905..d2527ede995 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -58,9 +58,15 @@
= f.label :admin, class: 'control-label'
- if current_user == @user
.col-sm-10= f.check_box :admin, disabled: true
- .col-sm-10 You cannot remove your own admin rights
+ .col-sm-10 You cannot remove your own admin rights.
- else
.col-sm-10= f.check_box :admin
+
+ .form-group
+ = f.label :external, class: 'control-label'
+ .col-sm-10= f.check_box :external
+ .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups.
+
%fieldset
%legend Profile
.form-group
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index b6b1168bd37..0ee8dc962b9 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -19,6 +19,10 @@
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
%small.badge= number_with_delimiter(User.without_two_factor.count)
+ %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"}
+ = link_to admin_users_path(filter: 'external') do
+ External
+ %small.badge= number_with_delimiter(User.external.count)
%li{class: "#{'active' if params[:filter] == "blocked"}"}
= link_to admin_users_path(filter: "blocked") do
Blocked
@@ -70,12 +74,14 @@
%li
.list-item-name
- if user.blocked?
- %i.fa.fa-lock.cred
+ = icon("lock", class: "cred")
- else
- %i.fa.fa-user.cgreen
+ = icon("user", class: "cgreen")
= link_to user.name, [:admin, user]
- if user.admin?
%strong.cred (Admin)
+ - if user.external?
+ %strong.cred (External)
- if user == current_user
%span.cred It's you!
.pull-right
diff --git a/app/views/admin/users/keys.html.haml b/app/views/admin/users/keys.html.haml
index 07110717082..0f644121e62 100644
--- a/app/views/admin/users/keys.html.haml
+++ b/app/views/admin/users/keys.html.haml
@@ -1,3 +1,3 @@
-- page_title "Keys", @user.name, "Users"
+- page_title "SSH Keys", @user.name, "Users"
= render 'admin/users/head'
= render 'profiles/keys/key_table', admin: true
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 2bdbae19588..d37489bebea 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -48,6 +48,10 @@
Disabled
%li
+ %span.light External User:
+ %strong
+ = @user.external? ? "Yes" : "No"
+ %li
%span.light Can create groups:
%strong
= @user.can_create_group ? "Yes" : "No"
diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml
deleted file mode 100644
index 11163813f3e..00000000000
--- a/app/views/ci/commits/_commit.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-%tr.build
- %td.status
- = ci_status_with_icon(commit.status)
- - if commit.running?
- &middot;
- = commit.stage
-
-
- %td.build-link
- = link_to ci_status_path(commit) do
- %strong #{commit.short_sha}
-
- %td.build-message
- %span= truncate_first_line(commit.git_commit_message)
-
- %td.build-branch
- - unless @ref
- %span
- - commit.refs.each do |ref|
- = link_to truncate(ref, length: 25), ci_project_path(@project, ref: ref)
-
- %td.duration
- - if commit.duration > 0
- #{time_interval_in_words commit.duration}
-
- %td.timestamp
- - if commit.finished_at
- %span #{time_ago_in_words commit.finished_at} ago
-
- - if commit.coverage
- %td.coverage
- #{commit.coverage}%
diff --git a/app/views/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml
deleted file mode 100644
index 9c2290bc4a5..00000000000
--- a/app/views/ci/projects/index.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-.wiki
- %h1
- GitLab CI is now integrated in GitLab UI
- %h2 For existing projects
-
- %p
- Check the following pages to find the CI status you're looking for:
-
- %ul
- %li Projects page - shows CI status for each project.
- %li Project commits page - show CI status for each commit.
-
-
-
- %h2 For new projects
-
- %p
- If you want to enable CI for a new project it is easy as adding
- = link_to ".gitlab-ci.yml", "http://doc.gitlab.com/ce/ci/yaml/README.html"
- file to your repository
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 4bc761b3738..9da3fcbd986 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -14,8 +14,8 @@
.nav-controls
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field'
- = render 'explore/projects/dropdown'
+ = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2"
+ = render 'shared/projects/dropdown'
- if current_user.can_create_project?
= link_to new_project_path, class: 'btn btn-new' do
= icon('plus')
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index dfa5f80eef8..1eec4db45a0 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -10,6 +10,8 @@
- if current_user
= link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
= icon('rss')
+ %span.icon-label
+ Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
diff --git a/app/views/dashboard/milestones/_issue.html.haml b/app/views/dashboard/milestones/_issue.html.haml
deleted file mode 100644
index 1408ebdd5dc..00000000000
--- a/app/views/dashboard/milestones/_issue.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
- %span.milestone-row
- - project = issue.project
- %strong #{project.name_with_namespace} &middot;
- = link_to [project.namespace.becomes(Namespace), project, issue] do
- %span.cgray ##{issue.iid}
- = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title
- .pull-right.assignee-icon
- - if issue.assignee
- = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16"
diff --git a/app/views/dashboard/milestones/_issues.html.haml b/app/views/dashboard/milestones/_issues.html.haml
deleted file mode 100644
index 9f350b772bd..00000000000
--- a/app/views/dashboard/milestones/_issues.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list issues-sortable-list" }
- - if issues
- - issues.each do |issue|
- = render 'issue', issue: issue
diff --git a/app/views/dashboard/milestones/_merge_request.html.haml b/app/views/dashboard/milestones/_merge_request.html.haml
deleted file mode 100644
index 77c46de030b..00000000000
--- a/app/views/dashboard/milestones/_merge_request.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid }
- %span.milestone-row
- - project = merge_request.project
- %strong #{project.name_with_namespace} &middot;
- = link_to [project.namespace.becomes(Namespace), project, merge_request] do
- %span.cgray ##{merge_request.iid}
- = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title
- .pull-right.assignee-icon
- - if merge_request.assignee
- = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16"
diff --git a/app/views/dashboard/milestones/_merge_requests.html.haml b/app/views/dashboard/milestones/_merge_requests.html.haml
deleted file mode 100644
index 50057e2c636..00000000000
--- a/app/views/dashboard/milestones/_merge_requests.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list merge_requests-sortable-list" }
- - if merge_requests
- - merge_requests.each do |merge_request|
- = render 'merge_request', merge_request: merge_request
diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml
index 7c882a32702..6173ca6ab9b 100644
--- a/app/views/dashboard/milestones/_milestone.html.haml
+++ b/app/views/dashboard/milestones/_milestone.html.haml
@@ -1,25 +1,6 @@
-%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
- .row
- .col-sm-6
- %strong
- = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- .col-sm-6
- .pull-right.light #{milestone.percent_complete}% complete
- .row
- .col-sm-6
- = link_to issues_dashboard_path(milestone_title: milestone.title) do
- = pluralize milestone.issue_count, 'Issue'
- &middot;
- = link_to merge_requests_dashboard_path(milestone_title: milestone.title) do
- = pluralize milestone.merge_requests_count, 'Merge Request'
- .col-sm-6
- = milestone_progress_bar(milestone)
- .row
- .col-sm-6
- .expiration
- = render 'shared/milestone_expired', milestone: milestone
- .projects
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label.label-gray
- = milestone.project.name_with_namespace
+= render 'shared/milestones/milestone',
+ milestone_path: dashboard_milestone_path(milestone.safe_title, title: milestone.title),
+ issues_path: issues_dashboard_path(milestone_title: milestone.title),
+ merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title),
+ milestone: milestone,
+ dashboard: true
diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml
index 3810267577c..60c84a26420 100644
--- a/app/views/dashboard/milestones/show.html.haml
+++ b/app/views/dashboard/milestones/show.html.haml
@@ -1,105 +1,5 @@
-- page_title @milestone.title, "Milestones"
- header_title "Milestones", dashboard_milestones_path
-.detail-page-header
- .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" }
- - if @milestone.closed?
- Closed
- - else
- Open
- %span.identifier
- Milestone #{@milestone.title}
-
-.detail-page-description.gray-content-block.second-block
- %h2.title
- = markdown escape_once(@milestone.title), pipeline: :single_line
-
-- if @milestone.complete? && @milestone.active?
- .alert.alert-success.prepend-top-default
- %span All issues for this milestone are closed. Navigate to the project to close the milestone.
-
-.table-holder
- %table.table
- %thead
- %tr
- %th Project
- %th Open issues
- %th State
- %th Due date
- - @milestone.milestones.each do |milestone|
- %tr
- %td
- = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
- %td
- = milestone.issues.opened.count
- %td
- - if milestone.closed?
- Closed
- - else
- Open
- %td
- = milestone.expires_at
-
-.context
- %p.lead
- Progress:
- #{@milestone.closed_items_count} closed
- &ndash;
- #{@milestone.open_items_count} open
- = milestone_progress_bar(@milestone)
-
-%ul.nav-links.no-top.no-bottom
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab' do
- Issues
- %span.badge= @milestone.issue_count
- %li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
- Merge Requests
- %span.badge= @milestone.merge_requests_count
- %li
- = link_to '#tab-participants', 'data-toggle' => 'tab' do
- Participants
- %span.badge= @milestone.participants.count
-
-.tab-content
- .tab-pane.active#tab-issues
- .gray-content-block.middle-block
- .pull-right
- = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All issues in this milestone
-
- .row.prepend-top-default
- .col-md-6
- = render 'issues', title: "Open", issues: @milestone.opened_issues
- .col-md-6
- = render 'issues', title: "Closed", issues: @milestone.closed_issues
-
- .tab-pane#tab-merge-requests
- .gray-content-block.middle-block
- .pull-right
- = link_to 'Browse Merge Requests', merge_requests_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All merge requests in this milestone
-
- .row.prepend-top-default
- .col-md-6
- = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
- .col-md-6
- = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
-
- .tab-pane#tab-participants
- .gray-content-block.middle-block
- .oneline
- All participants to this milestone
- %ul.bordered-list
- - @milestone.participants.each do |user|
- %li
- = link_to user, title: user.name, class: "darken" do
- = image_tag avatar_icon(user, 32), class: "avatar s32"
- %strong= truncate(user.name, lenght: 40)
- %br
- %small.cgray= user.username
+= render 'shared/milestones/top', milestone: @milestone
+= render 'shared/milestones/summary', milestone: @milestone
+= render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true
diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml
index 933a3edd0f0..0ebd7c01bab 100644
--- a/app/views/dashboard/projects/_projects.html.haml
+++ b/app/views/dashboard/projects/_projects.html.haml
@@ -1,6 +1 @@
-.projects-list-holder
-
- = render 'shared/projects/list', projects: @projects, ci: true
-
- :javascript
- Dashboard.init()
+= render 'shared/projects/list', projects: @projects, ci: true
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index c3efa7727b1..d54c7cad7be 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,4 +1,4 @@
-- publicish_project_count = Project.publicish(current_user).count
+- publicish_project_count = ProjectsFinder.new.execute(current_user).count
%h3.page-title Welcome to GitLab!
%p.light Self hosted Git management application.
%hr
@@ -18,7 +18,7 @@
- if current_user.can_create_project?
.link_holder
= link_to new_project_path, class: "btn btn-new" do
- %i.fa.fa-plus
+ = icon('plus')
New Project
- if current_user.can_create_group?
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 53abf274bdb..4565e752c1f 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -10,7 +10,7 @@
- if @last_push
= render "events/event_last_push", event: @last_push
-- if @projects.any?
+- if @projects.any? || params[:filter_projects]
= render 'projects'
- else
= render "zero_authorized_projects"
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 6975f6ed0db..e3a4d64df01 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,11 +1,14 @@
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) }
- .todo-item{class: 'todo-block'}
+ .todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
- .todo-title
- %span.author_name
- = link_to_author todo
- %span.todo_label
+ .todo-title.title
+ %span.author-name
+ - if todo.author
+ = link_to_author(todo)
+ - else
+ (removed)
+ %span.todo-label
= todo_action_name(todo)
= todo_target_link(todo)
@@ -13,7 +16,9 @@
- if todo.pending?
.todo-actions.pull-right
- = link_to 'Done', [:dashboard, todo], method: :delete, class: 'btn'
+ = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
+ Done
+ = icon('spinner spin')
.todo-body
.todo-note
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 946d7df3933..f9ec3a89158 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -3,13 +3,15 @@
.top-area
%ul.nav-links
- %li{class: ('active' if params[:state].blank? || params[:state] == 'pending')}
+ - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
+ %li{class: "todos-pending #{todo_pending_active}"}
= link_to todos_filter_path(state: 'pending') do
%span
To do
%span{class: 'badge'}
= todos_pending_count
- %li{class: ('active' if params[:state] == 'done')}
+ - todo_done_active = ('active' if params[:state] == 'done')
+ %li{class: "todos-done #{todo_done_active}"}
= link_to todos_filter_path(state: 'done') do
%span
Done
@@ -18,7 +20,9 @@
.nav-controls
- if @todos.any?(&:pending?)
- = link_to 'Mark all as done', destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn', method: :delete
+ = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
+ Mark all as done
+ = icon('spinner spin')
.todos-filters
.gray-content-block.second-block
@@ -42,12 +46,12 @@
.prepend-top-default
- if @todos.any?
- @todos.group_by(&:project).each do |group|
- .panel.panel-default.panel-small
+ .panel.panel-default.panel-small.js-todos-list
- project = group[0]
.panel-heading
= link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
- %ul.well-list.todos-list
+ %ul.content-list.todos-list
= render group[1]
= paginate @todos, theme: "gitlab"
- else
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index 4974bb7f7fb..8e81671b7e7 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -6,4 +6,4 @@
%label{for: "remember_me"}
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
- = button_tag "Sign in", class: "btn-save btn" \ No newline at end of file
+ = button_tag "Sign in", class: "btn-save btn"
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index dbc8eda6196..d65fa60025c 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,10 +1,10 @@
- page_title "Sign in"
%div
- - if signin_enabled? || ldap_enabled?
+ - if signin_enabled? || ldap_enabled? || crowd_enabled?
= render 'devise/shared/signin_box'
-# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box
- - if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?
+ - if omniauth_enabled? && devise_mapping.omniauthable?
.clearfix.prepend-top-20
= render 'devise/shared/omniauth_box'
@@ -14,6 +14,6 @@
= render 'devise/shared/signup_box'
-# Show a message if none of the mechanisms above are enabled
- - if !signin_enabled? && !ldap_enabled? && !(Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?)
+ - if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
%div
No authentication methods configured.
diff --git a/app/views/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml
index 6a5c917049d..001a711b1dd 100644
--- a/app/views/doorkeeper/applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/applications/_delete_form.html.haml
@@ -1,4 +1,10 @@
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag oauth_application_path(application) do
%input{:name => "_method", :type => "hidden", :value => "delete"}/
- = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css \ No newline at end of file
+ - if defined? small
+ = button_tag type: "submit", class: "btn btn-transparent", data: { confirm: "Are you sure?" } do
+ %span.sr-only
+ Destroy
+ = icon('trash')
+ - else
+ = submit_tag 'Destroy', data: { confirm: "Are you sure?" }, class: submit_btn_css
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index 98a61ab211b..906b0676150 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for application, url: doorkeeper_submit_path(application), html: {class: 'form-horizontal', role: 'form'} do |f|
+= form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f|
- if application.errors.any?
.alert.alert-danger
%ul
@@ -6,25 +6,20 @@
%li= msg
.form-group
- = f.label :name, class: 'control-label'
-
- .col-sm-10
- = f.text_field :name, class: 'form-control', required: true
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: 'form-control', required: true
.form-group
- = f.label :redirect_uri, class: 'control-label'
-
- .col-sm-10
- = f.text_area :redirect_uri, class: 'form-control', required: true
+ = f.label :redirect_uri, class: 'label-light'
+ = f.text_area :redirect_uri, class: 'form-control', required: true
+ %span.help-block
+ Use one line per URI
+ - if Doorkeeper.configuration.native_redirect_uri
%span.help-block
- Use one line per URI
- - if Doorkeeper.configuration.native_redirect_uri
- %span.help-block
- Use
- %code= Doorkeeper.configuration.native_redirect_uri
- for local tests
+ Use
+ %code= Doorkeeper.configuration.native_redirect_uri
+ for local tests
- .form-actions
- = f.submit 'Submit', class: "btn btn-create"
- = link_to "Cancel", applications_profile_path, class: "btn btn-cancel"
+ .prepend-top-default
+ = f.submit 'Save application', class: "btn btn-create"
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index ba4c5b86efb..55f4a6f287d 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -1,19 +1,83 @@
- page_title "Applications"
-%h3.page-title Your applications
-%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
+- header_title page_title, applications_profile_path
-.table-holder
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Callback URL
- %th
- %th
- %tbody
- - @applications.each do |application|
- %tr{:id => "application_#{application.id}"}
- %td= link_to application.name, oauth_application_path(application)
- %td= application.redirect_uri
- %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link'
- %td= render 'delete_form', application: application
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ - if user_oauth_applications?
+ Manage applications that can use GitLab as an OAuth provider,
+ and applications that you've authorized to use your account.
+ - else
+ Manage applications that you've authorized to use your account.
+ .col-lg-9
+ - if user_oauth_applications?
+ %h5.prepend-top-0
+ Add new application
+ = render 'form', application: @application
+ %hr
+ - if user_oauth_applications?
+ .oauth-applications
+ %h5
+ Your applications (#{@applications.size})
+ - if @applications.any?
+ .table-responsive
+ %table.table
+ %thead
+ %tr
+ %th Name
+ %th Callback URL
+ %th Clients
+ %th.last-heading
+ %tbody
+ - @applications.each do |application|
+ %tr{id: "application_#{application.id}"}
+ %td= link_to application.name, oauth_application_path(application)
+ %td
+ - application.redirect_uri.split.each do |uri|
+ %div= uri
+ %td= application.access_tokens.count
+ %td
+ = link_to edit_oauth_application_path(application), class: "btn btn-transparent append-right-5" do
+ %span.sr-only
+ Edit
+ = icon('pencil')
+ = render 'delete_form', application: application, small: true
+ - else
+ .profile-settings-message.text-center
+ You don't have any applications
+ .oauth-authorized-applications.prepend-top-20.append-bottom-default
+ - if user_oauth_applications?
+ %h5
+ Authorized applications (#{@authorized_tokens.size})
+
+ - if @authorized_tokens.any?
+ .table-responsive
+ %table.table.table-striped
+ %thead
+ %tr
+ %th Name
+ %th Authorized At
+ %th Scope
+ %th
+ %tbody
+ - @authorized_apps.each do |app|
+ - token = app.authorized_tokens.order('created_at desc').first
+ %tr{id: "application_#{app.id}"}
+ %td= app.name
+ %td= token.created_at
+ %td= token.scopes
+ %td= render 'delete_form', application: app
+ - @authorized_anonymous_tokens.each do |token|
+ %tr
+ %td
+ Anonymous
+ %div.help-block
+ %em Authorization was granted by entering your username and password in the application.
+ %td= token.created_at
+ %td= token.scopes
+ %td= render 'doorkeeper/authorized_applications/delete_form', token: token
+ - else
+ .profile-settings-message.text-center
+ You don't have any authorized applications
diff --git a/app/views/doorkeeper/applications/new.html.haml b/app/views/doorkeeper/applications/new.html.haml
index fd32a468b45..d3692d1f759 100644
--- a/app/views/doorkeeper/applications/new.html.haml
+++ b/app/views/doorkeeper/applications/new.html.haml
@@ -4,4 +4,4 @@
%hr
-= render 'form', application: @application \ No newline at end of file
+= render 'form', application: @application
diff --git a/app/views/doorkeeper/authorizations/error.html.haml b/app/views/doorkeeper/authorizations/error.html.haml
index 7561ec85ed9..a4c607cea60 100644
--- a/app/views/doorkeeper/authorizations/error.html.haml
+++ b/app/views/doorkeeper/authorizations/error.html.haml
@@ -1,3 +1,3 @@
%h3.page-title An error has occurred
%main{:role => "main"}
- %pre= @pre_auth.error_response.body[:error_description] \ No newline at end of file
+ %pre= @pre_auth.error_response.body[:error_description]
diff --git a/app/views/doorkeeper/authorizations/show.html.haml b/app/views/doorkeeper/authorizations/show.html.haml
index 9a402007194..01f9e46f142 100644
--- a/app/views/doorkeeper/authorizations/show.html.haml
+++ b/app/views/doorkeeper/authorizations/show.html.haml
@@ -1,3 +1,3 @@
%h3.page-title Authorization code:
%main{:role => "main"}
- %code#authorization_code= params[:code] \ No newline at end of file
+ %code#authorization_code= params[:code]
diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml
new file mode 100644
index 00000000000..3443a8e2307
--- /dev/null
+++ b/app/views/emojis/index.html.haml
@@ -0,0 +1,11 @@
+.emoji-menu
+ .emoji-menu-content
+ = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
+ - AwardEmoji.emoji_by_category.each do |category, emojis|
+ %h5.emoji-menu-title
+ = AwardEmoji::CATEGORIES[category]
+ %ul.clearfix.emoji-menu-list
+ - emojis.each do |emoji|
+ %li.pull-left.text-center.emoji-menu-list-item
+ %button.emoji-menu-btn.text-center.js-emoji-btn{type: "button"}
+ = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml
index 4ba8b84fd92..dce4081288c 100644
--- a/app/views/events/_commit.html.haml
+++ b/app/views/events/_commit.html.haml
@@ -1,5 +1,5 @@
%li.commit
.commit-row-title
- = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: ''
+ = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '', title: truncate_sha(commit[:id])
&middot;
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 36fb2d51629..42c2764e7e2 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,4 +1,4 @@
-- if event.proper?
+- if event.visible_to_user?(current_user)
.event-item{class: "#{event.body? ? "event-block" : "event-inline" }"}
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml
index abea86b026a..5753158c24d 100644
--- a/app/views/events/_event_last_push.html.haml
+++ b/app/views/events/_event_last_push.html.haml
@@ -3,7 +3,7 @@
.event-last-push
.event-last-push-text
%span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
+ = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.project.name) do
%strong= event.ref_name
%span at
%strong= link_to_project event.project
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index 4ecf1c33d2a..c994e3b997d 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -4,7 +4,7 @@
= event_action_name(event)
- if event.target
- %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target]
+ %strong= link_to event.target.reference_link_text, [event.project.namespace.becomes(Namespace), event.project, event.target]
= event_preposition(event)
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 8bed5cdb9cc..235bd46107e 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -5,7 +5,7 @@
%strong= event.ref_name
- else
%strong
- = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name)
+ = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.target_title)
at
= link_to_project event.project
diff --git a/app/views/explore/projects/_dropdown.html.haml b/app/views/explore/projects/_dropdown.html.haml
deleted file mode 100644
index 87c556adc7d..00000000000
--- a/app/views/explore/projects/_dropdown.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-.dropdown.inline
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_updated
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to explore_projects_filter_path(sort: sort_value_name) do
- = sort_title_name
- = link_to explore_projects_filter_path(sort: sort_value_recently_created) do
- = sort_title_recently_created
- = link_to explore_projects_filter_path(sort: sort_value_oldest_created) do
- = sort_title_oldest_created
- = link_to explore_projects_filter_path(sort: sort_value_recently_updated) do
- = sort_title_recently_updated
- = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do
- = sort_title_oldest_updated
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index c248dbb695f..cd485da5104 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,41 +1,40 @@
-.pull-right.hidden-sm.hidden-xs
- - if current_user
- .dropdown.inline.append-right-10
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %i.fa.fa-globe
- %span.light Visibility:
- - if params[:visibility_level].present?
- = visibility_level_label(params[:visibility_level].to_i)
- - else
+- if current_user
+ .dropdown
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ = icon('globe')
+ %span.light Visibility:
+ - if params[:visibility_level].present?
+ = visibility_level_label(params[:visibility_level].to_i)
+ - else
+ Any
+ %b.caret
+ %ul.dropdown-menu
+ %li
+ = link_to filter_projects_path(visibility_level: nil) do
Any
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to explore_projects_filter_path(visibility_level: nil) do
- Any
- - Gitlab::VisibilityLevel.values.each do |level|
- %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' }
- = link_to explore_projects_filter_path(visibility_level: level) do
- = visibility_level_icon(level)
- = visibility_level_label(level)
+ - Gitlab::VisibilityLevel.values.each do |level|
+ %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' }
+ = link_to filter_projects_path(visibility_level: level) do
+ = visibility_level_icon(level)
+ = visibility_level_label(level)
- - if @tags.present?
- .dropdown.inline.append-right-10
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %i.fa.fa-tags
- %span.light Tags:
- - if params[:tag].present?
- = params[:tag]
- - else
+- if @tags.present?
+ .dropdown
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ = icon('tags')
+ %span.light Tags:
+ - if params[:tag].present?
+ = params[:tag]
+ - else
+ Any
+ %b.caret
+ %ul.dropdown-menu
+ %li
+ = link_to filter_projects_path(tag: nil) do
Any
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to explore_projects_filter_path(tag: nil) do
- Any
- - @tags.each do |tag|
- %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
- = link_to explore_projects_filter_path(tag: tag.name) do
- %i.fa.fa-tag
- = tag.name
+ - @tags.each do |tag|
+ %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
+ = link_to filter_projects_path(tag: tag.name) do
+ = icon('tag')
+ = tag.name
diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml
index 999a933390b..708fbc27f55 100644
--- a/app/views/explore/projects/_projects.html.haml
+++ b/app/views/explore/projects/_projects.html.haml
@@ -1,6 +1 @@
-- if projects.any?
- .projects-list-holder
- = render 'shared/projects/list', projects: projects
-- else
- .nothing-here-block
- No such projects
+= render 'shared/projects/list', projects: projects
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index dca75498573..42b50481b9d 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -9,7 +9,7 @@
.top-area
= render 'explore/projects/nav'
-.gray-content-block.second-block.clearfix
- = render 'filter'
+ .nav-controls
+ = render 'filter'
= render 'projects', projects: @projects
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
new file mode 100644
index 00000000000..dc76599b776
--- /dev/null
+++ b/app/views/groups/_activities.html.haml
@@ -0,0 +1,12 @@
+.hidden-xs
+ = render "events/event_last_push", event: @last_push
+
+.nav-block
+ - if current_user
+ .controls
+ = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
+ %i.fa.fa-rss
+ = render 'shared/event_filter'
+
+.content_list
+= spinner
diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml
index 9c16ab7e30f..cca7dc27b1c 100644
--- a/app/views/groups/_projects.html.haml
+++ b/app/views/groups/_projects.html.haml
@@ -1,11 +1 @@
-.top-area
- .nav-controls
- = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'input-short project-filter-form-field form-control projects-list-filter', spellcheck: false, id: 'project-filter-form-field'
- - if current_user && current_user.can_create_project?
- = link_to new_project_path, class: 'btn btn-new' do
- = icon('plus')
- New Project
-
-.projects-list-holder
- = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false, skip_namespace: true
+= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: true
diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml
new file mode 100644
index 00000000000..b1694c919d0
--- /dev/null
+++ b/app/views/groups/_shared_projects.html.haml
@@ -0,0 +1 @@
+= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: false
diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml
new file mode 100644
index 00000000000..f73e1d9e865
--- /dev/null
+++ b/app/views/groups/activity.html.haml
@@ -0,0 +1,9 @@
+= content_for :meta_tags do
+ - if current_user
+ = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
+
+- page_title "Activity"
+- header_title group_title(@group, "Activity", activity_group_path(@group))
+
+%section.activities
+ = render 'activities'
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 3430f56a9c9..ea5a0358392 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -23,6 +23,18 @@
%hr
= link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
+ = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
+
+ .form-group
+ %hr
+ = f.label :share_with_group_lock, class: 'control-label' do
+ Share with group lock
+ .col-sm-10
+ .checkbox
+ = f.check_box :share_with_group_lock
+ %span.descr Prevent sharing a project with another group within this group
+
+
.form-actions
= f.submit 'Save group', class: "btn btn-save"
diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml
index a79a0fcdc8e..60234be8f83 100644
--- a/app/views/groups/group_members/_group_member.html.haml
+++ b/app/views/groups/group_members/_group_member.html.haml
@@ -1,5 +1,6 @@
- user = member.user
- return unless user || member.invite?
+- show_roles = local_assigns.fetch(:show_roles, true)
%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
%span{class: ("list-item-name" if show_controls)}
@@ -28,7 +29,7 @@
= link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
Resend invite
- - if should_user_see_group_roles?(current_user, @group)
+ - if show_roles && should_user_see_group_roles?(current_user, @group)
%span.pull-right
%strong.member-access-level= member.human_access
- if show_controls
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index b0805593fdc..aea35c50862 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -10,6 +10,8 @@
- if current_user
= link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
= icon('rss')
+ %span.icon-label
+ Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml
deleted file mode 100644
index 9b85d83d6d8..00000000000
--- a/app/views/groups/milestones/_issue.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
- %span.milestone-row
- - project = issue.project
- %strong #{project.name} &middot;
- = link_to [project.namespace.becomes(Namespace), project, issue] do
- %span.cgray ##{issue.iid}
- = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title
- .pull-right.assignee-icon
- - if issue.assignee
- = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16", alt: ''
diff --git a/app/views/groups/milestones/_issues.html.haml b/app/views/groups/milestones/_issues.html.haml
deleted file mode 100644
index 9f350b772bd..00000000000
--- a/app/views/groups/milestones/_issues.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list issues-sortable-list" }
- - if issues
- - issues.each do |issue|
- = render 'issue', issue: issue
diff --git a/app/views/groups/milestones/_merge_request.html.haml b/app/views/groups/milestones/_merge_request.html.haml
deleted file mode 100644
index e3aa4aad198..00000000000
--- a/app/views/groups/milestones/_merge_request.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid }
- %span.milestone-row
- - project = merge_request.project
- %strong #{project.name} &middot;
- = link_to [project.namespace.becomes(Namespace), project, merge_request] do
- %span.cgray ##{merge_request.iid}
- = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title
- .pull-right.assignee-icon
- - if merge_request.assignee
- = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: ''
diff --git a/app/views/groups/milestones/_merge_requests.html.haml b/app/views/groups/milestones/_merge_requests.html.haml
deleted file mode 100644
index 50057e2c636..00000000000
--- a/app/views/groups/milestones/_merge_requests.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list merge_requests-sortable-list" }
- - if merge_requests
- - merge_requests.each do |merge_request|
- = render 'merge_request', merge_request: merge_request
diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml
index a20bf75bc39..4c4e0a26728 100644
--- a/app/views/groups/milestones/_milestone.html.haml
+++ b/app/views/groups/milestones/_milestone.html.haml
@@ -1,29 +1,5 @@
-%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
- .row
- .col-sm-6
- %strong
- = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title)
- .col-sm-6
- .pull-right.light #{milestone.percent_complete}% complete
- .row
- .col-sm-6
- = link_to issues_group_path(@group, milestone_title: milestone.title) do
- = pluralize milestone.issue_count, 'Issue'
- &middot;
- = link_to merge_requests_group_path(@group, milestone_title: milestone.title) do
- = pluralize milestone.merge_requests_count, 'Merge Request'
- .col-sm-6
- = milestone_progress_bar(milestone)
- .row
- .col-sm-6
- %div
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label.label-gray
- = milestone.project.name
- .col-sm-6
- - if can?(current_user, :admin_milestones, @group)
- - if milestone.closed?
- = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
- - else
- = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close"
+= render 'shared/milestones/milestone',
+ milestone_path: group_milestone_path(@group, milestone.safe_title, title: milestone.title),
+ issues_path: issues_group_path(@group, milestone_title: milestone.title),
+ merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title),
+ milestone: milestone
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index 3894a0ece74..a8e1ed77da9 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -8,18 +8,18 @@
This will create milestone in every selected project
%hr
-= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-requires-input' } do |f|
+= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f|
.row
.col-md-6
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
- = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true
+ = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
- = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
+ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
.clearfix
.error-alert
.form-group
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index 1233da85524..fb6f0da28f8 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,112 +1,4 @@
-- page_title @milestone.title, "Milestones"
= render "header_title"
-
-.detail-page-header
- .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" }
- - if @milestone.closed?
- Closed
- - else
- Open
- %span.identifier
- Milestone #{@milestone.title}
- .pull-right
- - if can?(current_user, :admin_milestones, @group)
- - if @milestone.active?
- = link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close"
- - else
- = link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
-
-.detail-page-description.gray-content-block.second-block
- %h2.title
- = markdown escape_once(@milestone.title), pipeline: :single_line
-
-- if @milestone.complete? && @milestone.active?
- .alert.alert-success.prepend-top-default
- %span All issues for this milestone are closed. You may close the milestone now.
-
-.table-holder
- %table.table
- %thead
- %tr
- %th Project
- %th Open issues
- %th State
- %th Due date
- - @milestone.milestones.each do |milestone|
- %tr
- %td
- = link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
- %td
- = milestone.issues.opened.count
- %td
- - if milestone.closed?
- Closed
- - else
- Open
- %td
- = milestone.expires_at
-
-.context
- %p.lead
- Progress:
- #{@milestone.closed_items_count} closed
- &ndash;
- #{@milestone.open_items_count} open
- = milestone_progress_bar(@milestone)
-
-%ul.nav-links.no-top.no-bottom
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab' do
- Issues
- %span.badge= @milestone.issue_count
- %li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
- Merge Requests
- %span.badge= @milestone.merge_requests_count
- %li
- = link_to '#tab-participants', 'data-toggle' => 'tab' do
- Participants
- %span.badge= @milestone.participants.count
-
-.tab-content
- .tab-pane.active#tab-issues
- .gray-content-block.middle-block
- .pull-right
- = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All issues in this milestone
-
- .row.prepend-top-default
- .col-md-6
- = render 'issues', title: "Open", issues: @milestone.opened_issues
- .col-md-6
- = render 'issues', title: "Closed", issues: @milestone.closed_issues
-
- .tab-pane#tab-merge-requests
- .gray-content-block.middle-block
- .pull-right
- = link_to 'Browse Merge Requests', merge_requests_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped"
-
- .oneline
- All merge requests in this milestone
-
- .row.prepend-top-default
- .col-md-6
- = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
- .col-md-6
- = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
-
- .tab-pane#tab-participants
- .gray-content-block.middle-block
- .oneline
- All participants to this milestone
-
- %ul.bordered-list
- - @milestone.participants.each do |user|
- %li
- = link_to user, title: user.name, class: "darken" do
- = image_tag avatar_icon(user, 32), class: "avatar s32"
- %strong= truncate(user.name, lenght: 40)
- %br
- %small.cgray= user.username
+= render 'shared/milestones/top', milestone: @milestone, group: @group
+= render 'shared/milestones/summary', milestone: @milestone
+= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 4bc31cabea6..30ab8aeba13 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -17,6 +17,8 @@
.col-sm-10
= render 'shared/choose_group_avatar_button', f: f
+ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group
+
.form-group
.col-sm-offset-2.col-sm-10
= render 'shared/group_tips'
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index a0ba11b11a1..3d16ecb097a 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,8 +1,5 @@
- @no_container = true
-- unless can?(current_user, :read_group, @group)
- - @disable_search_panel = true
-
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
@@ -18,7 +15,10 @@
= link_to group_icon(@group), target: '_blank' do
= image_tag group_icon(@group), class: "avatar group-avatar s90"
.cover-title
- = @group.name
+ %h1
+ = @group.name
+ %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
+ = visibility_level_icon(@group.visibility_level, fw: false)
.cover-desc.username
@#{@group.path}
@@ -27,32 +27,29 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
-
- %ul.nav-links
- %li.active
- = link_to "#activity", 'data-toggle' => 'tab' do
- Activity
- - if @projects.present?
- %li
+%div{ class: container_class }
+ .top-area
+ %ul.nav-links
+ %li.active
= link_to "#projects", 'data-toggle' => 'tab' do
- Projects
-
-- if can?(current_user, :read_group, @group)
- %div{ class: container_class }
- .tab-content
- .tab-pane.active#activity
- .activity-filter-block
- - if current_user
- = render "events/event_last_push", event: @last_push
-
- = render 'shared/event_filter'
-
- .content_list{data: {href: events_group_path}}
- = spinner
-
- .tab-pane#projects
- = render "projects", projects: @projects
-
-- else
- %p.nav-links.no-top
- No projects to show
+ All Projects
+ - if @shared_projects.present?
+ %li
+ = link_to "#shared", 'data-toggle' => 'tab' do
+ Shared Projects
+ .nav-controls
+ = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
+ = render 'shared/projects/dropdown'
+ - if can? current_user, :create_projects, @group
+ = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
+ = icon('plus')
+ New Project
+
+ .tab-content
+ .tab-pane.active#projects
+ = render "projects", projects: @projects
+
+ - if @shared_projects.present?
+ .tab-pane#shared
+ = render "shared_projects", projects: @shared_projects
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 8e982718d23..da3c3711cdd 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -22,6 +22,14 @@
%td.shortcut
.key ?
%td Show this dialog
+ %tr
+ %td.shortcut
+ - if browser.mac?
+ .key &#8984; shift p
+ - else
+ .key ctrl shift p
+
+ %td Toggle Markdown preview
%tbody
%tr
%th
@@ -229,6 +237,10 @@
%td.shortcut
.key r
%td Reply (quoting selected text)
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit issue
%tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
%tr
%th
@@ -245,3 +257,7 @@
%td.shortcut
.key r
%td Reply (quoting selected text)
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit merge request
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 746386cab58..d084559abc3 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -19,6 +19,8 @@
%li
= link_to 'Buttons', '#buttons'
%li
+ = link_to 'Dropdowns', '#dropdowns'
+ %li
= link_to 'Panels', '#panels'
%li
= link_to 'Alerts', '#alerts'
@@ -31,64 +33,91 @@
%h2#blocks Blocks
- %h4
+ .lead
+ Content block separated with botton border
+ %code .content-block
+
+ .example
+ .content-block
+ %h4 Normal block inside content
+ = lorem
+
+ .content-block
+ %h4 Second block
+ = lorem
+
+ .lead
+ Gray content block with side padding using
%code .gray-content-block
- .gray-content-block.middle-block
- %h4 Normal block inside content
- = lorem
+ .example
+ .gray-content-block
+ %h4 Normal block inside content
+ = lorem
- .gray-content-block.second-block
- %h4 Second block
- = lorem
+ .gray-content-block.second-block
+ %h4 Second block
+ = lorem
- %h4
+ .lead
+ Cover block for profile page with avatar, name and description
%code .cover-block
- %br
- .cover-block
- .avatar-holder
- = image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
- .cover-title
- John Smith
-
- .cover-desc
- = lorem
+ .example
+ .cover-block
+ .avatar-holder
+ = image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
+ .cover-title
+ John Smith
- .cover-controls
- = link_to '#', class: 'btn btn-gray' do
- = icon('pencil')
- &nbsp;
- = link_to '#', class: 'btn btn-gray' do
- = icon('rss')
+ .cover-desc
+ = lorem
+
+ .cover-controls
+ = link_to '#', class: 'btn btn-gray' do
+ = icon('pencil')
+ &nbsp;
+ = link_to '#', class: 'btn btn-gray' do
+ = icon('rss')
%h2#lists Lists
- %h4
+ .lead
+ Simple list using
%code .content-list
- %ul.content-list
- %li
- One item
- %li
- One item
- %li
- One item
- %h4
- %code .well-list
- %ul.well-list
- %li
- One item
- %li
- One item
- %li
- One item
+ .example
+ %ul.content-list
+ %li
+ One item
+ %li
+ One item
+ %li
+ One item
- %h4
- %code .panel .well-list
+ .lead
+ List with avatar, title and description using
+ %code .content-list
- .panel.panel-default
- .panel-heading Your list
+ .example
+ %ul.content-list
+ %li
+ = image_tag 'no_avatar.png', class: 'avatar s40'
+ .title Title
+ .description Description
+ %li
+ = image_tag 'no_avatar.png', class: 'avatar s40'
+ .title Title
+ .description Description
+ %li
+ = image_tag 'no_avatar.png', class: 'avatar s40'
+ .title Title
+ .description Description
+
+ .lead
+ List with hover effect
+ %code .well-list
+ .example
%ul.well-list
%li
One item
@@ -97,17 +126,18 @@
%li
One item
- %h4
- %code .bordered-list
- %ul.bordered-list
- %li
- One item
- %li
- One item
- %li
- One item
-
-
+ .lead
+ List inside panel
+ .example
+ .panel.panel-default
+ .panel-heading Your list
+ %ul.well-list
+ %li
+ One item
+ %li
+ One item
+ %li
+ One item
%h2#tables Tables
@@ -138,9 +168,9 @@
%h2#navs Navigation
- %h4
+ .lead
+ Holder for top page navigation. Includes navigation, search field, sorting and button
%code .top-area
- %p Holder for top page navigation. Includes navigation, search field, sorting and button
.example
.top-area
@@ -152,18 +182,18 @@
.nav-controls
= text_field_tag 'sample', nil, class: 'form-control'
.dropdown
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span Sort by name
- %b.caret
+ = icon('chevron-down')
%ul.dropdown-menu
%li
%a Sort by date
= link_to 'New issue', '#', class: 'btn btn-new'
- %h4
+ .lead
+ Only nav links without button and search
%code .nav-links
- %p Only nav links without button and search
.example
%ul.nav-links
%li.active
@@ -184,6 +214,227 @@
%button.btn.btn-danger{:type => "button"} Danger
%button.btn.btn-link{:type => "button"} Link
+ %h2#dropdowns Dropdowns
+
+ .example
+ .clearfix
+ .dropdown.inline.pull-left
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ %ul.dropdown-menu
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .dropdown.inline.pull-right
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-selectable
+ %li
+ %a.is-active{href: "#"}
+ Dropdown Option
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.is-active{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li.divider
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .dropdown-footer
+ %strong Tip:
+ If an author is not a member of this project, you can still filter by his name while using the search field.
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown loading
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading
+ .dropdown-title
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.is-active{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li.divider
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .dropdown-footer
+ %strong Tip:
+ If an author is not a member of this project, you can still filter by his name while using the search field.
+ .dropdown-loading
+ = icon('spinner spin')
+
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown user
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user
+ .dropdown-title
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.dropdown-menu-user-link.is-active{href: "#"}
+ = link_to_member_avatar(current_user, size: 30)
+ %strong.dropdown-menu-user-full-name
+ = current_user.name
+ .dropdown-menu-user-username
+ = current_user.to_reference
+
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown page 2
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two
+ .dropdown-page-one
+ .dropdown-title
+ %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
+ = icon('arrow-left')
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.dropdown-menu-user-link.is-active{href: "#"}
+ = link_to_member_avatar(current_user, size: 30)
+ %strong.dropdown-menu-user-full-name
+ = current_user.name
+ .dropdown-menu-user-username
+ = current_user.to_reference
+ .dropdown-page-two
+ .dropdown-title
+ %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
+ = icon('arrow-left')
+ %span Create label
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Name new label"}
+ .dropdown-content
+ %button.btn.btn-primary
+ Create
+
+ .example
+ %div
+ .dropdown.inline
+ %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Projects
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Go to project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ .dropdown-loading
+ = icon('spinner spin')
+ :javascript
+ $('#js-project-dropdown').glDropdown({
+ data: function (term, callback) {
+ Api.projects(term, "last_activity_at", function (data) {
+ callback(data);
+ });
+ },
+ text: function (project) {
+ return project.name_with_namespace || project.name;
+ },
+ selectable: true,
+ fieldName: "author_id",
+ filterable: true,
+ search: {
+ fields: ['name_with_namespace']
+ },
+ id: function (data) {
+ return data.id;
+ },
+ isSelected: function (data) {
+ return data.id === 2;
+ }
+ })
+
+ .example
+ %div
+ = dropdown_tag("Projects", options: { title: "Go to project", filter: true, placeholder: "Filter projects" })
+
%h2#panels Panels
.row
@@ -228,43 +479,47 @@
%h2#forms Forms
- %h4
+ .lead
+ Horizontal form when label rendered inline with input
%code form.horizontal-form
- %form.form-horizontal
- .form-group
- %label.col-sm-2.control-label{:for => "inputEmail3"} Email
- .col-sm-10
- %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/
- .form-group
- %label.col-sm-2.control-label{:for => "inputPassword3"} Password
- .col-sm-10
- %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- %label
- %input{:type => "checkbox"}/
- Remember me
- .form-group
- .col-sm-offset-2.col-sm-10
- %button.btn.btn-default{:type => "submit"} Sign in
-
- %h4
+ .example
+ %form.form-horizontal
+ .form-group
+ %label.col-sm-2.control-label{:for => "inputEmail3"} Email
+ .col-sm-10
+ %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/
+ .form-group
+ %label.col-sm-2.control-label{:for => "inputPassword3"} Password
+ .col-sm-10
+ %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ %label
+ %input{:type => "checkbox"}/
+ Remember me
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ %button.btn.btn-default{:type => "submit"} Sign in
+
+ .lead
+ Form when label rendered above input
%code form
- %form
- .form-group
- %label{:for => "exampleInputEmail1"} Email address
- %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/
- .form-group
- %label{:for => "exampleInputPassword1"} Password
- %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/
- .checkbox
- %label
- %input{:type => "checkbox"}/
- Remember me
- %button.btn.btn-default{:type => "submit"} Sign in
+ .example
+ %form
+ .form-group
+ %label{:for => "exampleInputEmail1"} Email address
+ %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/
+ .form-group
+ %label{:for => "exampleInputPassword1"} Password
+ %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/
+ .checkbox
+ %label
+ %input{:type => "checkbox"}/
+ Remember me
+ %button.btn.btn-default{:type => "submit"} Sign in
%h2#file File
%h4
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 38ca4f91c4d..79cdbac1f37 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -44,6 +44,7 @@
= favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76'
= favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120'
= favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152'
+ %link{rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)'}
-# Windows 8 pinned site tile
%meta{name: 'msapplication-TileImage', content: image_path('msapplication-tile.png')}
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index e53d5b07801..c799e9c588d 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -4,7 +4,7 @@
.header-logo
%a#logo
= brand_header_logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do
+ = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do
.gitlab-text-container
%h3 GitLab
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 20042e21bf2..54af2c3063c 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -1,6 +1,6 @@
.search
= form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f|
- = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false
+ = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1"
= hidden_field_tag :group_id, @group.try(:id)
- if @project && @project.persisted?
= hidden_field_tag :project_id, @project.id
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 678ed3c2c1f..babfb032236 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -5,11 +5,7 @@
-# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
= yield :scripts_body_top
- - if current_user
- = render "layouts/header/default", title: header_title
- - else
- = render "layouts/header/public", title: header_title
-
+ = render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar
= yield :scripts_body
diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml
index 3cfd36720f0..a13241bebee 100644
--- a/app/views/layouts/ci/_page.html.haml
+++ b/app/views/layouts/ci/_page.html.haml
@@ -4,7 +4,7 @@
.header-logo
%a#logo
= brand_header_logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do
+ = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do
.gitlab-text-container
%h3 GitLab
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 4781ff23507..0f3b8119379 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -6,41 +6,48 @@
= icon('bars')
.navbar-collapse.collapse
- %ul.nav.navbar-nav.pull-right
- - unless @disable_search_panel
- %li.hidden-sm.hidden-xs
- = render 'layouts/search'
+ %ul.nav.navbar-nav
+ %li.hidden-sm.hidden-xs
+ = render 'layouts/search'
%li.visible-sm.visible-xs
= link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
- - if session[:impersonator_id]
- %li.impersonation
- = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
- = icon('user-secret fw')
- - if current_user.is_admin?
+ - if current_user
+ - if session[:impersonator_id]
+ %li.impersonation
+ = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = icon('user-secret fw')
+ - if current_user.is_admin?
+ %li
+ = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('wrench fw')
%li
- = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('wrench fw')
- %li
- = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- %span.badge.todos-pending-count
- = todos_pending_count
- - if current_user.can_create_project?
+ = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ %span.badge.todos-pending-count
+ = todos_pending_count
+ - if current_user.can_create_project?
+ %li
+ = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('plus fw')
+ - if Gitlab::Sherlock.enabled?
+ %li
+ = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
+ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('tachometer fw')
%li
- = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('plus fw')
- - if Gitlab::Sherlock.enabled?
+ = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('sign-out')
+ - else
%li
- = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
- data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('tachometer fw')
- %li
- = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('sign-out')
+ %div
+ = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
+
%h1.title= title
= render 'shared/outdated_browser'
+
- if @project && !@project.empty_repo?
- :javascript
- var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}";
+ - if ref = @ref || @project.repository.root_ref
+ :javascript
+ var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, ref)}";
diff --git a/app/views/layouts/header/_public.html.haml b/app/views/layouts/header/_public.html.haml
deleted file mode 100644
index a6a26518a0e..00000000000
--- a/app/views/layouts/header/_public.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
- %div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
- .header-content
- - unless current_controller?('sessions')
- .pull-right
- = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
-
- %h1.title= title
-
-= render 'shared/outdated_browser'
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index ac1d5429382..280a1b93729 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -56,6 +56,11 @@
= icon('cog fw')
%span
Background Jobs
+ = nav_link(controller: :appearances) do
+ = link_to admin_appearances_path, title: 'Appearances' do
+ = icon('image')
+ %span
+ Appearance
= nav_link(controller: :applications) do
= link_to admin_applications_path, title: 'Applications' do
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index db0cf393922..4a0069f18f8 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,7 +1,7 @@
%ul.nav.nav-sidebar
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: 'home'}) do
= link_to dashboard_projects_path, title: 'Projects' do
- = icon('home fw')
+ = icon('bookmark fw')
%span
Projects
= nav_link(controller: :todos) do
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index 48039ca2918..f08c5edf99c 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -1,7 +1,7 @@
%ul.nav.nav-sidebar
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
= link_to explore_root_path, title: 'Projects' do
- = icon('home fw')
+ = icon('bookmark fw')
%span
Projects
= nav_link(controller: :groups) do
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index e5e2a59eaed..55940741dc0 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -9,38 +9,41 @@
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do
- = icon('dashboard fw')
+ = icon('group fw')
%span
Group
- - if can?(current_user, :read_group, @group)
- - if current_user
- = nav_link(controller: [:group, :milestones]) do
- = link_to group_milestones_path(@group), title: 'Milestones' do
- = icon('clock-o fw')
- %span
- Milestones
- = nav_link(path: 'groups#issues') do
- = link_to issues_group_path(@group), title: 'Issues' do
- = icon('exclamation-circle fw')
- %span
- Issues
- - if current_user
- %span.count= number_with_delimiter(Issue.opened.of_group(@group).count)
- = nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
- = icon('tasks fw')
- %span
- Merge Requests
- - if current_user
- %span.count= number_with_delimiter(MergeRequest.opened.of_group(@group).count)
- = nav_link(controller: [:group_members]) do
- = link_to group_group_members_path(@group), title: 'Members' do
- = icon('users fw')
+ = nav_link(path: 'groups#activity') do
+ = link_to activity_group_path(@group), title: 'Activity' do
+ = icon('dashboard fw')
+ %span
+ Activity
+ = nav_link(controller: [:group, :milestones]) do
+ = link_to group_milestones_path(@group), title: 'Milestones' do
+ = icon('clock-o fw')
+ %span
+ Milestones
+ = nav_link(path: 'groups#issues') do
+ = link_to issues_group_path(@group), title: 'Issues' do
+ = icon('exclamation-circle fw')
+ %span
+ Issues
+ - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
+ %span.count= number_with_delimiter(issues.count)
+ = nav_link(path: 'groups#merge_requests') do
+ = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
+ = icon('tasks fw')
+ %span
+ Merge Requests
+ - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute
+ %span.count= number_with_delimiter(merge_requests.count)
+ = nav_link(controller: [:group_members]) do
+ = link_to group_group_members_path(@group), title: 'Members' do
+ = icon('users fw')
+ %span
+ Members
+ - if can?(current_user, :admin_group, @group)
+ = nav_link(html_options: { class: "separate-item" }) do
+ = link_to edit_group_path(@group), title: 'Settings' do
+ = icon ('cogs fw')
%span
- Members
- - if can?(current_user, :admin_group, @group)
- = nav_link(html_options: { class: "separate-item" }) do
- = link_to edit_group_path(@group), title: 'Settings' do
- = icon ('cogs fw')
- %span
- Settings
+ Settings
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index f3ded04419b..3b9d31a6fc5 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -17,7 +17,7 @@
= icon('gear fw')
%span
Account
- = nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new', 'applications#create']) do
+ = nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path, title: 'Applications' do
= icon('cloud fw')
%span
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 319974e12c5..86b46e8c75e 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -16,7 +16,7 @@
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
- = icon('home fw')
+ = icon('bookmark fw')
%span
Project
= nav_link(path: 'projects#activity') do
@@ -67,7 +67,7 @@
%span
Issues
- if @project.default_issues_tracker?
- %span.count.issue_counter= number_with_delimiter(@project.issues.opened.count)
+ %span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 970da78a5c9..dc3050f02e5 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -13,16 +13,22 @@
= icon('pencil-square-o fw')
%span
Project Settings
+ - if @project.allowed_to_share_with_group?
+ = nav_link(controller: :group_links) do
+ = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
+ = icon('share-square-o fw')
+ %span
+ Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
= icon('key fw')
%span
Deploy Keys
= nav_link(controller: :hooks) do
- = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do
+ = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
= icon('link fw')
%span
- Web Hooks
+ Webhooks
= nav_link(controller: :services) do
= link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
= icon('cogs fw')
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 325c68c69dc..2997f59d946 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -1,33 +1,9 @@
%html{lang: "en"}
%head
%meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"}
- %title
- GitLab
- :css
- img {
- max-width: 100%;
- height: auto;
- }
- p.details {
- font-style:italic;
- color:#777
- }
- .footer p {
- font-size:small;
- color:#777
- }
- pre.commit-message {
- white-space: pre-wrap;
- }
- .file-stats a {
- text-decoration: none;
- }
- .file-stats .new-file {
- color: #090;
- }
- .file-stats .deleted-file {
- color: #B00;
- }
+ %title
+ GitLab
+ = stylesheet_link_tag 'notify'
%body
%div.content
= yield
@@ -42,12 +18,15 @@
- else
#{link_to "View it on GitLab", @target_url}.
%br
- -# Don't link the host is the line below, one link in the email is easier to quickly click than two.
+ -# Don't link the host in the line below, one link in the email is easier to quickly click than two.
You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
If you'd like to receive fewer emails, you can
- - if @sent_notification && @sent_notification.unsubscribable?
- = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
- from this thread or
- adjust your notification settings.
+ - if @labels_url
+ adjust your #{link_to 'label subscriptions', @labels_url}.
+ - else
+ - if @sent_notification && @sent_notification.unsubscribable?
+ = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
+ from this thread or
+ adjust your notification settings.
= email_action @target_url
diff --git a/app/views/notify/_reassigned_issuable_email.text.erb b/app/views/notify/_reassigned_issuable_email.text.erb
index 855d37429d9..daf20a226dd 100644
--- a/app/views/notify/_reassigned_issuable_email.text.erb
+++ b/app/views/notify/_reassigned_issuable_email.text.erb
@@ -1,6 +1,6 @@
Reassigned <%= issuable.class.model_name.human.titleize %> <%= issuable.iid %>
-<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, {only_path: false}]) %>
+<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %>
diff --git a/app/views/notify/_relabeled_issuable_email.html.haml b/app/views/notify/_relabeled_issuable_email.html.haml
new file mode 100644
index 00000000000..80a0de255be
--- /dev/null
+++ b/app/views/notify/_relabeled_issuable_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ #{'Label'.pluralize(@label_names.size)} added:
+ %em= @label_names.to_sentence
diff --git a/app/views/notify/_relabeled_issuable_email.text.erb b/app/views/notify/_relabeled_issuable_email.text.erb
new file mode 100644
index 00000000000..6a83d79fd61
--- /dev/null
+++ b/app/views/notify/_relabeled_issuable_email.text.erb
@@ -0,0 +1,3 @@
+<%= 'Label'.pluralize(@label_names.size) %> added: <%= @label_names.to_sentence %>
+
+<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
diff --git a/app/views/notify/issue_moved_email.html.haml b/app/views/notify/issue_moved_email.html.haml
new file mode 100644
index 00000000000..40f7d61fe19
--- /dev/null
+++ b/app/views/notify/issue_moved_email.html.haml
@@ -0,0 +1,6 @@
+%p
+ Issue was moved to another project.
+%p
+ New issue:
+ = link_to namespace_project_issue_url(@new_project.namespace, @new_project, @new_issue) do
+ = @new_issue.title
diff --git a/app/views/notify/issue_moved_email.text.erb b/app/views/notify/issue_moved_email.text.erb
new file mode 100644
index 00000000000..b3bd43c2055
--- /dev/null
+++ b/app/views/notify/issue_moved_email.text.erb
@@ -0,0 +1,4 @@
+Issue was moved to another project.
+
+New issue location:
+<%= namespace_project_issue_url(@new_project.namespace, @new_project, @new_issue) %>
diff --git a/app/views/notify/relabeled_issue_email.html.haml b/app/views/notify/relabeled_issue_email.html.haml
new file mode 100644
index 00000000000..b17b16e1814
--- /dev/null
+++ b/app/views/notify/relabeled_issue_email.html.haml
@@ -0,0 +1 @@
+= render 'relabeled_issuable_email', issuable: @issue
diff --git a/app/views/notify/relabeled_issue_email.text.erb b/app/views/notify/relabeled_issue_email.text.erb
new file mode 100644
index 00000000000..eeced97f601
--- /dev/null
+++ b/app/views/notify/relabeled_issue_email.text.erb
@@ -0,0 +1 @@
+<%= render 'relabeled_issuable_email', issuable: @issue %>
diff --git a/app/views/notify/relabeled_merge_request_email.html.haml b/app/views/notify/relabeled_merge_request_email.html.haml
new file mode 100644
index 00000000000..9eaa9afa5b1
--- /dev/null
+++ b/app/views/notify/relabeled_merge_request_email.html.haml
@@ -0,0 +1 @@
+= render 'relabeled_issuable_email', issuable: @merge_request
diff --git a/app/views/notify/relabeled_merge_request_email.text.erb b/app/views/notify/relabeled_merge_request_email.text.erb
new file mode 100644
index 00000000000..87bc80ead32
--- /dev/null
+++ b/app/views/notify/relabeled_merge_request_email.text.erb
@@ -0,0 +1 @@
+<%= render 'relabeled_issuable_email', issuable: @merge_request %>
diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml
index 58af79716a7..879fc170f92 100644
--- a/app/views/profiles/_event_table.html.haml
+++ b/app/views/profiles/_event_table.html.haml
@@ -1,17 +1,15 @@
-.table-holder
- %table.table#audits
- %thead
- %tr
- %th Action
- %th When
+%h5.prepend-top-0
+ History of authentications
+
+%ul.well-list
+ - events.each do |event|
+ %li
+ %span.description
+ = audit_icon(event.details[:with], class: "append-right-5")
+ Signed in with
+ = event.details[:with]
+ authentication
+ %span.pull-right
+ #{time_ago_in_words event.created_at} ago
- %tbody
- - events.each do |event|
- %tr
- %td
- %span
- Signed in with
- %b= event.details[:with]
- authentication
- %td #{time_ago_in_words event.created_at} ago
= paginate events, theme: "gitlab"
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 9fa96084f94..6efd119f260 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -5,114 +5,113 @@
.alert.alert-info
Some options are unavailable for LDAP accounts
-.account-page.prepend-top-default
- .panel.panel-default.update-token
- .panel-heading
- Reset Private token
- .panel-body
- = form_for @user, url: reset_private_token_profile_path, method: :put do |f|
- .data
- %p
- Your private token is used to access application resources without authentication.
- %br
- It can be used for atom feeds or the API.
- %span.cred
- Keep it secret!
-
- %p.cgray
- - if current_user.private_token
- = text_field_tag "token", current_user.private_token, class: "form-control"
- - else
- %span You don`t have one yet. Click generate to fix it.
-
- .form-actions
- - if current_user.private_token
- = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default"
- - else
- = f.submit 'Generate', class: "btn btn-default"
-
- .panel.panel-default
- .panel-heading
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Private Token
+ %p
+ Your private token is used to access application resources without authentication.
+ .col-lg-9
+ = form_for @user, url: reset_private_token_profile_path, method: :put, html: {class: "private-token"} do |f|
+ %p.cgray
+ - if current_user.private_token
+ = label_tag "token", "Private token", class: "label-light"
+ = text_field_tag "token", current_user.private_token, class: "form-control"
+ - else
+ %span You don`t have one yet. Click generate to fix it.
+ %p.help-block
+ It can be used for atom feeds or the API. Keep it secret!
+ .prepend-top-default
+ - if current_user.private_token
+ = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default"
+ - else
+ = f.submit 'Generate', class: "btn btn-default"
+%hr
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
Two-factor Authentication
- .panel-body
- - if current_user.two_factor_enabled?
- .pull-right
- = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm',
+ %p
+ Increase your account's security by enabling two-factor authentication (2FA).
+ .col-lg-9
+ %p
+ Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'}
+ - if !current_user.two_factor_enabled?
+ %p
+ Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
+ More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
+ .append-bottom-10
+ = link_to 'Enable two-factor authentication', new_profile_two_factor_auth_path, class: 'btn btn-success'
+ - else
+ = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger',
data: { confirm: 'Are you sure?' }
- %p.text-success
- %strong
- Two-factor Authentication is enabled
- %p
- If you lose your recovery codes you can
- %strong
- = succeed ',' do
- = link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' }
- invalidating all previous codes.
-
- - else
- %p
- Increase your account's security by enabling two-factor authentication (2FA).
- %p
- Each time you log in you’ll be required to provide your username and
- password as usual, plus a randomly-generated code from your phone.
-
- .form-actions
- = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success'
-
- - if button_based_providers.any?
- .panel.panel-default
- .panel-heading
+%hr
+- if button_based_providers.any?
+ .row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Social sign-in
+ %p
+ Activate signin with one of the following services
+ .col-lg-9
+ %label.label-light
Connected Accounts
- .panel-body
- .oauth-buttons.append-bottom-10
- %p Click on icon to activate signin with one of the following services
- - button_based_providers.each do |provider|
- .btn-group
- = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: "btn btn-lg #{'active' if auth_active?(provider)}", "data-no-turbolink" => "true"
-
- - if auth_active?(provider)
- = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do
- = icon('close')
-
- - if current_user.can_change_username?
- .panel.panel-warning.update-username
- .panel-heading
- Change Username
- .panel-body
- = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f|
- %p
- Changing your username will change path to all personal projects!
- %div
- .input-group
- .input-group-addon
- = "#{root_url}u/"
- = f.text_field :username, required: true, class: 'form-control'
- &nbsp;
- .loading-gif.hide
- %p
- = icon('spinner spin')
- Saving new username
- .form-actions
- = f.submit 'Save username', class: "btn btn-warning"
+ %p Click on icon to activate signin with one of the following services
+ - button_based_providers.each do |provider|
+ .provider-btn-group
+ .provider-btn-image
+ = provider_image_tag(provider)
+ - if auth_active?(provider)
+ = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
+ Disconnect
+ - else
+ = link_to user_omniauth_authorize_path(provider), method: :post, class: "provider-btn #{'not-active' if !auth_active?(provider)}", "data-no-turbolink" => "true" do
+ Connect
+ %hr
+- if current_user.can_change_username?
+ .row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0.change-username-title
+ Change username
+ %p
+ Changing your username will change path to all personal projects!
+ .col-lg-9
+ = form_for @user, url: update_username_profile_path, method: :put, remote: true, html: {class: "update-username"} do |f|
+ .form-group
+ = f.label :username, "Path", class: "label-light"
+ .input-group
+ .input-group-addon
+ = "#{root_url}u/"
+ = f.text_field :username, required: true, class: 'form-control'
+ .help-block
+ Current path:
+ = "#{root_url}u/#{current_user.username}"
+ .prepend-top-default
+ = f.button class: "btn btn-warning", type: "submit" do
+ = icon "spinner spin", class: "hidden loading-username"
+ Update username
+ %hr
- - if signup_enabled?
- .panel.panel-danger.remove-account
- .panel-heading
+- if signup_enabled?
+ .row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0.remove-account-title
Remove account
- .panel-body
- - if @user.can_be_removed?
- %p Deleting an account has the following effects:
- %ul
- %li All user content like authored issues, snippets, comments will be removed
- - rp = current_user.personal_projects.count
- - unless rp.zero?
- %li #{pluralize rp, 'personal project'} will be removed and cannot be restored
- .form-actions
- = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
- - else
- - if @user.solo_owned_groups.present?
- %p
- Your account is currently an owner in these groups:
- %strong #{@user.solo_owned_groups.map(&:name).join(', ')}
- %p
- You must transfer ownership or delete these groups before you can delete your account.
+ .col-lg-9
+ - if @user.can_be_removed?
+ %p
+ Deleting an account has the following effects:
+ %ul
+ %li All user content like authored issues, snippets, comments will be removed
+ - rp = current_user.personal_projects.count
+ - unless rp.zero?
+ %li #{pluralize rp, 'personal project'} will be removed and cannot be restored
+ = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
+ - else
+ - if @user.solo_owned_groups.present?
+ %p
+ Your account is currently an owner in these groups:
+ %strong #{@user.solo_owned_groups.map(&:name).join(', ')}
+ %p
+ You must transfer ownership or delete these groups before you can delete your account.
+.append-bottom-default
diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml
deleted file mode 100644
index 0436c2213da..00000000000
--- a/app/views/profiles/applications.html.haml
+++ /dev/null
@@ -1,70 +0,0 @@
-- page_title "Applications"
-- header_title page_title, applications_profile_path
-
-.gray-content-block.top-block
- - if user_oauth_applications?
- Manage applications that can use GitLab as an OAuth provider,
- and applications that you've authorized to use your account.
- - else
- Manage applications that you've authorized to use your account.
-
-- if user_oauth_applications?
- .oauth-applications
- %h3
- Your applications
- .pull-right
- = link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
- - if @applications.any?
- .table-holder
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Callback URL
- %th Clients
- %th
- %th
- %tbody
- - @applications.each do |application|
- %tr{:id => "application_#{application.id}"}
- %td= link_to application.name, oauth_application_path(application)
- %td
- - application.redirect_uri.split.each do |uri|
- %div= uri
- %td= application.access_tokens.count
- %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm'
- %td= render 'doorkeeper/applications/delete_form', application: application
-
-.oauth-authorized-applications.prepend-top-20
- - if user_oauth_applications?
- %h3
- Authorized applications
-
- - if @authorized_tokens.any?
- .table-holder
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Authorized At
- %th Scope
- %th
- %tbody
- - @authorized_apps.each do |app|
- - token = app.authorized_tokens.order('created_at desc').first
- %tr{:id => "application_#{app.id}"}
- %td= app.name
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', application: app
- - @authorized_anonymous_tokens.each do |token|
- %tr
- %td
- Anonymous
- %div.help-block
- %em Authorization was granted by entering your username and password in the application.
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', token: token
- - else
- %p.light You don't have any authorized applications
diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml
index 8fdba45b193..f630c03e5f6 100644
--- a/app/views/profiles/audit_log.html.haml
+++ b/app/views/profiles/audit_log.html.haml
@@ -1,8 +1,11 @@
- page_title "Audit Log"
- header_title page_title, audit_log_profile_path
-.gray-content-block.top-block
- History of authentications
-
-.prepend-top-default
-= render 'event_table', events: @events
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h3.prepend-top-0
+ = page_title
+ %p
+ This is a security log of important events involving your account.
+ .col-lg-9
+ = render 'event_table', events: @events
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 1d140347a5f..3f328f96cea 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,52 +1,49 @@
- page_title "Emails"
- header_title page_title, profile_emails_path
-.gray-content-block.top-block
- Control emails linked to your account
-
-%ul.prepend-top-default
- %li
- Your
- %b Primary Email
- will be used for avatar detection and web based operations, such as edits and merges.
- %li
- Your
- %b Notification Email
- will be used for account notifications.
- %li
- Your
- %b Public Email
- will be displayed on your public profile.
- %li
- All email addresses will be used to identify your commits.
-
-.panel.panel-default
- .panel-heading
- Emails (#{@emails.count + 1})
- %ul.well-list#emails-table
- %li
- %strong= @primary
- %span.label.label-success Primary Email
- - if @primary === current_user.public_email
- %span.label.label-info Public Email
- - if @primary === current_user.notification_email
- %span.label.label-info Notification Email
- - @emails.each do |email|
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ Control emails linked to your account
+ .col-lg-9
+ %h4.prepend-top-0
+ Add email address
+ = form_for 'email', url: profile_emails_path do |f|
+ .form-group
+ = f.label :email, class: 'label-light'
+ = f.text_field :email, class: 'form-control'
+ .prepend-top-default
+ = f.submit 'Add email address', class: 'btn btn-create'
+ %hr
+ %h4.prepend-top-0
+ Linked emails (#{@emails.count + 1})
+ .account-well.append-bottom-default
+ %ul
+ %li
+ Your Primary Email will be used for avatar detection and web based operations, such as edits and merges.
+ %li
+ Your Notification Email will be used for account notifications.
+ %li
+ Your Public Email will be displayed on your public profile.
+ %li
+ All email addresses will be used to identify your commits.
+ %ul.well-list
%li
- %strong= email.email
- - if email.email === current_user.public_email
- %span.label.label-info Public Email
- - if email.email === current_user.notification_email
- %span.label.label-info Notification Email
- %span.cgray
- added #{time_ago_with_tooltip(email.created_at)}
- = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right'
-
-%h4 Add email address
-= form_for 'email', url: profile_emails_path, html: { class: 'form-horizontal' } do |f|
- .form-group
- = f.label :email, class: 'control-label'
- .col-sm-10
- = f.text_field :email, class: 'form-control'
- .form-actions
- = f.submit 'Add email address', class: 'btn btn-create'
+ = @primary
+ %span.pull-right
+ %span.label.label-success Primary Email
+ - if @primary === current_user.public_email
+ %span.label.label-info Public Email
+ - if @primary === current_user.notification_email
+ %span.label.label-info Notification Email
+ - @emails.each do |email|
+ %li
+ = email.email
+ %span.pull-right
+ - if email.email === current_user.public_email
+ %span.label.label-info Public Email
+ - if email.email === current_user.notification_email
+ %span.label.label-info Notification Email
+ = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right'
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 2a8800de60e..4d78215ed3c 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -1,5 +1,5 @@
%div
- = form_for [:profile, @key], html: { class: 'form-horizontal js-requires-input' } do |f|
+ = form_for [:profile, @key], html: { class: 'js-requires-input' } do |f|
- if @key.errors.any?
.alert.alert-danger
%ul
@@ -7,13 +7,11 @@
%li= msg
.form-group
- = f.label :key, class: 'control-label'
- .col-sm-10
- = f.text_area :key, class: "form-control", rows: 8, autofocus: true, required: true
+ = f.label :key, class: 'label-light'
+ = f.text_area :key, class: "form-control", rows: 8, required: true
.form-group
- = f.label :title, class: 'control-label'
- .col-sm-10= f.text_field :title, class: "form-control", required: true
+ = f.label :title, class: 'label-light'
+ = f.text_field :title, class: "form-control", required: true
- .form-actions
+ .prepend-top-default
= f.submit 'Add key', class: "btn btn-create"
- = link_to "Cancel", profile_keys_path, class: "btn btn-cancel"
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 9bbccbc45ea..4dbaa662b66 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -1,11 +1,14 @@
-%tr
- %td
- = link_to path_to_key(key, is_admin) do
- %strong= key.title
- %td
- %code.key-fingerprint= key.fingerprint
- %td
- %span.cgray
- added #{time_ago_with_tooltip(key.created_at)}
- %td
- = link_to 'Remove', path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right"
+%li.key-list-item
+ .pull-left.append-right-10
+ = icon 'key', class: "key-icon hidden-xs"
+ .key-list-item-info
+ = link_to path_to_key(key, is_admin), class: "title" do
+ = key.title
+ .description
+ = key.fingerprint
+ .pull-right
+ %span.key-created-at
+ created #{time_ago_with_tooltip(key.created_at)}
+ = link_to path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent prepend-left-10" do
+ %span.sr-only Remove
+ = icon('trash')
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 3bd1f1af162..dd7615400dc 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -1,5 +1,5 @@
- is_admin = defined?(admin) ? true : false
-.row
+.row.prepend-top-default
.col-md-4
.panel.panel-default
.panel-heading
diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml
index 8c9d546af4c..296cafa6e31 100644
--- a/app/views/profiles/keys/_key_table.html.haml
+++ b/app/views/profiles/keys/_key_table.html.haml
@@ -1,19 +1,11 @@
-- is_admin = defined?(admin) ? true : false
+- is_admin = local_assigns.fetch(:admin, false)
+
- if @keys.any?
- .table-holder
- %table.table
- %thead.panel-heading
- %tr
- %th Title
- %th Fingerprint
- %th Added at
- %th
- %tbody
- - @keys.each do |key|
- = render 'profiles/keys/key', key: key, is_admin: is_admin
+ %ul.well-list
+ = render partial: 'profiles/keys/key', collection: @keys, locals: { is_admin: is_admin }
- else
- .nothing-here-block
+ %p.profile-settings-message.text-center
- if is_admin
- User has no ssh keys
+ There are no SSH keys associated with this account.
- else
There are no SSH keys with access to your account.
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 17a4195030e..e0f8c9a5733 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,14 +1,21 @@
- page_title "SSH Keys"
- header_title page_title, profile_keys_path
-.gray-content-block.top-block
- .pull-right
- = link_to new_profile_key_path, class: "btn btn-new" do
- = icon('plus')
- Add SSH Key
- .oneline
- Before you can add an SSH key you need to
- = link_to "generate it.", help_page_path("ssh", "README")
-
-.prepend-top-default
-= render 'key_table'
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ SSH keys allow you to establish a secure connection between your computer and GitLab.
+ .col-lg-9
+ %h5.prepend-top-0
+ Add an SSH key
+ %p.profile-settings-content
+ Before you can add an SSH key you need to
+ = link_to "generate it.", help_page_path("ssh", "README")
+ = render 'form'
+ %hr
+ %h5
+ Your SSH keys (#{@keys.count})
+ %div.append-bottom-default
+ = render 'key_table'
diff --git a/app/views/profiles/keys/new.html.haml b/app/views/profiles/keys/new.html.haml
deleted file mode 100644
index 13a18269d11..00000000000
--- a/app/views/profiles/keys/new.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-- page_title "Add SSH Keys"
-%h3.page-title Add an SSH Key
-%p.light
- Paste your public key here. Read more about how to generate a key on #{link_to "the SSH help page", help_page_path("ssh", "README")}.
-%hr
-= render 'form'
-
-:javascript
- $('#key_key').on('focusout', function(){
- var title = $('#key_title'),
- val = $('#key_key').val(),
- comment = val.match(/^\S+ \S+ (.+)\n?$/);
-
- if( comment && comment.length > 1 && title.val() == '' ){
- $('#key_title').val( comment[1] ).change();
- }
- });
diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml
index 742c5c4b68d..d0d044136f6 100644
--- a/app/views/profiles/notifications/_settings.html.haml
+++ b/app/views/profiles/notifications/_settings.html.haml
@@ -1,5 +1,5 @@
-%li
- %span.notification.fa.fa-holder
+%li.notification-list-item
+ %span.notification.fa.fa-holder.append-right-5
- if notification.global?
= notification_icon(@notification)
- else
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 0bcadc965fa..de80abd7f4d 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -1,11 +1,7 @@
- page_title "Notifications"
- header_title page_title, profile_notifications_path
-.gray-content-block.top-block
- These are your global notification settings.
-
-.prepend-top-default
-= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications form-horizontal global-notifications-form' } do |f|
+= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
-if @user.errors.any?
%div.alert.alert-danger
%ul
@@ -13,65 +9,66 @@
%li= msg
= hidden_field_tag :notification_type, 'global'
+ .row
+ .col-lg-3.profile-settings-sidebar
+ %h4
+ = page_title
+ %p
+ You can specify notification level per group or per project.
+ %p
+ By default, all projects and groups will use the global notifications setting.
+ .col-lg-9
+ %h5
+ Global notification settings
+ .form-group
+ = f.label :notification_email, class: "label-light"
+ = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
+ .form-group
+ = f.label :notification_level, class: 'label-light'
+ .radio
+ = f.label :notification_level, value: Notification::N_DISABLED do
+ = f.radio_button :notification_level, Notification::N_DISABLED
+ .level-title
+ Disabled
+ %p You will not get any notifications via email
- .form-group
- = f.label :notification_email, class: "control-label"
- .col-sm-10
- = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "form-control"
-
- .form-group
- = f.label :notification_level, class: 'control-label'
- .col-sm-10
- .radio
- = f.label :notification_level, value: Notification::N_DISABLED do
- = f.radio_button :notification_level, Notification::N_DISABLED
- .level-title
- Disabled
- %p You will not get any notifications via email
-
- .radio
- = f.label :notification_level, value: Notification::N_MENTION do
- = f.radio_button :notification_level, Notification::N_MENTION
- .level-title
- On Mention
- %p You will receive notifications only for comments in which you were @mentioned
-
- .radio
- = f.label :notification_level, value: Notification::N_PARTICIPATING do
- = f.radio_button :notification_level, Notification::N_PARTICIPATING
- .level-title
- Participating
- %p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
-
- .radio
- = f.label :notification_level, value: Notification::N_WATCH do
- = f.radio_button :notification_level, Notification::N_WATCH
- .level-title
- Watch
- %p You will receive notifications for any activity
+ .radio
+ = f.label :notification_level, value: Notification::N_MENTION do
+ = f.radio_button :notification_level, Notification::N_MENTION
+ .level-title
+ On Mention
+ %p You will receive notifications only for comments in which you were @mentioned
- .gray-content-block
- = f.submit 'Save changes', class: "btn btn-create"
+ .radio
+ = f.label :notification_level, value: Notification::N_PARTICIPATING do
+ = f.radio_button :notification_level, Notification::N_PARTICIPATING
+ .level-title
+ Participating
+ %p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
-.row.all-notifications.prepend-top-default
- .col-md-6
- %p
- You can also specify notification level per group or per project.
- %br
- By default, all projects and groups will use the notification level set above.
- %h4 Groups:
- %ul.bordered-list
- - @group_members.each do |group_member|
- - notification = Notification.new(group_member)
- = render 'settings', type: 'group', membership: group_member, notification: notification
+ .radio
+ = f.label :notification_level, value: Notification::N_WATCH do
+ = f.radio_button :notification_level, Notification::N_WATCH
+ .level-title
+ Watch
+ %p You will receive notifications for any activity
- .col-md-6
- %p
- To specify the notification level per project of a group you belong to,
- %br
- you need to be a member of the project itself, not only its group.
- %h4 Projects:
- %ul.bordered-list
- - @project_members.each do |project_member|
- - notification = Notification.new(project_member)
- = render 'settings', type: 'project', membership: project_member, notification: notification
+ .prepend-top-default
+ = f.submit 'Update settings', class: "btn btn-create"
+ %hr
+ %h5
+ Groups (#{@group_members.count})
+ %div
+ %ul.bordered-list
+ - @group_members.each do |group_member|
+ - notification = Notification.new(group_member)
+ = render 'settings', type: 'group', membership: group_member, notification: notification
+ %h5
+ Projects (#{@project_members.count})
+ %p.account-well
+ To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group.
+ .append-bottom-default
+ %ul.bordered-list
+ - @project_members.each do |project_member|
+ - notification = Notification.new(project_member)
+ = render 'settings', type: 'project', membership: project_member, notification: notification
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index fab7c45c9b2..44d758dceb3 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -1,20 +1,18 @@
- page_title "Password"
- header_title page_title, edit_profile_password_path
-.gray-content-block.top-block
- - if @user.password_automatically_set?
- Set your password.
- - else
- Change your password or recover your current one.
-
-.update-password.prepend-top-default
- = form_for @user, url: profile_password_path, method: :put, html: { class: 'form-horizontal' } do |f|
- %div
- %p.slead
- - unless @user.password_automatically_set?
- You must provide current password in order to change it.
- %br
- After a successful password update, you will be redirected to the login page where you can log in with your new password.
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ After a successful password update, you will be redirected to the login page where you can log in with your new password.
+ .col-lg-9
+ %h5.prepend-top-0
+ Change your password
+ - unless @user.password_automatically_set?
+ or recover your current one
+ = form_for @user, url: profile_password_path, method: :put, html: {class: "update-password"} do |f|
-if @user.errors.any?
.alert.alert-danger
%ul
@@ -22,19 +20,17 @@
%li= msg
- unless @user.password_automatically_set?
.form-group
- = f.label :current_password, class: 'control-label'
- .col-sm-10
- = f.password_field :current_password, required: true, class: 'form-control'
- %div
- = link_to "Forgot your password?", reset_profile_password_path, method: :put
-
+ = f.label :current_password, class: 'label-light'
+ = f.password_field :current_password, required: true, class: 'form-control'
+ %p.help-block
+ You must provide your current password in order to change it.
.form-group
- = f.label :password, 'New password', class: 'control-label'
- .col-sm-10
- = f.password_field :password, required: true, class: 'form-control'
+ = f.label :password, 'New password', class: 'label-light'
+ = f.password_field :password, required: true, class: 'form-control'
.form-group
- = f.label :password_confirmation, class: 'control-label'
- .col-sm-10
- = f.password_field :password_confirmation, required: true, class: 'form-control'
- .form-actions
- = f.submit 'Save password', class: "btn btn-create"
+ = f.label :password_confirmation, class: 'label-light'
+ = f.password_field :password_confirmation, required: true, class: 'form-control'
+ .prepend-top-default.append-bottom-default
+ = f.submit 'Save password', class: "btn btn-create append-right-10"
+ - unless @user.password_automatically_set?
+ = link_to "I forgot my password", reset_profile_password_path, method: :put, class: "account-btn-link"
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 877589dc390..f80211669fb 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,57 +1,56 @@
- page_title 'Preferences'
- header_title page_title, profile_preferences_path
-- @blank_container = true
-.alert.alert-help
- These settings allow you to customize the appearance and behavior of the site.
- They are saved with your account and will persist to any device you use to
- access the site.
-
-= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'js-preferences-form form-horizontal'} do |f|
- .panel.panel-default.application-theme
- .panel-heading
+= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'row prepend-top-default js-preferences-form'} do |f|
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
Application theme
- .panel-body
- - Gitlab::Themes.each do |theme|
- = label_tag do
- .preview{class: theme.css_class}
- = f.radio_button :theme_id, theme.id
- = theme.name
-
- .panel.panel-default.syntax-theme
- .panel-heading
+ %p
+ This setting allows you to customize the appearance of the site, ex. sidebar.
+ .col-lg-9.application-theme
+ - Gitlab::Themes.each do |theme|
+ = label_tag do
+ .preview{class: theme.css_class}
+ = f.radio_button :theme_id, theme.id
+ = theme.name
+ .col-sm-12
+ %hr
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
Syntax highlighting theme
- .panel-body
- - Gitlab::ColorSchemes.each do |scheme|
- = label_tag do
- .preview= image_tag "#{scheme.css_class}-scheme-preview.png"
- = f.radio_button :color_scheme_id, scheme.id
- = scheme.name
-
- .panel.panel-default
- .panel-heading
+ %p
+ This setting allow you to customize the appearance of the syntax.
+ .col-lg-9.syntax-theme
+ - Gitlab::ColorSchemes.each do |scheme|
+ = label_tag do
+ .preview= image_tag "#{scheme.css_class}-scheme-preview.png"
+ = f.radio_button :color_scheme_id, scheme.id
+ = scheme.name
+ .col-sm-12
+ %hr
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
Behavior
- .panel-body
- .form-group
- = f.label :layout, class: 'control-label' do
- Layout width
- .col-sm-10
- = f.select :layout, layout_choices, {}, class: 'form-control'
- .help-block
- Choose between fixed (max. 1200px) and fluid (100%) application layout.
- .form-group
- = f.label :dashboard, class: 'control-label' do
- Default Dashboard
- = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank')
- .col-sm-10
- = f.select :dashboard, dashboard_choices, {}, class: 'form-control'
- .form-group
- = f.label :project_view, class: 'control-label' do
- Project view
- = link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank')
- .col-sm-10
- = f.select :project_view, project_view_choices, {}, class: 'form-control'
- .help-block
- Choose what content you want to see on a project's home page.
- .panel-footer
+ %p
+ This setting allows you to customize the behavior of the system layout and default views.
+ .col-lg-9
+ .form-group
+ = f.label :layout, class: 'label-light' do
+ Layout width
+ = f.select :layout, layout_choices, {}, class: 'form-control'
+ .help-block
+ Choose between fixed (max. 1200px) and fluid (100%) application layout.
+ .form-group
+ = f.label :dashboard, class: 'label-light' do
+ Default Dashboard
+ = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank')
+ = f.select :dashboard, dashboard_choices, {}, class: 'form-control'
+ .form-group
+ = f.label :project_view, class: 'label-light' do
+ Project view
+ = link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank')
+ = f.select :project_view, project_view_choices, {}, class: 'form-control'
+ .help-block
+ Choose what content you want to see on a project's home page.
+ .form-group
= f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index add9a00138b..dcb3be9585d 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,101 +1,118 @@
-.gray-content-block.top-block
- This information will appear on your profile.
- - if current_user.ldap_user?
- Some options are unavailable for LDAP accounts
-
-.prepend-top-default
-= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit_user form-horizontal" }, authenticity_token: true do |f|
+= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f|
-if @user.errors.any?
%div.alert.alert-danger
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
.row
- .col-md-7
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Public Avatar
+ %p
+ - if @user.avatar?
+ You can change your avatar here
+ - if Gitlab.config.gravatar.enabled
+ or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
+ - else
+ You can upload an avatar here
+ - if Gitlab.config.gravatar.enabled
+ or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
+ .col-lg-9
+ .clearfix.avatar-image.append-bottom-default
+ = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
+ %h5.prepend-top-0
+ Upload new avatar
+ .prepend-top-5.append-bottom-10
+ %a.btn.js-choose-user-avatar-button
+ Browse file...
+ %span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen
+ = f.file_field :avatar, class: "js-user-avatar-input hidden", accept: "image/*"
+ .help-block
+ The maximum file size allowed is 200KB.
+ - if @user.avatar?
+ %hr
+ = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-gray"
+ %hr
+ .row
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Main settings
+ %p
+ This information will appear on your profile.
+ - if current_user.ldap_user?
+ Some options are unavailable for LDAP accounts
+ .col-lg-9
.form-group
- = f.label :name, class: "control-label"
- .col-sm-10
- = f.text_field :name, class: "form-control", required: true
- %span.help-block Enter your name, so people you know can recognize you.
+ = f.label :name, class: "label-light"
+ = f.text_field :name, class: "form-control", required: true
+ %span.help-block Enter your name, so people you know can recognize you.
.form-group
- = f.label :email, class: "control-label"
- .col-sm-10
- - if @user.ldap_user? && @user.ldap_email?
- = f.text_field :email, class: "form-control", required: true, readonly: true
- %span.help-block.light
- Your email address was automatically set based on the LDAP server.
+ = f.label :email, class: "label-light"
+ - if @user.ldap_user? && @user.ldap_email?
+ = f.text_field :email, class: "form-control", required: true, readonly: true
+ %span.help-block.light
+ Your email address was automatically set based on the LDAP server.
+ - else
+ - if @user.temp_oauth_email?
+ = f.text_field :email, class: "form-control", required: true, value: nil
- else
- - if @user.temp_oauth_email?
- = f.text_field :email, class: "form-control", required: true, value: nil
- - else
- = f.text_field :email, class: "form-control", required: true
- - if @user.unconfirmed_email.present?
- %span.help-block
- Please click the link in the confirmation email before continuing. It was sent to
- = succeed "." do
- %strong #{@user.unconfirmed_email}
- %p
- = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
+ = f.text_field :email, class: "form-control", required: true
+ - if @user.unconfirmed_email.present?
+ %span.help-block
+ Please click the link in the confirmation email before continuing. It was sent to
+ = succeed "." do
+ %strong #{@user.unconfirmed_email}
+ %p
+ = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
- - else
- %span.help-block We also use email for avatar detection if no avatar is uploaded.
+ - else
+ %span.help-block We also use email for avatar detection if no avatar is uploaded.
.form-group
- = f.label :public_email, class: "control-label"
- .col-sm-10
- = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2"
- %span.help-block This email will be displayed on your public profile.
+ = f.label :public_email, class: "label-light"
+ = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2"
+ %span.help-block This email will be displayed on your public profile.
.form-group
- = f.label :skype, class: "control-label"
- .col-sm-10= f.text_field :skype, class: "form-control"
+ = f.label :skype, class: "label-light"
+ = f.text_field :skype, class: "form-control"
.form-group
- = f.label :linkedin, class: "control-label"
- .col-sm-10= f.text_field :linkedin, class: "form-control"
+ = f.label :linkedin, class: "label-light"
+ = f.text_field :linkedin, class: "form-control"
.form-group
- = f.label :twitter, class: "control-label"
- .col-sm-10= f.text_field :twitter, class: "form-control"
+ = f.label :twitter, class: "label-light"
+ = f.text_field :twitter, class: "form-control"
.form-group
- = f.label :website_url, 'Website', class: "control-label"
- .col-sm-10= f.text_field :website_url, class: "form-control"
+ = f.label :website_url, 'Website', class: "label-light"
+ = f.text_field :website_url, class: "form-control"
.form-group
- = f.label :location, 'Location', class: "control-label"
- .col-sm-10= f.text_field :location, class: "form-control"
+ = f.label :location, 'Location', class: "label-light"
+ = f.text_field :location, class: "form-control"
.form-group
- = f.label :bio, class: "control-label"
- .col-sm-10
- = f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
- %span.help-block Tell us about yourself in fewer than 250 characters.
-
- .col-md-5
- .light-well
- = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
-
- .clearfix
- .profile-avatar-form-option
- %p.light
- - if @user.avatar?
- You can change your avatar here
- - if Gitlab.config.gravatar.enabled
- %br
- or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
- - else
- You can upload an avatar here
- - if Gitlab.config.gravatar.enabled
- %br
- or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
- %hr
- %a.choose-btn.btn.btn-sm.js-choose-user-avatar-button
- %i.fa.fa-paperclip
- %span Choose File ...
- &nbsp;
- %span.file_name.js-avatar-filename File name...
- = f.file_field :avatar, class: "js-user-avatar-input hidden"
- .light The maximum file size allowed is 200KB.
- - if @user.avatar?
- %hr
- = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
-
+ = f.label :bio, class: "label-light"
+ = f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
+ %span.help-block Tell us about yourself in fewer than 250 characters.
+ .prepend-top-default.append-bottom-default
+ = f.submit 'Update profile settings', class: "btn btn-success"
+ = link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
- .form-actions
- = f.submit 'Save changes', class: "btn btn-success"
- = link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
+.modal.modal-profile-crop
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %button.close{:type => "button", :'data-dismiss' => "modal"}
+ %span
+ &times;
+ %h4.modal-title
+ Position and size your new avatar
+ .modal-body
+ .profile-crop-image-container
+ %img.modal-profile-crop-image
+ .crop-controls
+ .btn-group
+ %button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } }
+ %span.fa.fa-search-plus
+ %button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } }
+ %span.fa.fa-search-minus
+ .modal-footer
+ %button.btn.btn-primary.js-upload-user-avatar{:type => "button"}
+ Set new profile picture
diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml
index b2830aa0834..5d342ef58e5 100644
--- a/app/views/profiles/two_factor_auths/new.html.haml
+++ b/app/views/profiles/two_factor_auths/new.html.haml
@@ -1,41 +1,41 @@
- page_title 'Two-factor Authentication', 'Account'
-%h2.page-title Two-factor Authentication (2FA)
-%p
- Download the Google Authenticator application from App Store for iOS or Google
- Play for Android and scan this code.
-
- More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
-
-%hr
-
-= form_tag profile_two_factor_auth_path, method: :post, class: 'form-horizontal two-factor-new' do |f|
- - if @error
- .alert.alert-danger
- = @error
- .form-group
- .col-lg-2.col-lg-offset-2
- = raw @qr_code
- .col-lg-7.col-lg-offset-1.manual-instructions
- %h3 Can't scan the code?
-
- %p
- To add the entry manually, provide the following details to the
- application on your phone.
-
- %dl
- %dt Account
- %dd= current_user.email
- %dl
- %dt Key
- %dd= current_user.otp_secret.scan(/.{4}/).join(' ')
- %dl
- %dt Time based
- %dd Yes
- .form-group
- = label_tag :pin_code, nil, class: "control-label"
- .col-lg-10
- = text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true
- .form-actions
- = submit_tag 'Submit', class: 'btn btn-success'
- = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
+.row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Two-factor Authentication (2FA)
+ %p
+ Increase your account's security by enabling two-factor authentication (2FA).
+ .col-lg-9
+ %p
+ Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'}
+ %p
+ Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
+ More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
+ .row.append-bottom-10
+ .col-md-3
+ = raw @qr_code
+ .col-md-9
+ .account-well
+ %p.prepend-top-0.append-bottom-0
+ Can't scan the code?
+ %p.prepend-top-0.append-bottom-0
+ To add the entry manually, provide the following details to the application on your phone.
+ %p.prepend-top-0.append-bottom-0
+ Account:
+ = current_user.email
+ %p.prepend-top-0.append-bottom-0
+ Key:
+ = current_user.otp_secret.scan(/.{4}/).join(' ')
+ %p.two-factor-new-manual-content
+ Time based: Yes
+ = form_tag profile_two_factor_auth_path, method: :post do |f|
+ - if @error
+ .alert.alert-danger
+ = @error
+ .form-group
+ = label_tag :pin_code, nil, class: "label-light"
+ = text_field_tag :pin_code, nil, class: "form-control", required: true
+ .prepend-top-default
+ = submit_tag 'Enable two-factor authentication', class: 'btn btn-success'
+ = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml
new file mode 100644
index 00000000000..9ae6964aaac
--- /dev/null
+++ b/app/views/projects/_builds_settings.html.haml
@@ -0,0 +1,68 @@
+%fieldset.builds-feature
+ %legend
+ Builds:
+
+ - unless @repository.gitlab_ci_yml
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ %p Builds need to be configured before you can begin using Continuous Integration.
+ = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
+ %hr
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ %p Get recent application code using the following command:
+ .radio
+ = f.label :build_allow_git_fetch_false do
+ = f.radio_button :build_allow_git_fetch, 'false'
+ %strong git clone
+ %br
+ %span.descr Slower but makes sure you have a clean dir before every build
+ .radio
+ = f.label :build_allow_git_fetch_true do
+ = f.radio_button :build_allow_git_fetch, 'true'
+ %strong git fetch
+ %br
+ %span.descr Faster
+
+ .form-group
+ = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label'
+ .col-sm-10
+ = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
+ %p.help-block per build in minutes
+ .form-group
+ = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label'
+ .col-sm-10
+ .input-group
+ %span.input-group-addon /
+ = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
+ %span.input-group-addon /
+ %p.help-block
+ We will use this regular expression to find test coverage output in build trace.
+ Leave blank if you want to disable this feature
+ .bs-callout.bs-callout-info
+ %p Below are examples of regex for existing tools:
+ %ul
+ %li
+ Simplecov (Ruby) -
+ %code \(\d+.\d+\%\) covered
+ %li
+ pytest-cov (Python) -
+ %code \d+\%\s*$
+ %li
+ phpunit --coverage-text --colors=never (PHP) -
+ %code ^\s*Lines:\s*\d+.\d+\%
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :public_builds do
+ = f.check_box :public_builds
+ %strong Public builds
+ .help-block Allow everyone to access builds for Public and Internal projects
+
+ .form-group
+ = f.label :runners_token, "Runners token", class: 'control-label'
+ .col-sm-10
+ = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
+ %p.help-block The secure token used to checkout project.
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index b45df44f270..9b5de17dd3b 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -2,21 +2,21 @@
.project-home-panel.cover-block.clearfix{:class => ("empty-project" if empty_repo)}
.project-identicon-holder
= project_icon(@project, alt: '', class: 'project-avatar avatar s90')
- .project-home-desc
+ .cover-title.project-home-desc
%h1
= @project.name
- %span.visibility-icon.has_tooltip{data: { container: 'body' },
- title: "#{visibility_level_label(@project.visibility_level)} - #{project_visibility_level_description(@project.visibility_level)}"}
+ %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
= visibility_level_icon(@project.visibility_level, fw: false)
- - if @project.description.present?
+ - if @project.description.present?
+ .cover-desc.project-home-desc
= markdown(@project.description, pipeline: :description)
- - if forked_from_project = @project.forked_from_project
- %p
- Forked from
- = link_to project_path(forked_from_project) do
- = forked_from_project.namespace.try(:name)
+ - if forked_from_project = @project.forked_from_project
+ .cover-desc
+ Forked from
+ = link_to project_path(forked_from_project) do
+ = forked_from_project.namespace.try(:name)
.cover-controls
- if current_user
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index eb6fbfaffa0..5f9a92ff93f 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -3,7 +3,7 @@
%h3.page-title Blame view
-#tree-holder.tree-holder
+#blob-content-holder.tree-holder
.file-holder
.file-title
= blob_icon @blob.mode, @blob.name
@@ -33,7 +33,9 @@
%td.line-numbers
- line_count = blame_group[:lines].count
- (current_line...(current_line + line_count)).each do |i|
- %a.diff-line-num= i
+ %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
+ = icon("link")
+ = i
\
- current_line += line_count
%td.lines
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 10b02813733..f8b6fa253c4 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -10,7 +10,7 @@
%span.editor-file-name
\/
= text_field_tag 'file_name', params[:file_name], placeholder: "File name",
- required: true, class: 'form-control new-file-name js-quick-submit'
+ required: true, class: 'form-control new-file-name'
.pull-right
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index 3c11b97921f..18caddabd39 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -6,4 +6,4 @@
- blob = sanitize_svg(blob)
%img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
- else
- %img{src: namespace_project_raw_path(@project.namespace, @project, @id)}
+ %img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))}
diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml
index 084608bbba3..84694203d7d 100644
--- a/app/views/projects/blob/_new_dir.html.haml
+++ b/app/views/projects/blob/_new_dir.html.haml
@@ -5,7 +5,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title Create New Directory
.modal-body
- = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do
+ = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-quick-submit js-requires-input' do
.form-group
= label_tag :dir_name, 'Directory name', class: 'control-label'
.col-sm-10
diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml
index 1cf19a7d3db..2e1f32fd15e 100644
--- a/app/views/projects/blob/_remove.html.haml
+++ b/app/views/projects/blob/_remove.html.haml
@@ -6,7 +6,7 @@
%h3.page-title Delete #{@blob.name}
.modal-body
- = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-requires-input' do
+ = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-quick-submit js-requires-input' do
= render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}"
.form-group
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 676924dc6ca..b1f50eb5f34 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -5,7 +5,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title #{title}
.modal-body
- = form_tag form_path, method: method, class: 'js-upload-blob-form form-horizontal' do
+ = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do
.dropzone
.dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index a279e6eda55..effcce5a1c4 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -13,7 +13,7 @@
= icon('eye')
= editing_preview_title(@blob.name)
- = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input js-edit-blob-form') do
+ = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 167fa615182..1dd2b5c0af7 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -5,7 +5,7 @@
New File
.file-editor
- = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-requires-input') do
+ = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-quick-submit js-requires-input') do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 76a823d3828..57e507e68c8 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -11,7 +11,7 @@
- if branch.name == @repository.root_ref
%span.label.label-primary default
- elsif @repository.merged_to_root_ref? branch.name
- %span.label.label-info.has_tooltip(title="Merged into #{@repository.root_ref}")
+ %span.label.label-info.has-tooltip(title="Merged into #{@repository.root_ref}")
merged
- if @project.protected_branch? branch.name
@@ -30,7 +30,7 @@
Compare
- if can_remove_branch?(@project, branch.name)
- = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
- if branch.name != @repository.root_ref
diff --git a/app/views/projects/branches/destroy.js.haml b/app/views/projects/branches/destroy.js.haml
index 882a4d0c5e2..a21ddaf4930 100644
--- a/app/views/projects/branches/destroy.js.haml
+++ b/app/views/projects/branches/destroy.js.haml
@@ -1 +1 @@
-$('.js-totalbranch-count').html("#{@repository.branches.size}")
+$('.js-totalbranch-count').html("#{@repository.branch_count}")
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 7afea5a5049..88266e21230 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -16,7 +16,7 @@
- else
Name
%b.caret
- %ul.dropdown-menu
+ %ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to namespace_project_branches_path(sort: nil) do
Name
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 5e3bd14565e..aa85f495e39 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -27,6 +27,9 @@
= link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project),
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
+ - unless @repository.gitlab_ci_yml
+ = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
+
= link_to ci_lint_path, class: 'btn btn-default' do
= icon('wrench')
%span CI Lint
@@ -51,9 +54,10 @@
%th Name
%th Duration
%th Finished at
+ - if @project.build_coverage_enabled?
+ %th Coverage
%th
- - @builds.each do |build|
- = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, allow_retry: true
+ = render @builds, commit_sha: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled?
= paginate @builds, theme: 'gitlab'
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 8eec78a557c..b02aee3db21 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -13,9 +13,10 @@
= link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request)
#up-build-trace
- - if @commit.matrix_for_ref?(@build.ref)
+ - builds = @build.commit.matrix_builds(@build)
+ - if builds.size > 1
%ul.nav-links.no-top.no-bottom
- - @commit.latest_builds_for_ref(@build.ref).each do |build|
+ - builds.each do |build|
%li{class: ('active' if build == @build) }
= link_to namespace_project_build_path(@project.namespace, @project, build) do
= ci_icon_for_status(build.status)
@@ -44,7 +45,7 @@
.pull-right
#{time_ago_with_tooltip(@build.finished_at) if @build.finished_at}
- - if @build.show_warning?
+ - if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning
%p
@@ -70,7 +71,7 @@
.autoscroll-container
%button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
.clearfix
- .scroll-controls
+ #js-build-scroll.scroll-controls
= link_to '#up-build-trace', class: 'btn' do
%i.fa.fa-angle-up
= link_to '#down-build-trace', class: 'btn' do
@@ -100,12 +101,12 @@
%h4.title Build artifacts
.center
.btn-group{ role: :group }
- = link_to @build.artifacts_download_url, class: 'btn btn-sm btn-primary' do
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
= icon('download')
Download
- if @build.artifacts_metadata?
- = link_to @build.artifacts_browse_url, class: 'btn btn-sm btn-primary' do
+ = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
= icon('folder-open')
Browse
@@ -115,10 +116,10 @@
- if can?(current_user, :update_build, @project)
.center
.btn-group{ role: :group }
- - if @build.cancel_url
- = link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post
- - elsif @build.retry_url
- = link_to "Retry", @build.retry_url, class: 'btn btn-sm btn-primary', method: :post
+ - if @build.active?
+ = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger', method: :post
+ - elsif @build.retryable?
+ = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post
- if @build.erasable?
= link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 14ee2263b7d..58f43ecb5d5 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,4 +1,4 @@
- unless @project.empty_repo?
- if can? current_user, :download_code, @project
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', rel: 'nofollow', title: "Download ZIP" do
+ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has-tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do
= icon('download')
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 133531887a2..5fb5fe5af2f 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -1,7 +1,7 @@
- unless @project.empty_repo?
- if current_user && can?(current_user, :fork_project, @project)
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
- = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has_tooltip' do
+ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has-tooltip' do
= icon('code-fork fw')
Fork
%div.count-with-arrow
@@ -9,10 +9,10 @@
%span.count
= @project.forks_count
- else
- = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do
+ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do
= icon('code-fork fw')
Fork
- %div.count-with-arrow
+ = link_to namespace_project_forks_path(@project.namespace, @project), class: 'count-with-arrow' do
%span.arrow
%span.count
= @project.forks_count
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
index 3e83ec3912f..a3786c35a1f 100644
--- a/app/views/projects/buttons/_notifications.html.haml
+++ b/app/views/projects/buttons/_notifications.html.haml
@@ -14,7 +14,7 @@
= notification_list_item(level, @membership)
- when GroupMember
- .btn.disabled.notifications-btn.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
+ .btn.disabled.notifications-btn.has-tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
= icon('bell')
= notification_label(@membership)
= icon('angle-down')
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 21ba426aaa1..02dbb2985a4 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,5 +1,5 @@
- if current_user
- = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has_tooltip', method: :post, remote: true, title: "Star project" do
+ = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: "Star project" do
- if current_user.starred?(@project)
= icon('star fw')
%span.starred Unstar
@@ -12,7 +12,7 @@
= @project.star_count
- else
- = link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do
+ = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: 'You must sign in to star a project' do
= icon('star fw')
Star
%div.count-with-arrow
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
new file mode 100644
index 00000000000..d22d1da8402
--- /dev/null
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -0,0 +1,76 @@
+%tr.build
+ %td.status
+ - if can?(current_user, :read_build, build)
+ = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
+ - else
+ = ci_status_with_icon(build.status)
+
+ %td.build-link
+ - if can?(current_user, :read_build, build)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
+ %strong ##{build.id}
+ - else
+ %strong ##{build.id}
+
+ - if build.stuck?
+ %i.fa.fa-warning.text-warning
+
+ - if defined?(commit_sha) && commit_sha
+ %td
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
+
+ %td
+ - if build.ref
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
+ - else
+ .light none
+
+ - if defined?(runner) && runner
+ %td
+ - if build.try(:runner)
+ = runner_link(build.runner)
+ - else
+ .light none
+
+ - if defined?(stage) && stage
+ %td
+ = build.stage
+
+ %td
+ = build.name
+
+ .pull-right
+ - if build.tags.any?
+ - build.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if build.try(:trigger_request)
+ %span.label.label-info triggered
+ - if build.try(:allow_failure)
+ %span.label.label-danger allowed to fail
+
+ %td.duration
+ - if build.duration
+ #{duration_in_words(build.finished_at, build.started_at)}
+
+ %td.timestamp
+ - if build.finished_at
+ %span #{time_ago_with_tooltip(build.finished_at)}
+
+ - if defined?(coverage) && coverage
+ %td.coverage
+ - if build.try(:coverage)
+ #{build.coverage}%
+
+ %td
+ .pull-right
+ - if can?(current_user, :read_build, build) && build.artifacts?
+ = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
+ %i.fa.fa-download
+ - if can?(current_user, :update_build, build)
+ - if build.active?
+ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
+ %i.fa.fa-remove.cred
+ - elsif defined?(allow_retry) && allow_retry && build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
+ %i.fa.fa-repeat
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
index befad27666c..003b7c18d0e 100644
--- a/app/views/projects/commit/_builds.html.haml
+++ b/app/views/projects/commit/_builds.html.haml
@@ -43,8 +43,8 @@
%th Coverage
%th
- @ci_commit.refs.each do |ref|
- = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered,
- locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true }
+ - builds = @ci_commit.statuses.for_ref(ref).latest.ordered
+ = render builds, coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true
- if @ci_commit.retried.any?
.gray-content-block.second-block
@@ -64,5 +64,4 @@
- if @ci_commit.project.build_coverage_enabled?
%th Coverage
%th
- = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried,
- locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true }
+ = render @ci_commit.retried, coverage: @ci_commit.project.build_coverage_enabled?, stage: true
diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml
deleted file mode 100644
index a3449d1ae05..00000000000
--- a/app/views/projects/commit_statuses/_commit_status.html.haml
+++ /dev/null
@@ -1,79 +0,0 @@
-%tr.commit_status
- %td.status
- - if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url
- = link_to commit_status.target_url, class: "ci-status ci-#{commit_status.status}" do
- = ci_icon_for_status(commit_status.status)
- = commit_status.status
- - else
- = ci_status_with_icon(commit_status.status)
-
- %td.commit_status-link
- - if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url
- = link_to commit_status.target_url do
- %strong ##{commit_status.id}
- - else
- %strong ##{commit_status.id}
-
- - if commit_status.show_warning?
- %i.fa.fa-warning.text-warning{data: { toggle: "tooltip" }, title: "This build is stuck, open it to know more"}
-
- - if defined?(commit_sha) && commit_sha
- %td
- = link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace"
-
- %td
- - if commit_status.ref
- = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref)
- - else
- .light none
-
- - if defined?(runner) && runner
- %td
- - if commit_status.try(:runner)
- = runner_link(commit_status.runner)
- - else
- .light none
-
- - if defined?(stage) && stage
- %td
- = commit_status.stage
-
- %td
- = commit_status.name
-
- .pull-right
- - if commit_status.tags.any?
- - commit_status.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if commit_status.try(:trigger_request)
- %span.label.label-info triggered
- - if commit_status.try(:allow_failure)
- %span.label.label-danger allowed to fail
-
- %td.duration
- - if commit_status.duration
- #{duration_in_words(commit_status.finished_at, commit_status.started_at)}
-
- %td.timestamp
- - if commit_status.finished_at
- %span #{time_ago_with_tooltip(commit_status.finished_at)}
-
- - if defined?(coverage) && coverage
- %td.coverage
- - if commit_status.try(:coverage)
- #{commit_status.coverage}%
-
- %td
- .pull-right
- - if can?(current_user, :read_commit_status, commit_status) && commit_status.artifacts_download_url
- = link_to commit_status.artifacts_download_url, title: 'Download artifacts' do
- %i.fa.fa-download
- - if can?(current_user, :update_commit_status, commit_status)
- - if commit_status.active?
- - if commit_status.cancel_url
- = link_to commit_status.cancel_url, method: :post, title: 'Cancel' do
- %i.fa.fa-remove.cred
- - elsif defined?(allow_retry) && allow_retry && commit_status.retry_url
- = link_to commit_status.retry_url, method: :post, title: 'Retry' do
- %i.fa.fa-repeat
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index ce60fbdf032..46e4de40042 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -1,11 +1,14 @@
+- commits, hidden = limited_commits(@commits)
+- commits = Commit.decorate(commits, @project)
+
%div.panel.panel-default
.panel-heading
Commits (#{@commits.count})
- - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
- %ul.well-list
- - Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), @project).each do |commit|
+ - if hidden > 0
+ %ul.content-list
+ - commits.each do |commit|
= render "projects/commits/inline_commit", commit: commit, project: @project
%li.warning-row.unstyled
- other #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} commits hidden to prevent performance issues.
+ #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else
- %ul.well-list= render Commit.decorate(@commits, @project), project: @project
+ %ul.content-list= render commits, project: @project
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 6c631228002..64e8da9201d 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -1,7 +1,9 @@
- unless defined?(project)
- project = @project
-- @commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits|
+- commits, hidden = limited_commits(@commits)
+
+- commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits|
.row.commits-row
.col-md-2.hidden-xs.hidden-sm
%h5.commits-row-date
@@ -10,6 +12,10 @@
.light
= pluralize(commits.count, 'commit')
.col-md-10.col-sm-12
- %ul.bordered-list
+ %ul.content-list
= render commits, project: project
%hr.lists-separator
+
+- if hidden > 0
+ .alert.alert-warning
+ #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index 498c5e05b32..7a5b0d993db 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -15,9 +15,9 @@
= nav_link(html_options: {class: branches_tab_class}) do
= link_to namespace_project_branches_path(@project.namespace, @project) do
Branches
- %span.badge.js-totalbranch-count= @repository.branches.size
+ %span.badge.js-totalbranch-count= @repository.branch_count
= nav_link(controller: [:tags, :releases]) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
Tags
- %span.badge.js-totaltags-count= @repository.tags.length
+ %span.badge.js-totaltags-count= @repository.tag_count
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 4ab81f3635c..dd590a4b8ec 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,7 +1,7 @@
= form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do
.clearfix
- if params[:to] && params[:from]
- = link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has_tooltip', title: 'Switch base of comparison'}
+ = link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
.form-group
.input-group.inline-input-group
%span.input-group-addon from
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index d668f483bcb..2e1a37aa06d 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -10,8 +10,8 @@
= parallel_diff_btn
= render 'projects/diffs/stats', diff_files: diff_files
-- if diff_files.count < diffs.size
- = render 'projects/diffs/warning', diffs: diffs, shown_files_count: diff_files.count
+- if diff_files.overflow?
+ = render 'projects/diffs/warning', diff_files: diff_files
.files
- diff_files.each_with_index do |diff_file, index|
@@ -20,11 +20,4 @@
- next unless blob
= render 'projects/diffs/file', i: index, project: project,
- diff_file: diff_file, diff_commit: diff_commit, blob: blob
-
-- if @diff_timeout
- .alert.alert-danger
- %h4
- Failed to collect changes
- %p
- Maybe diff is really big and operation failed with timeout. Try to get diff locally
+ diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diff_refs
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 3ac058a3bf8..698ed02ea0e 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -28,7 +28,7 @@
.file-actions.hidden-xs
- if blob_text_viewable?(blob)
- = link_to '#', class: 'js-toggle-diff-comments btn active has_tooltip', title: "Toggle comments for this file" do
+ = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file" do
= icon('comments')
\
@@ -42,13 +42,17 @@
.diff-content.diff-wrap-lines
-# Skipp all non non-supported blobs
- return unless blob.respond_to?('text?')
- - if blob_text_viewable?(blob)
- - if diff_view == 'parallel'
- = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
- - else
- = render "projects/diffs/text_file", diff_file: diff_file, index: i
- - elsif blob.image?
- - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file)
- = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i
+ - if diff_file.too_large?
+ .nothing-here-block
+ This diff could not be displayed because it is too large.
- else
- .nothing-here-block No preview for this file type
+ - if blob_text_viewable?(blob)
+ - if diff_view == 'parallel'
+ = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
+ - else
+ = render "projects/diffs/text_file", diff_file: diff_file, index: i
+ - elsif blob.image?
+ - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file)
+ = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i, diff_refs: diff_refs
+ - else
+ .nothing-here-block No preview for this file type
diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml
index 752e92e2e6b..8367112a9cb 100644
--- a/app/views/projects/diffs/_image.html.haml
+++ b/app/views/projects/diffs/_image.html.haml
@@ -1,6 +1,7 @@
- diff = diff_file.diff
- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))
-- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))
+- old_commit_id = diff_refs.first.id
+- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))
- if diff.renamed_file || diff.new_file || diff.deleted_file
.image
%span.wrap
@@ -12,7 +13,7 @@
%div.two-up.view
%span.wrap
.frame.deleted
- %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))}
+ %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))}
%img{src: old_file_raw_path}
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size old_file.size}"
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
new file mode 100644
index 00000000000..9464c8dc996
--- /dev/null
+++ b/app/views/projects/diffs/_line.html.haml
@@ -0,0 +1,26 @@
+- type = line.type
+%tr.line_holder{id: line_code, class: type}
+ - case type
+ - when 'match'
+ = render "projects/diffs/match_line", {line: line.text,
+ line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file}
+ - when 'nonewline'
+ %td.old_line.diff-line-num
+ %td.new_line.diff-line-num
+ %td.line_content.match= line.text
+ - else
+ %td.old_line.diff-line-num{class: type}
+ - link_text = raw(type == "new" ? "&nbsp;" : line.old_pos)
+ - if defined?(plain) && plain
+ = link_text
+ - else
+ = link_to link_text, "##{line_code}", id: line_code
+ - if @comments_allowed && can?(current_user, :create_note, @project)
+ = link_to_new_diff_note(line_code)
+ %td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}}
+ - link_text = raw(type == "old" ? "&nbsp;" : line.new_pos)
+ - if defined?(plain) && plain
+ = link_text
+ - else
+ = link_to link_text, "##{line_code}", id: line_code
+ %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text)
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index d75e9ef2a49..e7169d7b599 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -6,28 +6,11 @@
%table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' }
- last_line = 0
- - raw_diff_lines = diff_file.diff_lines
+ - raw_diff_lines = diff_file.diff_lines.to_a
- diff_file.highlighted_diff_lines.each_with_index do |line, index|
- - type = line.type
- - last_line = line.new_pos
- line_code = generate_line_code(diff_file.file_path, line)
- - line_old = line.old_pos
- %tr.line_holder{ id: line_code, class: "#{type}" }
- - if type == "match"
- = render "projects/diffs/match_line", {line: line.text,
- line_old: line_old, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file}
- - elsif type == 'nonewline'
- %td.old_line.diff-line-num
- %td.new_line.diff-line-num
- %td.line_content.match= line.text
- - else
- %td.old_line.diff-line-num{class: type}
- = link_to raw(type == "new" ? "&nbsp;" : line_old), "##{line_code}", id: line_code
- - if @comments_allowed && can?(current_user, :create_note, @project)
- = link_to_new_diff_note(line_code)
- %td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}}
- = link_to raw(type == "old" ? "&nbsp;" : line.new_pos), "##{line_code}", id: line_code
- %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text)
+ - last_line = line.new_pos
+ = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code}
- if @reply_allowed
- comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at)
diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml
index f99bc9a85eb..15536c17f8e 100644
--- a/app/views/projects/diffs/_warning.html.haml
+++ b/app/views/projects/diffs/_warning.html.haml
@@ -3,17 +3,16 @@
Too many changes to show.
.pull-right
- unless diff_hard_limit_enabled?
- = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: nil)), class: "btn btn-sm btn-warning"
+ = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: nil)), class: "btn btn-sm"
- if current_controller?(:commit) or current_controller?(:merge_requests)
- if current_controller?(:commit)
- = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-warning btn-sm"
- = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-warning btn-sm"
+ = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-sm"
+ = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-sm"
- elsif @merge_request && @merge_request.persisted?
- = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-warning btn-sm"
- = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-warning btn-sm"
+ = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-sm"
+ = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-sm"
%p
To preserve performance only
- %strong #{shown_files_count} of #{diffs.size}
+ %strong #{diff_files.count} of #{diff_files.real_size}
files are displayed.
-
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 042f660077e..6d872cd0b21 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,5 +1,3 @@
-- @blank_container = true
-
.project-edit-container.prepend-top-default
.project-edit-errors
.project-edit-content
@@ -86,6 +84,8 @@
%br
%span.descr Share code pastes with others out of git repository
+ = render 'builds_settings', f: f
+
%fieldset.features
%legend
Project avatar:
@@ -112,69 +112,6 @@
%hr
= link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
- %fieldset.features
- %legend
- Continuous Integration
- .form-group
- .col-sm-offset-2.col-sm-10
- %p Get recent application code using the following command:
- .radio
- = f.label :build_allow_git_fetch_false do
- = f.radio_button :build_allow_git_fetch, 'false'
- %strong git clone
- %br
- %span.descr Slower but makes sure you have a clean dir before every build
- .radio
- = f.label :build_allow_git_fetch_true do
- = f.radio_button :build_allow_git_fetch, 'true'
- %strong git fetch
- %br
- %span.descr Faster
-
- .form-group
- = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label'
- .col-sm-10
- = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
- %p.help-block per build in minutes
- .form-group
- = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label'
- .col-sm-10
- .input-group
- %span.input-group-addon /
- = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
- %span.input-group-addon /
- %p.help-block
- We will use this regular expression to find test coverage output in build trace.
- Leave blank if you want to disable this feature
- .bs-callout.bs-callout-info
- %p Below are examples of regex for existing tools:
- %ul
- %li
- Simplecov (Ruby) -
- %code \(\d+.\d+\%\) covered
- %li
- pytest-cov (Python) -
- %code \d+\%\s*$
- %li
- phpunit --coverage-text --colors=never (PHP) -
- %code ^\s*Lines:\s*\d+.\d+\%
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :public_builds do
- = f.check_box :public_builds
- %strong Public builds
- .help-block Allow everyone to access builds for Public and Internal projects
-
- %fieldset.features
- %legend
- Advanced settings
- .form-group
- = f.label :runners_token, "CI token", class: 'control-label'
- .col-sm-10
- = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
- %p.help-block The secure token used to checkout project.
.form-actions
= f.submit 'Save changes', class: "btn btn-save"
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index b34d106d565..6ad7b05155a 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -18,7 +18,7 @@
= link_to "adding README", new_readme_path, class: 'underlined-link'
file to this project.
-- if can?(current_user, :download_code, @project)
+- if can?(current_user, :push_code, @project)
%div{ class: container_class }
.prepend-top-20
.empty_wrapper
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index 905f6bbbd48..1fe1d98bf13 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -2,7 +2,7 @@
- header_title project_title(@project, "Files", project_files_path(@project))
.file-finder-holder.tree-holder.clearfix
- .gray-content-block.top-block
+ .nav-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'find_file', path: @path
%ul.breadcrumb.repo-breadcrumb
diff --git a/app/views/projects/forks/_projects.html.haml b/app/views/projects/forks/_projects.html.haml
new file mode 100644
index 00000000000..2946e6dcbd0
--- /dev/null
+++ b/app/views/projects/forks/_projects.html.haml
@@ -0,0 +1,2 @@
+= render 'shared/projects/list', projects: projects, use_creator_avatar: true,
+ forks: true, show_last_commit_as_description: true
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 42fa6fdb782..4bcf2d9d533 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -1,13 +1,12 @@
.top-area
.nav-text
- - public_count = @public_forks.size
- - protected_count = @protected_forks.size
- - full_count_title = "#{public_count} public and #{protected_count} private"
- == #{pluralize(@all_forks.size, 'fork')}: #{full_count_title}
+ - full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private"
+ == #{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
.nav-controls
- = search_field_tag :filter_projects, nil, placeholder: 'Search forks', class: 'projects-list-filter project-filter-form-field form-control input-short',
- spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
+ = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = search_field_tag :filter_projects, nil, placeholder: 'Search forks', class: 'projects-list-filter project-filter-form-field form-control input-short',
+ spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
.dropdown
%button.dropdown-toggle.btn.sort-forks{type: 'button', 'data-toggle' => 'dropdown'}
@@ -40,18 +39,10 @@
Fork
-.projects-list-holder
- - if @public_forks.blank?
- %ul.content-list
- %li
- .nothing-here-block No forks to show
- - else
- = render 'shared/projects/list', projects: @public_forks, use_creator_avatar: true,
- forks: true, show_last_commit_as_description: true
+= render 'projects', projects: @forks
- - if protected_count > 0
- %ul.projects-list.private-forks-notice
- %li.project-row
- = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
- %strong= pluralize(protected_count, 'private fork')
- %span you have no access to.
+- if @private_forks_count > 0
+ .private-forks-notice
+ = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
+ %strong= pluralize(@private_forks_count, 'private fork')
+ %span you have no access to.
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index edabc2d3b44..73a7fc0e1ac 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -12,7 +12,7 @@
.col-md-2.col-sm-3
- if fork = namespace.find_fork_of(@project)
.fork-thumbnail
- = link_to project_path(fork), title: "Visit project fork", class: 'has_tooltip' do
+ = link_to project_path(fork), title: "Visit project fork", class: 'has-tooltip' do
= image_tag namespace_icon(namespace, 100)
.caption
%strong
@@ -22,7 +22,7 @@
- else
.fork-thumbnail
- = link_to namespace_project_forks_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do
+ = link_to namespace_project_forks_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has-tooltip' do
= image_tag namespace_icon(namespace, 100)
.caption
%strong
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
new file mode 100644
index 00000000000..c15386b4883
--- /dev/null
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -0,0 +1,58 @@
+%tr.generic_commit_status
+ %td.status
+ - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
+ = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url)
+ - else
+ = ci_status_with_icon(generic_commit_status.status)
+
+ %td.generic_commit_status-link
+ - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
+ = link_to generic_commit_status.target_url do
+ %strong ##{generic_commit_status.id}
+ - else
+ %strong ##{generic_commit_status.id}
+
+ - if defined?(commit_sha) && commit_sha
+ %td
+ = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
+
+ %td
+ - if generic_commit_status.ref
+ = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
+ - else
+ .light none
+
+ - if defined?(runner) && runner
+ %td
+ - if generic_commit_status.try(:runner)
+ = runner_link(generic_commit_status.runner)
+ - else
+ .light none
+
+ - if defined?(stage) && stage
+ %td
+ = generic_commit_status.stage
+
+ %td
+ = generic_commit_status.name
+
+ .pull-right
+ - if generic_commit_status.tags.any?
+ - generic_commit_status.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+
+ %td.duration
+ - if generic_commit_status.duration
+ #{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)}
+
+ %td.timestamp
+ - if generic_commit_status.finished_at
+ %span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
+
+ - if defined?(coverage) && coverage
+ %td.coverage
+ - if generic_commit_status.try(:coverage)
+ #{generic_commit_status.coverage}%
+
+ %td
diff --git a/app/views/projects/go_import.html.haml b/app/views/projects/go_import.html.haml
deleted file mode 100644
index 87ac75a350f..00000000000
--- a/app/views/projects/go_import.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-!!! 5
-%html
- %head
- - web_url = [Gitlab.config.gitlab.url, @namespace, @id].join('/')
- %meta{name: "go-import", content: "#{web_url.split('://')[1]} git #{web_url}.git"}
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
new file mode 100644
index 00000000000..13f5fc141fa
--- /dev/null
+++ b/app/views/projects/group_links/index.html.haml
@@ -0,0 +1,41 @@
+- page_title "Groups"
+%h3.page_title Share project with other groups
+%p.light
+ Projects can be stored in only one group at once. However you can share a project with other groups here.
+%hr
+- if @group_links.present?
+ .enabled-groups.panel.panel-default
+ .panel-heading
+ Already shared with
+ %ul.well-list
+ - @group_links.each do |group_link|
+ - group = group_link.group
+ %li
+ .pull-right
+ = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: 'btn btn-sm' do
+ %i.icon-remove
+ disable sharing
+ = link_to group do
+ %strong
+ %i.icon-folder-open
+ = group.name
+ %br
+ .light up to #{group_link.human_access}
+
+
+.available-groups
+ %h4
+ Can be shared with
+ %div
+ = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post, class: 'form-horizontal' do
+ .form-group
+ = label_tag :link_group_id, 'Group', class: 'control-label'
+ .col-sm-10
+ = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path))
+ .form-group
+ = label_tag :link_group_access, 'Max access level', class: 'control-label'
+ .col-sm-10
+ = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control"
+ .form-actions
+ = submit_tag "Share", class: "btn btn-create"
+
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index a0511819c9f..67d016bd871 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -1,9 +1,9 @@
-- page_title "Web Hooks"
+- page_title "Webhooks"
%h3.page-title
- Web hooks
+ Webhooks
%p.light
- #{link_to "Web hooks ", help_page_path("web_hooks", "web_hooks"), class: "vlink"} can be
+ #{link_to "Webhooks ", help_page_path("web_hooks", "web_hooks"), class: "vlink"} can be
used for binding events when something is happening within the project.
%hr.clearfix
@@ -70,12 +70,12 @@
= f.check_box :enable_ssl_verification
%strong Enable SSL verification
.form-actions
- = f.submit "Add Web Hook", class: "btn btn-create"
+ = f.submit "Add Webhook", class: "btn btn-create"
-if @hooks.any?
.panel.panel-default
.panel-heading
- Web hooks (#{@hooks.count})
+ Webhooks (#{@hooks.count})
%ul.well-list
- @hooks.each do |hook|
%li
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 673020a4e30..b151393abab 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,7 +1,7 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
- = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'projects/notes/notes_with_form'
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 6588d9bdbe1..33c48199ba5 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-requires-input' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-quick-submit js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
:javascript
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 5b0edcfa978..4aa92d0b39e 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -3,10 +3,11 @@
.issue-check
= check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
- .issue-title
+ .issue-title.title
%span.issue-title-text
- = link_to_gfm issue.title, issue_path(issue), class: "row_title"
- %ul.controls.light
+ = confidential_icon(issue)
+ = link_to_gfm issue.title, issue_path(issue)
+ %ul.controls
- if issue.closed?
%li
CLOSED
@@ -16,23 +17,15 @@
= link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
- upvotes, downvotes = issue.upvotes, issue.downvotes
- - if upvotes > 0 || downvotes > 0
+ - if upvotes > 0
%li
= icon('thumbs-up')
= upvotes
- - else
- %li{ class: 'issue-no-votes' }
- = icon('thumbs-up')
- = upvotes
- - if upvotes > 0 || downvotes > 0
+ - if downvotes > 0
%li
= icon('thumbs-down')
= downvotes
- - else
- %li{ class: 'issue-no-votes' }
- = icon('thumbs-down')
- = downvotes
- note_count = issue.notes.user.count
- if note_count > 0
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 640a1962ffc..d6b38b327ff 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -1,4 +1,4 @@
--if @merge_requests.any?
+- if @merge_requests.any?
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list
@@ -11,7 +11,7 @@
- elsif has_any_ci
= icon('blank fw')
%span.merge-request-id
- \!#{merge_request.iid}
+ = merge_request.to_reference
%span.merge-request-info
%strong
= link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
new file mode 100644
index 00000000000..6da8e4f33a9
--- /dev/null
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -0,0 +1,5 @@
+- if current_user && can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
+ .pull-right
+ = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name do
+ = icon('code-fork')
+ New Branch
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
new file mode 100644
index 00000000000..b10cd03515f
--- /dev/null
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -0,0 +1,15 @@
+- if @related_branches.any?
+ %h2.related-branches-title
+ = pluralize(@related_branches.count, 'Related Branch')
+ %ul.unstyled-list
+ - @related_branches.each do |branch|
+ %li
+ - sha = @project.repository.find_branch(branch).target
+ - ci_commit = @project.ci_commit(sha) if sha
+ - if ci_commit
+ %span.related-branch-ci-status
+ = render_ci_status(ci_commit)
+ %span.related-branch-info
+ %strong
+ = link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do
+ = branch
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index fde9304c0f8..efa7642b2dc 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -11,6 +11,8 @@
- if current_user
= link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do
= icon('rss')
+ %span.icon-label
+ Subscribe
= render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
- if can? current_user, :create_issue, @project
= link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 69a0e2a0c4d..6fa059cbe68 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -5,32 +5,50 @@
= render "header_title"
.issue
- .detail-page-header
- .pull-right
+ .detail-page-header.issuable-header
+ .pull-left
+ .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"}
+ %span.hidden-xs
+ Closed
+ %span.hidden-sm.hidden-md.hidden-lg
+ = icon('check')
+ .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"}
+ %span.hidden-xs
+ Open
+ %span.hidden-sm.hidden-md.hidden-lg
+ = icon('circle-o')
+
+ %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+
+ .issue-meta
+ = confidential_icon(@issue)
+ %strong.identifier
+ Issue ##{@issue.iid}
+ %span.creator
+ opened
+ .editor-details
+ .editor-details
+ = time_ago_with_tooltip(@issue.created_at)
+ by
+ %strong
+ = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-xs")
+ %strong
+ = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
+ by_username: true, avatar: false)
+
+ .pull-right.issue-btn-group
- if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New Issue', id: 'new_issue_link' do
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
= icon('plus')
- New Issue
+ New issue
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
- = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
-
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do
= icon('pencil-square-o')
Edit
- .pull-left
- .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} Closed
- .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} Open
-
- .issue-meta
- %span.identifier
- Issue ##{@issue.iid}
- %span.creator
- &middot;
- by #{link_to_member(@project, @issue.author, size: 24)}
- &middot;
- = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago')
.issue-details.issuable-details
.detail-page-description.content-block
@@ -44,15 +62,14 @@
= markdown(@issue.description, cache_key: [@issue, "description"])
%textarea.hidden.js-task-list-field
= @issue.description
- - if @issue.updated_at != @issue.created_at
- %small
- Edited
- = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago')
+ = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
.merge-requests
= render 'merge_requests'
+ = render 'related_branches'
- .content-block
+ .content-block.content-block-small
+ = render 'new_branch'
= render 'votes/votes_block', votable: @issue
.row
diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml
index d63d3a3ec20..be7a0bb5628 100644
--- a/app/views/projects/labels/_form.html.haml
+++ b/app/views/projects/labels/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
-if @label.errors.any?
.row
.col-sm-offset-2.col-sm-10
@@ -10,7 +10,7 @@
.form-group
= f.label :title, class: 'control-label'
.col-sm-10
- = f.text_field :title, class: "form-control js-quick-submit", required: true, autofocus: true
+ = f.text_field :title, class: "form-control", required: true, autofocus: true
.form-group
= f.label :description, class: 'control-label'
.col-sm-10
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index 5b35acc66c0..0612863296a 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -3,9 +3,23 @@
.pull-right
%strong.append-right-20
+ = link_to_label(label, type: :merge_request) do
+ = pluralize label.open_merge_requests_count, 'open merge request'
+
+ %strong.append-right-20
= link_to_label(label) do
- = pluralize label.open_issues_count, 'open issue'
+ = pluralize label.open_issues_count(current_user), 'open issue'
+
+ - if current_user
+ .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
+ .subscription-status{data: {status: label_subscription_status(label)}}
+ %button.btn.btn-sm.btn-info.subscribe-button
+ %span= label_subscription_toggle_button_text(label)
- if can? current_user, :admin_label, @project
= link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm'
= link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
+
+- if current_user
+ :javascript
+ new Subscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index 1c7de94acfd..393998f15b9 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -1,8 +1,8 @@
- content_for :note_actions do
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
- = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request"
+ = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- if @merge_request.closed?
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request"
+ = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index b230b3a0110..391193eed6c 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,8 +1,8 @@
%li{ class: mr_css_classes(merge_request) }
- .merge-request-title
+ .merge-request-title.title
%span.merge-request-title-text
- = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
- %ul.controls.light
+ = link_to_gfm merge_request.title, merge_request_path(merge_request)
+ %ul.controls
- if merge_request.merged?
%li
MERGED
@@ -17,7 +17,7 @@
- if merge_request.open? && merge_request.broken?
%li
- = link_to merge_request_path(merge_request), class: "has_tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do
+ = link_to merge_request_path(merge_request), class: "has-tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do
= icon('exclamation-triangle')
- if merge_request.assignee
@@ -25,23 +25,15 @@
= link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
- upvotes, downvotes = merge_request.upvotes, merge_request.downvotes
- - if upvotes > 0 || downvotes > 0
+ - if upvotes > 0
%li
= icon('thumbs-up')
= upvotes
- - else
- %li{ class: 'merge-request-no-votes' }
- = icon('thumbs-up')
- = upvotes
- - if upvotes > 0 || downvotes > 0
+ - if downvotes > 0
%li
= icon('thumbs-down')
= downvotes
- - else
- %li{ class: 'merge-request-no-votes' }
- = icon('thumbs-down')
- = downvotes
- note_count = merge_request.mr_and_commit_notes.user.count
- if note_count > 0
@@ -56,7 +48,7 @@
= note_count
.merge-request-info
- \##{merge_request.iid} &middot;
+ #{merge_request.to_reference} &middot;
opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
by #{link_to_member(@project, merge_request.author, avatar: false)}
- if merge_request.target_project.default_branch != merge_request.target_branch
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 236a545c840..01dc7519bee 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -33,23 +33,18 @@
%div= msg
- elsif @merge_request.source_branch.present? && @merge_request.target_branch.present?
- - if @merge_request.compare_failed
- .alert.alert-danger
- %h4 Compare failed
- %p We can't compare selected branches. It may be because of huge diff. Please try again or select different branches.
- - else
- .light-well.append-bottom-default
- .center
- %h4
- There isn't anything to merge.
- %p.slead
- - if @merge_request.source_branch == @merge_request.target_branch
- You'll need to use different branch names to get a valid comparison.
- - else
- %span.label-branch #{@merge_request.source_branch}
- and
- %span.label-branch #{@merge_request.target_branch}
- are the same.
+ .light-well.append-bottom-default
+ .center
+ %h4
+ There isn't anything to merge.
+ %p.slead
+ - if @merge_request.source_branch == @merge_request.target_branch
+ You'll need to use different branch names to get a valid comparison.
+ - else
+ %span.label-branch #{@merge_request.source_branch}
+ and
+ %span.label-branch #{@merge_request.target_branch}
+ are the same.
.form-actions
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 4c5a9818e3e..9e59f7df71b 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -31,22 +31,18 @@
%li.diffs-tab.active
= link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
Changes
- %span.badge= @diffs.size
+ %span.badge= @diffs.real_size
.tab-content
#commits.commits.tab-pane
= render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane.active
- - if @diffs.present?
- = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs
- - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
+ - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
.alert.alert-danger
%h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits.
%p To preserve performance the line changes are not shown.
- else
- .alert.alert-danger
- %h4 This comparison includes a huge diff.
- %p To preserve performance the line changes are not shown.
+ = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs
- if @ci_commit
#builds.builds.tab-pane
= render "projects/merge_requests/show/builds"
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 648512e5379..1dd8f721f7e 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,4 +1,4 @@
-- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
@@ -10,7 +10,7 @@
.merge-request{'data-url' => merge_request_path(@merge_request)}
= render "projects/merge_requests/show/mr_title"
- .merge-request-details.issuable-details
+ .merge-request-details.issuable-details{data: {id: @merge_request.project.id}}
= render "projects/merge_requests/show/mr_box"
.append-bottom-default.mr-source-target.prepend-top-default
- if @merge_request.open?
@@ -34,6 +34,8 @@
%span into
= link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do
= @merge_request.target_branch
+ - if @merge_request.open? && @merge_request.diverged_from_target_branch?
+ %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
= render "projects/merge_requests/show/how_to_merge"
= render "projects/merge_requests/widget/show.html.haml"
@@ -62,11 +64,11 @@
%li.diffs-tab
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
Changes
- %span.badge= @merge_request.diffs.size
+ %span.badge= @merge_request.diff_size
.tab-content
#notes.notes.tab-pane.voting_notes
- .content-block.oneline-block
+ .content-block.content-block-small.oneline-block
= render 'votes/votes_block', votable: @merge_request
.row
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 64cd484193e..1b0bae86ad4 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,5 +1,5 @@
- if @merge_request_diff.collected?
- = render "projects/diffs/diffs", diffs: params[:w] == '1' ? @merge_request.diffs_no_whitespace : @merge_request.diffs,
+ = render "projects/diffs/diffs", diffs: @merge_request.diffs(diff_options),
project: @merge_request.project, diff_refs: @merge_request.diff_refs
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index 602f787e6cf..a23bd8d18d0 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -11,7 +11,4 @@
%textarea.hidden.js-task-list-field
= @merge_request.description
- - if @merge_request.updated_at != @merge_request.created_at
- %small
- Edited
- = time_ago_with_tooltip(@merge_request.updated_at, placement: 'bottom')
+ = edited_time_ago_with_tooltip(@merge_request, placement: 'bottom')
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index 14ea7b17786..ab4b1f14be5 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -1,20 +1,35 @@
.detail-page-header
.status-box{ class: status_box_class(@merge_request) }
- = @merge_request.state_human_name
- %span.identifier
- Merge Request ##{@merge_request.iid}
- %span.creator
- &middot;
- by #{link_to_member(@project, @merge_request.author, size: 24)}
- &middot;
- = time_ago_with_tooltip(@merge_request.created_at)
+ %span.hidden-xs
+ = @merge_request.state_human_name
+ %span.hidden-sm.hidden-md.hidden-lg
+ = icon(@merge_request.state_icon_name)
+ %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+ .issue-meta
+ %strong.identifier
+ %span.hidden-sm.hidden-md.hidden-lg
+ MR
+ %span.hidden-xs
+ Merge Request
+ !#{@merge_request.iid}
+ %span.creator
+ opened
+ .editor-details
+ = time_ago_with_tooltip(@merge_request.created_at)
+ by
+ %strong
+ = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-xs")
+ %strong
+ = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
+ by_username: true, avatar: false)
.issue-btn-group.pull-right
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close merge request'
= link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-nr btn-grouped issuable-edit', id: 'edit_merge_request' do
- %i.fa.fa-pencil-square-o
+ = icon('pencil-square-o')
Edit
- if @merge_request.closed?
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-nr btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request'
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index d9a1730a8bc..807833741af 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -1,6 +1,6 @@
- status_class = @ci_commit ? " ci-#{@ci_commit.status}" : nil
-= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f|
+= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
= hidden_field_tag :authenticity_token, form_authenticity_token
.accept-merge-holder.clearfix.js-toggle-container
.clearfix
diff --git a/app/views/projects/merge_requests/widget/open/_wip.html.haml b/app/views/projects/merge_requests/widget/open/_wip.html.haml
index 0cf16542cc1..c296422a9cf 100644
--- a/app/views/projects/merge_requests/widget/open/_wip.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_wip.html.haml
@@ -1,5 +1,11 @@
%h4
This merge request is currently a Work In Progress
-%p
- When this merge request is ready, remove the "WIP" prefix from the title to allow it to be merged.
+- if can?(current_user, :update_merge_request, @merge_request)
+ %p
+ When this merge request is ready,
+ = link_to remove_wip_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), method: :post do
+ remove the
+ %code WIP:
+ prefix from the title
+ to allow it to be merged.
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 39aa2437e18..23f2bca7baf 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-requires-input'} do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f|
-if @milestone.errors.any?
.alert.alert-danger
%ul
@@ -9,12 +9,12 @@
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
- = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true
+ = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
- = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
+ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
= render 'projects/notes/hints'
.clearfix
.error-alert
diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml
deleted file mode 100644
index ca51b8c745d..00000000000
--- a/app/views/projects/milestones/_issue.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) }
- %span
- = link_to_gfm issue.title, [@project.namespace.becomes(Namespace), @project, issue], title: issue.title
- .issue-detail
- = link_to [@project.namespace.becomes(Namespace), @project, issue] do
- %span.issue-number ##{issue.iid}
- - issue.labels.each do |label|
- = render_colored_label(label)
- - if issue.assignee
- = image_tag avatar_icon(issue.assignee, 16), class: "avatar s24", alt: ''
diff --git a/app/views/projects/milestones/_issues.html.haml b/app/views/projects/milestones/_issues.html.haml
deleted file mode 100644
index 6f8a341e478..00000000000
--- a/app/views/projects/milestones/_issues.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-.panel.panel-default
- .panel-heading
- = title
- .pull-right= issues.size
- %ul{ class: "well-list issues-sortable-list", id: "issues-list-#{id}", "data-state" => id }
- - issues.sort_by(&:position).each do |issue|
- = render 'issue', issue: issue
diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml
deleted file mode 100644
index a1033607c5d..00000000000
--- a/app/views/projects/milestones/_merge_request.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => merge_request_path(merge_request) }
- %span.str-truncated
- = link_to [@project.namespace.becomes(Namespace), @project, merge_request] do
- %span.cgray ##{merge_request.iid}
- = link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title
- .pull-right.assignee-icon
- - if merge_request.assignee
- = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: ''
diff --git a/app/views/projects/milestones/_merge_requests.html.haml b/app/views/projects/milestones/_merge_requests.html.haml
deleted file mode 100644
index 9a5a02af215..00000000000
--- a/app/views/projects/milestones/_merge_requests.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-.panel.panel-default
- .panel-heading= title
- %ul{ class: "well-list merge_requests-sortable-list", id: "merge_requests-list-#{id}", "data-state" => id }
- - merge_requests.sort_by(&:position).each do |merge_request|
- = render 'merge_request', merge_request: merge_request
diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml
index 67d95ab0364..77b566db6b6 100644
--- a/app/views/projects/milestones/_milestone.html.haml
+++ b/app/views/projects/milestones/_milestone.html.haml
@@ -1,31 +1,5 @@
-%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) }
- .row
- .col-sm-6
- %strong
- = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
-
- .col-sm-6
- .pull-right.light #{milestone.percent_complete}% complete
- .row
- .col-sm-6
- = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do
- = pluralize milestone.issues.count, 'Issue'
- &middot;
- = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do
- = pluralize milestone.merge_requests.count, 'Merge Request'
- .col-sm-6
- = milestone_progress_bar(milestone)
-
- .row
- .col-sm-6
- = render 'shared/milestone_expired', milestone: milestone
- .col-sm-6
- - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
- = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do
- = icon('pencil-square-o')
- Edit
- \
- = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close"
- = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
- = icon('trash-o')
- Delete
+= render 'shared/milestones/milestone',
+ milestone_path: namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone),
+ issues_path: namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title),
+ merge_requests_path: namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title),
+ milestone: milestone
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 631bc8c3e9d..be63875ab34 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -42,104 +42,9 @@
= preserve do
= markdown @milestone.description
-- if @milestone.issues.any? && @milestone.can_be_closed?
+- if @milestone.complete?(current_user) && @milestone.active?
.alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close milestone now.
-.context.prepend-top-default
- .milestone-summary
- %h4 Progress
- %strong= @milestone.issues.count
- issues:
- %span.milestone-stat
- %strong= @milestone.open_items_count
- open and
- %strong= @milestone.closed_items_count
- closed
- %span.milestone-stat
- %strong== #{@milestone.percent_complete}%
- complete
- %span.milestone-stat
- %span.time-elapsed
- %strong== #{@milestone.percent_time_used}%
- time elapsed
- %span.pull-right.tab-issues-buttons
- - if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
- %i.fa.fa-plus
- New Issue
- - if can?(current_user, :read_issue, @project)
- = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped"
- %span.pull-right.tab-merge-requests-buttons.hidden
- - if can?(current_user, :read_merge_request, @project)
- = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped"
-
- = milestone_progress_bar(@milestone)
-
-%ul.nav-links.no-top.no-bottom
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
- Issues
- %span.badge= @issues.count
- %li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
- Merge Requests
- %span.badge= @merge_requests.count
- %li
- = link_to '#tab-participants', 'data-toggle' => 'tab' do
- Participants
- %span.badge= @users.count
- %li
- = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
- Labels
- %span.badge= @labels.count
-
-.tab-content.milestone-content
- .tab-pane.active#tab-issues
- .row.prepend-top-default
- .col-md-4
- = render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned, id: 'unassigned')
- .col-md-4
- = render('issues', title: 'Ongoing Issues (open and assigned)', issues: @issues.opened.assigned, id: 'ongoing')
- .col-md-4
- = render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed')
-
- .tab-pane#tab-merge-requests
- .row.prepend-top-default
- .col-md-3
- = render('merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: @merge_requests.opened.unassigned, id: 'unassigned')
- .col-md-3
- = render('merge_requests', title: 'Waiting for merge (open and assigned)', merge_requests: @merge_requests.opened.assigned, id: 'ongoing')
- .col-md-3
- = render('merge_requests', title: 'Rejected (closed)', merge_requests: @merge_requests.closed, id: 'closed')
- .col-md-3
- .panel.panel-primary
- .panel-heading Merged
- %ul.well-list
- - @merge_requests.merged.each do |merge_request|
- = render 'merge_request', merge_request: merge_request
-
- .tab-pane#tab-participants
- %ul.bordered-list
- - @users.each do |user|
- %li
- = link_to user, title: user.name, class: "darken" do
- = image_tag avatar_icon(user, 32), class: "avatar s32"
- %strong= truncate(user.name, lenght: 40)
- %br
- %small.cgray= user.username
-
- .tab-pane#tab-labels
- %ul.bordered-list.manage-labels-list
- - @labels.each do |label|
- %li
- = render_colored_label(label)
- - args = [@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title, label_name: label.title]
- - options = args.extract_options!
-
- %span.issues-count
- = link_to namespace_project_issues_path(*args, options.merge(state: 'opened')) do
- = pluralize label.open_issues_count, 'open issue'
- %span.issues-count
- = link_to namespace_project_issues_path(*args, options.merge(state: 'closed')) do
- = pluralize label.closed_issues_count, 'closed issue'
+= render 'shared/milestones/summary', milestone: @milestone, project: @project
+= render 'shared/milestones/tabs', milestone: @milestone
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 5d78652befa..2999befffc6 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -1,10 +1,10 @@
.note-edit-form
- = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f|
+ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f|
= note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field js-quick-submit'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
= render 'projects/notes/hints'
- .note-form-actions
+ .note-form-actions.clearfix
= f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button'
= link_to 'Cancel', '#', class: 'btn btn-nr btn-cancel note-edit-cancel'
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index f10a4145d62..f675f092da1 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= note_target_fields(@note)
@@ -8,11 +8,12 @@
= f.hidden_field :noteable_type
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-quick-submit'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text'
= render 'projects/notes/hints'
.error-alert
.note-form-actions.clearfix
- = f.submit 'Add Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button"
+ = f.submit 'Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button"
= yield(:note_actions)
- %a.btn.btn-nr.btn-cancel.js-close-discussion-note-form Cancel
+ %a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}}
+ Discard draft
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index e858c412836..2cf32e6093d 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -27,20 +27,13 @@
%span.note-last-update
%a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'}
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago')
- - if note.updated_at != note.created_at
- %span
- &middot;
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago')
- - if note.updated_by && note.updated_by != note.author
- by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)}
-
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
= preserve do
= markdown(note.note, pipeline: :note, cache_key: [note, "note"])
- if note_editable?(note)
= render 'projects/notes/edit_form', note: note
+ = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note.attachment.url
.note-attachment
@@ -54,4 +47,3 @@
= link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note),
title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
= icon('trash-o', class: 'cred')
- .clear
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
new file mode 100644
index 00000000000..62888e41935
--- /dev/null
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -0,0 +1,21 @@
+- @project_group_links.each do |group_links|
+ - shared_group = group_links.group
+ - shared_group_users_count = group_links.group.group_members.count
+ .panel.panel-default
+ .panel-heading
+ Shared with
+ %strong #{shared_group.name}
+ group, members with
+ %strong #{group_links.human_access}
+ role (#{shared_group_users_count})
+ - if current_user.can?(:admin_group, shared_group)
+ .panel-head-actions
+ = link_to group_group_members_path(shared_group), class: 'btn btn-sm' do
+ %i.fa.fa-pencil-square-o
+ Edit group members
+ %ul.content-list
+ - shared_group.group_members.order('access_level DESC').limit(20).each do |member|
+ = render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false
+ - if shared_group_users_count > 20
+ %li
+ and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)}
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 0f8848a5cbe..ebcfc907ebb 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -18,3 +18,6 @@
- if @group
= render "group_members", members: @group_members
+
+ - if @project_group_links.any? && @project.allowed_to_share_with_group?
+ = render "shared_group_members"
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index db7f244d002..8ee2aef0e61 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -8,12 +8,9 @@
row.find("td.tree_time_ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
row.find("td.tree_commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
-- if @logs.present?
+- if @more_log_url
:plain
- var current_url = location.href.replace(/\/?$/, '/');
- var log_url = "#{escape_javascript(@log_url)}".replace(/\/?$/, '/');
-
- if(current_url == log_url) {
+ if($('#tree-slider').length) {
// Load more commit logs for each file in tree
// if we still on the same page
var url = "#{escape_javascript(@more_log_url)}";
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index bc80f2f29ad..c4a3f06ee06 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -9,9 +9,9 @@
%strong #{@tag.name}
.prepend-top-default
- = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form' }) do |f|
+ = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :description, classes: 'description js-quick-submit form-control'
+ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
= render 'projects/notes/hints'
.error-alert
.form-actions.prepend-top-default
diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml
index b9486a9b492..24658319060 100644
--- a/app/views/projects/repositories/_download_archive.html.haml
+++ b/app/views/projects/repositories/_download_archive.html.haml
@@ -10,7 +10,7 @@
%span.caret
%span.sr-only
Select Archive Format
- %ul.col-xs-10.dropdown-menu{ role: 'menu' }
+ %ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do
%i.fa.fa-download
diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml
index 667057ef2d8..093d1d1bb0f 100644
--- a/app/views/projects/tags/_download.html.haml
+++ b/app/views/projects/tags/_download.html.haml
@@ -6,7 +6,7 @@
%span.caret
%span.sr-only
Select Archive Format
- %ul.col-xs-10.dropdown-menu{ role: 'menu' }
+ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
%i.fa.fa-download
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 399782273d3..dbc35c16feb 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -15,11 +15,11 @@
= render 'projects/tags/download', ref: tag.name, project: @project
- if can?(current_user, :push_code, @project)
- = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn has_tooltip', title: "Edit release notes" do
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn has-tooltip', title: "Edit release notes" do
= icon("pencil")
- if can?(current_user, :admin_project, @project)
- = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
- if commit
diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml
new file mode 100644
index 00000000000..ffeacb5a004
--- /dev/null
+++ b/app/views/projects/tags/destroy.js.haml
@@ -0,0 +1,3 @@
+$('.js-totaltags-count').html("#{@repository.tags.size}");
+- if @repository.tags.empty?
+ $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 3a2f75fecaa..77c7c4d23de 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -10,7 +10,7 @@
New Tag
%hr
-= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-requires-input" do
+= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-quick-submit js-requires-input" do
.form-group
= label_tag :tag_name, nil, class: 'control-label'
.col-sm-10
@@ -30,7 +30,7 @@
= label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', attr: :release_description, classes: 'description js-quick-submit form-control'
+ = render 'projects/zen', attr: :release_description, classes: 'description form-control'
= render 'projects/notes/hints'
.help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
.form-actions
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 8c7f93f93b6..1dc9b799a95 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -5,17 +5,17 @@
.gray-content-block
.pull-right
- if can?(current_user, :push_code, @project)
- = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn has_tooltip', title: 'Edit release notes' do
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn has-tooltip', title: 'Edit release notes' do
= icon("pencil")
- = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has_tooltip', title: 'Browse files' do
+ = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has-tooltip', title: 'Browse files' do
= icon('files-o')
- = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has_tooltip', title: 'Browse commits' do
+ = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has-tooltip', title: 'Browse commits' do
= icon('history')
- if can? current_user, :download_code, @project
= render 'projects/tags/download', ref: @tag.name, project: @project
- if can?(current_user, :admin_project, @project)
.pull-right
- = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
+ = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
%i.fa.fa-trash-o
.title
%span.item-title= @tag.name
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 3eb626e6dca..1c5f8b3928b 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -15,11 +15,11 @@
- if current_user
%li
- if !on_top_of_branch?
- %span.btn.btn-sm.add-to-tree.disabled.has_tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }}
+ %span.btn.add-to-tree.disabled.has-tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }}
= icon('plus')
- else
%span.dropdown
- %a.dropdown-toggle.btn.btn-sm.add-to-tree{href: '#', "data-toggle" => "dropdown"}
+ %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
%ul.dropdown-menu
- if can_edit_tree?
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 1d257818dcd..f0d1932e23c 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f|
-if @page.errors.any?
#error_explanation
.alert.alert-danger
@@ -15,7 +15,7 @@
= f.label :content, class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
- = render 'projects/zen', f: f, attr: :content, classes: 'description form-control js-quick-submit'
+ = render 'projects/zen', f: f, attr: :content, classes: 'description form-control'
= render 'projects/notes/hints'
.clearfix
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 29bf5d62abe..2b91b7e8f65 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,12 +1,11 @@
-%span.pull-right
- - if (@page && @page.persisted?)
- = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
- Page History
- - if can?(current_user, :create_wiki, @project)
- = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
- %i.fa.fa-pencil-square-o
- Edit
- - if can?(current_user, :admin_wiki, @project)
- = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
- = icon('trash')
- Delete
+- if (@page && @page.persisted?)
+ = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ Page History
+ - if can?(current_user, :create_wiki, @project)
+ = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ %i.fa.fa-pencil-square-o
+ Edit
+ - if can?(current_user, :admin_wiki, @project)
+ = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
+ = icon('trash')
+ Delete
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index 56a53ffff2a..a722fbc5352 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -16,4 +16,4 @@
= icon('plus')
New Page
- = render 'projects/wikis/new'
+= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index 53b37b1104e..919daf0a7b2 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -5,9 +5,10 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title New Wiki Page
.modal-body
- .form-group
- = label_tag :new_wiki_path do
- %span Page slug
- = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project)
- .form-actions
- = link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create'
+ %form.new-wiki-page
+ .form-group
+ = label_tag :new_wiki_path do
+ %span Page slug
+ = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true
+ .form-actions
+ = button_tag 'Create Page', class: 'build-new-wiki btn btn-create'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 23f64fbbd10..4dd818c7f67 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,16 +1,20 @@
- page_title "Edit", @page.title.capitalize, "Wiki"
= render "header_title"
-
= render 'nav'
-.gray-content-block
- .pull-right
+
+.top-area
+ .nav-text
+ %strong
+ - if @page.persisted?
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ - else
+ = @page.title.capitalize
+ %span.light
+ &middot;
+ Edit Page
+
+ .nav-controls
= render 'main_links'
- %h3.page-title.oneline
- %span.light Edit Page
- - if @page.persisted?
- = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
- - else
- = @page.title
= render 'form'
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 4322146ce34..dcaddae2b04 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,11 +1,14 @@
- page_title "History", @page.title.capitalize, "Wiki"
= render "header_title"
-
= render 'nav'
-.gray-content-block
- %h3.page-title
- %span.light History for
- = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
+
+.top-area
+ .nav-text
+ %strong
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ %span.light
+ &middot;
+ History
.table-holder
%table.table
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index aae1ad69ad9..92b494a513c 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -2,15 +2,12 @@
= render "header_title"
= render 'nav'
-.gray-content-block
- All pages in this wiki are listed below.
-
+
%ul.content-list
- @wiki_pages.each do |wiki_page|
%li
- %h4
- = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
- %small (#{wiki_page.format})
- .pull-right
- %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
+ = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
+ %small (#{wiki_page.format})
+ .pull-right
+ %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
= paginate @wiki_pages, theme: 'gitlab'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 309d40f52bc..067fb7f8f54 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,17 +1,18 @@
- page_title @page.title.capitalize, "Wiki"
= render "header_title"
-
= render 'nav'
-.gray-content-block
- = render 'main_links'
- %h3.page-title.oneline
- = @page.title.capitalize
+.top-area
+ .nav-text
+ %strong= @page.title.capitalize
%span.wiki-last-edit-by
&middot;
last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+ .nav-controls
+ = render 'main_links'
+
- if @page.historical?
.warning_message
This is an old version of this page.
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index ec478a5963d..4ef544136a8 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -6,14 +6,21 @@
- else
Any
%b.caret
- %ul.dropdown-menu
- %li
- = link_to search_filter_path(group_id: nil) do
- Any
- - current_user.authorized_groups.sort_by(&:name).each do |group|
- %li
- = link_to search_filter_path(group_id: group.id, project_id: nil) do
- = group.name
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Filter results by group
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-content
+ %ul
+ %li
+ = link_to search_filter_path(group_id: nil), class: ("is-active" if !params[:group_id].present?) do
+ Any
+ %li.divider
+ - current_user.authorized_groups.sort_by(&:name).each do |group|
+ %li
+ = link_to search_filter_path(group_id: group.id, project_id: nil), class: ("is-active" if params[:group_id] == group.id.to_s) do
+ = group.name
.dropdown.inline.prepend-left-10.project-filter
%button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'}
@@ -23,11 +30,18 @@
- else
Any
%b.caret
- %ul.dropdown-menu
- %li
- = link_to search_filter_path(project_id: nil) do
- Any
- - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project|
- %li
- = link_to search_filter_path(project_id: project.id, group_id: nil) do
- = project.name_with_namespace
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Filter results by project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-content
+ %ul
+ %li
+ = link_to search_filter_path(project_id: nil), class: ("is-active" if !params[:project_id].present?) do
+ Any
+ %li.divider
+ - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project|
+ %li
+ = link_to search_filter_path(project_id: project.id, group_id: nil), class: ("is-active" if params[:project_id] == project.id.to_s) do
+ = project.name_with_namespace
diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml
index 17b0981f073..a9dbc84da29 100644
--- a/app/views/search/_form.html.haml
+++ b/app/views/search/_form.html.haml
@@ -11,4 +11,4 @@
= button_tag 'Search', class: "btn btn-primary"
- unless params[:snippets].eql? 'true'
%br
- = render 'filter'
+ = render 'filter' if current_user
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index d0e64537621..60df348891c 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -18,6 +18,8 @@
= render 'shared/projects/list', projects: @objects
- else
= render partial: "search/results/#{@scope.singularize}", collection: @objects
+
+ - if @scope != 'projects'
= paginate @objects, theme: 'gitlab'
:javascript
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 45d700781f3..710f5613c81 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -1,5 +1,6 @@
.search-result-row
%h4
+ = confidential_icon(issue)
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title
.pull-right ##{issue.iid}
diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml
index e0b18733d74..b31595d8d1c 100644
--- a/app/views/search/results/_milestone.html.haml
+++ b/app/views/search/results/_milestone.html.haml
@@ -6,4 +6,4 @@
- if milestone.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(milestone.description)) \ No newline at end of file
+ = search_md_sanitize(markdown(milestone.description))
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 6b77d24f50c..c9b7bd154af 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -30,7 +30,7 @@
.line-numbers
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
- - chunk[:data].lines.to_a.size.times do |index|
+ - Gitlab::Git::Util.count_lines(chunk[:data]).times do |index|
- offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1
- i = index + offset
= link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index f5859481d46..235106c4f74 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -2,9 +2,9 @@
.blob-result
.file-holder
.file-title
- = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.filename) do
+ = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.basename) do
%i.fa.fa-file
%strong
- = wiki_blob.filename
+ = wiki_blob.basename
.file-content.code.term
= render 'shared/file_highlight', blob: wiki_blob, first_line_number: wiki_blob.startline
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index faf7e49ed29..974751d9970 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -8,11 +8,9 @@
= icon('angle-down')
%ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown
%li
- %a#ssh-selector{href: @project.ssh_url_to_repo}
- SSH
+ = ssh_clone_button(project)
%li
- %a#http-selector{href: @project.http_url_to_repo}
- HTTPS
+ = http_clone_button(project)
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
.input-group-btn
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 7c57924277e..7afbaeddee8 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -7,7 +7,7 @@
.max-width-marker
= text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text]),
- class: 'form-control js-commit-message js-quick-submit', placeholder: local_assigns[:placeholder],
+ class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder],
required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}"
- if local_assigns[:hint]
diff --git a/app/views/shared/_group_tips.html.haml b/app/views/shared/_group_tips.html.haml
index e5cf783beb7..46e4340511a 100644
--- a/app/views/shared/_group_tips.html.haml
+++ b/app/views/shared/_group_tips.html.haml
@@ -1,6 +1,5 @@
%ul
%li A group is a collection of several projects
- %li Groups are private by default
%li Members of a group may only view projects they have permission to access
%li Group project URLs are prefixed with the group namespace
%li Existing projects may be moved into a group
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 4b4c9e9eabe..8ff9d4c1c7f 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -8,7 +8,7 @@
.pull-right
= link_to 'New issue', new_namespace_project_issue_path(project.namespace, project)
- %ul.well-list.issues-list
+ %ul.content-list.issues-list
- group[1].each do |issue|
= render 'projects/issues/issue', issue: issue
= paginate @issues, theme: "gitlab"
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index 8134b15d245..4b47b0291be 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -1,4 +1,4 @@
%span.label-row
- = link_to_label(label)
+ = link_to_label(label, tooltip: false)
%span.prepend-left-10
= markdown(label.description, pipeline: :single_line)
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index be17a511b26..e74fc36c797 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -8,7 +8,7 @@
.pull-right
= link_to 'New merge request', new_namespace_project_merge_request_path(project.namespace, project)
- %ul.well-list.mr-list
+ %ul.content-list.mr-list
- group[1].each do |merge_request|
= render 'projects/merge_requests/merge_request', merge_request: merge_request
= paginate @merge_requests, theme: "gitlab"
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index 089179e677a..bb5fff2d3bb 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -1,6 +1,6 @@
- if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key?
.no-ssh-key-message.alert.alert-warning.hidden-xs
- You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path, class: 'alert-link'} to your profile
+ You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', profile_keys_path, class: 'alert-link'} to your profile
.pull-right
= link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link'
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 289b0bfe1eb..40c6eb9be45 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -10,25 +10,29 @@
%i.fa.fa-cogs
= link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do
- %i.fa.fa-sign-out
+ = icon('sign-out')
.stats
%span
- = icon('home')
+ = icon('bookmark')
= number_with_delimiter(group.projects.count)
%span
= icon('users')
= number_with_delimiter(group.users.count)
+ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
+ = visibility_level_icon(group.visibility_level, fw: false)
+
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
- = link_to group, class: 'group-name' do
- %span.item-title= group.name
+ .title
+ = link_to group, class: 'group-name' do
+ = group.name
- - if group_member
- as
- %span #{group_member.human_access}
+ - if group_member
+ as
+ %span #{group_member.human_access}
- if group.description.present?
- .light
+ .description
= markdown(group.description, pipeline: :description)
diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml
new file mode 100644
index 00000000000..1aa7ed1f2eb
--- /dev/null
+++ b/app/views/shared/groups/_list.html.haml
@@ -0,0 +1,6 @@
+- if groups.any?
+ %ul.content-list
+ - groups.each_with_index do |group, i|
+ = render "shared/groups/group", group: group
+- else
+ %h3 No groups found
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index e55159d996b..c99da92be9f 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -7,23 +7,22 @@
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
- = users_select_tag(:author_id, selected: params[:author_id],
- placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
+ - if params[:author_id]
+ = hidden_field_tag(:author_id, params[:author_id])
+ = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline
- = users_select_tag(:assignee_id, selected: params[:assignee_id],
- placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true)
+ - if params[:assignee_id]
+ = hidden_field_tag(:assignee_id, params[:assignee_id])
+ = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter
- = select_tag('milestone_title', projects_milestones_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Milestone'})
+ = render "shared/issuable/milestone_dropdown"
.filter-item.inline.labels-filter
- = select_tag('label_name', projects_labels_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Label'})
-
+ = render "shared/issuable/label_dropdown"
.pull-right
= render 'shared/sort_dropdown'
@@ -31,11 +30,17 @@
.issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
.filter-item.inline
- = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), include_blank: true, data: { placeholder: "Status" })
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ %ul
+ %li
+ %a{href: "#", data: {id: "reopen"}} Open
+ %li
+ %a{href: "#", data: {id: "close"}} Closed
.filter-item.inline
- = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true)
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline
- = select_tag('update[milestone_id]', bulk_update_milestone_options, include_blank: true, data: { placeholder: "Milestone" })
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
@@ -47,6 +52,9 @@
:javascript
new UsersSelect();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new IssueStatusSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 90dc0062481..e2a9e5bfb92 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -9,26 +9,44 @@
= f.label :title, class: 'control-label'
.col-sm-10
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
- class: 'form-control pad js-gfm-input js-quick-submit', required: true
+ class: 'form-control pad js-gfm-input', required: true
- if issuable.is_a?(MergeRequest)
%p.help-block
- - if issuable.work_in_progress?
- Remove the <code>WIP</code> prefix from the title to allow this
- <strong>Work In Progress</strong> merge request to be merged when it's ready.
- - else
- Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a
- <strong>Work In Progress</strong> merge request from being merged before it's ready.
+ .js-wip-explanation
+ %a.js-toggle-wip{href: "", tabindex: -1}
+ Remove the
+ %code WIP:
+ prefix from the title
+ to allow this
+ %strong Work In Progress
+ merge request to be merged when it's ready.
+ .js-no-wip-explanation
+ %a.js-toggle-wip{href: "", tabindex: -1}
+ Start the title with
+ %code WIP:
+ to prevent a
+ %strong Work In Progress
+ merge request from being merged before it's ready.
.form-group.detail-page-description
= f.label :description, 'Description', class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description,
- classes: 'description form-control js-quick-submit'
+ classes: 'description form-control'
= render 'projects/notes/hints'
.clearfix
.error-alert
+
+- if issuable.is_a?(Issue) && !issuable.project.private?
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :confidential do
+ = f.check_box :confidential
+ This issue is confidential and should only be visible to team members
+
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
%hr
.form-group
@@ -67,13 +85,26 @@
- if can? current_user, :admin_label, issuable.project
= link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank
+- if issuable.can_move?(current_user)
+ %hr
+ .form-group
+ = label_tag :move_to_project_id, 'Move', class: 'control-label'
+ .col-sm-10
+ - projects = project_options(issuable, current_user, ability: :admin_issue)
+ = select_tag(:move_to_project_id, projects, include_blank: true,
+ class: 'select2', data: { placeholder: 'Select project' })
+ &nbsp;
+ %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
+ title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
+ = icon('question-circle')
+
- if issuable.is_a?(MergeRequest)
%hr
- - if @merge_request.new_record?
- .form-group
- = f.label :source_branch, class: 'control-label'
- .col-sm-10
- = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
+ - if @merge_request.new_record?
+ .form-group
+ = f.label :source_branch, class: 'control-label'
+ .col-sm-10
+ = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
.form-group
= f.label :target_branch, class: 'control-label'
.col-sm-10
@@ -96,7 +127,12 @@
for this project.
- if issuable.new_record?
- - cancel_project = issuable.source_project
+ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
- else
- - cancel_project = issuable.project
- = link_to 'Cancel', [cancel_project.namespace.becomes(Namespace), cancel_project, issuable], class: 'btn btn-cancel'
+ .pull-right
+ - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
+ = link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
+ method: :delete, class: 'btn btn-grouped' do
+ = icon('trash-o')
+ Delete
+ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
new file mode 100644
index 00000000000..006a34a11e3
--- /dev/null
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -0,0 +1,44 @@
+- if params[:label_name]
+ = hidden_field_tag(:label_name, params[:label_name])
+.dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}}
+ %span.dropdown-toggle-text
+ = h(params[:label_name].presence || "Label")
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-page-one
+ = dropdown_title("Filter by label")
+ = dropdown_filter("Search labels")
+ = dropdown_content
+ - if @project
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_label, @project
+ %li
+ %a.dropdown-toggle-page{href: "#"}
+ Create new
+ %li
+ = link_to namespace_project_labels_path(@project.namespace, @project) do
+ - if can? current_user, :admin_label, @project
+ Manage labels
+ - else
+ View labels
+ - if can? current_user, :admin_label, @project and @project
+ .dropdown-page-two.dropdown-new-label
+ = dropdown_title("Create new label", back: true)
+ = dropdown_content do
+ .dropdown-labels-error.js-label-error
+ %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
+ .suggest-colors.suggest-colors-dropdown
+ - suggested_colors.each do |color|
+ = link_to '#', style: "background-color: #{color}", data: { color: color } do
+ &nbsp
+ .dropdown-label-color-input
+ .dropdown-label-color-preview.js-dropdown-label-color-preview
+ %input#new_label_color.dropdown-input-field{ type: "text" }
+ .clearfix
+ %button.btn.btn-primary.pull-left.js-new-label-btn{type: "button"}
+ Create
+ %button.btn.btn-default.pull-right.js-cancel-label-btn{type: "button"}
+ Cancel
+ = dropdown_loading
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
new file mode 100644
index 00000000000..1c79494f816
--- /dev/null
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -0,0 +1,16 @@
+- if params[:milestone_title]
+ = hidden_field_tag(:milestone_title, params[:milestone_title])
+= dropdown_tag(h(params[:milestone_title].presence || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit js-extra-options', filter: true, dropdown_class: "dropdown-menu-selectable",
+ placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
+ - if @project
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_milestone, @project
+ %li
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
+ Create new
+ %li
+ = link_to namespace_project_milestones_path(@project.namespace, @project) do
+ - if can? current_user, :admin_milestone, @project
+ Manage milestones
+ - else
+ View milestones
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index ea61935487c..33a9a494857 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -1,9 +1,20 @@
+- participants_row = 7
+- participants_size = participants.size
+- participants_extra = participants_size - participants_row
.block.participants
.sidebar-collapsed-icon
= icon('users')
%span
= participants.count
- .title
+ .title.hide-collapsed
= pluralize participants.count, "participant"
- - participants.each do |participant|
- = link_to_member(@project, participant, name: false, size: 24)
+ .hide-collapsed.participants-list
+ - participants.each do |participant|
+ .participants-author.js-participants-author
+ = link_to_member(@project, participant, name: false, size: 24)
+ - if participants_extra > 0
+ %div.participants-more
+ %a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}}
+ + #{participants_extra} more
+:javascript
+ IssuableContext.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index ef351fe8093..56f19ef3d6b 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,14 +1,13 @@
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar
- .block
- %span.issuable-count.pull-left
+ .block.issuable-sidebar-header
+ %span.issuable-count.hide-collapsed.pull-left
= issuable.iid
of
= issuables_count(issuable)
- %span.pull-right
- %a.gutter-toggle{href: '#'}
- = sidebar_gutter_toggle_icon
- .issuable-nav.pull-right.btn-group{role: 'group', "aria-label" => '...'}
+ %a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'}
+ = sidebar_gutter_toggle_icon
+ .issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
- if prev_issuable = prev_issuable_for(issuable)
= link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn'
- else
@@ -22,31 +21,33 @@
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
.block.assignee
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.to_reference if issuable.assignee)}
- if issuable.assignee
- = link_to_member_avatar(issuable.assignee, size: 24)
+ = link_to_member(@project, issuable.assignee, size: 24)
- else
= icon('user')
- .title
- %label
- Assignee
+ .title.hide-collapsed
+ Assignee
+ = icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.bold.hide-collapsed
- if issuable.assignee
- %strong= link_to_member(@project, issuable.assignee, size: 24)
- - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
- %a.pull-right.cannot-be-merged{href: '#', data: {toggle: 'tooltip'}, title: 'Not allowed to merge'}
- = icon('exclamation-triangle')
+ = link_to_member(@project, issuable.assignee, size: 32) do
+ - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
+ %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
+ = icon('exclamation-triangle')
+ %span.username
+ = issuable.assignee.to_reference
- else
- .light None
+ %span.assign-yourself
+ No assignee -
+ %a.js-assign-yourself{ href: '#' }
+ assign yourself
- .selectbox
- = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]",
- placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee',
- selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true,
- first_user: true, author_id: issuable.author_id)
+ .selectbox.hide-collapsed
+ = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
+ = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
.block.milestone
.sidebar-collapsed-icon
@@ -56,25 +57,21 @@
= issuable.milestone.title
- else
No
- .title
- %label
- Milestone
+ .title.hide-collapsed
+ Milestone
+ = icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.bold.hide-collapsed
- if issuable.milestone
- %span.back-to-milestone
- = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
- %strong
- = icon('clock-o')
- = issuable.milestone.title
+ = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
+ = issuable.milestone.title
- else
.light None
- .selectbox
- = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }})
- = hidden_field_tag :issuable_context
- = f.submit class: 'btn hide'
+
+ .selectbox.hide-collapsed
+ = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
+ = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
- if issuable.project.labels.any?
.block.labels
@@ -82,34 +79,56 @@
= icon('tags')
%span
= issuable.labels.count
- .title
- %label Labels
+ .title.hide-collapsed
+ Labels
+ = icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value.issuable-show-labels
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) }
- if issuable.labels.any?
- issuable.labels.each do |label|
= link_to_label(label, type: issuable.to_ability_name)
- else
.light None
- .selectbox
- = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
- { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" }
+ .selectbox.hide-collapsed
+ - issuable.labels.each do |label|
+ = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
+ %span.dropdown-toggle-text
+ Label
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-page-one
+ = dropdown_title("Assign labels")
+ = dropdown_filter("Search labels")
+ = dropdown_content
+ - if @project
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_label, @project
+ %li
+ %a.dropdown-toggle-page{href: "#"}
+ Create new
+ %li
+ = link_to namespace_project_labels_path(@project.namespace, @project) do
+ - if can? current_user, :admin_label, @project
+ Manage labels
+ - else
+ View labels
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- %hr
- if current_user
- subscribed = issuable.subscribed?(current_user)
- .block.light
+ .block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
.sidebar-collapsed-icon
= icon('rss')
- .title
- %label.light Notifications
+ .title.hide-collapsed
+ Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-block.btn-gray.subscribe-button{:type => 'button'}
+ %button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'}
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
- .subscription-status{data: {status: subscribtion_status}}
+ .subscription-status.hide-collapsed{data: {status: subscribtion_status}}
.unsubscribed{class: ( 'hidden' if subscribed )}
You're not receiving notifications from this thread.
.subscribed{class: ( 'hidden' unless subscribed )}
@@ -119,8 +138,7 @@
.block.project-reference
.sidebar-collapsed-icon
= clipboard_button(clipboard_text: project_ref)
- .title
- .cross-project-reference
+ .cross-project-reference.hide-collapsed
%span
Reference:
%cite{title: project_ref}
@@ -128,5 +146,7 @@
= clipboard_button(clipboard_text: project_ref)
:javascript
- new Subscription("#{toggle_subscription_path(issuable)}");
- new IssuableContext();
+ new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
+ new LabelsSelect();
+ new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}');
+ new Subscription('.subscription')
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
new file mode 100644
index 00000000000..e1127b2311c
--- /dev/null
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -0,0 +1,27 @@
+-# @project is present when viewing Project's milestone
+- project = @project || issuable.project
+- assignee = issuable.assignee
+- issuable_type = issuable.class.table_name
+- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
+
+%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) }
+ %span
+ - if show_project_name
+ %strong #{project.name} &middot;
+ - elsif show_full_project_name
+ %strong #{project.name_with_namespace} &middot;
+ - if issuable.is_a?(Issue)
+ = confidential_icon(issuable)
+ = link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title
+ %div{class: 'issuable-detail'}
+ = link_to [project.namespace.becomes(Namespace), project, issuable] do
+ %span{ class: 'issuable-number' }>= issuable.to_reference
+
+ - issuable.labels.each do |label|
+ = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
+ - render_colored_label(label)
+
+ - if assignee
+ = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
+ class: 'has-tooltip', data: { 'original-title' => "Assigned to #{sanitize(assignee.name)}", container: 'body' } do
+ - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
new file mode 100644
index 00000000000..8619939dde7
--- /dev/null
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -0,0 +1,16 @@
+- show_counter = local_assigns.fetch(:show_counter, false)
+- primary = local_assigns.fetch(:primary, false)
+- panel_class = primary ? 'panel-primary' : 'panel-default'
+
+.panel{ class: panel_class }
+ .panel-heading
+ = title
+ - if show_counter
+ .pull-right= issuables.size
+
+ - class_prefix = dom_class(issuables).pluralize
+ %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
+ = render partial: 'shared/milestones/issuable',
+ collection: issuables.sort_by(&:position),
+ as: :issuable,
+ locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name }
diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml
new file mode 100644
index 00000000000..a8db7f8a556
--- /dev/null
+++ b/app/views/shared/milestones/_issues_tab.html.haml
@@ -0,0 +1,10 @@
+- args = { show_project_name: local_assigns.fetch(:show_project_name, false),
+ show_full_project_name: local_assigns.fetch(:show_full_project_name, false) }
+
+.row.prepend-top-default
+ .col-md-4
+ = render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true)
+ .col-md-4
+ = render 'shared/milestones/issuables', args.merge(title: 'Ongoing Issues (open and assigned)', issuables: issues.opened.assigned, id: 'ongoing', show_counter: true)
+ .col-md-4
+ = render 'shared/milestones/issuables', args.merge(title: 'Completed Issues (closed)', issuables: issues.closed, id: 'closed', show_counter: true)
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
new file mode 100644
index 00000000000..868b2357003
--- /dev/null
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -0,0 +1,18 @@
+%ul.bordered-list.manage-labels-list
+ - labels.each do |label|
+ - options = { milestone_title: @milestone.title, label_name: label.title }
+
+ %li
+ %span.label-row
+ = link_to milestones_label_path(options) do
+ - render_colored_label(label, tooltip: false)
+ %span.prepend-left-10
+ = markdown(label.description, pipeline: :single_line)
+
+ .pull-right
+ %strong.issues-count
+ = link_to milestones_label_path(options.merge(state: 'opened')) do
+ - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue'
+ %strong.issues-count
+ = link_to milestones_label_path(options.merge(state: 'closed')) do
+ - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue'
diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml
new file mode 100644
index 00000000000..c29d8ee6737
--- /dev/null
+++ b/app/views/shared/milestones/_merge_requests_tab.haml
@@ -0,0 +1,12 @@
+- args = { show_project_name: local_assigns.fetch(:show_project_name, false),
+ show_full_project_name: local_assigns.fetch(:show_full_project_name, false) }
+
+.row.prepend-top-default
+ .col-md-3
+ = render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned')
+ .col-md-3
+ = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing')
+ .col-md-3
+ = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed')
+ .col-md-3
+ = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true)
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
new file mode 100644
index 00000000000..6b25745c554
--- /dev/null
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -0,0 +1,45 @@
+- dashboard = local_assigns[:dashboard]
+- custom_dom_id = dom_id(@project ? milestone : milestone.milestones.first)
+
+%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
+ .row
+ .col-sm-6
+ %strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path
+ .col-sm-6
+ .pull-right.light #{milestone.percent_complete(current_user)}% complete
+ .row
+ .col-sm-6
+ = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path
+ &middot;
+ = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
+ .col-sm-6= milestone_progress_bar(milestone)
+ - if milestone.is_a?(GlobalMilestone)
+ .row
+ .col-sm-6
+ .expiration= render('shared/milestone_expired', milestone: milestone)
+ .projects
+ - milestone.milestones.each do |milestone|
+ = link_to milestone_path(milestone) do
+ %span.label.label-gray
+ = dashboard ? milestone.project.name_with_namespace : milestone.project.name
+ - if @group
+ .col-sm-6
+ - if can?(current_user, :admin_milestones, @group)
+ - if milestone.closed?
+ = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
+ - else
+ = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close"
+
+ - if @project
+ .row
+ .col-sm-6= render('shared/milestone_expired', milestone: milestone)
+ .col-sm-6
+ - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
+ = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do
+ = icon('pencil-square-o')
+ Edit
+ \
+ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close"
+ = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
+ = icon('trash-o')
+ Delete
diff --git a/app/views/shared/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml
new file mode 100644
index 00000000000..67ae85ac276
--- /dev/null
+++ b/app/views/shared/milestones/_participants_tab.html.haml
@@ -0,0 +1,8 @@
+%ul.bordered-list
+ - users.each do |user|
+ %li
+ = link_to user, title: user.name, class: "darken" do
+ = image_tag avatar_icon(user, 32), class: "avatar s32"
+ %strong= truncate(user.name, lenght: 40)
+ %br
+ %small.cgray= user.username
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
new file mode 100644
index 00000000000..385c6596606
--- /dev/null
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -0,0 +1,28 @@
+- project = local_assigns[:project]
+
+.context.prepend-top-default
+ .milestone-summary
+ %h4 Progress
+ %strong= milestone.issues_visible_to_user(current_user).size
+ issues:
+ %span.milestone-stat
+ %strong= milestone.issues_visible_to_user(current_user).opened.size
+ open and
+ %strong= milestone.issues_visible_to_user(current_user).closed.size
+ closed
+ %span.milestone-stat
+ %strong== #{milestone.percent_complete(current_user)}%
+ complete
+
+ %span.milestone-stat
+ %span.remaining-days= milestone_remaining_days(milestone)
+ %span.pull-right.tab-issues-buttons
+ - if project && can?(current_user, :create_issue, project)
+ = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do
+ %i.fa.fa-plus
+ New Issue
+ = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped"
+ %span.pull-right.tab-merge-requests-buttons.hidden
+ = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped"
+
+ = milestone_progress_bar(milestone)
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
new file mode 100644
index 00000000000..2b6ce2d7e7a
--- /dev/null
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -0,0 +1,30 @@
+%ul.nav-links.no-top.no-bottom
+ %li.active
+ = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
+ Issues
+ %span.badge= milestone.issues_visible_to_user(current_user).size
+ %li
+ = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
+ Merge Requests
+ %span.badge= milestone.merge_requests.size
+ %li
+ = link_to '#tab-participants', 'data-toggle' => 'tab' do
+ Participants
+ %span.badge= milestone.participants.count
+ %li
+ = link_to '#tab-labels', 'data-toggle' => 'tab' do
+ Labels
+ %span.badge= milestone.labels.count
+
+- show_project_name = local_assigns.fetch(:show_project_name, false)
+- show_full_project_name = local_assigns.fetch(:show_full_project_name, false)
+
+.tab-content.milestone-content
+ .tab-pane.active#tab-issues
+ = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ .tab-pane#tab-merge-requests
+ = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ .tab-pane#tab-participants
+ = render 'shared/milestones/participants_tab', users: milestone.participants
+ .tab-pane#tab-labels
+ = render 'shared/milestones/labels_tab', labels: milestone.labels
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
new file mode 100644
index 00000000000..cab8743a077
--- /dev/null
+++ b/app/views/shared/milestones/_top.html.haml
@@ -0,0 +1,58 @@
+- page_title milestone.title, "Milestones"
+
+- group = local_assigns[:group]
+
+.detail-page-header
+ .status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" }
+ - if milestone.closed?
+ Closed
+ - elsif milestone.expired?
+ Expired
+ - else
+ Open
+ %span.identifier
+ Milestone #{milestone.title}
+ - if milestone.expires_at
+ %span.creator
+ &middot;
+ = milestone.expires_at
+ - if group
+ .pull-right
+ - if can?(current_user, :admin_milestones, group)
+ - if milestone.active?
+ = link_to 'Close Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close"
+ - else
+ = link_to 'Reopen Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
+
+.detail-page-description.gray-content-block.second-block
+ %h2.title
+ = markdown escape_once(milestone.title), pipeline: :single_line
+
+- if milestone.complete?(current_user) && milestone.active?
+ .alert.alert-success.prepend-top-default
+ - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
+ %span All issues for this milestone are closed. #{close_msg}
+
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %th Project
+ %th Open issues
+ %th State
+ %th Due date
+ - milestone.milestones.each do |ms|
+ %tr
+ %td
+ - project_name = group ? ms.project.name : ms.project.name_with_namespace
+ = link_to project_name, namespace_project_milestone_path(ms.project.namespace, ms.project, ms)
+ %td
+ = ms.issues_visible_to_user(current_user).opened.count
+ %td
+ - if ms.closed?
+ Closed
+ - else
+ Open
+ %td
+ = ms.expires_at
+
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
new file mode 100644
index 00000000000..e7e04621ff4
--- /dev/null
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -0,0 +1,22 @@
+- @sort ||= sort_value_recently_updated
+- archived = params[:archived]
+.dropdown.inline
+ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light
+ = projects_sort_options_hash[@sort]
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ Sort by
+ - projects_sort_options_hash.each do |value, title|
+ %li
+ = link_to filter_projects_path(sort: value, archived: archived), class: ("is-active" if @sort == value) do
+ = title
+
+ %li.divider
+ %li
+ = link_to filter_projects_path(sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do
+ Hide archived projects
+ %li
+ = link_to filter_projects_path(sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do
+ Show archived projects
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 75684b972f1..2e08bb2ac08 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -6,25 +6,19 @@
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
+- remote = false unless local_assigns[:remote] == true
-%ul.projects-list
+.projects-list-holder
- if projects.any?
- - projects.each_with_index do |project, i|
- - css_class = (i >= projects_limit) ? 'hide' : nil
- = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
- avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
- forks: forks, show_last_commit_as_description: show_last_commit_as_description
-
- - if projects.size > projects_limit && projects.kind_of?(Array)
- %li.bottom.center
- .light
- #{projects_limit} of #{pluralize(projects.count, 'project')} displayed.
- = link_to '#', class: 'js-expand' do
- Show all
- = paginate projects, theme: "gitlab" if projects.respond_to? :total_pages
+ %ul.projects-list.content-list
+ - projects.each_with_index do |project, i|
+ - css_class = (i >= projects_limit) ? 'hide' : nil
+ = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
+ avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
+ forks: forks, show_last_commit_as_description: show_last_commit_as_description
+ = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
- else
- %h3 No projects found
+ .nothing-here-block No projects found
:javascript
- new ProjectsList();
- Dashboard.init();
+ ProjectsList.init();
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 97db5b1d41e..53ff8959bc8 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -7,27 +7,15 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- ci_commit = project.ci_commit(project.commit.sha) if ci && !project.empty_repo? && project.commit
-- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2']
+- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3']
- cache_key.push(ci_commit.status) if ci_commit
%li.project-row{ class: css_class }
= cache(cache_key) do
- = link_to project_path(project), class: dom_class(project) do
- - if avatar
- .dash-project-avatar
- - if use_creator_avatar
- = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- - else
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
- %span.project-full-name
- %span.namespace-name
- - if project.namespace && !skip_namespace
- = project.namespace.human_name
- \/
- %span.project-name.filter-title
- = project.name
-
- .project-controls
+ .controls
+ - if project.main_language
+ %span
+ = project.main_language
- if ci_commit
%span
= render_ci_status(ci_commit)
@@ -39,10 +27,29 @@
%span
= icon('star')
= project.star_count
+ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)}
+ = visibility_level_icon(project.visibility_level, fw: false)
+
+ .title
+ = link_to project_path(project), class: dom_class(project) do
+ - if avatar
+ .dash-project-avatar
+ - if use_creator_avatar
+ = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
+ - else
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ %span.project-full-name
+ %span.namespace-name
+ - if project.namespace && !skip_namespace
+ = project.namespace.human_name
+ \/
+ %span.project-name.filter-title
+ = project.name
+
- if show_last_commit_as_description
- .project-description
+ .description
= link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit),
class: "commit-row-message"
- elsif project.description.present?
- .project-description
+ .description
= markdown(project.description, pipeline: :description)
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index e0e41fc4bea..773ce8ac240 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,5 +1,7 @@
- unless @snippet.content.empty?
- if markup?(@snippet.file_name)
+ %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
+ = @snippet.data
.file-content.wiki
= render_markup(@snippet.file_name, @snippet.data)
- else
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index aa5acee9c14..3c445f67236 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -1,5 +1,5 @@
.detail-page-header
- .snippet-box.has_tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: 'body' }}
+ .snippet-box.has-tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: 'body' }}
= visibility_level_icon(@snippet.visibility_level, fw: false)
= visibility_level_label(@snippet.visibility_level)
%span.identifier
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index c6294caddc7..c96dfefe17f 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,10 +1,12 @@
%li.snippet-row
- .snippet-title
+ = image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
+
+ .title
= link_to reliable_snippet_path(snippet) do
= truncate(snippet.title, length: 60)
- if snippet.private?
%span.label.label-gray
- %i.fa.fa-lock
+ = icon('lock')
private
%span.monospace.pull-right
= snippet.file_name
@@ -15,6 +17,5 @@
.snippet-info
= link_to user_snippets_path(snippet.author) do
- = image_tag avatar_icon(snippet.author_email), class: "avatar s24", alt: ''
= snippet.author_name
authored #{time_ago_with_tooltip(snippet.created_at)}
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index d9aa4dd1d2e..80a3e731e1d 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,4 +1,4 @@
-%ul.bordered-list
+%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets
- if @snippets.empty?
%li
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 3bfd781e51d..bca816f22cb 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -8,115 +8,110 @@
= render 'shared/show_aside'
-.cover-block
- .cover-controls
- - if @user == current_user
- = link_to profile_path, class: 'btn btn-gray' do
- = icon('pencil')
- - elsif current_user
- %span.report-abuse
- - if @user.abuse_report
- %button.btn.btn-danger{ title: 'Already reported for abuse',
- data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
- = icon('exclamation-circle')
- - else
- = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
- title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
- = icon('exclamation-circle')
- - if current_user
- &nbsp;
- = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
- = icon('rss')
-
- .avatar-holder
- = link_to avatar_icon(@user, 400), target: '_blank' do
- = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
- .cover-title
- = @user.name
-
- .cover-desc
- %span
- @#{@user.username}.
+.user-profile
+ .cover-block
+ .cover-controls
+ - if @user == current_user
+ = link_to profile_path, class: 'btn btn-gray' do
+ = icon('pencil')
+ - elsif current_user
+ %span.report-abuse
+ - if @user.abuse_report
+ %button.btn.btn-danger{ title: 'Already reported for abuse',
+ data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
+ = icon('exclamation-circle')
+ - else
+ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
+ title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
+ = icon('exclamation-circle')
+ - if current_user
+ &nbsp;
+ = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
+ = icon('rss')
+
+ .avatar-holder
+ = link_to avatar_icon(@user, 400), target: '_blank' do
+ = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
+ .cover-title
+ = @user.name
+
+ .cover-desc
+ %span.middle-dot-divider
+ @#{@user.username}
+ %span.middle-dot-divider
+ Member since #{@user.created_at.to_s(:medium)}
+
- if @user.bio.present?
- %span
- #{@user.bio}.
- %span
- Member since #{@user.created_at.to_s(:medium)}
-
- .cover-desc
- - unless @user.public_email.blank?
- .profile-link-holder
- = link_to @user.public_email, "mailto:#{@user.public_email}"
- - unless @user.skype.blank?
- .profile-link-holder
- = link_to "skype:#{@user.skype}", title: "Skype" do
- = icon('skype')
- - unless @user.linkedin.blank?
- .profile-link-holder
- = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
- = icon('linkedin-square')
- - unless @user.twitter.blank?
- .profile-link-holder
- = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
- = icon('twitter-square')
- - unless @user.website_url.blank?
- .profile-link-holder
- = link_to @user.short_website_url, @user.full_website_url
- - unless @user.location.blank?
- .profile-link-holder
- = icon('map-marker')
- = @user.location
-
- %ul.nav-links.center
- %li.active
- = link_to "#activity", 'data-toggle' => 'tab' do
- Activity
- - if @groups.any?
- %li
- = link_to "#groups", 'data-toggle' => 'tab' do
+ .cover-desc
+ %p.profile-user-bio
+ = @user.bio
+
+ .cover-desc
+ - unless @user.public_email.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.public_email, "mailto:#{@user.public_email}"
+ - unless @user.skype.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "skype:#{@user.skype}", title: "Skype" do
+ = icon('skype')
+ - unless @user.linkedin.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
+ = icon('linkedin-square')
+ - unless @user.twitter.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
+ = icon('twitter-square')
+ - unless @user.website_url.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.short_website_url, @user.full_website_url
+ - unless @user.location.blank?
+ .profile-link-holder.middle-dot-divider
+ = icon('map-marker')
+ = @user.location
+
+ %ul.nav-links.center.user-profile-nav
+ %li.activity-tab
+ = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
+ Activity
+ %li.groups-tab
+ = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
Groups
- - if @contributed_projects.present?
- %li
- = link_to "#contributed", 'data-toggle' => 'tab' do
+ %li.contributed-tab
+ = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
Contributed projects
- - if @projects.present?
- %li
- = link_to "#personal", 'data-toggle' => 'tab' do
+ %li.projects-tab
+ = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
Personal projects
-%div{ class: container_class }
- .tab-content
- .tab-pane.active#activity
- .gray-content-block.white.second-block
- %div{ class: container_class }
- .user-calendar
- %h4.center.light
- %i.fa.fa-spinner.fa-spin
- .user-calendar-activities
+ %div{ class: container_class }
+ .tab-content
+ #activity.tab-pane
+ .gray-content-block.white.second-block
+ %div{ class: container_class }
+ .user-calendar{data: {href: user_calendar_path}}
+ %h4.center.light
+ %i.fa.fa-spinner.fa-spin
+ .user-calendar-activities
+ .content_list{ data: {href: user_path} }
+ = spinner
- .content_list
- = spinner
+ #groups.tab-pane
+ - # This tab is always loaded via AJAX
+
+ #contributed.contributed-projects.tab-pane
+ - # This tab is always loaded via AJAX
- - if @groups.any?
- .tab-pane#groups
- %ul.content-list
- - @groups.each do |group|
- = render 'shared/groups/group', group: group
-
- - if @contributed_projects.present?
- .tab-pane#contributed
- .contributed-projects
- = render 'shared/projects/list',
- projects: @contributed_projects.sort_by(&:star_count).reverse,
- projects_limit: 10, stars: true, avatar: true
-
- - if @projects.present?
- .tab-pane#personal
- .personal-projects
- = render 'shared/projects/list',
- projects: @projects.sort_by(&:star_count).reverse,
- projects_limit: 10, stars: true, avatar: true
+ #projects.tab-pane
+ - # This tab is always loaded via AJAX
+
+ .loading-status
+ = spinner
:javascript
- $(".user-calendar").load("#{user_calendar_path}");
+ var userProfile;
+
+ userProfile = new User({
+ action: "#{controller.action_name}"
+ });
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
index 91c5b7eac5e..02647229776 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -1,23 +1,17 @@
.awards.votes-block
- awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
- .award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)}
+ %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user), data: {placement: "top"}}
= emoji_icon(emoji)
- .counter
+ %span.award-control-text.js-counter
= notes.count
- if current_user
- .awards-controls
- %a.add-award{"href" => "#"}
- = icon('smile-o')
- .emoji-menu
- .emoji-menu-content
- = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- - AwardEmoji.emoji_by_category.each do |category, emojis|
- %h5= AwardEmoji::CATEGORIES[category]
- %ul
- - emojis.each do |emoji|
- %li
- = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
+ %div.award-menu-holder.js-award-holder
+ %a.btn.award-control.js-add-award{"href" => "#"}
+ = icon('smile-o', {class: "award-control-icon"})
+ = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"})
+ %span.award-control-text
+ Add
- if current_user
:javascript
@@ -32,17 +26,3 @@
noteable_id,
aliases
);
-
- $(".awards").on("click", ".emoji-menu-content li", function(e) {
- var emoji = $(this).find(".emoji-icon").data("emoji");
- awards_handler.addAward(emoji);
- });
-
- $(".awards").on("click", ".award", function(e) {
- var emoji = $(this).find(".icon").data("emoji");
- awards_handler.addAward(emoji);
- });
-
- $(".award").tooltip();
-
- $(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false});
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
new file mode 100644
index 00000000000..6ff361e4d80
--- /dev/null
+++ b/app/workers/delete_user_worker.rb
@@ -0,0 +1,10 @@
+class DeleteUserWorker
+ include Sidekiq::Worker
+
+ def perform(current_user_id, delete_user_id, options = {})
+ delete_user = User.find(delete_user_id)
+ current_user = User.find(current_user_id)
+
+ DeleteUserService.new(current_user).execute(delete_user, options.symbolize_keys)
+ end
+end
diff --git a/app/workers/gitlab_shell_one_shot_worker.rb b/app/workers/gitlab_shell_one_shot_worker.rb
new file mode 100644
index 00000000000..4ddbcf574d5
--- /dev/null
+++ b/app/workers/gitlab_shell_one_shot_worker.rb
@@ -0,0 +1,10 @@
+class GitlabShellOneShotWorker
+ include Sidekiq::Worker
+ include Gitlab::ShellAdapter
+
+ sidekiq_options queue: :gitlab_shell, retry: false
+
+ def perform(action, *arg)
+ gitlab_shell.send(action, *arg)
+ end
+end
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 2d44d8d4dc6..605ec4f04e5 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -141,7 +141,7 @@ class IrkerWorker
end
def files_count(commit)
- files = "#{commit.diffs.count} file"
+ files = "#{commit.diffs.real_size} file"
files += 's' if commit.diffs.count > 1
files
end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 14d7813412e..3cc232ef1ae 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -1,6 +1,5 @@
class PostReceive
include Sidekiq::Worker
- include Gitlab::Identifier
sidekiq_options queue: :post_receive
@@ -11,51 +10,44 @@ class PostReceive
log("Check gitlab.yml config for correct gitlab_shell.repos_path variable. \"#{Gitlab.config.gitlab_shell.repos_path}\" does not match \"#{repo_path}\"")
end
- repo_path.gsub!(/\.git\z/, "")
- repo_path.gsub!(/\A\//, "")
+ post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes)
- project = Project.find_with_namespace(repo_path)
-
- if project.nil?
+ if post_received.project.nil?
log("Triggered hook for non-existing project with full path \"#{repo_path} \"")
return false
end
- changes = Base64.decode64(changes) unless changes.include?(" ")
- changes = utf8_encode_changes(changes)
- changes = changes.lines
+ if post_received.wiki?
+ # Nothing defined here yet.
+ elsif post_received.regular_project?
+ process_project_changes(post_received)
+ else
+ log("Triggered hook for unidentifiable repository type with full path \"#{repo_path} \"")
+ false
+ end
+ end
- changes.each do |change|
+ def process_project_changes(post_received)
+ post_received.changes.each do |change|
oldrev, newrev, ref = change.strip.split(' ')
- @user ||= identify(identifier, project, newrev)
+ @user ||= post_received.identify(newrev)
unless @user
- log("Triggered hook for non-existing user \"#{identifier} \"")
+ log("Triggered hook for non-existing user \"#{post_received.identifier} \"")
return false
end
if Gitlab::Git.tag_ref?(ref)
- GitTagPushService.new.execute(project, @user, oldrev, newrev, ref)
+ GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref)
else
- GitPushService.new(project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
+ GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
end
end
end
- def utf8_encode_changes(changes)
- changes = changes.dup
-
- changes.force_encoding("UTF-8")
- return changes if changes.valid_encoding?
-
- # Convert non-UTF-8 branch/tag names to UTF-8 so they can be dumped as JSON.
- detection = CharlockHolmes::EncodingDetector.detect(changes)
- return changes unless detection && detection[:encoding]
-
- CharlockHolmes::Converter.convert(changes, detection[:encoding], 'UTF-8')
- end
-
+ private
+
def log(message)
Gitlab::GitLogger.error("POST-RECEIVE: #{message}")
end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 2572b9d6d98..f9e32337983 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -20,14 +20,15 @@ class RepositoryForkWorker
return
end
+ project.repository.after_import
+
unless project.valid_repo?
- logger.error("Project #{id} had an invalid repository after fork")
+ logger.error("Project #{project_id} had an invalid repository after fork")
project.update(import_error: "The forked repository is invalid.")
project.import_fail
return
end
- project.repository.expire_emptiness_caches
project.import_finish
end
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 0b6f746e118..2937493c614 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -18,7 +18,7 @@ class RepositoryImportWorker
return
end
- project.repository.expire_emptiness_caches
+ project.repository.after_import
project.import_finish
end
end