summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2017-02-15 16:13:14 +0800
committerLin Jen-Shin <godfat@godfat.org>2017-02-15 16:13:14 +0800
commita065ee341c599f9500cd0b52a873cdb71a0bce55 (patch)
tree28277b71118c7f7385add7c8ae4493f1d964305b /app
parent188c231304845ff29506dc152aaea6ec42373015 (diff)
parent1452729304393978ec93b712130dff6687db01b9 (diff)
downloadgitlab-ce-a065ee341c599f9500cd0b52a873cdb71a0bce55.tar.gz
Merge remote-tracking branch 'upstream/master' into use-update-runner-service
* upstream/master: (488 commits) Remove duplicate CHANGELOG.md entries for 8.16.5 Update CHANGELOG.md for 8.14.9 Update CHANGELOG.md for 8.15.6 #27631: Add missing top-area div to activity header page Update CHANGELOG.md for 8.16.5 Update CHANGELOG.md for 8.16.5 Update CHANGELOG.md for 8.16.5 Fix yarn lock and package.json mismatch caused by MR 9133 sync yarn.lock with recent changes to package.json Add changelog Fix z index bugs Add Links to Branches in Calendar Activity SidekiqStatus need to be qualified in some cases Replace static fixture for behaviors/requires_input_spec.js (!9162) API: Consolidate /projects endpoint Add MySQL info in install requirements Fix timezone on issue boards due date Use Gitlab::Database.with_connection_pool from !9192 Disconnect the pool after done Use threads directly, introduce pool later: ...
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api.js2
-rw-r--r--app/assets/javascripts/application.js10
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es63
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js.es63
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es610
-rw-r--r--app/assets/javascripts/build.js18
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es626
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_service.js.es644
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_store.js.es650
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js.es6107
-rw-r--r--app/assets/javascripts/copy_as_gfm.js.es63
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es66
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es62
-rw-r--r--app/assets/javascripts/diff.js.es62
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es611
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es613
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es619
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js.es62
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es613
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es64
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es648
-rw-r--r--app/assets/javascripts/dispatcher.js.es615
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js4
-rw-r--r--app/assets/javascripts/due_date_select.js.es659
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es612
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es63
-rw-r--r--app/assets/javascripts/environments/vue_resource_interceptor.js.es612
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js.es62
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es62
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es67
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es62
-rw-r--r--app/assets/javascripts/gl_dropdown.js8
-rw-r--r--app/assets/javascripts/issuable_form.js16
-rw-r--r--app/assets/javascripts/label_manager.js.es626
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es637
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js101
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js.es6126
-rw-r--r--app/assets/javascripts/member_expiration_date.js.es633
-rw-r--r--app/assets/javascripts/merge_request.js8
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.es653
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es622
-rw-r--r--app/assets/javascripts/milestone.js160
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js.es64
-rw-r--r--app/assets/javascripts/notes.js2
-rw-r--r--app/assets/javascripts/profile/profile.js.es61
-rw-r--r--app/assets/javascripts/shortcuts_blob.js29
-rw-r--r--app/assets/javascripts/shortcuts_blob.js.es629
-rw-r--r--app/assets/javascripts/sidebar.js.es638
-rw-r--r--app/assets/javascripts/single_file_diff.js4
-rw-r--r--app/assets/javascripts/test_utils/simulate_drag.js (renamed from app/assets/javascripts/boards/test_utils/simulate_drag.js)5
-rw-r--r--app/assets/javascripts/todos.js.es620
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js.es661
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es68
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js.es6106
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es62
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stages.js.es621
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es611
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js.es63
-rw-r--r--app/assets/javascripts/vue_realtime_listener/index.js.es611
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js.es6 (renamed from app/assets/javascripts/vue_common_component/commit.js.es6)2
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js.es661
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6234
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js.es6 (renamed from app/assets/javascripts/vue_pagination/index.js.es6)0
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es623
-rw-r--r--app/assets/stylesheets/application.scss3
-rw-r--r--app/assets/stylesheets/framework/animations.scss3
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/calendar.scss55
-rw-r--r--app/assets/stylesheets/framework/common.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss124
-rw-r--r--app/assets/stylesheets/framework/files.scss43
-rw-r--r--app/assets/stylesheets/framework/header.scss6
-rw-r--r--app/assets/stylesheets/framework/jquery.scss57
-rw-r--r--app/assets/stylesheets/framework/lists.scss4
-rw-r--r--app/assets/stylesheets/framework/nav.scss14
-rw-r--r--app/assets/stylesheets/framework/pagination.scss14
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss15
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss7
-rw-r--r--app/assets/stylesheets/pages/boards.scss6
-rw-r--r--app/assets/stylesheets/pages/commits.scss1
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss7
-rw-r--r--app/assets/stylesheets/pages/events.scss1
-rw-r--r--app/assets/stylesheets/pages/issuable.scss16
-rw-r--r--app/assets/stylesheets/pages/issues.scss6
-rw-r--r--app/assets/stylesheets/pages/labels.scss21
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss67
-rw-r--r--app/assets/stylesheets/pages/milestone.scss6
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss95
-rw-r--r--app/assets/stylesheets/pages/profile.scss4
-rw-r--r--app/assets/stylesheets/pages/wiki.scss30
-rw-r--r--app/controllers/admin/dashboard_controller.rb4
-rw-r--r--app/controllers/admin/groups_controller.rb4
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/application_controller.rb17
-rw-r--r--app/controllers/concerns/issuable_collections.rb22
-rw-r--r--app/controllers/concerns/issues_action.rb3
-rw-r--r--app/controllers/concerns/merge_requests_action.rb3
-rw-r--r--app/controllers/concerns/spammable_actions.rb18
-rw-r--r--app/controllers/dashboard/groups_controller.rb2
-rw-r--r--app/controllers/dashboard/projects_controller.rb23
-rw-r--r--app/controllers/explore/application_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb35
-rw-r--r--app/controllers/help_controller.rb2
-rw-r--r--app/controllers/koding_controller.rb2
-rw-r--r--app/controllers/profiles/notifications_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb10
-rw-r--r--app/controllers/projects/commit_controller.rb3
-rw-r--r--app/controllers/projects/compare_controller.rb3
-rw-r--r--app/controllers/projects/environments_controller.rb13
-rw-r--r--app/controllers/projects/issues_controller.rb29
-rw-r--r--app/controllers/projects/lfs_api_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests_controller.rb40
-rw-r--r--app/controllers/projects/notes_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb11
-rw-r--r--app/controllers/projects/protected_branches_controller.rb8
-rw-r--r--app/controllers/projects/runners_controller.rb8
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb44
-rw-r--r--app/controllers/projects/triggers_controller.rb10
-rw-r--r--app/controllers/projects/uploads_controller.rb4
-rw-r--r--app/controllers/projects/variables_controller.rb9
-rw-r--r--app/controllers/projects/wikis_controller.rb5
-rw-r--r--app/controllers/registrations_controller.rb6
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/finders/environments_finder.rb55
-rw-r--r--app/finders/group_members_finder.rb20
-rw-r--r--app/finders/group_projects_finder.rb2
-rw-r--r--app/finders/groups_finder.rb2
-rw-r--r--app/finders/projects_finder.rb2
-rw-r--r--app/helpers/builds_helper.rb2
-rw-r--r--app/helpers/commits_helper.rb13
-rw-r--r--app/helpers/gitlab_routing_helper.rb6
-rw-r--r--app/helpers/merge_requests_helper.rb8
-rw-r--r--app/helpers/preferences_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb6
-rw-r--r--app/helpers/wiki_helper.rb13
-rw-r--r--app/mailers/notify.rb2
-rw-r--r--app/models/application_setting.rb28
-rw-r--r--app/models/award_emoji.rb8
-rw-r--r--app/models/ci/build.rb28
-rw-r--r--app/models/ci/pipeline.rb8
-rw-r--r--app/models/concerns/issuable.rb9
-rw-r--r--app/models/concerns/mentionable.rb19
-rw-r--r--app/models/concerns/milestoneish.rb2
-rw-r--r--app/models/concerns/routable.rb66
-rw-r--r--app/models/concerns/spammable.rb12
-rw-r--r--app/models/concerns/time_trackable.rb2
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/directly_addressed_user.rb7
-rw-r--r--app/models/environment.rb37
-rw-r--r--app/models/event.rb11
-rw-r--r--app/models/group.rb11
-rw-r--r--app/models/group_milestone.rb2
-rw-r--r--app/models/member.rb1
-rw-r--r--app/models/merge_request.rb32
-rw-r--r--app/models/namespace.rb44
-rw-r--r--app/models/note.rb6
-rw-r--r--app/models/project.rb75
-rw-r--r--app/models/project_services/chat_slash_commands_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb10
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb4
-rw-r--r--app/models/project_services/slack_slash_commands_service.rb4
-rw-r--r--app/models/repository.rb26
-rw-r--r--app/models/route.rb27
-rw-r--r--app/models/timelog.rb18
-rw-r--r--app/models/todo.rb16
-rw-r--r--app/models/user.rb42
-rw-r--r--app/models/wiki_directory.rb18
-rw-r--r--app/models/wiki_page.rb51
-rw-r--r--app/policies/project_policy.rb47
-rw-r--r--app/policies/project_snippet_policy.rb2
-rw-r--r--app/serializers/environment_entity.rb2
-rw-r--r--app/serializers/environment_serializer.rb47
-rw-r--r--app/serializers/pipeline_serializer.rb40
-rw-r--r--app/services/ci/stop_environments_service.rb7
-rw-r--r--app/services/create_tag_service.rb2
-rw-r--r--app/services/delete_tag_service.rb2
-rw-r--r--app/services/delete_user_service.rb31
-rw-r--r--app/services/destroy_group_service.rb29
-rw-r--r--app/services/files/destroy_service.rb (renamed from app/services/files/delete_service.rb)2
-rw-r--r--app/services/groups/destroy_service.rb25
-rw-r--r--app/services/issues/build_service.rb10
-rw-r--r--app/services/issues/create_service.rb12
-rw-r--r--app/services/merge_requests/refresh_service.rb6
-rw-r--r--app/services/notes/destroy_service.rb (renamed from app/services/notes/delete_service.rb)2
-rw-r--r--app/services/notification_service.rb11
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/projects/import_export/export_service.rb2
-rw-r--r--app/services/projects/participants_service.rb2
-rw-r--r--app/services/projects/transfer_service.rb17
-rw-r--r--app/services/spam_service.rb3
-rw-r--r--app/services/system_note_service.rb1
-rw-r--r--app/services/todo_service.rb18
-rw-r--r--app/services/users/destroy_service.rb33
-rw-r--r--app/services/wiki_pages/destroy_service.rb11
-rw-r--r--app/views/admin/logs/show.html.haml2
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml2
-rw-r--r--app/views/admin/spam_logs/index.html.haml1
-rw-r--r--app/views/admin/users/_access_levels.html.haml37
-rw-r--r--app/views/admin/users/_form.html.haml23
-rw-r--r--app/views/ci/lints/show.html.haml2
-rw-r--r--app/views/dashboard/_activity_head.html.haml15
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-rw-r--r--app/views/discussions/_resolve_all.html.haml3
-rw-r--r--app/views/groups/_home_panel.html.haml17
-rw-r--r--app/views/groups/_show_nav.html.haml7
-rw-r--r--app/views/groups/milestones/show.html.haml4
-rw-r--r--app/views/groups/show.html.haml43
-rw-r--r--app/views/groups/subgroups.html.haml20
-rw-r--r--app/views/help/_shortcuts.html.haml8
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml16
-rw-r--r--app/views/profiles/notifications/show.html.haml5
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml16
-rw-r--r--app/views/projects/_last_push.html.haml1
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_actions.html.haml5
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/boards/_show.html.haml2
-rw-r--r--app/views/projects/builds/_sidebar.html.haml2
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml25
-rw-r--r--app/views/projects/commit/_pipelines_list.haml40
-rw-r--r--app/views/projects/commit/pipelines.html.haml2
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml4
-rw-r--r--app/views/projects/compare/_form.html.haml4
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml3
-rw-r--r--app/views/projects/diffs/_file.html.haml9
-rw-r--r--app/views/projects/diffs/_file_header.html.haml6
-rw-r--r--app/views/projects/environments/_stop.html.haml2
-rw-r--r--app/views/projects/environments/show.html.haml2
-rw-r--r--app/views/projects/environments/terminal.html.haml2
-rw-r--r--app/views/projects/issues/_discussion.html.haml4
-rw-r--r--app/views/projects/issues/_issue.html.haml96
-rw-r--r--app/views/projects/issues/show.html.haml8
-rw-r--r--app/views/projects/issues/verify.html.haml20
-rw-r--r--app/views/projects/labels/index.html.haml3
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml126
-rw-r--r--app/views/projects/merge_requests/_new_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml6
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_pipelines.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml17
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml8
-rw-r--r--app/views/projects/merge_requests/widget/open/_build_failed.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml3
-rw-r--r--app/views/projects/new.html.haml5
-rw-r--r--app/views/projects/notes/_note.html.haml14
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml2
-rw-r--r--app/views/projects/pipelines/index.html.haml46
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml (renamed from app/views/projects/pipelines_settings/show.html.haml)6
-rw-r--r--app/views/projects/runners/_index.html.haml (renamed from app/views/projects/runners/index.html.haml)6
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml2
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml4
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml30
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml17
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml32
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml6
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--app/views/projects/triggers/_index.html.haml (renamed from app/views/projects/triggers/index.html.haml)12
-rw-r--r--app/views/projects/variables/_index.html.haml (renamed from app/views/projects/variables/index.html.haml)8
-rw-r--r--app/views/projects/wikis/_new.html.haml4
-rw-r--r--app/views/projects/wikis/_pages_wiki_page.html.haml5
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml8
-rw-r--r--app/views/projects/wikis/_sidebar_wiki_page.html.haml3
-rw-r--r--app/views/projects/wikis/_wiki_directory.html.haml4
-rw-r--r--app/views/projects/wikis/_wiki_page.html.haml1
-rw-r--r--app/views/projects/wikis/pages.html.haml10
-rw-r--r--app/views/projects/wikis/show.html.haml4
-rw-r--r--app/views/search/results/_blob.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml2
-rw-r--r--app/views/search/results/_wiki_blob.html.haml2
-rw-r--r--app/views/shared/_commit_message_container.html.haml4
-rw-r--r--app/views/shared/_group_form.html.haml7
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml19
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml18
-rw-r--r--app/views/shared/_visibility_level.html.haml11
-rw-r--r--app/views/shared/groups/_group.html.haml6
-rw-r--r--app/views/shared/issuable/_filter.html.haml10
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml7
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml3
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/projects/_dropdown.html.haml12
-rw-r--r--app/views/shared/snippets/_form.html.haml2
-rw-r--r--app/views/sherlock/file_samples/show.html.haml2
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/views/users/calendar.html.haml2
-rw-r--r--app/views/users/calendar_activities.html.haml12
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/workers/authorized_projects_worker.rb2
-rw-r--r--app/workers/delete_user_worker.rb2
-rw-r--r--app/workers/group_destroy_worker.rb2
302 files changed, 3122 insertions, 1778 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index b4a8c827d7f..84bbe90f3b1 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -11,7 +11,7 @@
licensePath: "/api/:version/templates/licenses/:key",
gitignorePath: "/api/:version/templates/gitignores/:key",
gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
- dockerfilePath: "/api/:version/dockerfiles/:key",
+ dockerfilePath: "/api/:version/templates/dockerfiles/:key",
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
group: function(group_id, callback) {
var url = Api.buildUrl(Api.groupPath)
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 637fca4d4da..4b5c9686cab 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -10,7 +10,6 @@ function requireAll(context) { return context.keys().map(context); }
window.$ = window.jQuery = require('jquery');
require('jquery-ui/ui/autocomplete');
-require('jquery-ui/ui/datepicker');
require('jquery-ui/ui/draggable');
require('jquery-ui/ui/effect-highlight');
require('jquery-ui/ui/sortable');
@@ -21,7 +20,7 @@ require('vendor/jquery.waitforimages');
require('vendor/jquery.caret');
require('vendor/jquery.atwho');
require('vendor/jquery.scrollTo');
-window.Cookies = require('vendor/js.cookie');
+window.Cookies = require('js-cookie');
require('./autosave');
require('bootstrap/js/affix');
require('bootstrap/js/alert');
@@ -35,8 +34,10 @@ require('bootstrap/js/transition');
require('bootstrap/js/tooltip');
require('bootstrap/js/popover');
require('select2/select2.js');
+window.Pikaday = require('pikaday');
window._ = require('underscore');
window.Dropzone = require('dropzone');
+window.Sortable = require('vendor/Sortable');
require('mousetrap');
require('mousetrap/plugins/pause/mousetrap-pause');
require('./shortcuts');
@@ -55,8 +56,7 @@ requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/));
require('vendor/fuzzaldrin-plus');
-window.ES6Promise = require('vendor/es6-promise.auto');
-window.ES6Promise.polyfill();
+require('es6-promise').polyfill();
(function () {
document.addEventListener('beforeunload', function () {
@@ -247,5 +247,7 @@ window.ES6Promise.polyfill();
new Aside();
// bind sidebar events
new gl.Sidebar();
+
+ gl.utils.initTimeagoTimeout();
});
}).call(this);
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index e3241974e59..8f30900198e 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -6,7 +6,6 @@ function requireAll(context) { return context.keys().map(context); }
window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
-window.Sortable = require('vendor/Sortable');
requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/));
@@ -16,7 +15,7 @@ require('./components/board');
require('./components/board_sidebar');
require('./components/new_list_dropdown');
require('./components/modal/index');
-require('./vue_resource_interceptor');
+require('../vue_shared/vue_resource_interceptor');
$(() => {
const $boardApp = document.getElementById('board-app');
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
index 7e192e90fe6..03425bb145b 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
@@ -1,6 +1,7 @@
/* global Vue */
+/* global dateFormat */
Vue.filter('due-date', (value) => {
const date = new Date(value);
- return $.datepicker.formatDate('M d, yy', date);
+ return dateFormat(date, 'mmm d, yyyy', true);
});
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
deleted file mode 100644
index 54c2b4ad369..00000000000
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */
-/* global Vue */
-
-Vue.http.interceptors.push((request, next) => {
- Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
-
- next(function (response) {
- Vue.activeResources -= 1;
- });
-});
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 0152be88b48..c5a962dd199 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -67,16 +67,8 @@
Build.prototype.initSidebar = function() {
this.$sidebar = $('.js-build-sidebar');
- this.sidebarTranslationLimits = {
- min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
- };
- this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight();
- this.$sidebar.css({
- top: this.sidebarTranslationLimits.max
- });
this.$sidebar.niceScroll();
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
- this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this));
};
Build.prototype.location = function() {
@@ -231,14 +223,6 @@
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
};
- Build.prototype.translateSidebar = function(e) {
- var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop);
- if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min;
- this.$sidebar.css({
- top: newPosition
- });
- };
-
Build.prototype.toggleSidebar = function(shouldHide) {
var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
@@ -285,7 +269,7 @@
e.preventDefault();
$currentTarget = $(e.currentTarget);
$.scrollTo($currentTarget.attr('href'), {
- offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
+ offset: 0
});
};
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6
new file mode 100644
index 00000000000..fbfec7743c7
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6
@@ -0,0 +1,26 @@
+/* eslint-disable no-new, no-param-reassign */
+/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
+
+window.Vue = require('vue');
+require('./pipelines_table');
+/**
+ * Commits View > Pipelines Tab > Pipelines Table.
+ * Merge Request View > Pipelines Tab > Pipelines Table.
+ *
+ * Renders Pipelines table in pipelines tab in the commits show view.
+ * Renders Pipelines table in pipelines tab in the merge request show view.
+ */
+
+$(() => {
+ window.gl = window.gl || {};
+ gl.commits = gl.commits || {};
+ gl.commits.pipelines = gl.commits.pipelines || {};
+
+ if (gl.commits.PipelinesTableBundle) {
+ gl.commits.PipelinesTableBundle.$destroy(true);
+ }
+
+ gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
+});
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6
new file mode 100644
index 00000000000..8ae98f9bf97
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6
@@ -0,0 +1,44 @@
+/* globals Vue */
+/* eslint-disable no-unused-vars, no-param-reassign */
+
+/**
+ * Pipelines service.
+ *
+ * Used to fetch the data used to render the pipelines table.
+ * Uses Vue.Resource
+ */
+class PipelinesService {
+
+ /**
+ * FIXME: The url provided to request the pipelines in the new merge request
+ * page already has `.json`.
+ * This should be fixed when the endpoint is improved.
+ *
+ * @param {String} root
+ */
+ constructor(root) {
+ let endpoint;
+
+ if (root.indexOf('.json') === -1) {
+ endpoint = `${root}.json`;
+ } else {
+ endpoint = root;
+ }
+ this.pipelines = Vue.resource(endpoint);
+ }
+
+ /**
+ * Given the root param provided when the class is initialized, will
+ * make a GET request.
+ *
+ * @return {Promise}
+ */
+ all() {
+ return this.pipelines.get();
+ }
+}
+
+window.gl = window.gl || {};
+gl.commits = gl.commits || {};
+gl.commits.pipelines = gl.commits.pipelines || {};
+gl.commits.pipelines.PipelinesService = PipelinesService;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
new file mode 100644
index 00000000000..f1b41911b73
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
@@ -0,0 +1,50 @@
+/* eslint-disable no-underscore-dangle*/
+/**
+ * Pipelines' Store for commits view.
+ *
+ * Used to store the Pipelines rendered in the commit view in the pipelines table.
+ */
+
+class PipelinesStore {
+ constructor() {
+ this.state = {};
+ this.state.pipelines = [];
+ }
+
+ storePipelines(pipelines = []) {
+ this.state.pipelines = pipelines;
+
+ return pipelines;
+ }
+
+ /**
+ * Once the data is received we will start the time ago loops.
+ *
+ * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
+ * update the time to show how long as passed.
+ *
+ */
+ startTimeAgoLoops() {
+ const startTimeLoops = () => {
+ this.timeLoopInterval = setInterval(() => {
+ this.$children[0].$children.reduce((acc, component) => {
+ const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
+ acc.push(timeAgoComponent);
+ return acc;
+ }, []).forEach(e => e.changeTime());
+ }, 10000);
+ };
+
+ startTimeLoops();
+
+ const removeIntervals = () => clearInterval(this.timeLoopInterval);
+ const startIntervals = () => startTimeLoops();
+
+ gl.VueRealtimeListener(removeIntervals, startIntervals);
+ }
+}
+
+window.gl = window.gl || {};
+gl.commits = gl.commits || {};
+gl.commits.pipelines = gl.commits.pipelines || {};
+gl.commits.pipelines.PipelinesStore = PipelinesStore;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
new file mode 100644
index 00000000000..ce0dbd4d56b
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
@@ -0,0 +1,107 @@
+/* eslint-disable no-new, no-param-reassign */
+/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
+
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('../../lib/utils/common_utils');
+require('../../vue_shared/vue_resource_interceptor');
+require('../../vue_shared/components/pipelines_table');
+require('../../vue_realtime_listener/index');
+require('./pipelines_service');
+require('./pipelines_store');
+
+/**
+ *
+ * Uses `pipelines-table-component` to render Pipelines table with an API call.
+ * Endpoint is provided in HTML and passed as `endpoint`.
+ * We need a store to store the received environemnts.
+ * We need a service to communicate with the server.
+ *
+ * Necessary SVG in the table are provided as props. This should be refactored
+ * as soon as we have Webpack and can load them directly into JS files.
+ */
+
+(() => {
+ window.gl = window.gl || {};
+ gl.commits = gl.commits || {};
+ gl.commits.pipelines = gl.commits.pipelines || {};
+
+ gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
+
+ components: {
+ 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
+ },
+
+ /**
+ * Accesses the DOM to provide the needed data.
+ * Returns the necessary props to render `pipelines-table-component` component.
+ *
+ * @return {Object}
+ */
+ data() {
+ const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
+ const svgsData = document.querySelector('.pipeline-svgs').dataset;
+ const store = new gl.commits.pipelines.PipelinesStore();
+
+ // Transform svgs DOMStringMap to a plain Object.
+ const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
+
+ return {
+ endpoint: pipelinesTableData.endpoint,
+ svgs: svgsObject,
+ store,
+ state: store.state,
+ isLoading: false,
+ };
+ },
+
+ /**
+ * When the component is created the service to fetch the data will be
+ * initialized with the correct endpoint.
+ *
+ * A request to fetch the pipelines will be made.
+ * In case of a successfull response we will store the data in the provided
+ * store, in case of a failed response we need to warn the user.
+ *
+ */
+ created() {
+ const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
+
+ this.isLoading = true;
+ return pipelinesService.all()
+ .then(response => response.json())
+ .then((json) => {
+ this.store.storePipelines(json);
+ this.store.startTimeAgoLoops.call(this, Vue);
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert');
+ });
+ },
+
+ template: `
+ <div>
+ <div class="pipelines realtime-loading" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.pipelines.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ No pipelines to show
+ </h2>
+ </div>
+
+ <div class="table-holder pipelines"
+ v-if="!isLoading && state.pipelines.length > 0">
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :svgs="svgs">
+ </pipelines-table-component>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6
index 2bfe57b4100..4bd537a6f28 100644
--- a/app/assets/javascripts/copy_as_gfm.js.es6
+++ b/app/assets/javascripts/copy_as_gfm.js.es6
@@ -91,6 +91,9 @@ require('./lib/utils/common_utils');
},
},
SanitizationFilter: {
+ 'a[name]:not([href]):empty'(el, text) {
+ return el.outerHTML;
+ },
'dl'(el, text) {
let lines = text.trim().split('\n');
// Add two spaces to the front of subsequent list items lines,
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
index 513298ba4e7..8652479e7bf 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
@@ -13,6 +13,12 @@
<div>
<div class="events-description">
{{ stage.description }}
+ <span v-if="items.length === 50" class="events-info pull-right">
+ <i class="fa fa-warning has-tooltip"
+ title="Limited to showing 50 events at most"
+ data-placement="top"></i>
+ Showing 50 events
+ </span>
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
index c41c57c1dcd..dbdb01c8c68 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
@@ -3,7 +3,7 @@
/* global Flash */
window.Vue = require('vue');
-window.Cookies = require('vendor/js.cookie');
+window.Cookies = require('js-cookie');
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/));
diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6
index c39e30fb7e0..ccccd0a36ff 100644
--- a/app/assets/javascripts/diff.js.es6
+++ b/app/assets/javascripts/diff.js.es6
@@ -76,7 +76,7 @@ require('./lib/utils/url_utility');
const diffFile = diffTitle.closest('.diff-file');
const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
if (nothingHereBlock.length) {
- const clickTarget = $('.file-title, .click-to-expand', diffFile);
+ const clickTarget = $('.js-file-title, .click-to-expand', diffFile);
diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => {
this.highlighSelectedLine();
if (cb) cb();
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
index 2514459e65e..d948dff58ec 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -1,6 +1,6 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */
-/* global Vue */
/* global CommentsStore */
+const Vue = require('vue');
(() => {
const CommentAndResolveBtn = Vue.extend({
@@ -9,13 +9,11 @@
},
data() {
return {
- textareaIsEmpty: true
+ textareaIsEmpty: true,
+ discussion: {},
};
},
computed: {
- discussion: function () {
- return CommentsStore.state[this.discussionId];
- },
showButton: function () {
if (this.discussion) {
return this.discussion.isResolvable();
@@ -42,6 +40,9 @@
}
}
},
+ created() {
+ this.discussion = CommentsStore.state[this.discussionId];
+ },
mounted: function () {
const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
this.textareaIsEmpty = $textarea.val() === '';
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
index c3898873eaa..283dc330cad 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */
-/* global Vue */
/* global DiscussionMixins */
/* global CommentsStore */
+const Vue = require('vue');
(() => {
const JumpToDiscussion = Vue.extend({
@@ -12,12 +12,10 @@
data: function () {
return {
discussions: CommentsStore.state,
+ discussion: {},
};
},
computed: {
- discussion: function () {
- return this.discussions[this.discussionId];
- },
allResolved: function () {
return this.unresolvedDiscussionCount === 0;
},
@@ -183,10 +181,13 @@
}
$.scrollTo($target, {
- offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
+ offset: 0
});
}
- }
+ },
+ created() {
+ this.discussion = this.discussions[this.discussionId];
+ },
});
Vue.component('jump-to-discussion', JumpToDiscussion);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
index 5852b8bbdb7..d1873d6c7a2 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -1,8 +1,8 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */
-/* global Vue */
/* global CommentsStore */
/* global ResolveService */
/* global Flash */
+const Vue = require('vue');
(() => {
const ResolveBtn = Vue.extend({
@@ -10,14 +10,14 @@
noteId: Number,
discussionId: String,
resolved: Boolean,
- projectPath: String,
canResolve: Boolean,
resolvedBy: String
},
data: function () {
return {
discussions: CommentsStore.state,
- loading: false
+ loading: false,
+ note: {},
};
},
watch: {
@@ -30,13 +30,6 @@
discussion: function () {
return this.discussions[this.discussionId];
},
- note: function () {
- if (this.discussion) {
- return this.discussion.getNote(this.noteId);
- } else {
- return undefined;
- }
- },
buttonText: function () {
if (this.isResolved) {
return `Resolved by ${this.resolvedByName}`;
@@ -73,10 +66,10 @@
if (this.isResolved) {
promise = ResolveService
- .unresolve(this.projectPath, this.noteId);
+ .unresolve(this.noteId);
} else {
promise = ResolveService
- .resolve(this.projectPath, this.noteId);
+ .resolve(this.noteId);
}
promise.then((response) => {
@@ -106,6 +99,8 @@
},
created: function () {
CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
+
+ this.note = this.discussion.getNote(this.noteId);
}
});
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
index 72cdae812bc..de9367f2136 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
-/* global Vue */
/* global DiscussionMixins */
/* global CommentsStore */
+const Vue = require('vue');
((w) => {
w.ResolveCount = Vue.extend({
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
index ee5f62b2d9e..7c5fcd04d2d 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -1,25 +1,22 @@
/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */
-/* global Vue */
/* global CommentsStore */
/* global ResolveService */
+const Vue = require('vue');
+
(() => {
const ResolveDiscussionBtn = Vue.extend({
props: {
discussionId: String,
mergeRequestId: Number,
- projectPath: String,
canResolve: Boolean,
},
data: function() {
return {
- discussions: CommentsStore.state
+ discussion: {},
};
},
computed: {
- discussion: function () {
- return this.discussions[this.discussionId];
- },
showButton: function () {
if (this.discussion) {
return this.discussion.isResolvable();
@@ -51,11 +48,13 @@
},
methods: {
resolve: function () {
- ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId);
+ ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
}
},
created: function () {
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
+
+ this.discussion = CommentsStore.state[this.discussionId];
}
});
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
index f0edfb8aaf1..190461451d5 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -3,6 +3,7 @@
/* global ResolveCount */
function requireAll(context) { return context.keys().map(context); }
+const Vue = require('vue');
requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/));
@@ -10,11 +11,14 @@ requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/));
$(() => {
+ const projectPath = document.querySelector('.merge-request').dataset.projectPath;
const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn';
window.gl = window.gl || {};
window.gl.diffNoteApps = {};
+ window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
+
gl.diffNotesCompileComponents = () => {
const $components = $(COMPONENT_SELECTOR).filter(function () {
return $(this).closest('resolve-count').length !== 1;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
index a52c476352d..090c454e9e4 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -1,45 +1,37 @@
/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */
-/* global Vue */
/* global Flash */
/* global CommentsStore */
-((w) => {
- class ResolveServiceClass {
- constructor() {
- this.noteResource = Vue.resource('notes{/noteId}/resolve');
- this.discussionResource = Vue.resource('merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve');
- }
+const Vue = window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('../../vue_shared/vue_resource_interceptor');
- setCSRF() {
- Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
- }
+(() => {
+ window.gl = window.gl || {};
- prepareRequest(root) {
- this.setCSRF();
- Vue.http.options.root = root;
+ class ResolveServiceClass {
+ constructor(root) {
+ this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`);
+ this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`);
}
- resolve(projectPath, noteId) {
- this.prepareRequest(projectPath);
-
+ resolve(noteId) {
return this.noteResource.save({ noteId }, {});
}
- unresolve(projectPath, noteId) {
- this.prepareRequest(projectPath);
-
+ unresolve(noteId) {
return this.noteResource.delete({ noteId }, {});
}
- toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
+ toggleResolveForDiscussion(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
const isResolved = discussion.isResolved();
let promise;
if (isResolved) {
- promise = this.unResolveAll(projectPath, mergeRequestId, discussionId);
+ promise = this.unResolveAll(mergeRequestId, discussionId);
} else {
- promise = this.resolveAll(projectPath, mergeRequestId, discussionId);
+ promise = this.resolveAll(mergeRequestId, discussionId);
}
promise.then((response) => {
@@ -62,11 +54,9 @@
});
}
- resolveAll(projectPath, mergeRequestId, discussionId) {
+ resolveAll(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
- this.prepareRequest(projectPath);
-
discussion.loading = true;
return this.discussionResource.save({
@@ -75,11 +65,9 @@
}, {});
}
- unResolveAll(projectPath, mergeRequestId, discussionId) {
+ unResolveAll(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
- this.prepareRequest(projectPath);
-
discussion.loading = true;
return this.discussionResource.delete({
@@ -89,5 +77,5 @@
}
}
- w.ResolveService = new ResolveServiceClass();
-})(window);
+ gl.DiffNotesResolveServiceClass = ResolveServiceClass;
+})();
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index edec21e3b63..7eec2d39a9c 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -19,7 +19,6 @@
/* global UsersSelect */
/* global GroupAvatar */
/* global LineHighlighter */
-/* global ShortcutsBlob */
/* global ProjectFork */
/* global BuildArtifacts */
/* global GroupsSelect */
@@ -36,6 +35,8 @@
/* global Labels */
/* global Shortcuts */
+const ShortcutsBlob = require('./shortcuts_blob');
+
(function() {
var Dispatcher;
@@ -96,6 +97,7 @@
break;
case 'projects:milestones:new':
case 'projects:milestones:edit':
+ case 'projects:milestones:update':
new ZenMode();
new gl.DueDateSelectors();
new gl.GLForm($('.milestone-form'));
@@ -162,7 +164,7 @@
case 'projects:commit:pipelines':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
- });
+ }).bindEvents();
break;
case 'projects:commits:show':
case 'projects:activity':
@@ -225,7 +227,12 @@
case 'projects:blame:show':
new LineHighlighter();
shortcut_handler = new ShortcutsNavigation();
- new ShortcutsBlob(true);
+ const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
+ const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+ new ShortcutsBlob({
+ skipResetBindings: true,
+ fileBlobPermalinkUrl,
+ });
break;
case 'groups:labels:new':
case 'groups:labels:edit':
@@ -259,7 +266,7 @@
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
break;
- case 'projects:variables:index':
+ case 'projects:ci_cd:show':
new gl.ProjectVariables();
break;
case 'ci:lints:create':
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index c290e1a8355..5cdf11c6a2c 100644
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -78,8 +78,8 @@ require('../window')(function(w){
},
destroy: function() {
- if (this.listTemplate) {
- var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate;
}
}
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
index d81d4cf8425..ab5ce23d261 100644
--- a/app/assets/javascripts/due_date_select.js.es6
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -1,4 +1,6 @@
/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
+/* global dateFormat */
+/* global Pikaday */
(function(global) {
class DueDateSelect {
@@ -25,11 +27,14 @@
this.initGlDropdown();
this.initRemoveDueDate();
this.initDatePicker();
- this.initStopPropagation();
}
initGlDropdown() {
this.$dropdown.glDropdown({
+ opened: () => {
+ const calendar = this.$datePicker.data('pikaday');
+ calendar.show();
+ },
hidden: () => {
this.$selectbox.hide();
this.$value.css('display', '');
@@ -38,25 +43,37 @@
}
initDatePicker() {
- this.$datePicker.datepicker({
- dateFormat: 'yy-mm-dd',
- defaultDate: $("input[name='" + this.fieldName + "']").val(),
- altField: "input[name='" + this.fieldName + "']",
- onSelect: () => {
+ const $dueDateInput = $(`input[name='${this.fieldName}']`);
+
+ const calendar = new Pikaday({
+ field: $dueDateInput.get(0),
+ theme: 'gitlab-theme',
+ format: 'YYYY-MM-DD',
+ onSelect: (dateText) => {
+ const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
+
+ $dueDateInput.val(formattedDate);
+
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
- gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val();
+ gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
this.updateIssueBoardIssue();
} else {
- return this.saveDueDate(true);
+ this.saveDueDate(true);
}
}
});
+
+ this.$datePicker.append(calendar.el);
+ this.$datePicker.data('pikaday', calendar);
}
initRemoveDueDate() {
this.$block.on('click', '.js-remove-due-date', (e) => {
+ const calendar = this.$datePicker.data('pikaday');
e.preventDefault();
+ calendar.setDate(null);
+
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
this.updateIssueBoardIssue();
@@ -67,12 +84,6 @@
});
}
- initStopPropagation() {
- $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', (e) => {
- return e.stopImmediatePropagation();
- });
- }
-
saveDueDate(isDropdown) {
this.parseSelectedDate();
this.prepSelectedDate();
@@ -86,7 +97,7 @@
// Construct Date object manually to avoid buggy dateString support within Date constructor
const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10));
const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
- this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj);
+ this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy');
} else {
this.displayedDate = 'No due date';
}
@@ -153,14 +164,24 @@
}
initMilestoneDatePicker() {
- $('.datepicker').datepicker({
- dateFormat: 'yy-mm-dd'
+ $('.datepicker').each(function() {
+ const $datePicker = $(this);
+ const calendar = new Pikaday({
+ field: $datePicker.get(0),
+ theme: 'gitlab-theme',
+ format: 'YYYY-MM-DD',
+ onSelect(dateText) {
+ $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
+ }
+ });
+
+ $datePicker.data('pikaday', calendar);
});
$('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
e.preventDefault();
- const datepicker = $(e.target).siblings('.datepicker');
- $.datepicker._clearDate(datepicker);
+ const calendar = $(e.target).siblings('.datepicker').data('pikaday');
+ calendar.setDate(null);
});
}
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
index 521873b14b4..39746621c43 100644
--- a/app/assets/javascripts/environments/components/environment_item.js.es6
+++ b/app/assets/javascripts/environments/components/environment_item.js.es6
@@ -2,9 +2,9 @@
/* global timeago */
window.Vue = require('vue');
-window.timeago = require('vendor/timeago');
+window.timeago = require('timeago.js');
require('../../lib/utils/text_utility');
-require('../../vue_common_component/commit');
+require('../../vue_shared/components/commit');
require('./environment_actions');
require('./environment_external_url');
require('./environment_stop');
@@ -147,12 +147,12 @@ require('./environment_terminal_button');
},
/**
- * Returns the value of the `stoppable?` key provided in the response.
+ * Returns the value of the `stop_action?` key provided in the response.
*
* @returns {Boolean}
*/
- isStoppable() {
- return this.model['stoppable?'];
+ hasStopAction() {
+ return this.model['stop_action?'];
},
/**
@@ -508,7 +508,7 @@ require('./environment_terminal_button');
</external-url-component>
</div>
- <div v-if="isStoppable && canCreateDeployment"
+ <div v-if="hasStopAction && canCreateDeployment"
class="inline js-stop-component-container">
<stop-component
:stop-url="model.stop_path">
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6
index 58f4c6eadb2..05c59d92fd4 100644
--- a/app/assets/javascripts/environments/environments_bundle.js.es6
+++ b/app/assets/javascripts/environments/environments_bundle.js.es6
@@ -1,8 +1,7 @@
window.Vue = require('vue');
-
require('./stores/environments_store');
require('./components/environment');
-require('./vue_resource_interceptor');
+require('../vue_shared/vue_resource_interceptor');
$(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
deleted file mode 100644
index 406bdbc1c7d..00000000000
--- a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
+++ /dev/null
@@ -1,12 +0,0 @@
-/* global Vue */
-Vue.http.interceptors.push((request, next) => {
- Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
-
- next((response) => {
- if (typeof response.data === 'string') {
- response.data = JSON.parse(response.data); // eslint-disable-line
- }
-
- Vue.activeResources--; // eslint-disable-line
- });
-});
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
index f93605a5a21..7e9c6f74aa5 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
@@ -8,7 +8,7 @@ require('./filtered_search_dropdown');
super(droplab, dropdown, input, filter);
this.config = {
droplabAjaxFilter: {
- endpoint: '/autocomplete/users.json',
+ endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
searchKey: 'search',
params: {
per_page: 20,
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
index 859d6515531..e8c2df03a46 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
@@ -4,7 +4,7 @@
class FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) {
this.droplab = droplab;
- this.hookId = input.getAttribute('data-id');
+ this.hookId = input && input.getAttribute('data-id');
this.input = input;
this.filter = filter;
this.dropdown = dropdown;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
index 547989a6ff5..8ce4cf4fc36 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
@@ -2,7 +2,8 @@
(() => {
class FilteredSearchDropdownManager {
- constructor() {
+ constructor(baseEndpoint = '') {
+ this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = gl.FilteredSearchTokenizer;
this.filteredSearchInput = document.querySelector('.filtered-search');
@@ -38,13 +39,13 @@
milestone: {
reference: null,
gl: 'DropdownNonUser',
- extraArguments: ['milestones.json', '%'],
+ extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
element: document.querySelector('#js-dropdown-milestone'),
},
label: {
reference: null,
gl: 'DropdownNonUser',
- extraArguments: ['labels.json', '~'],
+ extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
element: document.querySelector('#js-dropdown-label'),
},
hint: {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
index 4e02ab7c8c1..ffc7d29e4c5 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
@@ -6,7 +6,7 @@
if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer;
- this.dropdownManager = new gl.FilteredSearchDropdownManager();
+ this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '');
this.bindEvents();
this.loadSearchParamsFromURL();
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index d9101b55c7f..0d618caf350 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -47,9 +47,11 @@
}
// Only filter asynchronously only if option remote is set
if (this.options.remote) {
+ $inputContainer.parent().addClass('is-loading');
clearTimeout(timeout);
return timeout = setTimeout(function() {
return this.options.query(this.input.val(), function(data) {
+ $inputContainer.parent().removeClass('is-loading');
return this.options.callback(data);
}.bind(this));
}.bind(this), 250);
@@ -437,7 +439,7 @@
}
};
- GitLabDropdown.prototype.opened = function() {
+ GitLabDropdown.prototype.opened = function(e) {
var contentHtml;
this.resetRows();
this.addArrowKeyEvent();
@@ -457,6 +459,10 @@
this.positionMenuAbove();
}
+ if (this.options.opened) {
+ this.options.opened.call(this, e);
+ }
+
return this.dropdown.trigger('shown.gl.dropdown');
};
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 293b856dc4d..2ec545db665 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -3,6 +3,8 @@
/* global UsersSelect */
/* global ZenMode */
/* global Autosave */
+/* global dateFormat */
+/* global Pikaday */
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -13,7 +15,7 @@
IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
function IssuableForm(form) {
- var $issuableDueDate;
+ var $issuableDueDate, calendar;
this.form = form;
this.toggleWip = bind(this.toggleWip, this);
this.renderWipExplanation = bind(this.renderWipExplanation, this);
@@ -35,12 +37,14 @@
this.initMoveDropdown();
$issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) {
- $('.datepicker').datepicker({
- dateFormat: 'yy-mm-dd',
- onSelect: function(dateText, inst) {
- return $issuableDueDate.val(dateText);
+ calendar = new Pikaday({
+ field: $issuableDueDate.get(0),
+ theme: 'gitlab-theme',
+ format: 'YYYY-MM-DD',
+ onSelect: function(dateText) {
+ $issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
- }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val()));
+ });
}
}
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
index 2a50b72c8aa..38b2eb9ff14 100644
--- a/app/assets/javascripts/label_manager.js.es6
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -1,5 +1,6 @@
/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
/* global Flash */
+/* global Sortable */
((global) => {
class LabelManager {
@@ -9,11 +10,12 @@
this.otherLabels = otherLabels || $('.js-other-labels');
this.errorMessage = 'Unable to update label prioritization at this time';
this.emptyState = document.querySelector('#js-priority-labels-empty-state');
- this.prioritizedLabels.sortable({
- items: 'li',
- placeholder: 'list-placeholder',
- axis: 'y',
- update: this.onPrioritySortUpdate.bind(this)
+ this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
+ filter: '.empty-message',
+ forceFallback: true,
+ fallbackClass: 'is-dragging',
+ dataIdAttr: 'data-id',
+ onUpdate: this.onPrioritySortUpdate.bind(this),
});
this.bindEvents();
}
@@ -51,13 +53,13 @@
$target = this.otherLabels;
$from = this.prioritizedLabels;
}
- if ($from.find('li').length === 1) {
+ $label.detach().appendTo($target);
+ if ($from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
- if (!$target.find('li').length) {
+ if ($target.find('> li:not(.empty-message)').length) {
$target.find('.empty-message').addClass('hidden');
}
- $label.detach().appendTo($target);
// Return if we are not persisting state
if (!persistState) {
return;
@@ -101,8 +103,12 @@
getSortedLabelsIds() {
const sortedIds = [];
- this.prioritizedLabels.find('li').each(function() {
- sortedIds.push($(this).data('id'));
+ this.prioritizedLabels.find('> li').each(function() {
+ const id = $(this).data('id');
+
+ if (id) {
+ sortedIds.push(id);
+ }
});
return sortedIds;
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
index e3bff2559fd..bcb3a706b51 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -69,27 +69,21 @@
var hash = w.gl.utils.getLocationHash();
if (!hash) return;
- var navbar = document.querySelector('.navbar-gitlab');
- var subnav = document.querySelector('.layout-nav');
- var fixedTabs = document.querySelector('.js-tabs-affix');
-
- var adjustment = 0;
- if (navbar) adjustment -= navbar.offsetHeight;
- if (subnav) adjustment -= subnav.offsetHeight;
+ // This is required to handle non-unicode characters in hash
+ hash = decodeURIComponent(hash);
// scroll to user-generated markdown anchor if we cannot find a match
if (document.getElementById(hash) === null) {
var target = document.getElementById('user-content-' + hash);
if (target && target.scrollIntoView) {
target.scrollIntoView(true);
- window.scrollBy(0, adjustment);
}
} else {
// only adjust for fixedTabs when not targeting user-generated content
+ var fixedTabs = document.querySelector('.js-tabs-affix');
if (fixedTabs) {
- adjustment -= fixedTabs.offsetHeight;
+ window.scrollBy(0, -fixedTabs.offsetHeight);
}
- window.scrollBy(0, adjustment);
}
};
@@ -134,14 +128,20 @@
return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
};
+ gl.utils.isMetaClick = function(e) {
+ // Identify following special clicks
+ // 1) Cmd + Click on Mac (e.metaKey)
+ // 2) Ctrl + Click on PC (e.ctrlKey)
+ // 3) Middle-click or Mouse Wheel Click (e.which is 2)
+ return e.metaKey || e.ctrlKey || e.which === 2;
+ };
+
gl.utils.scrollToElement = function($el) {
var top = $el.offset().top;
- gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height();
- gl.navLinksHeight = gl.navLinksHeight || $('.nav-links').height();
gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();
return $('body, html').animate({
- scrollTop: top - (gl.navBarHeight + gl.navLinksHeight + gl.mrTabsHeight)
+ scrollTop: top - (gl.mrTabsHeight)
}, 200);
};
@@ -230,5 +230,16 @@
return upperCaseHeaders;
};
+
+ /**
+ * Transforms a DOMStringMap into a plain object.
+ *
+ * @param {DOMStringMap} DOMStringMapObject
+ * @returns {Object}
+ */
+ w.gl.utils.DOMStringMapToObject = DOMStringMapObject => Object.keys(DOMStringMapObject).reduce((acc, element) => {
+ acc[element] = DOMStringMapObject[element];
+ return acc;
+ }, {});
})(window);
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
deleted file mode 100644
index 5128ffd8c6f..00000000000
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */
-/* global timeago */
-/* global dateFormat */
-
-window.timeago = require('vendor/timeago');
-window.dateFormat = require('vendor/date.format');
-
-(function() {
- (function(w) {
- var base;
- if (w.gl == null) {
- w.gl = {};
- }
- if ((base = w.gl).utils == null) {
- base.utils = {};
- }
- w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
-
- w.gl.utils.formatDate = function(datetime) {
- return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
- };
-
- w.gl.utils.getDayName = function(date) {
- return this.days[date.getDay()];
- };
-
- w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) {
- if (setTimeago == null) {
- setTimeago = true;
- }
-
- $timeagoEls.filter(':not([data-timeago-rendered])').each(function() {
- var $el = $(this);
- $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
-
- if (setTimeago) {
- // Recreate with custom template
- $el.tooltip({
- template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
- });
- }
-
- $el.attr('data-timeago-rendered', true);
- gl.utils.renderTimeago($el);
- });
- };
-
- w.gl.utils.getTimeago = function() {
- var locale = function(number, index) {
- return [
- ['less than a minute ago', 'a while'],
- ['less than a minute ago', 'in %s seconds'],
- ['about a minute ago', 'in 1 minute'],
- ['%s minutes ago', 'in %s minutes'],
- ['about an hour ago', 'in 1 hour'],
- ['about %s hours ago', 'in %s hours'],
- ['a day ago', 'in 1 day'],
- ['%s days ago', 'in %s days'],
- ['a week ago', 'in 1 week'],
- ['%s weeks ago', 'in %s weeks'],
- ['a month ago', 'in 1 month'],
- ['%s months ago', 'in %s months'],
- ['a year ago', 'in 1 year'],
- ['%s years ago', 'in %s years']
- ][index];
- };
-
- timeago.register('gl_en', locale);
- return timeago();
- };
-
- w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
- var timefor;
- if (!time) {
- return '';
- }
- suffix || (suffix = 'remaining');
- expiredLabel || (expiredLabel = 'Past due');
- timefor = gl.utils.getTimeago().format(time).replace('in', '');
- if (timefor.indexOf('ago') > -1) {
- timefor = expiredLabel;
- } else {
- timefor = timefor.trim() + ' ' + suffix;
- }
- return timefor;
- };
-
- w.gl.utils.renderTimeago = function($element) {
- var timeagoInstance = gl.utils.getTimeago();
- timeagoInstance.render($element, 'gl_en');
- };
-
- w.gl.utils.getDayDifference = function(a, b) {
- var millisecondsPerDay = 1000 * 60 * 60 * 24;
- var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
- var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
-
- return Math.floor((date2 - date1) / millisecondsPerDay);
- };
- })(window);
-}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 b/app/assets/javascripts/lib/utils/datetime_utility.js.es6
new file mode 100644
index 00000000000..f41fa15b147
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js.es6
@@ -0,0 +1,126 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */
+/* global timeago */
+/* global dateFormat */
+
+window.timeago = require('timeago.js');
+window.dateFormat = require('vendor/date.format');
+
+(function() {
+ (function(w) {
+ var base;
+ var timeagoInstance;
+
+ if (w.gl == null) {
+ w.gl = {};
+ }
+ if ((base = w.gl).utils == null) {
+ base.utils = {};
+ }
+ w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+
+ w.gl.utils.formatDate = function(datetime) {
+ return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
+ };
+
+ w.gl.utils.getDayName = function(date) {
+ return this.days[date.getDay()];
+ };
+
+ w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
+ $timeagoEls.each((i, el) => {
+ el.setAttribute('title', gl.utils.formatDate(el.getAttribute('datetime')));
+
+ if (setTimeago) {
+ // Recreate with custom template
+ $(el).tooltip({
+ template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
+ });
+ }
+
+ el.classList.add('js-timeago-render');
+ });
+
+ gl.utils.renderTimeago($timeagoEls);
+ };
+
+ w.gl.utils.getTimeago = function() {
+ var locale;
+
+ if (!timeagoInstance) {
+ locale = function(number, index) {
+ return [
+ ['less than a minute ago', 'a while'],
+ ['less than a minute ago', 'in %s seconds'],
+ ['about a minute ago', 'in 1 minute'],
+ ['%s minutes ago', 'in %s minutes'],
+ ['about an hour ago', 'in 1 hour'],
+ ['about %s hours ago', 'in %s hours'],
+ ['a day ago', 'in 1 day'],
+ ['%s days ago', 'in %s days'],
+ ['a week ago', 'in 1 week'],
+ ['%s weeks ago', 'in %s weeks'],
+ ['a month ago', 'in 1 month'],
+ ['%s months ago', 'in %s months'],
+ ['a year ago', 'in 1 year'],
+ ['%s years ago', 'in %s years']
+ ][index];
+ };
+
+ timeago.register('gl_en', locale);
+ timeagoInstance = timeago();
+ }
+
+ return timeagoInstance;
+ };
+
+ w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
+ var timefor;
+ if (!time) {
+ return '';
+ }
+ suffix || (suffix = 'remaining');
+ expiredLabel || (expiredLabel = 'Past due');
+ timefor = gl.utils.getTimeago().format(time).replace('in', '');
+ if (timefor.indexOf('ago') > -1) {
+ timefor = expiredLabel;
+ } else {
+ timefor = timefor.trim() + ' ' + suffix;
+ }
+ return timefor;
+ };
+
+ w.gl.utils.cachedTimeagoElements = [];
+ w.gl.utils.renderTimeago = function($els) {
+ if (!$els && !w.gl.utils.cachedTimeagoElements.length) {
+ w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
+ } else if ($els) {
+ w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
+ }
+
+ w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
+ };
+
+ w.gl.utils.updateTimeagoText = function(el) {
+ const timeago = gl.utils.getTimeago();
+ const formattedDate = timeago.format(el.getAttribute('datetime'), 'gl_en');
+
+ if (el.textContent !== formattedDate) {
+ el.textContent = formattedDate;
+ }
+ };
+
+ w.gl.utils.initTimeagoTimeout = function() {
+ gl.utils.renderTimeago();
+
+ gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000);
+ };
+
+ w.gl.utils.getDayDifference = function(a, b) {
+ var millisecondsPerDay = 1000 * 60 * 60 * 24;
+ var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
+ var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
+
+ return Math.floor((date2 - date1) / millisecondsPerDay);
+ };
+ })(window);
+}).call(this);
diff --git a/app/assets/javascripts/member_expiration_date.js.es6 b/app/assets/javascripts/member_expiration_date.js.es6
index bf6c0ec2798..f57d4a20498 100644
--- a/app/assets/javascripts/member_expiration_date.js.es6
+++ b/app/assets/javascripts/member_expiration_date.js.es6
@@ -1,3 +1,5 @@
+/* global Pikaday */
+/* global dateFormat */
(() => {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
@@ -11,21 +13,34 @@
}
const inputs = $(selector);
- inputs.datepicker({
- dateFormat: 'yy-mm-dd',
- minDate: 1,
- onSelect: function onSelect() {
- $(this).trigger('change');
- toggleClearInput.call(this);
- },
+ inputs.each((i, el) => {
+ const $input = $(el);
+
+ const calendar = new Pikaday({
+ field: $input.get(0),
+ theme: 'gitlab-theme',
+ format: 'YYYY-MM-DD',
+ minDate: new Date(),
+ onSelect(dateText) {
+ $input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
+
+ $input.trigger('change');
+
+ toggleClearInput.call($input);
+ },
+ });
+
+ $input.data('pikaday', calendar);
});
inputs.next('.js-clear-input').on('click', function clicked(event) {
event.preventDefault();
const input = $(this).closest('.clearable-input').find(selector);
- input.datepicker('setDate', null)
- .trigger('change');
+ const calendar = input.data('pikaday');
+
+ calendar.setDate(null);
+ input.trigger('change');
toggleClearInput.call(input);
});
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 8762ec35b80..e65378cd610 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -115,8 +115,8 @@ require('./merge_request_tabs');
e.preventDefault();
textarea.val(textarea.data('messageWithDescription'));
- $('p.js-with-description-hint').hide();
- $('p.js-without-description-hint').show();
+ $('.js-with-description-hint').hide();
+ $('.js-without-description-hint').show();
});
$(document).on('click', 'a.js-without-description-link', function(e) {
@@ -124,8 +124,8 @@ require('./merge_request_tabs');
e.preventDefault();
textarea.val(textarea.data('messageWithoutDescription'));
- $('p.js-with-description-hint').show();
- $('p.js-without-description-hint').hide();
+ $('.js-with-description-hint').show();
+ $('.js-without-description-hint').hide();
});
};
diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6
index 7e74bebb81e..cc049e00477 100644
--- a/app/assets/javascripts/merge_request_tabs.js.es6
+++ b/app/assets/javascripts/merge_request_tabs.js.es6
@@ -4,7 +4,7 @@
/* global Flash */
require('./breakpoints');
-window.Cookies = require('vendor/js.cookie');
+window.Cookies = require('js-cookie');
require('./flash');
/* eslint-disable max-len */
@@ -61,7 +61,6 @@ require('./flash');
constructor({ action, setUrl, stubLocation } = {}) {
this.diffsLoaded = false;
- this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
@@ -83,12 +82,18 @@ require('./flash');
$(document)
.on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.on('click', '.js-show-tab', this.showTab);
+
+ $('.merge-request-tabs a[data-toggle="tab"]')
+ .on('click', this.clickTab);
}
unbindEvents() {
$(document)
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.off('click', '.js-show-tab', this.showTab);
+
+ $('.merge-request-tabs a[data-toggle="tab"]')
+ .off('click', this.clickTab);
}
showTab(e) {
@@ -96,6 +101,14 @@ require('./flash');
this.activateTab($(e.target).data('action'));
}
+ clickTab(e) {
+ if (e.target && gl.utils.isMetaClick(e)) {
+ const targetLink = e.target.getAttribute('href');
+ e.stopImmediatePropagation();
+ window.open(targetLink, '_blank');
+ }
+ }
+
tabShown(e) {
const $target = $(e.target);
const action = $target.data('action');
@@ -112,14 +125,9 @@ require('./flash');
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
- const navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo('.merge-request-details .merge-request-tabs', {
- offset: -navBarHeight,
+ offset: 0,
});
- } else if (action === 'pipelines') {
- this.loadPipelines($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
} else {
this.expandView();
this.resetViewContainer();
@@ -131,11 +139,7 @@ require('./flash');
scrollToElement(container) {
if (location.hash) {
- const offset = 0 - (
- $('.navbar-gitlab').outerHeight() +
- $('.layout-nav').outerHeight() +
- $('.js-tabs-affix').outerHeight()
- );
+ const offset = -$('.js-tabs-affix').outerHeight();
const $el = $(`${container} ${location.hash}:not(.match)`);
if ($el.length) {
$.scrollTo($el[0], { offset });
@@ -244,25 +248,6 @@ require('./flash');
});
}
- loadPipelines(source) {
- if (this.pipelinesLoaded) {
- return;
- }
- this.ajaxGet({
- url: `${source}.json`,
- success: (data) => {
- $('#pipelines').html(data.html);
- gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
- this.pipelinesLoaded = true;
- this.scrollToElement('#pipelines');
-
- new gl.MiniPipelineGraph({
- container: '.js-pipeline-table',
- });
- },
- });
- }
-
// Show or hide the loading spinner
//
// status - Boolean, true to show, false to hide
@@ -340,14 +325,12 @@ require('./flash');
if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
const $diffTabs = $('#diff-notes-app');
- const $fixedNav = $('.navbar-fixed-top');
- const $layoutNav = $('.layout-nav');
$tabs.off('affix.bs.affix affix-top.bs.affix')
.affix({
offset: {
top: () => (
- $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - $layoutNav.height()
+ $diffTabs.offset().top - $tabs.height()
),
},
})
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 05b9a63765f..69aed77c83d 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -51,6 +51,8 @@ require('./smart_interval');
this.getCIStatus(false);
this.retrieveSuccessIcon();
+ this.initMiniPipelineGraph();
+
this.ciStatusInterval = new global.SmartInterval({
callback: this.getCIStatus.bind(this, true),
startingInterval: 10000,
@@ -66,6 +68,7 @@ require('./smart_interval');
incrementByFactorOf: 15000,
immediateExecution: true,
});
+
notifyPermissions();
}
@@ -151,7 +154,7 @@ require('./smart_interval');
return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) {
var message, status, title;
- if (data.status === '') {
+ if (!data.status) {
return;
}
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
@@ -236,17 +239,20 @@ require('./smart_interval');
case "failed":
case "canceled":
case "not_found":
- return this.setMergeButtonClass('btn-danger');
+ this.setMergeButtonClass('btn-danger');
+ break;
case "running":
- return this.setMergeButtonClass('btn-info');
+ this.setMergeButtonClass('btn-info');
+ break;
case "success":
case "success_with_warnings":
- return this.setMergeButtonClass('btn-create');
+ this.setMergeButtonClass('btn-create');
}
} else {
$('.ci_widget.ci-error').show();
- return this.setMergeButtonClass('btn-danger');
+ this.setMergeButtonClass('btn-danger');
}
+ this.initMiniPipelineGraph();
};
MergeRequestWidget.prototype.showCICoverage = function(coverage) {
@@ -269,6 +275,12 @@ require('./smart_interval');
$('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/'));
};
+ MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-inline-mr-widget-graph:visible',
+ }).bindEvents();
+ };
+
return MergeRequestWidget;
})();
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 7ce1259e015..051cb9fe5c5 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
/* global Flash */
+/* global Sortable */
(function() {
this.Milestone = (function() {
@@ -8,11 +9,9 @@
type: "PUT",
url: issue_url,
data: data,
- success: (function(_this) {
- return function(_data) {
- return _this.successCallback(_data, li);
- };
- })(this),
+ success: function(_data) {
+ return Milestone.successCallback(_data, li);
+ },
error: function(data) {
return new Flash("Issue update failed", 'alert');
},
@@ -27,11 +26,9 @@
type: "PUT",
url: sort_issues_url,
data: data,
- success: (function(_this) {
- return function(_data) {
- return _this.successCallback(_data);
- };
- })(this),
+ success: function(_data) {
+ return Milestone.successCallback(_data);
+ },
error: function() {
return new Flash("Issues update failed", 'alert');
},
@@ -46,11 +43,9 @@
type: "PUT",
url: sort_mr_url,
data: data,
- success: (function(_this) {
- return function(_data) {
- return _this.successCallback(_data);
- };
- })(this),
+ success: function(_data) {
+ return Milestone.successCallback(_data);
+ },
error: function(data) {
return new Flash("Issue update failed", 'alert');
},
@@ -63,11 +58,9 @@
type: "PUT",
url: merge_request_url,
data: data,
- success: (function(_this) {
- return function(_data) {
- return _this.successCallback(_data, li);
- };
- })(this),
+ success: function(_data) {
+ return Milestone.successCallback(_data, li);
+ },
error: function(data) {
return new Flash("Issue update failed", 'alert');
},
@@ -81,65 +74,30 @@
img_tag = $('<img/>');
img_tag.attr('src', data.assignee.avatar_url);
img_tag.addClass('avatar s16');
- $(element).find('.assignee-icon').html(img_tag);
+ $(element).find('.assignee-icon img').replaceWith(img_tag);
} else {
- $(element).find('.assignee-icon').html('');
+ $(element).find('.assignee-icon').empty();
}
return $(element).effect('highlight');
};
function Milestone() {
var oldMouseStart;
- oldMouseStart = $.ui.sortable.prototype._mouseStart;
- $.ui.sortable.prototype._mouseStart = function(event, overrideHandle, noActivation) {
- this._trigger("beforeStart", event, this._uiHash());
- return oldMouseStart.apply(this, [event, overrideHandle, noActivation]);
- };
this.bindIssuesSorting();
this.bindMergeRequestSorting();
this.bindTabsSwitching();
}
Milestone.prototype.bindIssuesSorting = function() {
- return $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable({
- connectWith: ".issues-sortable-list",
- dropOnEmpty: true,
- items: "li:not(.ui-sort-disabled)",
- beforeStart: function(event, ui) {
- return $(".issues-sortable-list").css("min-height", ui.item.outerHeight());
- },
- stop: function(event, ui) {
- return $(".issues-sortable-list").css("min-height", "0px");
- },
- update: function(event, ui) {
- var data;
- // Prevents sorting from container which element has been removed.
- if ($(this).find(ui.item).length > 0) {
- data = $(this).sortable("serialize");
- return Milestone.sortIssues(data);
- }
- },
- receive: function(event, ui) {
- var data, issue_id, issue_url, new_state;
- new_state = $(this).data('state');
- issue_id = ui.item.data('iid');
- issue_url = ui.item.data('url');
- data = (function() {
- switch (new_state) {
- case 'ongoing':
- return "issue[assignee_id]=" + gon.current_user_id;
- case 'unassigned':
- return "issue[assignee_id]=";
- case 'closed':
- return "issue[state_event]=close";
- }
- })();
- if ($(ui.sender).data('state') === "closed") {
- data += "&issue[state_event]=reopen";
- }
- return Milestone.updateIssue(ui.item, issue_url, data);
- }
- }).disableSelection();
+ $('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) {
+ this.createSortable(el, {
+ group: 'issue-list',
+ listEls: $('.issues-sortable-list'),
+ fieldName: 'issue',
+ sortCallback: Milestone.sortIssues,
+ updateCallback: Milestone.updateIssue,
+ });
+ }.bind(this));
};
Milestone.prototype.bindTabsSwitching = function() {
@@ -154,42 +112,62 @@
};
Milestone.prototype.bindMergeRequestSorting = function() {
- return $("#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: function(event, ui) {
- return $(".merge_requests-sortable-list").css("min-height", ui.item.outerHeight());
+ $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) {
+ this.createSortable(el, {
+ group: 'merge-request-list',
+ listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"),
+ fieldName: 'merge_request',
+ sortCallback: Milestone.sortMergeRequests,
+ updateCallback: Milestone.updateMergeRequest,
+ });
+ }.bind(this));
+ };
+
+ Milestone.prototype.createSortable = function(el, opts) {
+ return Sortable.create(el, {
+ group: opts.group,
+ filter: '.is-disabled',
+ forceFallback: true,
+ onStart: function(e) {
+ opts.listEls.css('min-height', e.item.offsetHeight);
},
- stop: function(event, ui) {
- return $(".merge_requests-sortable-list").css("min-height", "0px");
+ onEnd: function () {
+ opts.listEls.css("min-height", "0px");
},
- update: function(event, ui) {
- var data;
- data = $(this).sortable("serialize");
- return Milestone.sortMergeRequests(data);
+ onUpdate: function(e) {
+ var ids = this.toArray(),
+ data;
+
+ if (ids.length) {
+ data = ids.map(function(id) {
+ return 'sortable_' + opts.fieldName + '[]=' + id;
+ }).join('&');
+
+ opts.sortCallback(data);
+ }
},
- receive: function(event, ui) {
- var data, merge_request_id, merge_request_url, new_state;
- new_state = $(this).data('state');
- merge_request_id = ui.item.data('iid');
- merge_request_url = ui.item.data('url');
+ onAdd: function (e) {
+ var data, issuableId, issuableUrl, newState;
+ newState = e.to.dataset.state;
+ issuableUrl = e.item.dataset.url;
data = (function() {
- switch (new_state) {
+ switch (newState) {
case 'ongoing':
- return "merge_request[assignee_id]=" + gon.current_user_id;
+ return opts.fieldName + '[assignee_id]=' + gon.current_user_id;
case 'unassigned':
- return "merge_request[assignee_id]=";
+ return opts.fieldName + '[assignee_id]=';
case 'closed':
- return "merge_request[state_event]=close";
+ return opts.fieldName + '[state_event]=close';
}
})();
- if ($(ui.sender).data('state') === "closed") {
- data += "&merge_request[state_event]=reopen";
+ if (e.from.dataset.state === 'closed') {
+ data += '&' + opts.fieldName + '[state_event]=reopen';
}
- return Milestone.updateMergeRequest(ui.item, merge_request_url, data);
+
+ opts.updateCallback(e.item, issuableUrl, data);
+ this.options.onUpdate.call(this, e);
}
- }).disableSelection();
+ });
};
return Milestone;
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
index 80549532ea9..919fcd0a07b 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
@@ -21,8 +21,6 @@
this.container = opts.container || '';
this.dropdownListSelector = '.js-builds-dropdown-container';
this.getBuildsList = this.getBuildsList.bind(this);
-
- this.bindEvents();
}
/**
@@ -30,7 +28,7 @@
* All dropdown events are fired at the .dropdown-menu's parent element.
*/
bindEvents() {
- $(this.container).on('shown.bs.dropdown', this.getBuildsList);
+ $(document).on('shown.bs.dropdown', this.container, this.getBuildsList);
}
/**
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index d108da29af7..3579843baed 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -455,7 +455,7 @@ require('vendor/task_list');
var mergeRequestId = $form.data('noteable-iid');
if (ResolveService != null) {
- ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
+ ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId);
}
}
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
index 5aec9c813fe..81374296522 100644
--- a/app/assets/javascripts/profile/profile.js.es6
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -25,6 +25,7 @@
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('#user_notification_email').on('change', this.submitForm);
+ $('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
deleted file mode 100644
index a3e549a2735..00000000000
--- a/app/assets/javascripts/shortcuts_blob.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return */
-/* global Shortcuts */
-/* global Mousetrap */
-
-require('./shortcuts');
-
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
- this.ShortcutsBlob = (function(superClass) {
- extend(ShortcutsBlob, superClass);
-
- function ShortcutsBlob(skipResetBindings) {
- ShortcutsBlob.__super__.constructor.call(this, skipResetBindings);
- Mousetrap.bind('y', ShortcutsBlob.copyToClipboard);
- }
-
- ShortcutsBlob.copyToClipboard = function() {
- var clipboardButton;
- clipboardButton = $('.btn-clipboard');
- if (clipboardButton) {
- return clipboardButton.click();
- }
- };
-
- return ShortcutsBlob;
- })(Shortcuts);
-}).call(this);
diff --git a/app/assets/javascripts/shortcuts_blob.js.es6 b/app/assets/javascripts/shortcuts_blob.js.es6
new file mode 100644
index 00000000000..bfe90aef71e
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_blob.js.es6
@@ -0,0 +1,29 @@
+/* global Mousetrap */
+/* global Shortcuts */
+
+require('./shortcuts');
+
+const defaults = {
+ skipResetBindings: false,
+ fileBlobPermalinkUrl: null,
+};
+
+class ShortcutsBlob extends Shortcuts {
+ constructor(opts) {
+ const options = Object.assign({}, defaults, opts);
+ super(options.skipResetBindings);
+ this.options = options;
+
+ Mousetrap.bind('y', this.moveToFilePermalink.bind(this));
+ }
+
+ moveToFilePermalink() {
+ if (this.options.fileBlobPermalinkUrl) {
+ const hash = gl.utils.getLocationHash();
+ const hashUrlString = hash ? `#${hash}` : '';
+ gl.utils.visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`);
+ }
+ }
+}
+
+module.exports = ShortcutsBlob;
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
index ee172f2fa6f..33e4b7db681 100644
--- a/app/assets/javascripts/sidebar.js.es6
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -1,14 +1,12 @@
/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
/* global Cookies */
-((global) => {
- let singleton;
-
+(() => {
const pinnedStateCookie = 'pin_nav';
const sidebarBreakpoint = 1024;
const pageSelector = '.page-with-sidebar';
- const navbarSelector = '.navbar-fixed-top';
+ const navbarSelector = '.navbar-gitlab';
const sidebarWrapperSelector = '.sidebar-wrapper';
const sidebarContentSelector = '.nav-sidebar';
@@ -23,11 +21,12 @@
class Sidebar {
constructor() {
- if (!singleton) {
- singleton = this;
- singleton.init();
+ if (!Sidebar.singleton) {
+ Sidebar.singleton = this;
+ Sidebar.singleton.init();
}
- return singleton;
+
+ return Sidebar.singleton;
}
init() {
@@ -36,13 +35,16 @@
window.innerWidth >= sidebarBreakpoint &&
$(pageSelector).hasClass(expandedPageClass)
);
+ $(window).on('resize', () => this.setSidebarHeight());
$(document)
.on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState())
- .on('click', 'html, body', (e) => this.handleClickEvent(e))
+ .on('click', 'html, body, a, button', (e) => this.handleClickEvent(e))
.on('DOMContentLoaded', () => this.renderState())
+ .on('scroll', () => this.setSidebarHeight())
.on('todo:toggle', (e, count) => this.updateTodoCount(count));
this.renderState();
+ this.setSidebarHeight();
}
handleClickEvent(e) {
@@ -65,6 +67,16 @@
this.renderState();
}
+ setSidebarHeight() {
+ const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
+ const diff = $navHeight - $('body').scrollTop();
+ if (diff > 0) {
+ $('.js-right-sidebar').outerHeight($(window).height() - diff);
+ } else {
+ $('.js-right-sidebar').outerHeight('100%');
+ }
+ }
+
togglePinnedState() {
this.isPinned = !this.isPinned;
if (!this.isPinned) {
@@ -88,10 +100,12 @@
$pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
if (this.isExpanded) {
- setTimeout(() => $(sidebarContentSelector).niceScroll().updateScrollBar(), 200);
+ const sidebarContent = $(sidebarContentSelector);
+ setTimeout(() => { sidebarContent.niceScroll().updateScrollBar(); }, 200);
}
}
}
- global.Sidebar = Sidebar;
-})(window.gl || (window.gl = {}));
+ window.gl = window.gl || {};
+ gl.Sidebar = Sidebar;
+})();
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 5b20c63384c..3ee0c73a8d2 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -33,13 +33,13 @@
this.$toggleIcon.addClass('fa-caret-down');
}
- $('.file-title, .click-to-expand', this.file).on('click', (function (e) {
+ $('.js-file-title, .click-to-expand', this.file).on('click', (function (e) {
this.toggleDiff($(e.target));
}).bind(this));
}
SingleFileDiff.prototype.toggleDiff = function($target, cb) {
- if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
+ if (!$target.hasClass('js-file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
this.content.hide();
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
index f05780167bf..7dba5840c8a 100644
--- a/app/assets/javascripts/boards/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -50,14 +50,15 @@
return (
children[target.index] ||
children[target.index === 'first' ? 0 : -1] ||
- children[target.index === 'last' ? children.length - 1 : -1]
+ children[target.index === 'last' ? children.length - 1 : -1] ||
+ el
);
}
function getRect(el) {
var rect = el.getBoundingClientRect();
var width = rect.right - rect.left;
- var height = rect.bottom - rect.top;
+ var height = rect.bottom - rect.top + 10;
return {
x: rect.left,
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
index 96c7d927509..b07e62a8c30 100644
--- a/app/assets/javascripts/todos.js.es6
+++ b/app/assets/javascripts/todos.js.es6
@@ -146,14 +146,26 @@
}
goToTodoUrl(e) {
- const todoLink = $(this).data('url');
+ const todoLink = this.dataset.url;
+ let targetLink = e.target.getAttribute('href');
+
+ if (e.target.tagName === 'IMG') { // See if clicked target was Avatar
+ targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link
+ }
+
if (!todoLink) {
return;
}
- // Allow Meta-Click or Mouse3-click to open in a new tab
- if (e.metaKey || e.which === 2) {
+
+ if (gl.utils.isMetaClick(e)) {
e.preventDefault();
- return window.open(todoLink, '_blank');
+ // Meta-Click on username leads to different URL than todoLink.
+ // Turbolinks can resolve that URL, but window.open requires URL manually.
+ if (targetLink !== todoLink) {
+ return window.open(targetLink, '_blank');
+ } else {
+ return window.open(todoLink, '_blank');
+ }
} else {
return gl.utils.visitUrl(todoLink);
}
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6
index e1bebe0fe5b..e7432afb56e 100644
--- a/app/assets/javascripts/vue_pipelines_index/index.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6
@@ -1,41 +1,36 @@
+/* eslint-disable no-param-reassign */
/* global Vue, VueResource, gl */
window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
-require('../vue_common_component/commit');
-require('../vue_pagination/index');
-require('../boards/vue_resource_interceptor');
-require('./status');
-require('./store');
-require('./pipeline_url');
-require('./stage');
-require('./stages');
-require('./pipeline_actions');
-require('./time_ago');
+require('../lib/utils/common_utils');
+require('../vue_shared/vue_resource_interceptor');
require('./pipelines');
-(() => {
- const project = document.querySelector('.pipelines');
- const entry = document.querySelector('.vue-pipelines-index');
- const svgs = document.querySelector('.pipeline-svgs');
+$(() => new Vue({
+ el: document.querySelector('.vue-pipelines-index'),
- if (!entry) return null;
- return new Vue({
- el: entry,
- data: {
+ data() {
+ const project = document.querySelector('.pipelines');
+ const svgs = document.querySelector('.pipeline-svgs').dataset;
+
+ // Transform svgs DOMStringMap to a plain Object.
+ const svgsObject = gl.utils.DOMStringMapToObject(svgs);
+
+ return {
scope: project.dataset.url,
store: new gl.PipelineStore(),
- svgs: svgs.dataset,
- },
- components: {
- 'vue-pipelines': gl.VuePipelines,
- },
- template: `
- <vue-pipelines
- :scope='scope'
- :store='store'
- :svgs='svgs'
- >
- </vue-pipelines>
- `,
- });
-})();
+ svgs: svgsObject,
+ };
+ },
+ components: {
+ 'vue-pipelines': gl.VuePipelines,
+ },
+ template: `
+ <vue-pipelines
+ :scope='scope'
+ :store='store'
+ :svgs='svgs'
+ >
+ </vue-pipelines>
+ `,
+}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
index 01f8b6519a4..8106934e864 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
@@ -50,9 +50,9 @@
<button
v-if='artifacts'
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
- data-toggle="dropdown"
title="Artifacts"
data-placement="top"
+ data-toggle="dropdown"
aria-label="Artifacts"
>
<i class="fa fa-download" aria-hidden="true"></i>
@@ -81,8 +81,7 @@
data-placement="top"
data-toggle="dropdown"
:href='pipeline.retry_path'
- aria-label="Retry"
- >
+ aria-label="Retry">
<i class="fa fa-repeat" aria-hidden="true"></i>
</a>
<a
@@ -94,8 +93,7 @@
data-placement="top"
data-toggle="dropdown"
:href='pipeline.cancel_path'
- aria-label="Cancel"
- >
+ aria-label="Cancel">
<i class="fa fa-remove" aria-hidden="true"></i>
</a>
</div>
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
index 194bbae07d9..e47dc6935d6 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
@@ -1,19 +1,19 @@
/* global Vue, gl */
/* eslint-disable no-param-reassign */
+window.Vue = require('vue');
+require('../vue_shared/components/table_pagination');
+require('./store');
+require('../vue_shared/components/pipelines_table');
+
((gl) => {
gl.VuePipelines = Vue.extend({
+
components: {
- runningPipeline: gl.VueRunningPipeline,
- pipelineActions: gl.VuePipelineActions,
- stages: gl.VueStages,
- commit: gl.CommitComponent,
- pipelineUrl: gl.VuePipelineUrl,
- pipelineHead: gl.VuePipelineHead,
- glPagination: gl.VueGlPagination,
- statusScope: gl.VueStatusScope,
- timeAgo: gl.VueTimeAgo,
+ 'gl-pagination': gl.VueGlPagination,
+ 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
},
+
data() {
return {
pipelines: [],
@@ -38,87 +38,29 @@
change(pagenum, apiScope) {
gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
},
- author(pipeline) {
- if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' };
- if (pipeline.commit.author) return pipeline.commit.author;
- return {
- avatar_url: pipeline.commit.author_gravatar_url,
- web_url: `mailto:${pipeline.commit.author_email}`,
- username: pipeline.commit.author_name,
- };
- },
- ref(pipeline) {
- const { ref } = pipeline;
- return { name: ref.name, tag: ref.tag, ref_url: ref.path };
- },
- commitTitle(pipeline) {
- return pipeline.commit ? pipeline.commit.title : '';
- },
- commitSha(pipeline) {
- return pipeline.commit ? pipeline.commit.short_id : '';
- },
- commitUrl(pipeline) {
- return pipeline.commit ? pipeline.commit.commit_path : '';
- },
- match(string) {
- return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
- },
},
template: `
<div>
- <div class="pipelines realtime-loading" v-if='pipelines.length < 1'>
+ <div class="pipelines realtime-loading" v-if='pageRequest'>
<i class="fa fa-spinner fa-spin"></i>
</div>
- <div class="table-holder" v-if='pipelines.length'>
- <table class="table ci-table">
- <thead>
- <tr>
- <th class="pipeline-status">Status</th>
- <th class="pipeline-info">Pipeline</th>
- <th class="pipeline-commit">Commit</th>
- <th class="pipeline-stages">Stages</th>
- <th class="pipeline-date"></th>
- <th class="pipeline-actions hidden-xs"></th>
- </tr>
- </thead>
- <tbody>
- <tr class="commit" v-for='pipeline in pipelines'>
- <status-scope
- :pipeline='pipeline'
- :match='match'
- :svgs='svgs'
- >
- </status-scope>
- <pipeline-url :pipeline='pipeline'></pipeline-url>
- <td>
- <commit
- :commit-icon-svg='svgs.commitIconSvg'
- :author='author(pipeline)'
- :tag="pipeline.ref.tag"
- :title='commitTitle(pipeline)'
- :commit-ref='ref(pipeline)'
- :short-sha='commitSha(pipeline)'
- :commit-url='commitUrl(pipeline)'
- >
- </commit>
- </td>
- <stages
- :pipeline='pipeline'
- :svgs='svgs'
- :match='match'
- >
- </stages>
- <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago>
- <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions>
- </tr>
- </tbody>
- </table>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!pageRequest && pipelines.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ No pipelines to show
+ </h2>
</div>
- <div class="pipelines realtime-loading" v-if='pageRequest'>
- <i class="fa fa-spinner fa-spin"></i>
+
+ <div class="table-holder" v-if='!pageRequest && pipelines.length'>
+ <pipelines-table-component
+ :pipelines='pipelines'
+ :svgs='svgs'>
+ </pipelines-table-component>
</div>
+
<gl-pagination
- v-if='pageInfo.total > pageInfo.perPage'
+ v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
:pagenum='pagenum'
:change='change'
:count='count.all'
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
index 496df9aaced..8cc417a9966 100644
--- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
@@ -15,7 +15,7 @@
required: true,
},
svgs: {
- type: DOMStringMap,
+ type: Object,
required: true,
},
match: {
diff --git a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 b/app/assets/javascripts/vue_pipelines_index/stages.js.es6
deleted file mode 100644
index cb176b3f0c6..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/stages.js.es6
+++ /dev/null
@@ -1,21 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VueStages = Vue.extend({
- components: {
- 'vue-stage': gl.VueStage,
- },
- props: ['pipeline', 'svgs', 'match'],
- template: `
- <td class="stage-cell">
- <div
- class="stage-container dropdown js-mini-pipeline-graph"
- v-for='stage in pipeline.details.stages'
- >
- <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage>
- </div>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
index 0f5ce2a9274..0ee21f00fdc 100644
--- a/app/assets/javascripts/vue_pipelines_index/store.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6
@@ -20,6 +20,7 @@ require('../vue_realtime_listener');
gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) {
+ this.pageRequest = true;
const updatePipelineNums = (count) => {
const { all } = count;
const running = count.running_or_pending;
@@ -41,16 +42,18 @@ require('../vue_realtime_listener');
this.pageRequest = false;
}, () => {
this.pageRequest = false;
- return new Flash('Something went wrong on our end.');
+ return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
goFetch();
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
- this.$children
- .filter(e => e.$options._componentTag === 'time-ago')
- .forEach(e => e.changeTime());
+ this.$children[0].$children.reduce((acc, component) => {
+ const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
+ acc.push(timeAgoComponent);
+ return acc;
+ }, []).forEach(e => e.changeTime());
}, 10000);
};
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
index 655110feba1..3598da11573 100644
--- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
@@ -1,6 +1,9 @@
/* global Vue, gl */
/* eslint-disable no-param-reassign */
+window.Vue = require('vue');
+require('../lib/utils/datetime_utility');
+
((gl) => {
gl.VueTimeAgo = Vue.extend({
data() {
diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js.es6
index 95564152cce..30f6680a673 100644
--- a/app/assets/javascripts/vue_realtime_listener/index.js.es6
+++ b/app/assets/javascripts/vue_realtime_listener/index.js.es6
@@ -14,5 +14,16 @@
window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals);
document.addEventListener('beforeunload', removeAll);
+
+ // add removeAll methods to stack
+ const stack = gl.VueRealtimeListener.reset;
+ gl.VueRealtimeListener.reset = () => {
+ gl.VueRealtimeListener.reset = stack;
+ removeAll();
+ stack();
+ };
};
+
+ // remove all event listeners and intervals
+ gl.VueRealtimeListener.reset = () => undefined; // noop
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6
index 4adad7bea31..7f7c18ddeb1 100644
--- a/app/assets/javascripts/vue_common_component/commit.js.es6
+++ b/app/assets/javascripts/vue_shared/components/commit.js.es6
@@ -1,7 +1,5 @@
/* global Vue */
-window.Vue = require('vue');
-
(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
new file mode 100644
index 00000000000..4bdaef31ee9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
@@ -0,0 +1,61 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+require('./pipelines_table_row');
+/**
+ * Pipelines Table Component.
+ *
+ * Given an array of objects, renders a table.
+ */
+
+(() => {
+ window.gl = window.gl || {};
+ gl.pipelines = gl.pipelines || {};
+
+ gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', {
+
+ props: {
+ pipelines: {
+ type: Array,
+ required: true,
+ default: () => ([]),
+ },
+
+ /**
+ * TODO: Remove this when we have webpack.
+ */
+ svgs: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+ },
+
+ components: {
+ 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent,
+ },
+
+ template: `
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th class="js-pipeline-status pipeline-status">Status</th>
+ <th class="js-pipeline-info pipeline-info">Pipeline</th>
+ <th class="js-pipeline-commit pipeline-commit">Commit</th>
+ <th class="js-pipeline-stages pipeline-stages">Stages</th>
+ <th class="js-pipeline-date pipeline-date"></th>
+ <th class="js-pipeline-actions pipeline-actions hidden-xs"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in pipelines"
+ v-bind:model="model">
+ <tr is="pipelines-table-row-component"
+ :pipeline="model"
+ :svgs="svgs"></tr>
+ </template>
+ </tbody>
+ </table>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6
new file mode 100644
index 00000000000..61c1b72d9d2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6
@@ -0,0 +1,234 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+require('../../vue_pipelines_index/status');
+require('../../vue_pipelines_index/pipeline_url');
+require('../../vue_pipelines_index/stage');
+require('../../vue_pipelines_index/pipeline_actions');
+require('../../vue_pipelines_index/time_ago');
+require('./commit');
+/**
+ * Pipeline table row.
+ *
+ * Given the received object renders a table row in the pipelines' table.
+ */
+(() => {
+ window.gl = window.gl || {};
+ gl.pipelines = gl.pipelines || {};
+
+ gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', {
+
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+
+ /**
+ * TODO: Remove this when we have webpack;
+ */
+ svgs: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+ },
+
+ components: {
+ 'commit-component': gl.CommitComponent,
+ 'pipeline-actions': gl.VuePipelineActions,
+ 'dropdown-stage': gl.VueStage,
+ 'pipeline-url': gl.VuePipelineUrl,
+ 'status-scope': gl.VueStatusScope,
+ 'time-ago': gl.VueTimeAgo,
+ },
+
+ computed: {
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * This field needs a lot of verification, because of different possible cases:
+ *
+ * 1. person who is an author of a commit might be a GitLab user
+ * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
+ * 3. If GitLab user does not have avatar he/she might have a Gravatar
+ * 4. If committer is not a GitLab User he/she can have a Gravatar
+ * 5. We do not have consistent API object in this case
+ * 6. We should improve API and the code
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ let commitAuthorInformation;
+
+ // 1. person who is an author of a commit might be a GitLab user
+ if (this.pipeline &&
+ this.pipeline.commit &&
+ this.pipeline.commit.author) {
+ // 2. if person who is an author of a commit is a GitLab user
+ // he/she can have a GitLab avatar
+ if (this.pipeline.commit.author.avatar_url) {
+ commitAuthorInformation = this.pipeline.commit.author;
+
+ // 3. If GitLab user does not have avatar he/she might have a Gravatar
+ } else if (this.pipeline.commit.author_gravatar_url) {
+ commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ });
+ }
+ }
+
+ // 4. If committer is not a GitLab User he/she can have a Gravatar
+ if (this.pipeline &&
+ this.pipeline.commit) {
+ commitAuthorInformation = {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ web_url: `mailto:${this.pipeline.commit.author_email}`,
+ username: this.pipeline.commit.author_name,
+ };
+ }
+
+ return commitAuthorInformation;
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.pipeline.ref &&
+ this.pipeline.ref.tag) {
+ return this.pipeline.ref.tag;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit ref.
+ * Needed to render the commit component column.
+ *
+ * Matches `path` prop sent in the API to `ref_url` prop needed
+ * in the commit component.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.pipeline.ref) {
+ return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
+ if (prop === 'path') {
+ accumulator.ref_url = this.pipeline.ref[prop];
+ } else {
+ accumulator[prop] = this.pipeline.ref[prop];
+ }
+ return accumulator;
+ }, {});
+ }
+
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit url.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.commit_path) {
+ return this.pipeline.commit.commit_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit short sha.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.short_id) {
+ return this.pipeline.commit.short_id;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit title.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.title) {
+ return this.pipeline.commit.title;
+ }
+ return undefined;
+ },
+ },
+
+ methods: {
+ /**
+ * FIXME: This should not be in this component but in the components that
+ * need this function.
+ *
+ * Used to render SVGs in the following components:
+ * - status-scope
+ * - dropdown-stage
+ *
+ * @param {String} string
+ * @return {String}
+ */
+ match(string) {
+ return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
+ },
+ },
+
+ template: `
+ <tr class="commit">
+ <status-scope
+ :pipeline="pipeline"
+ :svgs="svgs"
+ :match="match">
+ </status-scope>
+
+ <pipeline-url :pipeline="pipeline"></pipeline-url>
+
+ <td>
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"
+ :commit-icon-svg="svgs.commitIconSvg">
+ </commit-component>
+ </td>
+
+ <td class="stage-cell">
+ <div class="stage-container dropdown js-mini-pipeline-graph"
+ v-if="pipeline.details.stages.length > 0"
+ v-for="stage in pipeline.details.stages">
+ <dropdown-stage
+ :stage="stage"
+ :svgs="svgs"
+ :match="match">
+ </dropdown-stage>
+ </div>
+ </td>
+
+ <time-ago :pipeline="pipeline" :svgs="svgs"></time-ago>
+
+ <pipeline-actions :pipeline="pipeline" :svgs="svgs"></pipeline-actions>
+ </tr>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
index 67c6cb73761..67c6cb73761 100644
--- a/app/assets/javascripts/vue_pagination/index.js.es6
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6
new file mode 100644
index 00000000000..d3229f9f730
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6
@@ -0,0 +1,23 @@
+/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars,
+no-param-reassign, no-plusplus */
+/* global Vue */
+
+Vue.http.interceptors.push((request, next) => {
+ Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+
+ next((response) => {
+ if (typeof response.data === 'string') {
+ response.data = JSON.parse(response.data);
+ }
+
+ Vue.activeResources--;
+ });
+});
+
+Vue.http.interceptors.push((request, next) => {
+ // needed in order to not break the tests.
+ if ($.rails) {
+ request.headers['X-CSRF-Token'] = $.rails.csrfToken();
+ }
+ next();
+});
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 8b93665d085..1dcd1f8a6fc 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -2,7 +2,6 @@
* This is a manifest file that'll automatically include all the stylesheets available in this directory
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
* the top of the compiled file, but it's generally better to create a new file per style scope.
- *= require jquery-ui/datepicker
*= require jquery-ui/autocomplete
*= require jquery.atwho
*= require select2
@@ -19,6 +18,8 @@
* directory.
*/
+@import "../../../node_modules/pikaday/scss/pikaday";
+
/*
* GitLab UI framework
*/
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 0a26b4c6a8c..0ca5a9343f7 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -128,8 +128,7 @@
.note-action-button .link-highlight,
.toolbar-btn,
-.dropdown-toggle-caret,
-.fa:not(.fa-bell) {
+.dropdown-toggle-caret {
@include transition(color);
}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 1d59700543c..3f5b78ed445 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -28,6 +28,8 @@
.avatar {
@extend .avatar-circle;
+ @include transition-property(none);
+
width: 40px;
height: 40px;
padding: 0;
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 1d2d1bfc0d7..fb8ea18d122 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -9,6 +9,8 @@
}
.user-calendar-activities {
+ direction: ltr;
+
.str-truncated {
max-width: 70%;
}
@@ -43,3 +45,56 @@
float: right;
font-size: 12px;
}
+
+.pika-single.gitlab-theme {
+ .pika-label {
+ color: $gl-text-color-secondary;
+ font-size: 14px;
+ font-weight: normal;
+ }
+
+ th {
+ padding: 2px 0;
+ color: $note-disabled-comment-color;
+ font-weight: normal;
+ text-transform: lowercase;
+ border-top: 1px solid $calendar-border-color;
+ }
+
+ abbr {
+ cursor: default;
+ }
+
+ td {
+ border: 1px solid $calendar-border-color;
+
+ &:first-child {
+ border-left: 0;
+ }
+
+ &:last-child {
+ border-right: 0;
+ }
+ }
+
+ .pika-day {
+ border-radius: 0;
+ background-color: $white-light;
+ text-align: center;
+ }
+
+ .is-today {
+ .pika-day {
+ color: inherit;
+ font-weight: normal;
+ }
+ }
+
+ .is-selected .pika-day,
+ .pika-day:hover,
+ .is-today .pika-day:hover {
+ background: $gl-primary;
+ color: $white-light;
+ box-shadow: none;
+ }
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 0ce94a26a7f..a4b38723bbd 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -253,6 +253,8 @@ li.note {
.progress {
margin-bottom: 0;
margin-top: 4px;
+ box-shadow: none;
+ background-color: $border-gray-light;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index ca5861bf3e6..ff31e7f7b3d 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -125,7 +125,6 @@
top: 100%;
left: 0;
z-index: 9;
- max-width: 280px;
min-width: 240px;
margin-top: 2px;
margin-bottom: 0;
@@ -137,6 +136,10 @@
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
+ .filtered-search-input-container & {
+ max-width: 280px;
+ }
+
&.is-loading {
.dropdown-content {
display: none;
@@ -502,119 +505,16 @@
max-height: 230px;
}
- .ui-widget {
- table {
- margin: 0;
- }
-
- &.ui-datepicker-inline {
- padding: 0 10px;
- border: 0;
- width: 100%;
- }
-
- .ui-datepicker-header {
- padding: 0 8px 10px;
- border: 0;
-
- .ui-icon {
- background: none;
- font-size: 20px;
- text-indent: 0;
-
- &::before {
- display: block;
- position: relative;
- top: -2px;
- color: $dropdown-title-btn-color;
- font: normal normal normal 14px/1 FontAwesome;
- font-size: inherit;
- text-rendering: auto;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
- }
- }
-
- .ui-datepicker-calendar {
- .ui-state-hover,
- .ui-state-active {
- color: $white-light;
- border: 0;
- }
- }
-
- .ui-datepicker-prev,
- .ui-datepicker-next {
- top: 0;
- height: 15px;
- cursor: pointer;
-
- &:hover {
- background-color: transparent;
- border: 0;
-
- .ui-icon::before {
- color: $md-link-color;
- }
- }
- }
-
- .ui-datepicker-prev {
- left: 0;
-
- .ui-icon::before {
- content: '\f104';
- text-align: left;
- }
- }
-
- .ui-datepicker-next {
- right: 0;
-
- .ui-icon::before {
- content: '\f105';
- text-align: right;
- }
- }
-
- td {
- padding: 0;
- border: 1px solid $calendar-border-color;
-
- &:first-child {
- border-left: 0;
- }
-
- &:last-child {
- border-right: 0;
- }
-
- a {
- line-height: 17px;
- border: 0;
- border-radius: 0;
- }
- }
-
- .ui-datepicker-title {
- color: $gl-text-color;
- font-size: 14px;
- line-height: 1;
- font-weight: normal;
- }
- }
-
- th {
- padding: 2px 0;
- color: $note-disabled-comment-color;
- font-weight: normal;
- text-transform: lowercase;
- border-top: 1px solid $calendar-border-color;
+ .pika-single {
+ position: relative!important;
+ top: 0!important;
+ border: 0;
+ box-shadow: none;
}
- .ui-datepicker-unselectable {
- background-color: $gray-light;
+ .pika-lendar {
+ margin-top: -5px;
+ margin-bottom: 0;
}
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index c51912b4ac4..30f242a35db 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -231,3 +231,46 @@ span.idiff {
}
}
}
+
+.file-title-flex-parent {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: $gray-light;
+ border-bottom: 1px solid $border-color;
+ padding: 5px $gl-padding;
+ margin: 0;
+ border-radius: 3px 3px 0 0;
+
+ .file-header-content {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-right: 30px;
+ position: relative;
+ }
+
+ .btn-clipboard {
+ position: absolute;
+ right: 0;
+ }
+
+ a {
+ color: $gl-text-color;
+ }
+
+ small {
+ margin: 0 10px 0 0;
+ }
+
+ .file-actions {
+ white-space: nowrap;
+
+ .btn {
+ padding: 0 10px;
+ font-size: 13px;
+ line-height: 28px;
+ display: inline-block;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 2a01bc4d44d..34e010e0e8a 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -222,6 +222,10 @@ header {
float: right;
border-top: none;
+ @media (min-width: $screen-md-min) {
+ padding: 0;
+ }
+
@media (max-width: $screen-xs-max) {
float: none;
}
@@ -272,7 +276,7 @@ header {
.header-user {
.dropdown-menu-nav {
- width: 140px;
+ min-width: 140px;
margin-top: -5px;
}
}
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index 18f2f316f02..d335fedefe2 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -2,42 +2,6 @@
font-family: $regular_font;
font-size: $font-size-base;
- &.ui-datepicker,
- &.ui-datepicker-inline {
- border: 1px solid $jq-ui-border;
- padding: 10px;
- width: 270px;
-
- .ui-datepicker-header {
- background: $white-light;
- border-color: $jq-ui-border;
-
- .ui-datepicker-prev,
- .ui-datepicker-next {
- top: 4px;
- }
-
- .ui-datepicker-prev {
- left: 2px;
- }
-
- .ui-datepicker-next {
- right: 2px;
- }
-
- .ui-state-hover {
- background: transparent;
- border: 0;
- cursor: pointer;
- }
- }
-
- .ui-datepicker-calendar td a {
- padding: 5px;
- text-align: center;
- }
- }
-
&.ui-autocomplete {
border-color: $jq-ui-border;
padding: 0;
@@ -59,25 +23,4 @@
border: 0;
background: transparent;
}
-
- .ui-datepicker-calendar {
- .ui-state-active,
- .ui-state-hover,
- .ui-state-focus {
- border: 1px solid $gl-primary;
- background: $gl-primary;
- color: $white-light;
- }
- }
-}
-
-.ui-sortable-handle {
- cursor: move;
- cursor: -webkit-grab;
- cursor: -moz-grab;
-
- &:active {
- cursor: -webkit-grabbing;
- cursor: -moz-grabbing;
- }
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 426596027de..2bfdb9f9601 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -307,3 +307,7 @@ ul.controls {
}
}
}
+
+ul.indent-list {
+ padding: 10px 0 0 30px;
+}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index fd081c2d7e1..674d3bb45aa 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -283,10 +283,7 @@
}
.layout-nav {
- position: fixed;
- top: $header-height;
width: 100%;
- z-index: 11;
background: $gray-light;
border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration;
@@ -419,15 +416,20 @@
}
.page-with-layout-nav {
- margin-top: $header-height + 2;
-
.right-sidebar {
top: ($header-height * 2) + 2;
}
+
+ .build-sidebar {
+ top: ($header-height * 3) + 3;
+
+ &.affix {
+ top: 0;
+ }
+ }
}
.activities {
-
.nav-block {
border-bottom: 1px solid $border-color;
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index b37c1d0d670..c3ec9db0f07 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -6,8 +6,22 @@
.pagination {
padding: 0;
+
+ a {
+ cursor: pointer;
+ }
+
+ .separator,
+ .separator:hover {
+ a {
+ cursor: default;
+ background-color: $gray-light;
+ padding: $gl-vert-padding;
+ }
+ }
}
+
.gap,
.gap:hover {
background-color: $gray-light;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index f0b03710c79..20bcb1eeb23 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,5 +1,5 @@
.page-with-sidebar {
- padding: $header-height 0 25px;
+ padding-bottom: 25px;
transition: padding $sidebar-transition-duration;
&.page-sidebar-pinned {
@@ -208,7 +208,9 @@ header.header-sidebar-pinned {
padding-right: 0;
@media (min-width: $screen-sm-min) {
- padding-right: $sidebar_collapsed_width;
+ .content-wrapper {
+ padding-right: $sidebar_collapsed_width;
+ }
.merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width;
@@ -234,7 +236,9 @@ header.header-sidebar-pinned {
}
@media (min-width: $screen-md-min) {
- padding-right: $gutter_width;
+ .content-wrapper {
+ padding-right: $gutter_width;
+ }
&:not(.with-overlay) .merge-request-tabs-holder.affix {
right: $gutter_width;
@@ -252,4 +256,9 @@ header.header-sidebar-pinned {
.right-sidebar {
border-left: 1px solid $border-color;
+
+ &.affix {
+ position: fixed;
+ top: 0;
+ }
}
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index 60ff72c703e..ea40f449134 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -138,6 +138,13 @@ pre {
margin: 0;
}
+blockquote {
+ color: $gl-grayish-blue;
+ padding: 0 0 0 15px;
+ margin: 0;
+ border-left: 3px solid $white-dark;
+}
+
span.highlight_word {
background-color: $highlighted-highlight-word !important;
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index b362cc758cc..9a36d76136b 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -298,12 +298,8 @@
.issue-boards-sidebar {
&.right-sidebar {
- top: 153px;
+ top: 0;
bottom: 0;
-
- @media (min-width: $screen-sm-min) {
- top: 220px;
- }
}
.issuable-sidebar-header {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index fef8e8eec27..c3d45d708c1 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -159,7 +159,6 @@
.commit-row-description {
font-size: 14px;
- border-left: 1px solid $white-normal;
padding: 10px 15px;
margin: 10px 0;
background: $gray-light;
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index cda069e6c0e..5b777953fb0 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -284,7 +284,11 @@
.events-description {
line-height: 65px;
- padding-left: $gl-padding;
+ padding: 0 $gl-padding;
+ }
+
+ .events-info {
+ color: $gl-text-color-secondary;
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 96ba7c40634..92d7772da57 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -34,9 +34,14 @@
}
}
- .file-title {
+ .file-title,
+ .file-title-flex-parent {
cursor: pointer;
+ a:hover {
+ text-decoration: none;
+ }
+
&:hover {
background-color: $gray-normal;
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index b989d72ce1c..5776d86983a 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -41,7 +41,6 @@
word-wrap: break-word;
.md {
- color: $gl-grayish-blue;
font-size: $gl-font-size;
.label {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 4ef95d27f4f..a53cc27fac9 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -189,11 +189,10 @@
}
.right-sidebar {
- position: fixed;
+ position: absolute;
top: $header-height;
bottom: 0;
right: 0;
- z-index: 10;
transition: width .3s;
background: $gray-light;
padding: 10px 20px;
@@ -461,8 +460,19 @@
.issuable-list {
li {
+
+ .issue-box {
+ display: -webkit-flex;
+ display: flex;
+ }
+
+ .issue-info-container {
+ -webkit-flex: 1;
+ flex: 1;
+ padding-right: $gl-padding;
+ }
+
.issue-check {
- float: left;
padding-right: $gl-padding;
margin-bottom: 10px;
min-width: 15px;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 8734a3b1598..80b0c9493d8 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -1,6 +1,6 @@
.issues-list {
.issue {
- padding: 10px $gl-padding;
+ padding: 10px 0 10px $gl-padding;
position: relative;
.title {
@@ -148,3 +148,7 @@ ul.related-merge-requests > li {
border: 1px solid $border-gray-normal;
}
}
+
+.recaptcha {
+ margin-bottom: 30px;
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 21d9b4c54ea..e1ef0b029a5 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -116,6 +116,22 @@
}
.manage-labels-list {
+ > li:not(.empty-message) {
+ background-color: $white-light;
+ cursor: move;
+ cursor: -webkit-grab;
+ cursor: -moz-grab;
+
+ &:active {
+ cursor: -webkit-grabbing;
+ cursor: -moz-grabbing;
+ }
+
+ &.sortable-ghost {
+ opacity: 0.3;
+ }
+ }
+
.btn-action {
color: $gl-text-color;
@@ -259,3 +275,8 @@
}
}
}
+
+.label-link {
+ display: inline-block;
+ vertical-align: text-top;
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 0c013915a63..0b0c4bc130d 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -80,19 +80,28 @@
.ci_widget {
border-bottom: 1px solid $well-inner-border;
color: $gl-text-color;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-align-items: center;
+ align-items: center;
+
+ i,
+ svg {
+ margin-right: 8px;
+ }
svg {
- margin-right: 4px;
position: relative;
top: 1px;
overflow: visible;
}
- &.ci-success_with_warnings {
+ & > span {
+ padding-right: 4px;
+ }
- i {
- color: $gl-warning;
- }
+ @media (max-width: $screen-xs-max) {
+ flex-wrap: wrap;
}
}
@@ -102,6 +111,43 @@
padding: $gl-padding;
}
+ .mr-widget-pipeline-graph {
+ flex-shrink: 0;
+
+ .dropdown-menu {
+ margin-top: 11px;
+ }
+
+ .ci-action-icon-wrapper {
+ line-height: 16px;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ .stage-cell {
+ padding: 0 4px;
+ }
+ }
+
+ @media (max-width: $screen-xs-max) {
+ order: 1;
+ margin-top: $gl-padding-top;
+ border-radius: 3px;
+ background-color: $white-light;
+ border: 1px solid $gray-darker;
+ width: 100%;
+ text-align: center;
+
+ .dropdown-menu {
+ margin-left: -97.5px;
+ }
+
+ .arrow-up::before,
+ .arrow-up::after, {
+ margin-left: 97.5px;
+ }
+ }
+ }
+
.normal {
color: $gl-text-color;
}
@@ -223,8 +269,15 @@
.mr-list {
.merge-request {
- padding: 10px 15px;
+ padding: 10px 0 10px 15px;
position: relative;
+ display: -webkit-flex;
+ display: flex;
+
+ .issue-info-container {
+ -webkit-flex: 1;
+ flex: 1;
+ }
.merge-request-title {
margin-bottom: 2px;
@@ -430,7 +483,7 @@
background-color: $white-light;
&.affix {
- top: 100px;
+ top: 0;
left: 0;
z-index: 10;
transition: right .15s;
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 686b64cdd24..3da1150f89b 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -178,3 +178,9 @@
}
}
}
+
+.issuable-row {
+ background-color: $white-light;
+ cursor: -webkit-grab;
+ cursor: grab;
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 367a468e1ba..00eb5b30fd5 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -94,6 +94,10 @@
padding: 10px 8px;
}
+ td.stage-cell {
+ padding: 10px 0;
+ }
+
.commit-link {
padding: 9px 8px 10px;
}
@@ -183,52 +187,11 @@
}
}
- .stage-cell {
- font-size: 0;
- padding: 10px 4px;
-
- > .stage-container > div > button > span > svg,
- > .stage-container > button > svg {
- height: 22px;
- width: 22px;
- position: absolute;
- top: -1px;
- left: -1px;
- z-index: 2;
- overflow: visible;
- }
-
- .stage-container {
- display: inline-block;
- position: relative;
- height: 22px;
- margin: 3px 6px 3px 0;
-
- .tooltip {
- white-space: nowrap;
- }
-
- .tooltip-inner {
- padding: 3px 4px;
- }
-
- &:not(:last-child) {
- &::after {
- content: '';
- width: 7px;
- position: absolute;
- right: -7px;
- top: 10px;
- border-bottom: 2px solid $border-color;
- }
- }
- }
- }
-
.duration,
.finished-at {
color: $gl-text-color-secondary;
margin: 4px 0;
+ white-space: nowrap;
.fa {
font-size: 12px;
@@ -311,6 +274,50 @@
}
}
+.stage-cell {
+ font-size: 0;
+ padding: 10px 4px;
+
+ > .stage-container > div > button > span > svg,
+ > .stage-container > button > svg {
+ height: 22px;
+ width: 22px;
+ position: absolute;
+ top: -1px;
+ left: -1px;
+ z-index: 2;
+ overflow: visible;
+ }
+
+ .stage-container {
+ display: inline-block;
+ position: relative;
+ height: 22px;
+ margin: 3px 6px 3px 0;
+
+ // Hack to show a button tooltip inline
+ button.has-tooltip + .tooltip {
+ min-width: 105px;
+ }
+
+ // Bootstrap way of showing the content inline for anchors.
+ a.has-tooltip {
+ white-space: nowrap;
+ }
+
+ &:not(:last-child) {
+ &::after {
+ content: '';
+ width: 7px;
+ position: absolute;
+ right: -7px;
+ top: 10px;
+ border-bottom: 2px solid $border-color;
+ }
+ }
+ }
+}
+
.admin-builds-table {
.ci-table td:last-child {
min-width: 120px;
@@ -666,7 +673,7 @@
vertical-align: bottom;
display: inline-block;
position: relative;
- font-weight: 200;
+ font-weight: normal;
}
// Dropdown button in mini pipeline graph
@@ -857,7 +864,7 @@
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
- width: 90px;
+ max-width: 70%;
color: $gl-text-color-secondary;
margin-left: 2px;
display: inline-block;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 722b3006f7c..8031c4467a4 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -201,10 +201,6 @@
color: $note-disabled-comment-color;
}
-.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
- text-align: center;
-}
-
.created-personal-access-token-container {
#created-personal-access-token {
width: 90%;
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index d5783e14b21..9bc47bbe173 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -1,3 +1,11 @@
+.new-wiki-page {
+ .new-wiki-page-slug-tip {
+ display: inline-block;
+ max-width: 100%;
+ margin-top: 5px;
+ }
+}
+
.title .edit-wiki-header {
width: 780px;
margin-left: auto;
@@ -9,12 +17,18 @@
@extend .top-area;
position: relative;
+ .wiki-breadcrumb {
+ border-bottom: 1px solid $white-normal;
+ padding: 11px 0;
+ }
+
.wiki-page-title {
margin: 0;
font-size: 22px;
}
.wiki-last-edit-by {
+ display: block;
color: $gl-text-color-secondary;
strong {
@@ -121,6 +135,10 @@
margin: 5px 0 10px;
}
+ ul.wiki-pages ul {
+ padding-left: 15px;
+ }
+
.wiki-sidebar-header {
padding: 0 $gl-padding $gl-padding;
@@ -129,3 +147,15 @@
}
}
}
+
+ul.wiki-pages-list.content-list {
+ & ul {
+ list-style: none;
+ margin-left: 0;
+ padding-left: 15px;
+ }
+
+ & ul li {
+ padding: 5px 0;
+ }
+}
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index c491e5c7550..8360ce08bdc 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,7 +1,7 @@
class Admin::DashboardController < Admin::ApplicationController
def index
- @projects = Project.limit(10)
+ @projects = Project.with_route.limit(10)
@users = User.limit(10)
- @groups = Group.limit(10)
+ @groups = Group.with_route.limit(10)
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index b7722a1d15d..cea3d088e94 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -2,7 +2,7 @@ class Admin::GroupsController < Admin::ApplicationController
before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index
- @groups = Group.with_statistics
+ @groups = Group.with_statistics.with_route
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page])
@@ -49,7 +49,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def destroy
- DestroyGroupService.new(@group, current_user).async_execute
+ Groups::DestroyService.new(@group, current_user).async_execute
redirect_to admin_groups_path, alert: "Group '#{@group.name}' was scheduled for deletion."
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index aa0f8d434dc..1cd50852e89 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -175,7 +175,7 @@ class Admin::UsersController < Admin::ApplicationController
def user_params_ce
[
- :admin,
+ :access_level,
:avatar,
:bio,
:can_create_group,
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index bb47e2a8bf7..bf6be3d516b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -12,7 +12,6 @@ class ApplicationController < ActionController::Base
before_action :authenticate_user_from_private_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
- before_action :reject_blocked!
before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check
@@ -87,22 +86,8 @@ class ApplicationController < ActionController::Base
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
end
- def reject_blocked!
- if current_user && current_user.blocked?
- sign_out current_user
- flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it."
- redirect_to new_user_session_path
- end
- end
-
def after_sign_in_path_for(resource)
- if resource.is_a?(User) && resource.respond_to?(:blocked?) && resource.blocked?
- sign_out resource
- flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it."
- new_user_session_path
- else
- stored_location_for(:redirect) || stored_location_for(resource) || root_path
- end
+ stored_location_for(:redirect) || stored_location_for(resource) || root_path
end
def after_sign_out_path_for(resource)
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 6247934f81e..a6e158ebae6 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -9,6 +9,28 @@ module IssuableCollections
private
+ def issuable_meta_data(issuable_collection)
+ # map has to be used here since using pluck or select will
+ # throw an error when ordering issuables by priority which inserts
+ # a new order into the collection.
+ # We cannot use reorder to not mess up the paginated collection.
+ issuable_ids = issuable_collection.map(&:id)
+ issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
+ issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
+
+ issuable_ids.each_with_object({}) do |id, issuable_meta|
+ downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
+ upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
+ notes = issuable_note_count.find { |notes| notes.noteable_id == id }
+
+ issuable_meta[id] = Issuable::IssuableMeta.new(
+ upvotes.try(:count).to_i,
+ downvotes.try(:count).to_i,
+ notes.try(:count).to_i
+ )
+ end
+ end
+
def issues_collection
issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index b46adcceb60..fb5edb34370 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -9,6 +9,9 @@ module IssuesAction
.non_archived
.page(params[:page])
+ @collection_type = "Issue"
+ @issuable_meta_data = issuable_meta_data(@issues)
+
respond_to do |format|
format.html
format.atom { render layout: false }
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index fdb05bb3228..6229759dcf1 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -7,6 +7,9 @@ module MergeRequestsAction
@merge_requests = merge_requests_collection
.page(params[:page])
+
+ @collection_type = "MergeRequest"
+ @issuable_meta_data = issuable_meta_data(@merge_requests)
end
private
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index 562f92bd83c..a6891149bfa 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -1,6 +1,8 @@
module SpammableActions
extend ActiveSupport::Concern
+ include Recaptcha::Verify
+
included do
before_action :authorize_submit_spammable!, only: :mark_as_spam
end
@@ -15,6 +17,15 @@ module SpammableActions
private
+ def recaptcha_params
+ return {} unless params[:recaptcha_verification] && Gitlab::Recaptcha.load_configurations! && verify_recaptcha
+
+ {
+ recaptcha_verified: true,
+ spam_log_id: params[:spam_log_id]
+ }
+ end
+
def spammable
raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end
@@ -22,4 +33,11 @@ module SpammableActions
def authorize_submit_spammable!
access_denied! unless current_user.admin?
end
+
+ def render_recaptcha?
+ return false if spammable.errors.count > 1 # re-render "new" template in case there are other errors
+ return false unless Gitlab::Recaptcha.enabled?
+
+ spammable.spam
+ end
end
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index de6bc689bb7..0b7cf8167f0 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.includes(:source).page(params[:page])
+ @group_members = current_user.group_members.includes(source: :route).page(params[:page])
end
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 3ba8c2f8bb9..325ae565537 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -1,19 +1,14 @@
class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects
- before_action :event_filter
-
def index
- @projects = current_user.authorized_projects.sorted_by_activity
- @projects = filter_projects(@projects)
- @projects = @projects.includes(:namespace)
+ @projects = load_projects(current_user.authorized_projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
respond_to do |format|
format.html { @last_push = current_user.recent_push }
format.atom do
- event_filter
load_events
render layout: false
end
@@ -26,9 +21,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def starred
- @projects = current_user.viewable_starred_projects.sorted_by_activity
- @projects = filter_projects(@projects)
- @projects = @projects.includes(:namespace, :forked_from_project, :tags)
+ @projects = load_projects(current_user.viewable_starred_projects)
+ @projects = @projects.includes(:forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@@ -37,7 +31,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
respond_to do |format|
format.html
-
format.json do
render json: {
html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
@@ -48,9 +41,15 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
+ def load_projects(base_scope)
+ projects = base_scope.sorted_by_activity.includes(:namespace)
+
+ filter_projects(projects)
+ end
+
def load_events
- @events = Event.in_projects(@projects)
- @events = @event_filter.apply_filter(@events).with_associations
+ @events = Event.in_projects(load_projects(current_user.authorized_projects))
+ @events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
end
diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb
index a1ab8b99048..baf54520b9c 100644
--- a/app/controllers/explore/application_controller.rb
+++ b/app/controllers/explore/application_controller.rb
@@ -1,5 +1,5 @@
class Explore::ApplicationController < ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked!
+ skip_before_action :authenticate_user!
layout 'explore'
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 4f273a8d4f0..0cbf3eb58a3 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -9,7 +9,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
- @members = @group.group_members
+ @members = GroupMembersFinder.new(@group).execute
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort)
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 264b14713fb..7ed54479599 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -13,9 +13,11 @@ class GroupsController < Groups::ApplicationController
before_action :authorize_create_group!, only: [:new, :create]
# Load group projects
- before_action :group_projects, only: [:show, :projects, :activity, :issues, :merge_requests]
+ before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
before_action :event_filter, only: [:activity]
+ before_action :user_actions, only: [:show, :subgroups]
+
layout :determine_layout
def index
@@ -37,13 +39,6 @@ class GroupsController < Groups::ApplicationController
end
def show
- if current_user
- @last_push = current_user.recent_push
- @notification_setting = current_user.notification_settings_for(group)
- end
-
- @nested_groups = group.children
-
setup_projects
respond_to do |format|
@@ -62,6 +57,11 @@ class GroupsController < Groups::ApplicationController
end
end
+ def subgroups
+ @nested_groups = group.children
+ @nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
+ end
+
def activity
respond_to do |format|
format.html
@@ -91,7 +91,7 @@ class GroupsController < Groups::ApplicationController
end
def destroy
- DestroyGroupService.new(@group, current_user).async_execute
+ Groups::DestroyService.new(@group, current_user).async_execute
redirect_to root_path, alert: "Group '#{@group.name}' was scheduled for deletion."
end
@@ -99,13 +99,16 @@ class GroupsController < Groups::ApplicationController
protected
def setup_projects
+ options = {}
+ options[:only_owned] = true if params[:shared] == '0'
+ options[:only_shared] = true if params[:shared] == '1'
+
+ @projects = GroupProjectsFinder.new(group, options).execute(current_user)
@projects = @projects.includes(:namespace)
@projects = @projects.sorted_by_activity
@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)
end
def authorize_create_group!
@@ -138,7 +141,8 @@ class GroupsController < Groups::ApplicationController
:public,
:request_access_enabled,
:share_with_group_lock,
- :visibility_level
+ :visibility_level,
+ :parent_id
]
end
@@ -147,4 +151,11 @@ class GroupsController < Groups::ApplicationController
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
+
+ def user_actions
+ if current_user
+ @last_push = current_user.recent_push
+ @notification_setting = current_user.notification_settings_for(group)
+ end
+ end
end
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 37feff79999..87c0f8905ff 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -1,5 +1,5 @@
class HelpController < ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked!
+ skip_before_action :authenticate_user!
layout 'help'
diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb
index f3759b4c0ea..6b1e64ce819 100644
--- a/app/controllers/koding_controller.rb
+++ b/app/controllers/koding_controller.rb
@@ -1,5 +1,5 @@
class KodingController < ApplicationController
- before_action :check_integration!, :authenticate_user!, :reject_blocked!
+ before_action :check_integration!
layout 'koding'
def index
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index b8b71d295f6..a271e2dfc4b 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
- params.require(:user).permit(:notification_email)
+ params.require(:user).permit(:notification_email, :notified_of_own_activity)
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 9940263ae24..a1db856dcfb 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -30,6 +30,8 @@ class Projects::BlobController < Projects::ApplicationController
end
def show
+ environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
end
def edit
@@ -59,10 +61,10 @@ class Projects::BlobController < Projects::ApplicationController
end
def destroy
- create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.",
- success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch),
- failure_view: :show,
- failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
+ create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.",
+ success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch),
+ failure_view: :show,
+ failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
end
def diff
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index b5a7078a3a1..e10d7992db7 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -37,7 +37,6 @@ class Projects::CommitController < Projects::ApplicationController
format.json do
render json: PipelineSerializer
.new(project: @project, user: @current_user)
- .with_pagination(request, response)
.represent(@pipelines)
end
end
@@ -95,6 +94,8 @@ class Projects::CommitController < Projects::ApplicationController
@diffs = commit.diffs(opts)
@notes_count = commit.notes.count
+
+ @environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last
end
def define_note_vars
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 321cde255c3..c6651254d70 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -57,6 +57,9 @@ class Projects::CompareController < Projects::ApplicationController
@diffs = @compare.diffs(diff_options)
+ environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+
@diff_notes_disabled = true
@grouped_diff_discussions = {}
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 87cc36253f1..0ec8f5bd64a 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -10,7 +10,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def index
@scope = params[:scope]
- @environments = project.environments
+ @environments = project.environments.includes(:last_deployment)
respond_to do |format|
format.html
@@ -52,10 +52,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def stop
- return render_404 unless @environment.stoppable?
+ return render_404 unless @environment.available?
- new_action = @environment.stop!(current_user)
- redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
+ stop_action = @environment.stop_with_action!(current_user)
+
+ if stop_action
+ redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, stop_action])
+ else
+ redirect_to namespace_project_environment_path(project.namespace, project, @environment)
+ end
end
def terminal
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 8472ceca329..744a4af1c51 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -23,8 +23,11 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html
def index
- @issues = issues_collection
- @issues = @issues.page(params[:page])
+ @collection_type = "Issue"
+ @issues = issues_collection
+ @issues = @issues.page(params[:page])
+ @issuable_meta_data = issuable_meta_data(@issues)
+
if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages))
end
@@ -93,15 +96,13 @@ class Projects::IssuesController < Projects::ApplicationController
def create
extra_params = { request: request,
merge_request_for_resolving_discussions: merge_request_for_resolving_discussions }
+ extra_params.merge!(recaptcha_params)
+
@issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute
respond_to do |format|
format.html do
- if @issue.valid?
- redirect_to issue_path(@issue)
- else
- render :new
- end
+ html_response_create
end
format.js do
@link = @issue.attachment.url.to_js
@@ -178,6 +179,20 @@ class Projects::IssuesController < Projects::ApplicationController
protected
+ def html_response_create
+ if @issue.valid?
+ redirect_to issue_path(@issue)
+ elsif render_recaptcha?
+ if params[:recaptcha_verification]
+ flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ end
+
+ render :verify
+ else
+ render :new
+ end
+ end
+
def issue
# The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index 440259b643c..8a5a645ed0e 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -48,6 +48,10 @@ class Projects::LfsApiController < Projects::GitHttpClientController
objects.each do |object|
if existing_oids.include?(object[:oid])
object[:actions] = download_actions(object)
+
+ if Guest.can?(:download_code, project)
+ object[:authenticated] = true
+ end
else
object[:error] = {
code: 404,
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 6eb542e4bd8..c3e1760f168 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -36,8 +36,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts]
def index
- @merge_requests = merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page])
+ @collection_type = "MergeRequest"
+ @merge_requests = merge_requests_collection
+ @merge_requests = @merge_requests.page(params[:page])
+ @issuable_meta_data = issuable_meta_data(@merge_requests)
+
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
end
@@ -103,6 +106,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ @environment = @merge_request.environments_for(current_user).last
+
respond_to do |format|
format.html { define_discussion_vars }
format.json do
@@ -216,19 +221,24 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
format.json do
- render json: {
- html: view_to_html_string('projects/merge_requests/show/_pipelines'),
- pipelines: PipelineSerializer
- .new(project: @project, user: @current_user)
- .with_pagination(request, response)
- .represent(@pipelines)
- }
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent(@pipelines)
end
end
end
def new
- define_new_vars
+ respond_to do |format|
+ format.html { define_new_vars }
+ format.json do
+ define_pipelines_vars
+
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent(@pipelines)
+ end
+ end
end
def new_diffs
@@ -245,7 +255,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
@diff_notes_disabled = true
- render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
+ @environment = @merge_request.environments_for(current_user).last
+
+ render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs, environment: @environment) }
end
end
end
@@ -444,14 +456,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def ci_environments_status
environments =
begin
- @merge_request.environments.map do |environment|
- next unless can?(current_user, :read_environment, environment)
-
+ @merge_request.environments_for(current_user).map do |environment|
project = environment.project
deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
stop_url =
- if environment.stoppable? && can?(current_user, :create_deployment, environment)
+ if environment.stop_action? && can?(current_user, :create_deployment, environment)
stop_namespace_project_environment_path(project.namespace, project, environment)
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index c5d93ce25bc..b033f7b5ea9 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -51,7 +51,7 @@ class Projects::NotesController < Projects::ApplicationController
def destroy
if note.editable?
- Notes::DeleteService.new(project, current_user).execute(note)
+ Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 53ce23221ed..c8c80551ac9 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -2,20 +2,13 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
before_action :authorize_admin_pipeline!
def show
- @ref = params[:ref] || @project.default_branch || 'master'
-
- @badges = [Gitlab::Badge::Build::Status,
- Gitlab::Badge::Coverage::Report]
-
- @badges.map! do |badge|
- badge.new(@project, @ref).metadata
- end
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project, params: params)
end
def update
if @project.update_attributes(update_params)
flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated."
- redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
else
render 'show'
end
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index 9a438d5512c..2f422d352ed 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -68,8 +68,12 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
def access_levels_options
{
- push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
- merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
+ push_access_levels: {
+ "Roles" => ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
+ },
+ merge_access_levels: {
+ "Roles" => ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
+ }
}
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index ff75c408beb..8b50ea207a5 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -5,11 +5,7 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings'
def index
- @project_runners = project.runners.ordered
- @assignable_runners = current_user.ci_authorized_runners.
- assignable_for(project).ordered.page(params[:page]).per(20)
- @shared_runners = Ci::Runner.shared.active
- @shared_runners_count = @shared_runners.count(:all)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def edit
@@ -53,7 +49,7 @@ class Projects::RunnersController < Projects::ApplicationController
def toggle_shared_runners
project.toggle!(:shared_runners_enabled)
- redirect_to namespace_project_runners_path(project.namespace, project)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
protected
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
new file mode 100644
index 00000000000..6f009d61950
--- /dev/null
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -0,0 +1,44 @@
+module Projects
+ module Settings
+ class CiCdController < Projects::ApplicationController
+ before_action :authorize_admin_pipeline!
+
+ def show
+ define_runners_variables
+ define_secret_variables
+ define_triggers_variables
+ define_badges_variables
+ end
+
+ private
+
+ def define_runners_variables
+ @project_runners = @project.runners.ordered
+ @assignable_runners = current_user.ci_authorized_runners.
+ assignable_for(project).ordered.page(params[:page]).per(20)
+ @shared_runners = Ci::Runner.shared.active
+ @shared_runners_count = @shared_runners.count(:all)
+ end
+
+ def define_secret_variables
+ @variable = Ci::Variable.new
+ end
+
+ def define_triggers_variables
+ @triggers = @project.triggers
+ @trigger = Ci::Trigger.new
+ end
+
+ def define_badges_variables
+ @ref = params[:ref] || @project.default_branch || 'master'
+
+ @badges = [Gitlab::Badge::Build::Status,
+ Gitlab::Badge::Coverage::Report]
+
+ @badges.map! do |badge|
+ badge.new(@project, @ref).metadata
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index 92359745cec..b2c11ea4156 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -4,8 +4,7 @@ class Projects::TriggersController < Projects::ApplicationController
layout 'project_settings'
def index
- @triggers = project.triggers
- @trigger = Ci::Trigger.new
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def create
@@ -13,17 +12,18 @@ class Projects::TriggersController < Projects::ApplicationController
@trigger.save
if @trigger.valid?
- redirect_to namespace_project_triggers_path(@project.namespace, @project)
+ redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Trigger was created successfully.'
else
@triggers = project.triggers.select(&:persisted?)
- render :index
+ render action: "show"
end
end
def destroy
trigger.destroy
+ flash[:alert] = "Trigger removed"
- redirect_to namespace_project_triggers_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
private
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index 50ba33ed570..61686499bd3 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -1,6 +1,6 @@
class Projects::UploadsController < Projects::ApplicationController
- skip_before_action :reject_blocked!, :project,
- :repository, if: -> { action_name == 'show' && image_or_video? }
+ skip_before_action :project, :repository,
+ if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create]
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 6f068729390..a4d1b1ee69b 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -4,7 +4,7 @@ class Projects::VariablesController < Projects::ApplicationController
layout 'project_settings'
def index
- @variable = Ci::Variable.new
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def show
@@ -25,9 +25,10 @@ class Projects::VariablesController < Projects::ApplicationController
@variable = Ci::Variable.new(project_params)
if @variable.valid? && @project.variables << @variable
- redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.'
+ flash[:notice] = 'Variables were successfully updated.'
+ redirect_to namespace_project_settings_ci_cd_path(project.namespace, project)
else
- render action: "index"
+ render "show"
end
end
@@ -35,7 +36,7 @@ class Projects::VariablesController < Projects::ApplicationController
@key = @project.variables.find(params[:id])
@key.destroy
- redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully removed.'
+ redirect_to namespace_project_settings_ci_cd_path(project.namespace, project), notice: 'Variable was successfully removed.'
end
private
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index c3353446fd1..2d8064c9878 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -8,6 +8,7 @@ class Projects::WikisController < Projects::ApplicationController
def pages
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page])
+ @wiki_entries = WikiPage.group_by_directory(@wiki_pages)
end
def show
@@ -83,7 +84,7 @@ class Projects::WikisController < Projects::ApplicationController
def destroy
@page = @project_wiki.find_page(params[:id])
- @page.delete if @page
+ WikiPages::DestroyService.new(@project, current_user).execute(@page)
redirect_to(
namespace_project_wiki_path(@project.namespace, @project, :home),
@@ -116,7 +117,7 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
- @sidebar_wiki_pages = @project_wiki.pages.first(15)
+ @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index bf27f3d4d51..b44f38d4a0c 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -17,20 +17,20 @@ class RegistrationsController < Devise::RegistrationsController
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
super
else
- flash[:alert] = 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.'
+ flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
flash.delete :recaptcha_error
render action: 'new'
end
end
def destroy
- DeleteUserService.new(current_user).execute(current_user)
+ Users::DestroyService.new(current_user).execute(current_user)
respond_to do |format|
format.html do
session.try(:destroy)
redirect_to new_user_session_path, notice: "Account successfully removed."
- end
+ end
end
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 6576ebd5235..612d69cf557 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -1,5 +1,5 @@
class SearchController < ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked!
+ skip_before_action :authenticate_user!
include SearchHelper
diff --git a/app/finders/environments_finder.rb b/app/finders/environments_finder.rb
new file mode 100644
index 00000000000..a59f8c1efa3
--- /dev/null
+++ b/app/finders/environments_finder.rb
@@ -0,0 +1,55 @@
+class EnvironmentsFinder
+ attr_reader :project, :current_user, :params
+
+ def initialize(project, current_user, params = {})
+ @project, @current_user, @params = project, current_user, params
+ end
+
+ def execute
+ deployments = project.deployments
+ deployments =
+ if ref
+ deployments_query = params[:with_tags] ? 'ref = :ref OR tag IS TRUE' : 'ref = :ref'
+ deployments.where(deployments_query, ref: ref.to_s)
+ elsif commit
+ deployments.where(sha: commit.sha)
+ else
+ deployments.none
+ end
+
+ environment_ids = deployments
+ .group(:environment_id)
+ .select(:environment_id)
+
+ environments = project.environments.available
+ .where(id: environment_ids).order_by_last_deployed_at.to_a
+
+ environments.select! do |environment|
+ Ability.allowed?(current_user, :read_environment, environment)
+ end
+
+ if ref && commit
+ environments.select! do |environment|
+ environment.includes_commit?(commit)
+ end
+ end
+
+ if ref && params[:recently_updated]
+ environments.select! do |environment|
+ environment.recently_updated_on_branch?(ref)
+ end
+ end
+
+ environments
+ end
+
+ private
+
+ def ref
+ params[:ref].try(:to_s)
+ end
+
+ def commit
+ params[:commit]
+ end
+end
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
new file mode 100644
index 00000000000..9f2206346ce
--- /dev/null
+++ b/app/finders/group_members_finder.rb
@@ -0,0 +1,20 @@
+class GroupMembersFinder < Projects::ApplicationController
+ def initialize(group)
+ @group = group
+ end
+
+ def execute
+ group_members = @group.members
+
+ return group_members unless @group.parent
+
+ parents_members = GroupMember.non_request.
+ where(source_id: @group.ancestors.select(:id)).
+ where.not(user_id: @group.users.select(:id))
+
+ wheres = ["members.id IN (#{group_members.select(:id).to_sql})"]
+ wheres << "members.id IN (#{parents_members.select(:id).to_sql})"
+
+ GroupMember.where(wheres.join(' OR '))
+ end
+end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
index aa8f4c1d0e4..3b9a421b118 100644
--- a/app/finders/group_projects_finder.rb
+++ b/app/finders/group_projects_finder.rb
@@ -18,7 +18,7 @@ class GroupProjectsFinder < UnionFinder
projects = []
if current_user
- if @group.users.include?(current_user) || current_user.admin?
+ if @group.users.include?(current_user)
projects << @group.projects unless only_shared
projects << @group.shared_projects unless only_owned
else
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 4e43f42e9e1..d932a17883f 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -2,7 +2,7 @@ class GroupsFinder < UnionFinder
def execute(current_user = nil)
segments = all_groups(current_user)
- find_union(segments, Group).order_id_desc
+ find_union(segments, Group).with_route.order_id_desc
end
private
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index c7911736812..18ec45f300d 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -3,7 +3,7 @@ class ProjectsFinder < UnionFinder
segments = all_projects(current_user)
segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
- find_union(segments, Project)
+ find_union(segments, Project).with_route
end
private
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index 9fc69e12266..ff937b5ebd2 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -1,7 +1,7 @@
module BuildsHelper
def sidebar_build_class(build, current_build)
build_class = ''
- build_class += ' active' if build == current_build
+ build_class += ' active' if build.id === current_build.id
build_class += ' retried' if build.retried?
build_class
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 6dcb624c4da..8aad39e148b 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -194,7 +194,7 @@ module CommitsHelper
end
end
- def view_file_btn(commit_sha, diff_new_path, project)
+ def view_file_button(commit_sha, diff_new_path, project)
link_to(
namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff_new_path)),
@@ -205,6 +205,17 @@ module CommitsHelper
end
end
+ def view_on_environment_button(commit_sha, diff_new_path, environment)
+ return unless environment && commit_sha
+
+ external_url = environment.external_url_for(diff_new_path, commit_sha)
+ return unless external_url
+
+ link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
+ icon('external-link')
+ end
+ end
+
def truncate_sha(sha)
Commit.truncate_sha(sha)
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 2159e4ce21a..f16a63e2178 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -211,8 +211,12 @@ module GitlabRoutingHelper
def project_settings_integrations_path(project, *args)
namespace_project_settings_integrations_path(project.namespace, project, *args)
end
-
+
def project_settings_members_path(project, *args)
namespace_project_settings_members_path(project.namespace, project, *args)
end
+
+ def project_settings_ci_cd_path(project, *args)
+ namespace_project_settings_ci_cd_path(project.namespace, project, *args)
+ end
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 83ff898e68a..b5f8c23a667 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -20,8 +20,8 @@ module MergeRequestsHelper
end
def mr_widget_refresh_url(mr)
- if mr && mr.source_project
- merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr)
+ if mr && mr.target_project
+ merge_widget_refresh_namespace_project_merge_request_url(mr.target_project.namespace, mr.target_project, mr)
else
''
end
@@ -64,11 +64,11 @@ module MergeRequestsHelper
end
def mr_closes_issues
- @mr_closes_issues ||= @merge_request.closes_issues
+ @mr_closes_issues ||= @merge_request.closes_issues(current_user)
end
def mr_issues_mentioned_but_not_closing
- @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing
+ @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing(current_user)
end
def mr_change_branches_path(merge_request)
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 6e68aad4cb7..dd0a4ea03f0 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -23,7 +23,7 @@ module PreferencesHelper
if defined.size != DASHBOARD_CHOICES.size
# Ensure that anyone adding new options updates this method too
- raise RuntimeError, "`User` defines #{defined.size} dashboard choices," +
+ raise "`User` defines #{defined.size} dashboard choices," \
" but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}."
else
defined.map do |key, _|
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index c568cca9e5e..845f1a0e840 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -15,6 +15,7 @@ module TodosHelper
when Todo::MARKED then 'added a todo for'
when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
when Todo::UNMERGEABLE then 'Could not merge'
+ when Todo::DIRECTLY_ADDRESSED then 'directly addressed you on'
end
end
@@ -86,7 +87,10 @@ module TodosHelper
[
{ id: '', text: 'Any Action' },
{ id: Todo::ASSIGNED, text: 'Assigned' },
- { id: Todo::MENTIONED, text: 'Mentioned' }
+ { id: Todo::MENTIONED, text: 'Mentioned' },
+ { id: Todo::MARKED, text: 'Added' },
+ { id: Todo::BUILD_FAILED, text: 'Pipelines' },
+ { id: Todo::DIRECTLY_ADDRESSED, text: 'Directly addressed' }
]
end
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
new file mode 100644
index 00000000000..3e3f6246fc5
--- /dev/null
+++ b/app/helpers/wiki_helper.rb
@@ -0,0 +1,13 @@
+module WikiHelper
+ # Produces a pure text breadcrumb for a given page.
+ #
+ # page_slug - The slug of a WikiPage object.
+ #
+ # Returns a String composed of the capitalized name of each directory and the
+ # capitalized name of the page itself.
+ def breadcrumb(page_slug)
+ page_slug.split('/').
+ map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize }.
+ join(' / ')
+ end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0cd3456b4de..5b9226a6b81 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -151,7 +151,7 @@ class Notify < BaseMailer
headers['In-Reply-To'] = message_id(model)
headers['References'] = message_id(model)
- headers[:subject].prepend('Re: ') if headers[:subject]
+ headers[:subject]&.prepend('Re: ')
mail_thread(model, headers)
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 9a4557524c4..74b358d8c40 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -116,31 +116,25 @@ class ApplicationSetting < ActiveRecord::Base
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates_each :restricted_visibility_levels do |record, attr, value|
- unless value.nil?
- value.each do |level|
- unless Gitlab::VisibilityLevel.options.has_value?(level)
- record.errors.add(attr, "'#{level}' is not a valid visibility level")
- end
+ value&.each do |level|
+ unless Gitlab::VisibilityLevel.options.has_value?(level)
+ record.errors.add(attr, "'#{level}' is not a valid visibility level")
end
end
end
validates_each :import_sources do |record, attr, value|
- unless value.nil?
- value.each do |source|
- unless Gitlab::ImportSources.options.has_value?(source)
- record.errors.add(attr, "'#{source}' is not a import source")
- end
+ value&.each do |source|
+ unless Gitlab::ImportSources.options.has_value?(source)
+ record.errors.add(attr, "'#{source}' is not a import source")
end
end
end
validates_each :disabled_oauth_sign_in_sources do |record, attr, value|
- unless value.nil?
- value.each do |source|
- unless Devise.omniauth_providers.include?(source.to_sym)
- record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
- end
+ value&.each do |source|
+ unless Devise.omniauth_providers.include?(source.to_sym)
+ record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
end
end
end
@@ -230,11 +224,11 @@ class ApplicationSetting < ActiveRecord::Base
end
def domain_whitelist_raw
- self.domain_whitelist.join("\n") unless self.domain_whitelist.nil?
+ self.domain_whitelist&.join("\n")
end
def domain_blacklist_raw
- self.domain_blacklist.join("\n") unless self.domain_blacklist.nil?
+ self.domain_blacklist&.join("\n")
end
def domain_whitelist_raw=(values)
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index 46b17479d6d..6937ad3bdd9 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -16,6 +16,14 @@ class AwardEmoji < ActiveRecord::Base
scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) }
+ class << self
+ def votes_for_collection(ids, type)
+ select('name', 'awardable_id', 'COUNT(*) as count').
+ where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids).
+ group('name', 'awardable_id')
+ end
+ end
+
def downvote?
self.name == DOWNVOTE_NAME
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 44d4fb9d8d8..8c1b076c2d7 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -9,6 +9,7 @@ module Ci
belongs_to :erased_by, class_name: 'User'
has_many :deployments, as: :deployable
+ has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
@@ -19,7 +20,7 @@ module Ci
end
serialize :options
- serialize :yaml_variables, Gitlab::Serialize::Ci::Variables
+ serialize :yaml_variables, Gitlab::Serializer::Ci::Variables
validates :coverage, numericality: true, allow_blank: true
validates_presence_of :ref
@@ -41,7 +42,7 @@ module Ci
before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
- before_destroy { project }
+ before_destroy { unscoped_project }
after_create :execute_hooks
after_save :update_project_statistics, if: :artifacts_size_changed?
@@ -183,10 +184,6 @@ module Ci
success? && !last_deployment.try(:last?)
end
- def last_deployment
- deployments.last
- end
-
def depends_on_builds
# Get builds of the same type
latest_builds = self.pipeline.builds.latest
@@ -416,16 +413,23 @@ module Ci
# This method returns old path to artifacts only if it already exists.
#
def artifacts_path
+ # We need the project even if it's soft deleted, because whenever
+ # we're really deleting the project, we'll also delete the builds,
+ # and in order to delete the builds, we need to know where to find
+ # the artifacts, which is depending on the data of the project.
+ # We need to retain the project in this case.
+ the_project = project || unscoped_project
+
old = File.join(created_at.utc.strftime('%Y_%m'),
- project.ci_id.to_s,
+ the_project.ci_id.to_s,
id.to_s)
old_store = File.join(ArtifactUploader.artifacts_path, old)
- return old if project.ci_id && File.directory?(old_store)
+ return old if the_project.ci_id && File.directory?(old_store)
File.join(
created_at.utc.strftime('%Y_%m'),
- project.id.to_s,
+ the_project.id.to_s,
id.to_s
)
end
@@ -560,6 +564,10 @@ module Ci
self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
end
+ def unscoped_project
+ @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
+ end
+
def predefined_variables
variables = [
{ key: 'CI', value: 'true', public: true },
@@ -598,6 +606,8 @@ module Ci
end
def update_project_statistics
+ return unless project
+
ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index fab8497ec7d..bbc358adb83 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -283,13 +283,7 @@ module Ci
def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file)
- @ci_yaml_file ||= begin
- blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
- blob.load_all_data!(project.repository)
- blob.data
- rescue
- nil
- end
+ @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
end
def has_yaml_errors?
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 3517969eabc..5f53c48fc88 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -15,6 +15,11 @@ module Issuable
include Taskable
include TimeTrackable
+ # This object is used to gather issuable meta data for displaying
+ # upvotes, downvotes and notes count for issues and merge requests
+ # lists avoiding n+1 queries and improving performance.
+ IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count)
+
included do
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@@ -95,8 +100,8 @@ module Issuable
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignees(if they exist)
previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
- previous_assignee.update_cache_counts if previous_assignee
- assignee.update_cache_counts if assignee
+ previous_assignee&.update_cache_counts
+ assignee&.update_cache_counts
end
# We want to use optimistic lock for cases when only title or description are involved
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index ef2c1e5d414..7e56e371b27 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -44,8 +44,15 @@ module Mentionable
end
def all_references(current_user = nil, extractor: nil)
- extractor ||= Gitlab::ReferenceExtractor.
- new(project, current_user)
+ # Use custom extractor if it's passed in the function parameters.
+ if extractor
+ @extractor = extractor
+ else
+ @extractor ||= Gitlab::ReferenceExtractor.
+ new(project, current_user)
+
+ @extractor.reset_memoized_values
+ end
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
@@ -55,16 +62,20 @@ module Mentionable
skip_project_check: skip_project_check?
)
- extractor.analyze(text, options)
+ @extractor.analyze(text, options)
end
- extractor
+ @extractor
end
def mentioned_users(current_user = nil)
all_references(current_user).users
end
+ def directly_addressed_users(current_user = nil)
+ all_references(current_user).directly_addressed_users
+ end
+
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
def referenced_mentionables(current_user = self.author)
refs = all_references(current_user)
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index e9450dd0c26..f449229864d 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -73,7 +73,7 @@ module Milestoneish
def memoize_per_user(user, method_name)
@memoized ||= {}
@memoized[method_name] ||= {}
- @memoized[method_name][user.try!(:id)] ||= yield
+ @memoized[method_name][user&.id] ||= yield
end
# override in a class that includes this module to get a faster query
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 2b93aa30c0f..9f6d215ceb3 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -1,5 +1,5 @@
# Store object full path in separate table for easy lookup and uniq validation
-# Object must have path db field and respond to full_path and full_path_changed? methods.
+# Object must have name and path db fields and respond to parent and parent_changed? methods.
module Routable
extend ActiveSupport::Concern
@@ -9,7 +9,13 @@ module Routable
validates_associated :route
validates :route, presence: true
- before_validation :update_route_path, if: :full_path_changed?
+ scope :with_route, -> { includes(:route) }
+
+ before_validation do
+ if full_path_changed? || full_name_changed?
+ prepare_route
+ end
+ end
end
class_methods do
@@ -77,10 +83,62 @@ module Routable
end
end
+ def full_name
+ if route && route.name.present?
+ @full_name ||= route.name
+ else
+ update_route if persisted?
+
+ build_full_name
+ end
+ end
+
+ def full_path
+ if route && route.path.present?
+ @full_path ||= route.path
+ else
+ update_route if persisted?
+
+ build_full_path
+ end
+ end
+
private
- def update_route_path
+ def full_name_changed?
+ name_changed? || parent_changed?
+ end
+
+ def full_path_changed?
+ path_changed? || parent_changed?
+ end
+
+ def build_full_name
+ if parent && name
+ parent.human_name + ' / ' + name
+ else
+ name
+ end
+ end
+
+ def build_full_path
+ if parent && path
+ parent.full_path + '/' + path
+ else
+ path
+ end
+ end
+
+ def update_route
+ prepare_route
+ route.save
+ end
+
+ def prepare_route
route || build_route(source: self)
- route.path = full_path
+ route.path = build_full_path
+ route.name = build_full_name
+ @full_path = nil
+ @full_name = nil
end
end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 1acff093aa1..423ae98a60e 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -11,6 +11,7 @@ module Spammable
has_one :user_agent_detail, as: :subject, dependent: :destroy
attr_accessor :spam
+ attr_accessor :spam_log
after_validation :check_for_spam, on: :create
@@ -34,9 +35,14 @@ module Spammable
end
def check_for_spam
- if spam?
- self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.")
- end
+ error_msg = if Gitlab::Recaptcha.enabled?
+ "Your #{spammable_entity_type} has been recognized as spam. "\
+ "You can still submit it by solving Captcha."
+ else
+ "Your #{spammable_entity_type} has been recognized as spam and has been discarded."
+ end
+
+ self.errors.add(:base, error_msg) if spam?
end
def spammable_entity_type
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 040e3a2884e..9cf83440784 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -18,7 +18,7 @@ module TimeTrackable
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
validate :check_negative_time_spent
- has_many :timelogs, as: :trackable, dependent: :destroy
+ has_many :timelogs, dependent: :destroy
end
def spend_time(options)
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 91d85c2279b..afad001d50f 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -91,7 +91,7 @@ class Deployment < ActiveRecord::Base
@stop_action ||= manual_actions.find_by(name: on_stop)
end
- def stoppable?
+ def stop_action?
stop_action.present?
end
diff --git a/app/models/directly_addressed_user.rb b/app/models/directly_addressed_user.rb
new file mode 100644
index 00000000000..0d519c6ac22
--- /dev/null
+++ b/app/models/directly_addressed_user.rb
@@ -0,0 +1,7 @@
+class DirectlyAddressedUser
+ class << self
+ def reference_pattern
+ User.reference_pattern
+ end
+ end
+end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 577367f1eed..1a21b5e52b5 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -6,7 +6,8 @@ class Environment < ActiveRecord::Base
belongs_to :project, required: true, validate: true
- has_many :deployments
+ has_many :deployments, dependent: :destroy
+ has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment'
before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
@@ -37,6 +38,13 @@ class Environment < ActiveRecord::Base
scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) }
+ scope :order_by_last_deployed_at, -> do
+ max_deployment_id_sql =
+ Deployment.select(Deployment.arel_table[:id].maximum).
+ where(Deployment.arel_table[:environment_id].eq(arel_table[:id])).
+ to_sql
+ order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC'))
+ end
state_machine :state, initial: :available do
event :start do
@@ -62,10 +70,6 @@ class Environment < ActiveRecord::Base
ref.to_s == last_deployment.try(:ref)
end
- def last_deployment
- deployments.last
- end
-
def nullify_external_url
self.external_url = nil if self.external_url.blank?
end
@@ -87,6 +91,10 @@ class Environment < ActiveRecord::Base
last_deployment.includes_commit?(commit)
end
+ def last_deployed_at
+ last_deployment.try(:created_at)
+ end
+
def update_merge_request_metrics?
(environment_type || name) == "production"
end
@@ -110,15 +118,15 @@ class Environment < ActiveRecord::Base
external_url.gsub(/\A.*?:\/\//, '')
end
- def stoppable?
+ def stop_action?
available? && stop_action.present?
end
- def stop!(current_user)
- return unless stoppable?
+ def stop_with_action!(current_user)
+ return unless available?
- stop
- stop_action.play(current_user)
+ stop!
+ stop_action&.play(current_user)
end
def actions_for(environment)
@@ -171,6 +179,15 @@ class Environment < ActiveRecord::Base
self.slug = slugified
end
+ def external_url_for(path, commit_sha)
+ return unless self.external_url
+
+ public_path = project.public_path_for_source_path(path, commit_sha)
+ return unless public_path
+
+ [external_url, public_path].join('/')
+ end
+
private
# Slugifying a name may remove the uniqueness guarantee afforded by it being
diff --git a/app/models/event.rb b/app/models/event.rb
index 2662f170765..e5027df3f8a 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -36,18 +36,19 @@ class Event < ActiveRecord::Base
scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(projects) do
- where(project_id: projects.map(&:id)).recent
+ where(project_id: projects).recent
end
- scope :with_associations, -> { includes(project: :namespace) }
+ scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
class << self
# Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions
- where("action = ? OR (target_type in (?) AND action in (?))",
- Event::PUSHED, ["MergeRequest", "Issue"],
- [Event::CREATED, Event::CLOSED, Event::MERGED])
+ where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
+ Event::PUSHED,
+ ["MergeRequest", "Issue"], [Event::CREATED, Event::CLOSED, Event::MERGED],
+ "Note", Event::COMMENTED)
end
def limit_recent(limit = 20, offset = nil)
diff --git a/app/models/group.rb b/app/models/group.rb
index 4cdfd022094..240a17f1dc1 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -81,7 +81,7 @@ class Group < Namespace
end
def to_reference(_from_project = nil, full: nil)
- "#{self.class.reference_prefix}#{name}"
+ "#{self.class.reference_prefix}#{full_path}"
end
def web_url
@@ -197,11 +197,16 @@ class Group < Namespace
end
def refresh_members_authorized_projects
- UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute
+ UserProjectAccessChangedService.new(user_ids_for_project_authorizations).
+ execute
+ end
+
+ def user_ids_for_project_authorizations
+ users_with_parents.pluck(:id)
end
def members_with_parents
- GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id))
+ GroupMember.non_request.where(source_id: ancestors.map(&:id).push(id))
end
def users_with_parents
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
index 7b6db2634b7..86d38e5468b 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/group_milestone.rb
@@ -9,7 +9,7 @@ class GroupMilestone < GlobalMilestone
def self.build(group, projects, title)
super(projects, title).tap do |milestone|
- milestone.group = group if milestone
+ milestone&.group = group
end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 26a6054e00d..d07f270b757 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -47,6 +47,7 @@ class Member < ActiveRecord::Base
scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where(invite_token: nil) }
scope :request, -> { where.not(requested_at: nil) }
+ scope :non_request, -> { where(requested_at: nil) }
scope :has_access, -> { active.where('access_level > 0') }
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 082adcafcc8..38646eba3ac 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -546,7 +546,7 @@ class MergeRequest < ActiveRecord::Base
# Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
- def cache_merge_request_closes_issues!(current_user = self.author)
+ def cache_merge_request_closes_issues!(current_user)
return if project.has_external_issue_tracker?
transaction do
@@ -558,14 +558,10 @@ class MergeRequest < ActiveRecord::Base
end
end
- def closes_issue?(issue)
- closes_issues.include?(issue)
- end
-
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
- messages = [description]
+ messages = [title, description]
messages.concat(commits.map(&:safe_message)) if merge_request_diff
Gitlab::ClosingIssueExtractor.new(project, current_user).
@@ -575,13 +571,13 @@ class MergeRequest < ActiveRecord::Base
end
end
- def issues_mentioned_but_not_closing(current_user = self.author)
+ def issues_mentioned_but_not_closing(current_user)
return [] unless target_branch == project.default_branch
ext = Gitlab::ReferenceExtractor.new(project, current_user)
- ext.analyze(description)
+ ext.analyze("#{title}\n#{description}")
- ext.issues - closes_issues
+ ext.issues - closes_issues(current_user)
end
def target_project_path
@@ -715,18 +711,22 @@ class MergeRequest < ActiveRecord::Base
!head_pipeline || head_pipeline.success? || head_pipeline.skipped?
end
- def environments
+ def environments_for(current_user)
return [] unless diff_head_commit
- @environments ||= begin
- target_envs = target_project.environments_for(
- target_branch, commit: diff_head_commit, with_tags: true)
+ @environments ||= Hash.new do |h, current_user|
+ envs = EnvironmentsFinder.new(target_project, current_user,
+ ref: target_branch, commit: diff_head_commit, with_tags: true).execute
- source_envs = source_project.environments_for(
- source_branch, commit: diff_head_commit) if source_project
+ if source_project
+ envs.concat EnvironmentsFinder.new(source_project, current_user,
+ ref: source_branch, commit: diff_head_commit).execute
+ end
- (target_envs.to_a + source_envs.to_a).uniq
+ h[current_user] = envs.uniq
end
+
+ @environments[current_user]
end
def state_human_name
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 2fb2eb44aaa..6de4d08fc28 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -7,6 +7,11 @@ class Namespace < ActiveRecord::Base
include Gitlab::CurrentSettings
include Routable
+ # Prevent users from creating unreasonably deep level of nesting.
+ # The number 20 was taken based on maximum nesting level of
+ # Android repo (15) + some extra backup.
+ NUMBER_OF_ANCESTORS_ALLOWED = 20
+
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy
@@ -29,6 +34,8 @@ class Namespace < ActiveRecord::Base
length: { maximum: 255 },
namespace: true
+ validate :nesting_level_allowed
+
delegate :name, to: :owner, allow_nil: true, prefix: true
after_update :move_dir, if: :path_changed?
@@ -170,31 +177,14 @@ class Namespace < ActiveRecord::Base
Gitlab.config.lfs.enabled
end
- def full_path
- if parent
- parent.full_path + '/' + path
- else
- path
- end
- end
-
def shared_runners_enabled?
projects.with_shared_runners.any?
end
- def full_name
- @full_name ||=
- if parent
- parent.full_name + ' / ' + name
- else
- name
- end
- end
-
# Scopes the model on ancestors of the record
def ancestors
if parent_id
- path = route.path
+ path = route ? route.path : full_path
paths = []
until path.blank?
@@ -213,6 +203,14 @@ class Namespace < ActiveRecord::Base
self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
end
+ def user_ids_for_project_authorizations
+ [owner_id]
+ end
+
+ def parent_changed?
+ parent_id_changed?
+ end
+
private
def repository_storage_paths
@@ -251,10 +249,6 @@ class Namespace < ActiveRecord::Base
find_each(&:refresh_members_authorized_projects)
end
- def full_path_changed?
- path_changed? || parent_id_changed?
- end
-
def remove_exports!
Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
end
@@ -270,4 +264,10 @@ class Namespace < ActiveRecord::Base
path_was
end
end
+
+ def nesting_level_allowed
+ if ancestors.count > Group::NUMBER_OF_ANCESTORS_ALLOWED
+ errors.add(:parent_id, "has too deep level of nesting")
+ end
+ end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index bf090a0438c..029fe667a45 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -108,6 +108,12 @@ class Note < ActiveRecord::Base
Discussion.for_diff_notes(active_notes).
map { |d| [d.line_code, d] }.to_h
end
+
+ def count_for_collection(ids, type)
+ user.select('noteable_id', 'COUNT(*) as count').
+ group(:noteable_id).
+ where(noteable_type: type, noteable_id: ids)
+ end
end
def cross_reference?
diff --git a/app/models/project.rb b/app/models/project.rb
index 7c5fdad5122..aa408b4556e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -228,7 +228,12 @@ class Project < ActiveRecord::Base
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
- scope :inside_path, ->(path) { joins(:route).where('routes.path LIKE ?', "#{path}/%") }
+ scope :inside_path, ->(path) do
+ # We need routes alias rs for JOIN so it does not conflict with
+ # includes(:route) which we use in ProjectsFinder.
+ joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'").
+ where('rs.path LIKE ?', "#{path}/%")
+ end
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
@@ -464,7 +469,7 @@ class Project < ActiveRecord::Base
def reset_cache_and_import_attrs
ProjectCacheWorker.perform_async(self.id)
- self.import_data.destroy if self.import_data
+ self.import_data&.destroy
end
def import_url=(value)
@@ -810,26 +815,6 @@ class Project < ActiveRecord::Base
end
end
- def name_with_namespace
- @name_with_namespace ||= begin
- if namespace
- namespace.human_name + ' / ' + name
- else
- name
- end
- end
- end
- alias_method :human_name, :name_with_namespace
-
- def full_path
- if namespace && path
- namespace.full_path + '/' + path
- else
- path
- end
- end
- alias_method :path_with_namespace, :full_path
-
def execute_hooks(data, hooks_scope = :push_hooks)
hooks.send(hooks_scope).each do |hook|
hook.async_execute(data, hooks_scope.to_s)
@@ -1306,30 +1291,40 @@ class Project < ActiveRecord::Base
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end
- def environments_for(ref, commit: nil, with_tags: false)
- deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?'
+ def route_map_for(commit_sha)
+ @route_maps_by_commit ||= Hash.new do |h, sha|
+ h[sha] = begin
+ data = repository.route_map_for(sha)
+ next unless data
+
+ Gitlab::RouteMap.new(data)
+ rescue Gitlab::RouteMap::FormatError
+ nil
+ end
+ end
- environment_ids = deployments
- .where(deployments_query, ref.to_s)
- .group(:environment_id)
- .select(:environment_id)
+ @route_maps_by_commit[commit_sha]
+ end
- environments_found = environments.available
- .where(id: environment_ids).to_a
+ def public_path_for_source_path(path, commit_sha)
+ map = route_map_for(commit_sha)
+ return unless map
- return environments_found unless commit
+ map.public_path_for_source_path(path)
+ end
- environments_found.select do |environment|
- environment.includes_commit?(commit)
- end
+ def parent
+ namespace
end
- def environments_recently_updated_on_branch(branch)
- environments_for(branch).select do |environment|
- environment.recently_updated_on_branch?(branch)
- end
+ def parent_changed?
+ namespace_id_changed?
end
+ alias_method :name_with_namespace, :full_name
+ alias_method :human_name, :full_name
+ alias_method :path_with_namespace, :full_path
+
private
def cross_namespace_reference?(from)
@@ -1368,10 +1363,6 @@ class Project < ActiveRecord::Base
raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
end
- def full_path_changed?
- path_changed? || namespace_id_changed?
- end
-
def update_project_statistics
stats = statistics || build_statistics
stats.update(namespace_id: namespace_id)
diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb
index 5eb1bd86e9d..8b5bc24fd3c 100644
--- a/app/models/project_services/chat_slash_commands_service.rb
+++ b/app/models/project_services/chat_slash_commands_service.rb
@@ -23,7 +23,7 @@ class ChatSlashCommandsService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' }
+ { type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' }
]
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 80d002f9c32..eef403dba92 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -250,21 +250,11 @@ class JiraService < IssueTrackerService
end
end
- # Build remote link on JIRA properties
- # Icons here must be available on WEB so JIRA can read the URL
- # We are using a open word graphics icon which have LGPL license
def build_remote_link_props(url:, title:, resolved: false)
status = {
resolved: resolved
}
- if resolved
- status[:icon] = {
- title: 'Closed',
- url16x16: 'http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png'
- }
- end
-
{
GlobalID: 'GitLab',
object: {
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index b0f7a42f9a3..56f42d63b2d 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -8,11 +8,11 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
end
def title
- 'Mattermost Command'
+ 'Mattermost slash commands'
end
def description
- "Perform common operations on GitLab in Mattermost"
+ "Perform common operations in Mattermost"
end
def self.to_param
diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb
index c34991e4262..2182c1c7e4b 100644
--- a/app/models/project_services/slack_slash_commands_service.rb
+++ b/app/models/project_services/slack_slash_commands_service.rb
@@ -2,11 +2,11 @@ class SlackSlashCommandsService < ChatSlashCommandsService
include TriggersHelper
def title
- 'Slack Command'
+ 'Slack slash commands'
end
def description
- "Perform common operations on GitLab in Slack"
+ "Perform common operations in Slack"
end
def self.to_param
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 7cf09c52bf4..56c582cd9be 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -464,6 +464,8 @@ class Repository
unless Gitlab::Git.blank_ref?(sha)
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
end
+ rescue Gitlab::Git::Repository::NoRepository
+ nil
end
def blob_by_oid(oid)
@@ -1160,6 +1162,14 @@ class Repository
end
end
+ def route_map_for(sha)
+ blob_data_at(sha, '.gitlab/route-map.yml')
+ end
+
+ def gitlab_ci_yml_for(sha)
+ blob_data_at(sha, '.gitlab-ci.yml')
+ end
+
protected
def tree_entry_at(branch_name, path)
@@ -1186,6 +1196,14 @@ class Repository
private
+ def blob_data_at(sha, path)
+ blob = blob_at(sha, path)
+ return unless blob
+
+ blob.load_all_data!(self)
+ blob.data
+ end
+
def git_action(index, action)
path = normalize_path(action[:file_path])
@@ -1212,6 +1230,14 @@ class Repository
action[:content]
end
+ detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
+
+ unless detect && detect[:type] == :binary
+ # When writing to the repo directly as we are doing here,
+ # the `core.autocrlf` config isn't taken into account.
+ content.gsub!("\r\n", "\n") if self.autocrlf
+ end
+
oid = rugged.write(content, :blob)
index.add(path: path, oid: oid, mode: mode)
diff --git a/app/models/route.rb b/app/models/route.rb
index dd171fdb069..73574a6206b 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -8,16 +8,27 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
- after_update :rename_descendants, if: :path_changed?
+ after_update :rename_descendants
def rename_descendants
- # We update each row separately because MySQL does not have regexp_replace.
- # rubocop:disable Rails/FindEach
- Route.where('path LIKE ?', "#{path_was}/%").each do |route|
- # Note that update column skips validation and callbacks.
- # We need this to avoid recursive call of rename_descendants method
- route.update_column(:path, route.path.sub(path_was, path))
+ if path_changed? || name_changed?
+ descendants = Route.where('path LIKE ?', "#{path_was}/%")
+
+ descendants.each do |route|
+ attributes = {}
+
+ if path_changed? && route.path.present?
+ attributes[:path] = route.path.sub(path_was, path)
+ end
+
+ if name_changed? && route.name.present?
+ attributes[:name] = route.name.sub(name_was, name)
+ end
+
+ # Note that update_columns skips validation and callbacks.
+ # We need this to avoid recursive call of rename_descendants method
+ route.update_columns(attributes) unless attributes.empty?
+ end
end
- # rubocop:enable Rails/FindEach
end
end
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index f768c4e3da5..e166cf69703 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -1,6 +1,22 @@
class Timelog < ActiveRecord::Base
validates :time_spent, :user, presence: true
+ validate :issuable_id_is_present
- belongs_to :trackable, polymorphic: true
+ belongs_to :issue
+ belongs_to :merge_request
belongs_to :user
+
+ def issuable
+ issue || merge_request
+ end
+
+ private
+
+ def issuable_id_is_present
+ if issue_id && merge_request_id
+ errors.add(:base, 'Only Issue ID or Merge Request ID is required')
+ elsif issuable.nil?
+ errors.add(:base, 'Issue or Merge Request ID is required')
+ end
+ end
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 2adf494ce11..3dda7948d0b 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -1,12 +1,13 @@
class Todo < ActiveRecord::Base
include Sortable
- ASSIGNED = 1
- MENTIONED = 2
- BUILD_FAILED = 3
- MARKED = 4
- APPROVAL_REQUIRED = 5 # This is an EE-only feature
- UNMERGEABLE = 6
+ ASSIGNED = 1
+ MENTIONED = 2
+ BUILD_FAILED = 3
+ MARKED = 4
+ APPROVAL_REQUIRED = 5 # This is an EE-only feature
+ UNMERGEABLE = 6
+ DIRECTLY_ADDRESSED = 7
ACTION_NAMES = {
ASSIGNED => :assigned,
@@ -14,7 +15,8 @@ class Todo < ActiveRecord::Base
BUILD_FAILED => :build_failed,
MARKED => :marked,
APPROVAL_REQUIRED => :approval_required,
- UNMERGEABLE => :unmergeable
+ UNMERGEABLE => :unmergeable,
+ DIRECTLY_ADDRESSED => :directly_addressed
}
belongs_to :author, class_name: "User"
diff --git a/app/models/user.rb b/app/models/user.rb
index 54f5388eb2c..ad997ce2b13 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -51,7 +51,12 @@ class User < ActiveRecord::Base
has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id
# Profile
- has_many :keys, dependent: :destroy
+ has_many :keys, -> do
+ type = Key.arel_table[:type]
+ where(type.not_eq('DeployKey').or(type.eq(nil)))
+ end, dependent: :destroy
+ has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy
+
has_many :emails, dependent: :destroy
has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
@@ -83,8 +88,6 @@ class User < ActiveRecord::Base
has_many :events, dependent: :destroy, foreign_key: :author_id
has_many :subscriptions, dependent: :destroy
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
- has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
- has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
has_one :abuse_report, dependent: :destroy
has_many :spam_logs, dependent: :destroy
@@ -94,6 +97,9 @@ class User < ActiveRecord::Base
has_many :notification_settings, dependent: :destroy
has_many :award_emoji, dependent: :destroy
+ has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
+ has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
+
#
# Validations
#
@@ -118,7 +124,7 @@ class User < ActiveRecord::Base
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create
- before_validation :signup_domain_valid?, on: :create
+ before_validation :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
@@ -166,6 +172,15 @@ class User < ActiveRecord::Base
def blocked?
true
end
+
+ def active_for_authentication?
+ false
+ end
+
+ def inactive_message
+ "Your account has been blocked. Please contact your GitLab " \
+ "administrator if you think this is an error."
+ end
end
end
@@ -304,7 +319,7 @@ class User < ActiveRecord::Base
def find_by_personal_access_token(token_string)
personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
- personal_access_token.user if personal_access_token
+ personal_access_token&.user
end
# Returns a user for the given SSH key.
@@ -320,7 +335,7 @@ class User < ActiveRecord::Base
def reference_pattern
%r{
#{Regexp.escape(reference_prefix)}
- (?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR})
+ (?<user>#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR})
}x
end
end
@@ -903,6 +918,21 @@ class User < ActiveRecord::Base
end
end
+ def access_level
+ if admin?
+ :admin
+ else
+ :regular
+ end
+ end
+
+ def access_level=(new_level)
+ new_level = new_level.to_s
+ return unless %w(admin regular).include?(new_level)
+
+ self.admin = (new_level == 'admin')
+ end
+
private
def ci_projects_union
diff --git a/app/models/wiki_directory.rb b/app/models/wiki_directory.rb
new file mode 100644
index 00000000000..9340fc2dbbe
--- /dev/null
+++ b/app/models/wiki_directory.rb
@@ -0,0 +1,18 @@
+class WikiDirectory
+ include ActiveModel::Validations
+
+ attr_accessor :slug, :pages
+
+ validates :slug, presence: true
+
+ def initialize(slug, pages = [])
+ @slug = slug
+ @pages = pages
+ end
+
+ # Relative path to the partial to be used when rendering collections
+ # of this object.
+ def to_partial_path
+ 'projects/wikis/wiki_directory'
+ end
+end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index c3de278f5b7..2caebb496db 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -12,6 +12,32 @@ class WikiPage
ActiveModel::Name.new(self, nil, 'wiki')
end
+ # Sorts and groups pages by directory.
+ #
+ # pages - an array of WikiPage objects.
+ #
+ # Returns an array of WikiPage and WikiDirectory objects. The entries are
+ # sorted by alphabetical order (directories and pages inside each directory).
+ # Pages at the root level come before everything.
+ def self.group_by_directory(pages)
+ return [] if pages.blank?
+
+ pages.sort_by { |page| [page.directory, page.slug] }.
+ group_by(&:directory).
+ map do |dir, pages|
+ if dir.present?
+ WikiDirectory.new(dir, pages)
+ else
+ pages
+ end
+ end.
+ flatten
+ end
+
+ def self.unhyphenize(name)
+ name.gsub(/-+/, ' ')
+ end
+
def to_key
[:slug]
end
@@ -56,7 +82,7 @@ class WikiPage
# The formatted title of this page.
def title
if @attributes[:title]
- @attributes[:title].gsub(/-+/, ' ')
+ self.class.unhyphenize(@attributes[:title])
else
""
end
@@ -69,16 +95,17 @@ class WikiPage
# The raw content of this page.
def content
- @attributes[:content] ||= if @page
- @page.text_data
- end
+ @attributes[:content] ||= @page&.text_data
+ end
+
+ # The hierarchy of the directory this page is contained in.
+ def directory
+ wiki.page_title_and_dir(slug).last
end
# The processed/formatted content of this page.
def formatted_content
- @attributes[:formatted_content] ||= if @page
- @page.formatted_data
- end
+ @attributes[:formatted_content] ||= @page&.formatted_data
end
# The markup format for the page.
@@ -174,6 +201,16 @@ class WikiPage
end
end
+ # Relative path to the partial to be used when rendering collections
+ # of this object.
+ def to_partial_path
+ 'projects/wikis/wiki_page'
+ end
+
+ def id
+ page.version.to_s
+ end
+
private
def set_attributes
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index f5fd50745aa..f8594e29547 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -218,25 +218,7 @@ class ProjectPolicy < BasePolicy
def anonymous_rules
return unless project.public?
- can! :read_project
- can! :read_board
- can! :read_list
- can! :read_wiki
- can! :read_label
- can! :read_milestone
- can! :read_project_snippet
- can! :read_project_member
- can! :read_merge_request
- can! :read_note
- can! :read_pipeline
- can! :read_commit_status
- can! :read_container_image
- can! :download_code
- can! :download_wiki_code
- can! :read_cycle_analytics
-
- # NOTE: may be overridden by IssuePolicy
- can! :read_issue
+ base_readonly_access!
# Allow to read builds by anonymous user if guests are allowed
can! :read_build if project.public_builds?
@@ -269,4 +251,31 @@ class ProjectPolicy < BasePolicy
:"admin_#{name}"
]
end
+
+ private
+
+ # A base set of abilities for read-only users, which
+ # is then augmented as necessary for anonymous and other
+ # read-only users.
+ def base_readonly_access!
+ can! :read_project
+ can! :read_board
+ can! :read_list
+ can! :read_wiki
+ can! :read_label
+ can! :read_milestone
+ can! :read_project_snippet
+ can! :read_project_member
+ can! :read_merge_request
+ can! :read_note
+ can! :read_pipeline
+ can! :read_commit_status
+ can! :read_container_image
+ can! :download_code
+ can! :download_wiki_code
+ can! :read_cycle_analytics
+
+ # NOTE: may be overridden by IssuePolicy
+ can! :read_issue
+ end
end
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index 57acccfafd9..3a96836917e 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -3,7 +3,7 @@ class ProjectSnippetPolicy < BasePolicy
can! :read_project_snippet if @subject.public?
return unless @user
- if @user && @subject.author == @user || @user.admin?
+ if @user && (@subject.author == @user || @user.admin?)
can! :read_project_snippet
can! :update_project_snippet
can! :admin_project_snippet
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 5d15eb8d3d3..4c017960628 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity
expose :external_url
expose :environment_type
expose :last_deployment, using: DeploymentEntity
- expose :stoppable?
+ expose :stop_action?
expose :environment_path do |environment|
namespace_project_environment_path(
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index 91955542f25..fe16a3784c4 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -1,3 +1,50 @@
class EnvironmentSerializer < BaseSerializer
+ Item = Struct.new(:name, :size, :latest)
+
entity EnvironmentEntity
+
+ def within_folders
+ tap { @itemize = true }
+ end
+
+ def with_pagination(request, response)
+ tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
+ end
+
+ def itemized?
+ @itemize
+ end
+
+ def paginated?
+ @paginator.present?
+ end
+
+ def represent(resource, opts = {})
+ resource = @paginator.paginate(resource) if paginated?
+
+ if itemized?
+ itemize(resource).map do |item|
+ { name: item.name,
+ size: item.size,
+ latest: super(item.latest, opts) }
+ end
+ else
+ super(resource, opts)
+ end
+ end
+
+ private
+
+ def itemize(resource)
+ items = resource.group(:item_name).order('item_name ASC')
+ .pluck('COALESCE(environment_type, name) AS item_name',
+ 'COUNT(*) AS environments_count',
+ 'MAX(id) AS last_environment_id')
+
+ environments = resource.where(id: items.map(&:last)).index_by(&:id)
+
+ items.map do |name, size, id|
+ Item.new(name, size, environments[id])
+ end
+ end
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index b2de6c5832e..2bc6cf3266e 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -1,41 +1,25 @@
class PipelineSerializer < BaseSerializer
class InvalidResourceError < StandardError; end
- include API::Helpers::Pagination
- Struct.new('Pagination', :request, :response)
entity PipelineEntity
- def represent(resource, opts = {})
- if paginated?
- raise InvalidResourceError unless resource.respond_to?(:page)
-
- super(paginate(resource.includes(project: :namespace)), opts)
- else
- super(resource, opts)
- end
- end
-
- def paginated?
- defined?(@pagination)
- end
-
def with_pagination(request, response)
- tap { @pagination = Struct::Pagination.new(request, response) }
+ tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
end
- private
-
- # Methods needed by `API::Helpers::Pagination`
- #
- def params
- @pagination.request.query_parameters
+ def paginated?
+ @paginator.present?
end
- def request
- @pagination.request
- end
+ def represent(resource, opts = {})
+ if resource.is_a?(ActiveRecord::Relation)
+ resource = resource.includes(project: :namespace)
+ end
- def header(header, value)
- @pagination.response.headers[header] = value
+ if paginated?
+ super(@paginator.paginate(resource), opts)
+ else
+ super(resource, opts)
+ end
end
end
diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb
index cf590459cb2..42c72aba7dd 100644
--- a/app/services/ci/stop_environments_service.rb
+++ b/app/services/ci/stop_environments_service.rb
@@ -8,10 +8,9 @@ module Ci
return unless has_ref?
environments.each do |environment|
- next unless environment.stoppable?
next unless can?(current_user, :create_deployment, project)
- environment.stop!(current_user)
+ environment.stop_with_action!(current_user)
end
end
@@ -22,8 +21,8 @@ module Ci
end
def environments
- @environments ||= project
- .environments_recently_updated_on_branch(@ref)
+ @environments ||=
+ EnvironmentsFinder.new(project, current_user, ref: @ref, recently_updated: true).execute
end
end
end
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
index fe9353afeb8..6c75d1f04ff 100644
--- a/app/services/create_tag_service.rb
+++ b/app/services/create_tag_service.rb
@@ -4,7 +4,7 @@ class CreateTagService < BaseService
return error('Tag name invalid') unless valid_tag
repository = project.repository
- message.strip! if message
+ message&.strip!
new_tag = nil
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index 9d4bffb93e9..eb726cb04b1 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -9,7 +9,7 @@ class DeleteTagService < BaseService
if repository.rm_tag(current_user, tag_name)
release = project.releases.find_by(tag: tag_name)
- release.destroy if release
+ release&.destroy
push_data = build_push_data(tag)
EventCreateService.new.push(project, current_user, push_data)
diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb
deleted file mode 100644
index eaff88d6463..00000000000
--- a/app/services/delete_user_service.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-class DeleteUserService
- attr_accessor :current_user
-
- def initialize(current_user)
- @current_user = current_user
- end
-
- 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'
- return user
- end
-
- user.solo_owned_groups.each do |group|
- DestroyGroupService.new(group, current_user).execute
- end
-
- 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).async_execute
- end
-
- # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
- namespace = user.namespace
- user_data = user.destroy
- namespace.really_destroy!
-
- user_data
- end
-end
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
deleted file mode 100644
index 2316c57bf1e..00000000000
--- a/app/services/destroy_group_service.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-class DestroyGroupService
- attr_accessor :group, :current_user
-
- def initialize(group, user)
- @group, @current_user = group, user
- end
-
- def async_execute
- # Soft delete via paranoia gem
- group.destroy
- job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
- Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
- end
-
- def execute
- group.projects.each do |project|
- # Execute the destruction of the models immediately to ensure atomic cleanup.
- # Skip repository removal because we remove directory with namespace
- # that contain all these repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
- end
-
- group.children.each do |group|
- DestroyGroupService.new(group, current_user).async_execute
- end
-
- group.really_destroy!
- end
-end
diff --git a/app/services/files/delete_service.rb b/app/services/files/destroy_service.rb
index 50f0ffcac9f..c3be806a42d 100644
--- a/app/services/files/delete_service.rb
+++ b/app/services/files/destroy_service.rb
@@ -1,5 +1,5 @@
module Files
- class DeleteService < Files::BaseService
+ class DestroyService < Files::BaseService
def commit
repository.remove_file(
current_user,
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
new file mode 100644
index 00000000000..7f2d28086f5
--- /dev/null
+++ b/app/services/groups/destroy_service.rb
@@ -0,0 +1,25 @@
+module Groups
+ class DestroyService < Groups::BaseService
+ def async_execute
+ # Soft delete via paranoia gem
+ group.destroy
+ job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
+ Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
+ end
+
+ def execute
+ group.projects.each do |project|
+ # Execute the destruction of the models immediately to ensure atomic cleanup.
+ # Skip repository removal because we remove directory with namespace
+ # that contain all these repositories
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
+ end
+
+ group.children.each do |group|
+ DestroyService.new(group, current_user).async_execute
+ end
+
+ group.really_destroy!
+ end
+ end
+end
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index a63982f60c8..7cd927d8005 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -44,7 +44,15 @@ module Issues
end
def issue_params
- @issue_params ||= issue_params_with_info_from_merge_request.merge(params.slice(:title, :description))
+ @issue_params ||= issue_params_with_info_from_merge_request.merge(whitelisted_issue_params)
+ end
+
+ def whitelisted_issue_params
+ if can?(current_user, :admin_issue, project)
+ params.slice(:title, :description, :milestone_id)
+ else
+ params.slice(:title, :description)
+ end
end
end
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index d2eb46ac41b..961605a1005 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -3,6 +3,8 @@ module Issues
def execute
@request = params.delete(:request)
@api = params.delete(:api)
+ @recaptcha_verified = params.delete(:recaptcha_verified)
+ @spam_log_id = params.delete(:spam_log_id)
issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
@issue = BuildService.new(project, current_user, issue_attributes).execute
@@ -11,7 +13,13 @@ module Issues
end
def before_create(issuable)
- issuable.spam = spam_service.check(@api)
+ if @recaptcha_verified
+ spam_log = current_user.spam_logs.find_by(id: @spam_log_id, title: issuable.title)
+ spam_log&.update!(recaptcha_verified: true)
+ else
+ issuable.spam = spam_service.check(@api)
+ issuable.spam_log = spam_service.spam_log
+ end
end
def after_create(issuable)
@@ -35,7 +43,7 @@ module Issues
private
def spam_service
- SpamService.new(@issue, @request)
+ @spam_service ||= SpamService.new(@issue, @request)
end
def user_agent_detail_service
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index b4bfb0e5e8c..581d18032e6 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -144,7 +144,11 @@ module MergeRequests
return unless @commits.present?
merge_requests_for_source_branch.each do |merge_request|
- wip_commit = @commits.detect(&:work_in_progress?)
+ commit_shas = merge_request.commits_sha
+
+ wip_commit = @commits.detect do |commit|
+ commit.work_in_progress? && commit_shas.include?(commit.sha)
+ end
if wip_commit && !merge_request.work_in_progress?
merge_request.update(title: merge_request.wip_title)
diff --git a/app/services/notes/delete_service.rb b/app/services/notes/destroy_service.rb
index a673e8e9dde..b819bd17039 100644
--- a/app/services/notes/delete_service.rb
+++ b/app/services/notes/destroy_service.rb
@@ -1,5 +1,5 @@
module Notes
- class DeleteService < BaseService
+ class DestroyService < BaseService
def execute(note)
note.destroy
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index b2cc39763f3..3734e3c4253 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -217,7 +217,7 @@ class NotificationService
recipients = reject_unsubscribed_users(recipients, note.noteable)
recipients = reject_users_without_access(recipients, note.noteable)
- recipients.delete(note.author)
+ recipients.delete(note.author) unless note.author.notified_of_own_activity?
recipients = recipients.uniq
notify_method = "note_#{note.to_ability_name}_email".to_sym
@@ -327,8 +327,9 @@ class NotificationService
recipients ||= build_recipients(
pipeline,
pipeline.project,
- nil, # The acting user, who won't be added to recipients
- action: pipeline.status).map(&:notification_email)
+ pipeline.user,
+ action: pipeline.status,
+ skip_current_user: false).map(&:notification_email)
if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later
@@ -627,7 +628,7 @@ class NotificationService
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
- recipients.delete(current_user) if skip_current_user
+ recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity?
recipients.uniq
end
@@ -636,7 +637,7 @@ class NotificationService
recipients = add_labels_subscribers([], project, target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
- recipients.delete(current_user)
+ recipients.delete(current_user) unless current_user.notified_of_own_activity?
recipients.uniq
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index c7cce0c55b9..6dc3d8c2416 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -97,7 +97,7 @@ module Projects
@project.team << [current_user, :master, current_user]
end
- @project.group.refresh_members_authorized_projects if @project.group
+ @project.group&.refresh_members_authorized_projects
end
def skip_wiki?
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 06252c7b625..535da706159 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -26,7 +26,7 @@ module Projects
end
def project_tree_saver
- Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared)
+ Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared)
end
def uploads_saver
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index 96c363c8d1a..e6193fcacee 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -36,7 +36,7 @@ module Projects
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
- { username: group.path, name: group.name, count: count, avatar_url: group.avatar_url }
+ { username: group.full_path, name: group.full_name, count: count, avatar_url: group.avatar_url }
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 20b049b5973..484700c8c29 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -25,9 +25,10 @@ module Projects
end
def transfer(project, new_namespace)
+ old_namespace = project.namespace
+
Project.transaction do
old_path = project.path_with_namespace
- old_namespace = project.namespace
old_group = project.group
new_path = File.join(new_namespace.try(:path) || '', project.path)
@@ -70,8 +71,11 @@ module Projects
project.old_path_with_namespace = old_path
SystemHooksService.new.execute_hooks_for(project, :transfer)
- true
end
+
+ refresh_permissions(old_namespace, new_namespace)
+
+ true
end
def allowed_transfer?(current_user, project, namespace)
@@ -80,5 +84,14 @@ module Projects
namespace.id != project.namespace_id &&
current_user.can?(:create_projects, namespace)
end
+
+ def refresh_permissions(old_namespace, new_namespace)
+ # This ensures we only schedule 1 job for every user that has access to
+ # the namespaces.
+ user_ids = old_namespace.user_ids_for_project_authorizations |
+ new_namespace.user_ids_for_project_authorizations
+
+ UserProjectAccessChangedService.new(user_ids).execute
+ end
end
end
diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb
index 48903291799..024a7c19d33 100644
--- a/app/services/spam_service.rb
+++ b/app/services/spam_service.rb
@@ -1,5 +1,6 @@
class SpamService
attr_accessor :spammable, :request, :options
+ attr_reader :spam_log
def initialize(spammable, request = nil)
@spammable = spammable
@@ -63,7 +64,7 @@ class SpamService
end
def create_spam_log(api)
- SpamLog.create(
+ @spam_log = SpamLog.create!(
{
user_id: spammable_owner_id,
title: spammable.spam_title,
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 110072e3a16..87ba72cf991 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -385,6 +385,7 @@ module SystemNoteService
# Returns Boolean
def cross_reference_disallowed?(noteable, mentioner)
return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
+ return true if noteable.is_a?(Issuable) && (noteable.try(:closed?) || noteable.try(:merged?))
return false unless mentioner.is_a?(MergeRequest)
return false unless noteable.is_a?(Commit)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 1bd6ce416ab..8ab943f4639 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -243,6 +243,12 @@ class TodoService
end
def create_mention_todos(project, target, author, note = nil)
+ # Create Todos for directly addressed users
+ directly_addressed_users = filter_directly_addressed_users(project, note || target, author)
+ attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
+ create_todos(directly_addressed_users, attributes)
+
+ # Create Todos for mentioned users
mentioned_users = filter_mentioned_users(project, note || target, author)
attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
create_todos(mentioned_users, attributes)
@@ -282,10 +288,18 @@ class TodoService
)
end
+ def filter_todo_users(users, project, target)
+ reject_users_without_access(users, project, target).uniq
+ end
+
def filter_mentioned_users(project, target, author)
mentioned_users = target.mentioned_users(author)
- mentioned_users = reject_users_without_access(mentioned_users, project, target)
- mentioned_users.uniq
+ filter_todo_users(mentioned_users, project, target)
+ end
+
+ def filter_directly_addressed_users(project, target, author)
+ directly_addressed_users = target.directly_addressed_users(author)
+ filter_todo_users(directly_addressed_users, project, target)
end
def reject_users_without_access(users, project, target)
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
new file mode 100644
index 00000000000..2d11305be13
--- /dev/null
+++ b/app/services/users/destroy_service.rb
@@ -0,0 +1,33 @@
+module Users
+ class DestroyService
+ attr_accessor :current_user
+
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ 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'
+ return user
+ end
+
+ user.solo_owned_groups.each do |group|
+ Groups::DestroyService.new(group, current_user).execute
+ end
+
+ 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).async_execute
+ end
+
+ # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
+ namespace = user.namespace
+ user_data = user.destroy
+ namespace.really_destroy!
+
+ user_data
+ end
+ end
+end
diff --git a/app/services/wiki_pages/destroy_service.rb b/app/services/wiki_pages/destroy_service.rb
new file mode 100644
index 00000000000..6b93fb2f6d7
--- /dev/null
+++ b/app/services/wiki_pages/destroy_service.rb
@@ -0,0 +1,11 @@
+module WikiPages
+ class DestroyService < WikiPages::BaseService
+ def execute(page)
+ if page&.delete
+ execute_hooks(page, 'delete')
+ end
+
+ page
+ end
+ end
+end
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 0a954c20fcd..13d00dd1fcb 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -18,7 +18,7 @@
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
id: klass::file_name_noext }
.file-holder#README
- .file-title
+ .js-file-title.file-title
%i.fa.fa-file
= klass::file_name
.pull-right
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
index 4ce4eab8753..33f6d847782 100644
--- a/app/views/admin/spam_logs/_spam_log.html.haml
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -14,6 +14,8 @@
%td
= spam_log.via_api? ? 'Y' : 'N'
%td
+ = spam_log.recaptcha_verified ? 'Y' : 'N'
+ %td
= spam_log.noteable_type
%td
= spam_log.title
diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml
index 0fdd5bd9960..8aaa6379730 100644
--- a/app/views/admin/spam_logs/index.html.haml
+++ b/app/views/admin/spam_logs/index.html.haml
@@ -10,6 +10,7 @@
%th User
%th Source IP
%th API?
+ %th Recaptcha verified?
%th Type
%th Title
%th Description
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
new file mode 100644
index 00000000000..7855239dfe5
--- /dev/null
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -0,0 +1,37 @@
+%fieldset
+ %legend Access
+ .form-group
+ = f.label :projects_limit, class: 'control-label'
+ .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control'
+
+ .form-group
+ = f.label :can_create_group, class: 'control-label'
+ .col-sm-10= f.check_box :can_create_group
+
+ .form-group
+ = f.label :access_level, class: 'control-label'
+ .col-sm-10
+ - editing_current_user = (current_user == @user)
+
+ = f.radio_button :access_level, :regular, disabled: editing_current_user
+ = label_tag :regular do
+ Regular
+ %p.light
+ Regular users have access to their groups and projects
+
+ = f.radio_button :access_level, :admin, disabled: editing_current_user
+ = label_tag :admin do
+ Admin
+ %p.light
+ Administrators have access to all groups, projects and users and can manage all features in this installation
+ - if editing_current_user
+ %p.light
+ You cannot remove your own admin rights.
+
+ .form-group
+ = f.label :external, class: 'control-label'
+ .col-sm-10
+ = f.check_box :external do
+ External
+ %p.light
+ External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups.
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 3145212728f..e911af3f6f9 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -40,28 +40,7 @@
= f.label :password_confirmation, class: 'control-label'
.col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control'
- %fieldset
- %legend Access
- .form-group
- = f.label :projects_limit, class: 'control-label'
- .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control'
-
- .form-group
- = f.label :can_create_group, class: 'control-label'
- .col-sm-10= f.check_box :can_create_group
-
- .form-group
- = 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.
- - 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.
+ = render partial: 'access_levels', locals: { f: f }
%fieldset
%legend Profile
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index b0bee1c6204..dfbc7772698 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -11,7 +11,7 @@
.form-group
.col-sm-12
.file-holder
- .file-title.clearfix
+ .js-file-title.file-title.clearfix
Content of .gitlab-ci.yml
#ci-editor.ci-editor= @content
= text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index 02b94beee92..68a46f61eb7 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,7 +1,8 @@
-%ul.nav-links
- %li{ class: ("active" unless params[:filter]) }>
- = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
- Your Projects
- %li{ class: ("active" if params[:filter] == 'starred') }>
- = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
- Starred Projects
+.top-area
+ %ul.nav-links
+ %li{ class: ("active" unless params[:filter]) }>
+ = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
+ Your Projects
+ %li{ class: ("active" if params[:filter] == 'starred') }>
+ = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
+ Starred Projects
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 01ecf237925..5a44ec45b7b 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -23,7 +23,7 @@
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
%p.gl-field-hint Minimum length is #{@minimum_password_length} characters
%div
- - if current_application_settings.recaptcha_enabled
+ - if Gitlab::Recaptcha.enabled?
= recaptcha_tags
%div
= f.submit "Register", class: "btn-register btn"
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 3a95a652810..94408b92374 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -2,7 +2,7 @@
- blob = discussion.blob
.diff-file.file-holder
- .file-title
+ .js-file-title.file-title
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_diff_path(discussion)
.diff-content.code.js-syntax-highlight
diff --git a/app/views/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml
index f0b61e0f7de..e30ee1b0e05 100644
--- a/app/views/discussions/_resolve_all.html.haml
+++ b/app/views/discussions/_resolve_all.html.haml
@@ -1,6 +1,5 @@
- if discussion.for_merge_request?
- %resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'",
- ":discussion-id" => "'#{discussion.id}'",
+ %resolve-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
":merge-request-id" => discussion.noteable.iid,
":can-resolve" => discussion.can_resolve?(current_user),
"inline-template" => true }
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
new file mode 100644
index 00000000000..41f54f6bf42
--- /dev/null
+++ b/app/views/groups/_home_panel.html.haml
@@ -0,0 +1,17 @@
+.group-home-panel.text-center
+ %div{ class: container_class }
+ .avatar-container.s70.group-avatar
+ = image_tag group_icon(@group), class: "avatar s70 avatar-tile"
+ %h1.group-title
+ @#{@group.path}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
+ = visibility_level_icon(@group.visibility_level, fw: false)
+
+ - if @group.description.present?
+ .group-home-desc
+ = markdown_field(@group, :description)
+
+ - if current_user
+ .group-buttons
+ = render 'shared/members/access_request_buttons', source: @group
+ = render 'shared/notifications/button', notification_setting: @notification_setting
diff --git a/app/views/groups/_show_nav.html.haml b/app/views/groups/_show_nav.html.haml
new file mode 100644
index 00000000000..b2097e88741
--- /dev/null
+++ b/app/views/groups/_show_nav.html.haml
@@ -0,0 +1,7 @@
+%ul.nav-links
+ = nav_link(page: group_path(@group)) do
+ = link_to group_path(@group) do
+ Projects
+ = nav_link(page: subgroups_group_path(@group)) do
+ = link_to subgroups_group_path(@group) do
+ Subgroups
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index fb6f0da28f8..e66a8e0a3b3 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,4 +1,8 @@
= render "header_title"
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
+
= 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/show.html.haml b/app/views/groups/show.html.haml
index d256d14609e..b040f404ac4 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -4,38 +4,12 @@
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
-.group-home-panel.text-center
- %div{ class: container_class }
- .avatar-container.s70.group-avatar
- = image_tag group_icon(@group), class: "avatar s70 avatar-tile"
- %h1.group-title
- @#{@group.path}
- %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
- = visibility_level_icon(@group.visibility_level, fw: false)
+= render 'groups/home_panel'
- - if @group.description.present?
- .group-home-desc
- = markdown_field(@group, :description)
-
- - if current_user
- .group-buttons
- = render 'shared/members/access_request_buttons', source: @group
- = render 'shared/notifications/button', notification_setting: @notification_setting
.groups-header{ class: container_class }
.top-area
- %ul.nav-links
- %li.active
- = link_to "#projects", 'data-toggle' => 'tab' do
- All Projects
- - if @shared_projects.present?
- %li
- = link_to "#shared", 'data-toggle' => 'tab' do
- Shared Projects
- - if @nested_groups.present?
- %li
- = link_to "#groups", 'data-toggle' => 'tab' do
- Subgroups
+ = render 'groups/show_nav'
.nav-controls
= form_tag request.path, 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
@@ -44,15 +18,4 @@
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
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
-
- - if @nested_groups.present?
- .tab-pane#groups
- %ul.content-list
- = render partial: 'shared/groups/group', collection: @nested_groups
+ = render "projects", projects: @projects
diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml
new file mode 100644
index 00000000000..8610ae7e0ef
--- /dev/null
+++ b/app/views/groups/subgroups.html.haml
@@ -0,0 +1,20 @@
+- @no_container = true
+
+= render 'groups/home_panel'
+
+.groups-header{ class: container_class }
+ .top-area
+ = render 'groups/show_nav'
+ .nav-controls
+ = form_tag request.path, method: :get do |f|
+ = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name', class: 'form-control', spellcheck: false
+ - if can? current_user, :admin_group, @group
+ = link_to new_group_path(parent_id: @group.id), class: 'btn btn-new pull-right' do
+ New Subgroup
+
+ - if @nested_groups.present?
+ %ul.content-list
+ = render partial: 'shared/groups/group', collection: @nested_groups, locals: { full_name: false }
+ - else
+ .nothing-here-block
+ There are no subgroups to show.
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index da2df0d8080..705e20112fa 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -79,6 +79,14 @@
%td.shortcut
.key esc
%td Go back
+ %tbody
+ %tr
+ %th
+ %th Project File
+ %tr
+ %td.shortcut
+ .key y
+ %td Go to file permalink
.col-lg-4
%table.shortcut-mappings
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index dd1df46792b..87f9b503989 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -528,7 +528,7 @@
- blob = Snippet.new(content: "Wow\nSuch\nFile")
.example
.file-holder
- .file-title
+ .js-file-title.file-title
Awesome file
.file-actions
.btn-group
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 9ecc0d11c95..59082ce5fd5 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,4 +1,4 @@
-%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
+%header.navbar.navbar-gitlab{ class: nav_header_class }
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index d6c158b6de3..665725f6862 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -18,20 +18,8 @@
Protected Branches
- if @project.feature_available?(:builds, current_user)
- = nav_link(controller: :runners) do
- = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
- %span
- Runners
- = nav_link(controller: :variables) do
- = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do
- %span
- Variables
- = nav_link(controller: :triggers) do
- = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
- %span
- Triggers
- = nav_link(controller: :pipelines_settings) do
- = link_to namespace_project_pipelines_settings_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
+ = nav_link(controller: :ci_cd) do
+ = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
%span
CI/CD Pipelines
= nav_link(controller: :pages) do
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 5c5e5940365..51c4e8e5a73 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -34,6 +34,11 @@
.clearfix
+ = form_for @user, url: profile_notifications_path, method: :put do |f|
+ %label{ for: 'user_notified_of_own_activity' }
+ = f.check_box :notified_of_own_activity
+ %span Receive notifications about your own activity
+
%hr
%h5
Groups (#{@group_notifications.count})
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 60a561c9f9c..2c006e1712d 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -85,11 +85,17 @@
:javascript
- var date = $('#personal_access_token_expires_at').val();
-
- var datepicker = $(".datepicker").datepicker({
- dateFormat: "yy-mm-dd",
- minDate: 0
+ var $dateField = $('#personal_access_token_expires_at');
+ var date = $dateField.val();
+
+ new Pikaday({
+ field: $dateField.get(0),
+ theme: 'gitlab-theme',
+ format: 'YYYY-MM-DD',
+ minDate: new Date(),
+ onSelect: function(dateText) {
+ $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
+ }
});
$("#created-personal-access-token").click(function() {
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 1c3bccccb5c..a08436715d2 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -10,6 +10,7 @@
- if @project && event.project != @project
%span at
%strong= link_to_project event.project
+ = clipboard_button(clipboard_text: event.ref_name, class: 'btn-clipboard btn-transparent', title: 'Copy branch to clipboard')
#{time_ago_with_tooltip(event.created_at)}
.pull-right
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 23f54553014..8a40281e28c 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -7,7 +7,7 @@
#blob-content-holder.tree-holder
.file-holder
- .file-title
+ .js-file-title.file-title
= blob_icon @blob.mode, @blob.name
%strong
= @path
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index ff893ea74e1..7b9cfbbd067 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -1,3 +1,6 @@
+.btn-group
+ = view_on_environment_button(@commit.sha, @path, @environment) if @environment
+
.btn-group.tree-btn-group
= link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id),
class: 'btn btn-sm', target: '_blank'
@@ -12,7 +15,7 @@
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
class: 'btn btn-sm'
= link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
- tree_join(@commit.sha, @path)), class: 'btn btn-sm'
+ tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
- if current_user
.btn-group{ role: "group" }
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index f75f438ee4f..19fa4c78501 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -24,7 +24,7 @@
#blob-content-holder.blob-content-holder
%article.file-holder
- .file-title
+ .js-file-title.file-title
= blob_icon blob.mode, blob.name
%strong
= blob.name
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 228ac61fc8c..e7adef5558a 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -1,5 +1,5 @@
.file-holder.file.append-bottom-default
- .file-title.clearfix
+ .js-file-title.file-title.clearfix
.editor-ref
= icon('code-fork')
= ref
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index 05fe504d1c9..f5ca9607823 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -4,7 +4,7 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('boards')
- = page_specific_javascript_bundle_tag('boards_test') if Rails.env.test?
+ = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 56fc5f5e68b..78720d88e4e 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -1,6 +1,6 @@
- builds = @build.pipeline.builds.to_a
-%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
+%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } }
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
Job
%strong ##{@build.id}
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index cdab1e1b1a6..f852f2e3fd7 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -15,7 +15,7 @@
- else
%span.api.monospace API
- if pipeline.latest?
- %span.label.label-success.has-tooltip{ title: 'Latest job for this branch' } latest
+ %span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest
- if pipeline.triggered?
%span.label.label-primary triggered
- if pipeline.yaml_errors.present?
@@ -40,25 +40,8 @@
- else
Cant find HEAD commit for this branch
- %td.stage-cell
- - pipeline.stages.each do |stage|
- - if stage.status
- - detailed_status = stage.detailed_status(current_user)
- - icon_status = "#{detailed_status.icon}_borderless"
- - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
-
- .stage-container.dropdown.js-mini-pipeline-graph
- %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
- = custom_icon(icon_status)
- = icon('caret-down')
-
- %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
- .arrow-up
- .js-builds-dropdown-list.scrollable-menu
-
- .js-builds-dropdown-loading.builds-dropdown-loading.hidden
- %span.fa.fa-spinner.fa-spin
-
+ %td
+ = render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph'
%td
- if pipeline.duration
@@ -78,7 +61,7 @@
.btn-group.inline
- if actions.any?
.btn-group
- %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual job', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual job' }
+ %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' }
= custom_icon('icon_play')
= icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 1164627fa11..aae2cb8a04b 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -1,15 +1,25 @@
-%div
- - if pipelines.blank?
- %div
- .nothing-here-block No pipelines to show
- - else
- .table-holder.pipelines
- %table.table.ci-table.js-pipeline-table
- %thead
- %th.pipeline-status Status
- %th.pipeline-info Pipeline
- %th.pipeline-commit Commit
- %th.pipeline-stages Stages
- %th.pipeline-date
- %th.pipeline-actions
- = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false
+#commit-pipeline-table-view{ data: { endpoint: endpoint } }
+.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"),
+ "icon_status_canceled" => custom_icon("icon_status_canceled"),
+ "icon_status_running" => custom_icon("icon_status_running"),
+ "icon_status_skipped" => custom_icon("icon_status_skipped"),
+ "icon_status_created" => custom_icon("icon_status_created"),
+ "icon_status_pending" => custom_icon("icon_status_pending"),
+ "icon_status_success" => custom_icon("icon_status_success"),
+ "icon_status_failed" => custom_icon("icon_status_failed"),
+ "icon_status_warning" => custom_icon("icon_status_warning"),
+ "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
+ "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
+ "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
+ "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
+ "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
+ "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
+ "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
+ "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
+ "icon_play" => custom_icon("icon_play"),
+ "icon_timer" => custom_icon("icon_timer"),
+ "icon_status_manual" => custom_icon("icon_status_manual"),
+} }
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('commit_pipelines')
diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml
index 89968cf4e0d..ac93eac41ac 100644
--- a/app/views/projects/commit/pipelines.html.haml
+++ b/app/views/projects/commit/pipelines.html.haml
@@ -2,4 +2,4 @@
= render 'commit_box'
= render 'ci_menu'
-= render 'pipelines_list', pipelines: @pipelines
+= render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id)
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 7afd3d80ef5..d5fc283aa8d 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -9,7 +9,7 @@
= render "ci_menu"
- else
.block-connector
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
= render "projects/notes/notes_with_form"
- if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type|
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index d94f23f5a38..08cb8a04413 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -22,9 +22,7 @@
= link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn'
- elsif create_mr_button?(@repository.root_ref, @ref)
.control
- = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
- = icon('plus')
- Create Merge Request
+ = link_to "Create Merge Request", create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
.control
= form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index d76d48187cd..08236216421 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -23,6 +23,4 @@
- if @merge_request.present?
= link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'prepend-left-10 btn'
- elsif create_mr_button?
- = link_to create_mr_path, class: 'prepend-left-10 btn' do
- = icon("plus")
- Create Merge Request
+ = link_to "Create Merge Request", create_mr_path, class: 'prepend-left-10 btn'
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 9c8f58d4aea..0dfc9fe20ed 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -8,7 +8,7 @@
- if @commits.present?
= render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- else
.light-well
.center
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 58c20e225c6..4b49bed835f 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,3 +1,4 @@
+- environment = local_assigns.fetch(:environment, nil)
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
@@ -30,4 +31,4 @@
- file_hash = hexdigest(diff_file.file_path)
= render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
- diff_file: diff_file, diff_commit: diff_commit, blob: blob
+ diff_file: diff_file, diff_commit: diff_commit, blob: blob, environment: environment
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index fc478ccc995..0232a09b4a8 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,6 +1,8 @@
+- environment = local_assigns.fetch(:environment, nil)
.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) }
- .file-title
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
+ .js-file-title.file-title-flex-parent
+ .file-header-content
+ = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
- unless diff_file.submodule?
.file-actions.hidden-xs
@@ -13,6 +15,7 @@
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
- = view_file_btn(diff_commit.id, diff_file.new_path, project)
+ = view_file_button(diff_commit.id, diff_file.new_path, project)
+ = view_on_environment_button(diff_commit.id, diff_file.new_path, environment) if environment
= render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index ddec775b789..1dbfe830d52 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -10,13 +10,13 @@
- if diff_file.renamed_file
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
- %strong
+ %strong.file-title-name.has-tooltip{ data: { title: old_path, container: 'body' } }
= old_path
&rarr;
- %strong
+ %strong.file-title-name.has-tooltip{ data: { title: new_path, container: 'body' } }
= new_path
- else
- %strong
+ %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
= diff_file.new_path
- if diff_file.deleted_file
deleted
diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml
index 69848123c17..14a2d627203 100644
--- a/app/views/projects/environments/_stop.html.haml
+++ b/app/views/projects/environments/_stop.html.haml
@@ -1,4 +1,4 @@
-- if can?(current_user, :create_deployment, environment) && environment.stoppable?
+- if can?(current_user, :create_deployment, environment) && environment.stop_action?
.inline
= link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post,
class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 7800d6ac382..7036325fff8 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -12,7 +12,7 @@
= render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
- - if can?(current_user, :create_deployment, @environment) && @environment.stoppable?
+ - if can?(current_user, :create_deployment, @environment) && @environment.can_stop?
= link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
.deployments-container
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index 1d49e9cbaf7..ef0dd0eda3c 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -16,6 +16,8 @@
.col-sm-6
.nav-controls
+ = link_to @environment.external_url, class: 'btn btn-default' do
+ = icon('external-link')
= render 'projects/deployments/actions', deployment: @environment.last_deployment
.terminal-container{ class: container_class }
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index c2f4457b60b..5d4e593e4ef 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, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr 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-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}, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr 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}, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr 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/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index f3be343daae..0e3902c066a 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -1,60 +1,46 @@
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
- - if @bulk_edit
- .issue-check
- = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
+ .issue-box
+ - if @bulk_edit
+ .issue-check
+ = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
+ .issue-info-container
+ .issue-title.title
+ %span.issue-title-text
+ = confidential_icon(issue)
+ = link_to issue.title, issue_path(issue)
+ %ul.controls
+ - if issue.closed?
+ %li
+ CLOSED
- .issue-title.title
- %span.issue-title-text
- = confidential_icon(issue)
- = link_to issue.title, issue_path(issue)
- %ul.controls
- - if issue.closed?
- %li
- CLOSED
+ - if issue.assignee
+ %li
+ = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
- - if issue.assignee
- %li
- = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
+ = render 'shared/issuable_meta_data', issuable: issue
- - upvotes, downvotes = issue.upvotes, issue.downvotes
- - if upvotes > 0
- %li
- = icon('thumbs-up')
- = upvotes
+ .issue-info
+ #{issuable_reference(issue)} &middot;
+ opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
+ by #{link_to_member(@project, issue.author, avatar: false)}
+ - if issue.milestone
+ &nbsp;
+ = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
+ = icon('clock-o')
+ = issue.milestone.title
+ - if issue.due_date
+ %span{ class: "#{'cred' if issue.overdue?}" }
+ &nbsp;
+ = icon('calendar')
+ = issue.due_date.to_s(:medium)
+ - if issue.labels.any?
+ &nbsp;
+ - issue.labels.each do |label|
+ = link_to_label(label, subject: issue.project, css_class: 'label-link')
+ - if issue.tasks?
+ &nbsp;
+ %span.task-status
+ = issue.task_status
- - if downvotes > 0
- %li
- = icon('thumbs-down')
- = downvotes
-
- - note_count = issue.notes.user.count
- %li
- = link_to issue_path(issue, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
- = icon('comments')
- = note_count
-
- .issue-info
- #{issuable_reference(issue)} &middot;
- opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
- by #{link_to_member(@project, issue.author, avatar: false)}
- - if issue.milestone
- &nbsp;
- = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
- = icon('clock-o')
- = issue.milestone.title
- - if issue.due_date
- %span{ class: "#{'cred' if issue.overdue?}" }
- &nbsp;
- = icon('calendar')
- = issue.due_date.to_s(:medium)
- - if issue.labels.any?
- &nbsp;
- - issue.labels.each do |label|
- = link_to_label(label, subject: issue.project)
- - if issue.tasks?
- &nbsp;
- %span.task-status
- = issue.task_status
-
- .pull-right.issue-updated-at
- %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
+ .pull-right.issue-updated-at
+ %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index a2305f4f547..d3eb3b7055b 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -35,9 +35,9 @@
= link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
- if can?(current_user, :update_issue, @issue)
%li
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
%li
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if @issue.submittable_as_spam? && current_user.admin?
@@ -48,8 +48,8 @@
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
- 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: "hidden-xs hidden-sm btn 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: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- if @issue.submittable_as_spam? && current_user.admin?
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
diff --git a/app/views/projects/issues/verify.html.haml b/app/views/projects/issues/verify.html.haml
new file mode 100644
index 00000000000..1934b18c086
--- /dev/null
+++ b/app/views/projects/issues/verify.html.haml
@@ -0,0 +1,20 @@
+- page_title "Anti-spam verification"
+
+%h3.page-title
+ Anti-spam verification
+%hr
+
+%p
+ We detected potential spam in the issue description. Please verify that you are not a robot to submit the issue.
+
+= form_for [@project.namespace.becomes(Namespace), @project, @issue] do |f|
+ .recaptcha
+ - params[:issue].each do |field, value|
+ = hidden_field(:issue, field, value: value)
+ = hidden_field_tag(:merge_request_for_resolving_discussions, params[:merge_request_for_resolving_discussions])
+ = hidden_field_tag(:spam_log_id, @issue.spam_log.id)
+ = hidden_field_tag(:recaptcha_verification, true)
+ = recaptcha_tags
+
+ .row-content-block.footer-block
+ = f.submit "Submit #{@issue.class.model_name.human.downcase}", class: 'btn btn-create'
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 29f861c09c6..8d4a91cb64c 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -3,6 +3,9 @@
- hide_class = ''
= render "projects/issues/head"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
+
- if @labels.exists? || @prioritized_labels.exists?
%div{ class: container_class }
.top-area.adjust
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 513f0818169..11b7aaec704 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -3,73 +3,59 @@
.issue-check
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
- .merge-request-title.title
- %span.merge-request-title-text
- = link_to merge_request.title, merge_request_path(merge_request)
- %ul.controls
- - if merge_request.merged?
- %li
- MERGED
- - elsif merge_request.closed?
- %li
- = icon('ban')
- CLOSED
-
- - if merge_request.head_pipeline
- %li
- = render_pipeline_status(merge_request.head_pipeline)
-
- - 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
- = icon('exclamation-triangle')
-
- - if merge_request.assignee
- %li
- = 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
- %li
- = icon('thumbs-up')
- = upvotes
-
- - if downvotes > 0
- %li
- = icon('thumbs-down')
- = downvotes
-
- - note_count = merge_request.related_notes.user.count
- %li
- = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
- = icon('comments')
- = note_count
-
- .merge-request-info
- #{issuable_reference(merge_request)} &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
- &nbsp;
- = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do
- = icon('code-fork')
- = merge_request.target_branch
-
- - if merge_request.milestone
- &nbsp;
- = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do
- = icon('clock-o')
- = merge_request.milestone.title
-
- - if merge_request.labels.any?
- &nbsp;
- - merge_request.labels.each do |label|
- = link_to_label(label, subject: merge_request.project, type: :merge_request)
-
- - if merge_request.tasks?
- &nbsp;
- %span.task-status
- = merge_request.task_status
-
- .pull-right.hidden-xs
- %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
+ .issue-info-container
+ .merge-request-title.title
+ %span.merge-request-title-text
+ = link_to merge_request.title, merge_request_path(merge_request)
+ %ul.controls
+ - if merge_request.merged?
+ %li
+ MERGED
+ - elsif merge_request.closed?
+ %li
+ = icon('ban')
+ CLOSED
+
+ - if merge_request.head_pipeline
+ %li
+ = render_pipeline_status(merge_request.head_pipeline)
+
+ - 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
+ = icon('exclamation-triangle')
+
+ - if merge_request.assignee
+ %li
+ = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
+
+ = render 'shared/issuable_meta_data', issuable: merge_request
+
+ .merge-request-info
+ #{issuable_reference(merge_request)} &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
+ &nbsp;
+ = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do
+ = icon('code-fork')
+ = merge_request.target_branch
+
+ - if merge_request.milestone
+ &nbsp;
+ = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do
+ = icon('clock-o')
+ = merge_request.milestone.title
+
+ - if merge_request.labels.any?
+ &nbsp;
+ - merge_request.labels.each do |label|
+ = link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link')
+
+ - if merge_request.tasks?
+ &nbsp;
+ %span.task-status
+ = merge_request.task_status
+
+ .pull-right.hidden-xs
+ %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml
index 74367ab9b7b..627fc4e9671 100644
--- a/app/views/projects/merge_requests/_new_diffs.html.haml
+++ b/app/views/projects/merge_requests/_new_diffs.html.haml
@@ -1 +1 @@
-= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
+= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, show_whitespace_toggle: false
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index d3c013b3f21..bd72310c16b 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -46,7 +46,7 @@
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
- = render "projects/merge_requests/show/pipelines"
+ = render 'projects/merge_requests/show/pipelines', endpoint: url_for(params.merge(format: :json))
.mr-loading-status
= spinner
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index b46c4a13cc4..dd615d3036c 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -3,10 +3,9 @@
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('lib_vue')
= page_specific_javascript_bundle_tag('diff_notes')
-.merge-request{ 'data-url' => merge_request_path(@merge_request) }
+.merge-request{ 'data-url' => merge_request_path(@merge_request), 'data-project-path' => project_path(@merge_request.project) }
= render "projects/merge_requests/show/mr_title"
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
@@ -94,7 +93,8 @@
#commits.commits.tab-pane
-# This tab is always loaded via AJAX
#pipelines.pipelines.tab-pane
- -# This tab is always loaded via AJAX
+ - if @pipelines.any?
+ = render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
#diffs.diffs.tab-pane
-# This tab is always loaded via AJAX
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index dcf578b85f9..1ecd9924d88 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -23,7 +23,7 @@
.files-wrapper{ "v-if" => "!isLoading && !hasError" }
.files
.diff-file.file-holder.conflict{ "v-for" => "file in conflictsData.files" }
- .file-title
+ .js-file-title.file-title
%i.fa.fa-fw{ ":class" => "file.iconClass" }
%strong {{file.filePath}}
= render partial: 'projects/merge_requests/conflicts/file_actions'
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 5f048d04b27..7f0913ea516 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? || @merge_request_diff.overflow?
= render 'projects/merge_requests/show/versions'
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- 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/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml
index afe3f3430c6..de4aa255bbd 100644
--- a/app/views/projects/merge_requests/show/_pipelines.html.haml
+++ b/app/views/projects/merge_requests/show/_pipelines.html.haml
@@ -1 +1,3 @@
-= render "projects/commit/pipelines_list", pipelines: @pipelines, link_to_commit: true
+- endpoint_path = local_assigns[:endpoint] || pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :json)
+
+= render 'projects/commit/pipelines_list', endpoint: endpoint_path
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index ae134563ead..e3062f47788 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,16 +1,21 @@
- if @pipeline
.mr-widget-heading
- %w[success success_with_warnings skipped canceled failed running pending].each do |status|
- .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) }
- = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
- = ci_icon_for_status(status)
+ .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
+ %div{ class: "ci-status-icon-#{status}" }
+ = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
+ = ci_icon_for_status(status)
%span
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status)
- for
- = succeed "." do
- = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
+ - if @pipeline.stages.any?
+ .mr-widget-pipeline-graph
+ = render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph'
+ %span
+ for
+ = succeed "." do
+ = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
%span.ci-coverage
- elsif @merge_request.has_ci?
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index 5de59473840..0b0fb7854c2 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -16,13 +16,13 @@
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
ci_message: {
- normal: "Job {{status}} for \"{{title}}\"",
- preparing: "{{status}} job for \"{{title}}\""
+ normal: "Pipeline {{status}} for \"{{title}}\"",
+ preparing: "{{status}} pipeline for \"{{title}}\""
},
ci_enable: #{@project.ci_service ? "true" : "false"},
ci_title: {
- preparing: "{{status}} job",
- normal: "Job {{status}}"
+ preparing: "{{status}} pipeline",
+ normal: "Pipeline {{status}}"
},
ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}",
ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json},
diff --git a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
index a18c2ad768f..3979d5fa8ed 100644
--- a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
@@ -1,6 +1,6 @@
%h4
= icon('exclamation-triangle')
- The job for this merge request failed
+ The pipeline for this merge request failed
%p
Please retry the job or push a new commit to fix the failure.
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index c3a6096aa54..06a31698ee6 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -3,6 +3,9 @@
- page_description @milestone.description
= render "projects/issues/head"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
+
%div{ class: container_class }
.detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) }
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index cd685f7d0eb..41473fae4de 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -94,9 +94,8 @@
.form-group.project-visibility-level-holder
= f.label :visibility_level, class: 'label-light' do
Visibility Level
- = link_to "(?)", help_page_path("public_access/public_access")
- = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project
-
+ = link_to icon('question-circle'), help_page_path("public_access/public_access")
+ = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project, with_label: false
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 09339e520dd..e58de9f0e18 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -9,9 +9,12 @@
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
- = link_to_member(note.project, note.author, avatar: false)
- .note-headline-light
+ %a.visible-xs{ href: user_path(note.author) }
= note.author.to_reference
+ = link_to_member(note.project, note.author, avatar: false, extra_class: 'hidden-xs')
+ .note-headline-light
+ %span.hidden-xs
+ = note.author.to_reference
- unless note.system
commented
- if note.system
@@ -23,12 +26,11 @@
.note-actions
- access = note_max_access_for_user(note)
- if access
- %span.note-role.hidden-xs= access
+ %span.note-role= access
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
- %resolve-btn{ "project-path" => "#{project_path(note.project)}",
- "discussion-id" => "#{note.discussion_id}",
+ %resolve-btn{ "discussion-id" => "#{note.discussion_id}",
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
@@ -59,7 +61,7 @@
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
- = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
+ = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
.note-body{ class: note_editable ? 'js-task-list-container' : '' }
.note-text.md
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index fbd2bff5bbb..08c73d94a09 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -13,7 +13,7 @@
= image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form
= render "projects/notes/form", view: diff_view
- - else
+ - elsif !current_user
.disabled-comment.text-center
.disabled-comment-text.inline
Please
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index f776734556a..81e393d7626 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -36,31 +36,27 @@
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
.content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } }
- - if @pipelines.blank?
- %div
- .nothing-here-block No pipelines to show
- - else
- .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"),
- "icon_status_canceled" => custom_icon("icon_status_canceled"),
- "icon_status_running" => custom_icon("icon_status_running"),
- "icon_status_skipped" => custom_icon("icon_status_skipped"),
- "icon_status_created" => custom_icon("icon_status_created"),
- "icon_status_pending" => custom_icon("icon_status_pending"),
- "icon_status_success" => custom_icon("icon_status_success"),
- "icon_status_failed" => custom_icon("icon_status_failed"),
- "icon_status_warning" => custom_icon("icon_status_warning"),
- "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
- "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
- "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
- "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
- "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
- "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
- "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
- "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
- "icon_play" => custom_icon("icon_play"),
- "icon_timer" => custom_icon("icon_timer"),
- "icon_status_manual" => custom_icon("icon_status_manual"),
- } }
+ .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"),
+ "icon_status_canceled" => custom_icon("icon_status_canceled"),
+ "icon_status_running" => custom_icon("icon_status_running"),
+ "icon_status_skipped" => custom_icon("icon_status_skipped"),
+ "icon_status_created" => custom_icon("icon_status_created"),
+ "icon_status_pending" => custom_icon("icon_status_pending"),
+ "icon_status_success" => custom_icon("icon_status_success"),
+ "icon_status_failed" => custom_icon("icon_status_failed"),
+ "icon_status_warning" => custom_icon("icon_status_warning"),
+ "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
+ "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
+ "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
+ "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
+ "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
+ "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
+ "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
+ "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
+ "icon_play" => custom_icon("icon_play"),
+ "icon_timer" => custom_icon("icon_timer"),
+ "icon_status_manual" => custom_icon("icon_status_manual"),
+ } }
.vue-pipelines-index
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index 18328c67f02..8024fb8979d 100644
--- a/app/views/projects/pipelines_settings/show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -1,9 +1,7 @@
-- page_title "CI/CD Pipelines"
-
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
- = page_title
+ CI/CD Pipelines
.col-lg-9
= form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f|
%fieldset.builds-feature
@@ -95,4 +93,4 @@
%hr
.row.prepend-top-default
- = render partial: 'badge', collection: @badges
+ = render partial: 'projects/pipelines_settings/badge', collection: @badges
diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/_index.html.haml
index d6f691d9c24..f9808f7c990 100644
--- a/app/views/projects/runners/index.html.haml
+++ b/app/views/projects/runners/_index.html.haml
@@ -1,5 +1,3 @@
-- page_title "Runners"
-
.light.prepend-top-default
%p
A 'Runner' is a process which runs a job.
@@ -22,6 +20,6 @@
%p.lead To start serving your jobs you can either add specific Runners to your project or use shared Runners
.row
.col-sm-6
- = render 'specific_runners'
+ = render 'projects/runners/specific_runners'
.col-sm-6
- = render 'shared_runners'
+ = render 'projects/runners/shared_runners'
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 5afa193357e..0671dd66e78 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -22,7 +22,7 @@
- else
%h4.underlined-title Available shared Runners : #{@shared_runners_count}
%ul.bordered-list.available-shared-runners
- = render partial: 'runner', collection: @shared_runners, as: :runner
+ = render partial: 'projects/runners/runner', collection: @shared_runners, as: :runner
- if @shared_runners_count > 10
.light
and #{@shared_runners_count - 10} more...
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index dcff675eafc..6b8e6bd4fee 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -20,10 +20,10 @@
- if @project_runners.any?
%h4.underlined-title Runners activated for this project
%ul.bordered-list.activated-specific-runners
- = render partial: 'runner', collection: @project_runners, as: :runner
+ = render partial: 'projects/runners/runner', collection: @project_runners, as: :runner
- if @assignable_runners.any?
%h4.underlined-title Available specific runners
%ul.bordered-list.available-specific-runners
- = render partial: 'runner', collection: @assignable_runners, as: :runner
+ = render partial: 'projects/runners/runner', collection: @assignable_runners, as: :runner
= paginate @assignable_runners, theme: "gitlab"
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 8ca4c51a064..3a323d94cc2 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -1,16 +1,19 @@
-- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}"
-To setup this service:
-%ul.list-unstyled
+%p To setup this service:
+%ul.list-unstyled.indent-list
%li
1.
- = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Enable custom slash commands
+ = icon('external-link')
on your Mattermost installation
%li
2.
- = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command'
- in Mattermost with these options:
-
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = icon('external-link')
+ in your Mattermost team with these options:
%hr
.help-form
@@ -83,9 +86,14 @@ To setup this service:
%hr
-%ul.list-unstyled
+%ul.list-unstyled.indent-list
%li
- 3. After adding the slash command, paste the
-
- %strong token
+ 3. Paste the
+ %strong Token
into the field below
+ %li
+ 4. Select the
+ %strong Active
+ checkbox, press
+ %strong Save changes
+ and start using GitLab inside Mattermost!
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index c1e576b42fc..a04fd5035a6 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,13 +1,16 @@
- enabled = Gitlab.config.mattermost.enabled
.well
- This service allows GitLab users to perform common operations on this
- project by entering slash commands in Mattermost.
- %br
- See list of available commands in Mattermost after setting up this service,
- by entering
- %code /&lt;command_trigger_word&gt; help
-
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Mattermost.
+ = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+ View documentation
+ = icon('external-link')
+ %p.inline
+ See list of available commands in Mattermost after setting up this service,
+ by entering
+ %kbd.inline /&lt;trigger&gt; help
- unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 04b9100acc6..0d973a20d4c 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,21 +1,25 @@
-- pretty_name = defined?(@project) ? @project.name_with_namespace : "namespace / path"
-- run_actions_text = "Perform common operations on this project: #{pretty_name}"
+- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path'
+- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.well
- This service allows GitLab users to perform common operations on this
- project by entering slash commands in Slack.
- %br
- See list of available commands in Slack after setting up this service,
- by entering
- %code /&lt;command&gt; help
- %br
- %br
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Slack.
+ = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+ View documentation
+ = icon('external-link')
+ %p.inline
+ See list of available commands in Slack after setting up this service,
+ by entering
+ %kbd.inline /&lt;command&gt; help
- unless @service.template?
- To setup this service:
- %ul.list-unstyled
+ %p To setup this service:
+ %ul.list-unstyled.indent-list
%li
1.
- = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
+ = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = icon('external-link')
in your Slack team with these options:
%hr
@@ -82,7 +86,7 @@
%hr
- %ul.list-unstyled
+ %ul.list-unstyled.indent-list
%li
2. Paste the
%strong Token
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
new file mode 100644
index 00000000000..52f5f7b81e2
--- /dev/null
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -0,0 +1,6 @@
+- page_title "CI/CD Pipelines"
+
+= render 'projects/runners/index'
+= render 'projects/variables/index'
+= render 'projects/triggers/index'
+= render 'projects/pipelines_settings/show'
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 485b23815bc..6b3d7d4008b 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -4,7 +4,7 @@
.project-snippets
%article.file-holder.snippet-file-content
- .file-title
+ .js-file-title.file-title
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index a1f4e3e8ed6..bdcc160a067 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,5 +1,5 @@
%article.file-holder.readme-holder
- .file-title
+ .js-file-title.file-title
= blob_icon readme.mode, readme.name
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, @path, readme.name)) do
%strong
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/_index.html.haml
index b9c4e323430..33883facf9b 100644
--- a/app/views/projects/triggers/index.html.haml
+++ b/app/views/projects/triggers/_index.html.haml
@@ -1,9 +1,7 @@
-- page_title "Triggers"
-
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
- = page_title
+ Triggers
%p.prepend-top-20
Triggers can force a specific branch or tag to get rebuilt with an API call.
%p.append-bottom-0
@@ -25,12 +23,12 @@
%th
%strong Last used
%th
- = render partial: 'trigger', collection: @triggers, as: :trigger
+ = render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
- else
%p.settings-message.text-center.append-bottom-default
No triggers have been created yet. Add one using the button below.
- = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f|
+ = form_for @trigger, url: url_for(controller: '/projects/triggers', action: 'create') do |f|
= f.submit "Add trigger", class: 'btn btn-success'
.panel-footer
@@ -67,7 +65,7 @@
In the
%code .gitlab-ci.yml
of another project, include the following snippet.
- The project will be rebuilt at the end of the job.
+ The project will be rebuilt at the end of the pipeline.
%pre
:plain
@@ -91,7 +89,7 @@
%p.light
Add
%code variables[VARIABLE]=VALUE
- to an API request. Variable values can be used to distinguish between triggered jobs and normal jobs.
+ to an API request. Variable values can be used to distinguish between triggered pipelines and normal pipelines.
With cURL:
diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/_index.html.haml
index cf7ae0b489f..1b852a9c5b3 100644
--- a/app/views/projects/variables/index.html.haml
+++ b/app/views/projects/variables/_index.html.haml
@@ -1,12 +1,10 @@
-- page_title "Variables"
-
.row.prepend-top-default.append-bottom-default
.col-lg-3
- = render "content"
+ = render "projects/variables/content"
.col-lg-9
%h5.prepend-top-0
Add a variable
- = render "form", btn_text: "Add new variable"
+ = render "projects/variables/form", btn_text: "Add new variable"
%hr
%h5.prepend-top-0
Your variables (#{@project.variables.size})
@@ -14,5 +12,5 @@
%p.settings-message.text-center.append-bottom-0
No variables found, add one with the form above.
- else
- = render "table"
+ = render "projects/variables/table"
%button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index c74f53b4c39..3d33679f07d 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -13,5 +13,9 @@
= 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
+ %span.new-wiki-page-slug-tip
+ = icon('lightbulb-o')
+ Tip: You can specify the full path for the new file.
+ We will automatically create any missing directories.
.form-actions
= button_tag 'Create Page', class: 'build-new-wiki btn btn-create'
diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml
new file mode 100644
index 00000000000..6298cf6c8da
--- /dev/null
+++ b/app/views/projects/wikis/_pages_wiki_page.html.haml
@@ -0,0 +1,5 @@
+%li
+ = 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)}
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index cad9c15a49e..8c582f747b3 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -1,4 +1,4 @@
-%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar
+%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.block.wiki-sidebar-header.append-bottom-default
%a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" }
= icon('angle-double-right')
@@ -12,10 +12,8 @@
.blocks-container
.block.block-first
%ul.wiki-pages
- - @sidebar_wiki_pages.each do |wiki_page|
- %li{ class: params[:id] == wiki_page.slug ? 'active' : '' }
- = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do
- = wiki_page.title.capitalize
+ = render @sidebar_wiki_entries, context: 'sidebar'
+
.block
= link_to namespace_project_wikis_pages_path(@project.namespace, @project), class: 'btn btn-block' do
More Pages
diff --git a/app/views/projects/wikis/_sidebar_wiki_page.html.haml b/app/views/projects/wikis/_sidebar_wiki_page.html.haml
new file mode 100644
index 00000000000..eb9bd14920d
--- /dev/null
+++ b/app/views/projects/wikis/_sidebar_wiki_page.html.haml
@@ -0,0 +1,3 @@
+%li{ class: params[:id] == wiki_page.slug ? 'active' : '' }
+ = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do
+ = wiki_page.title.capitalize
diff --git a/app/views/projects/wikis/_wiki_directory.html.haml b/app/views/projects/wikis/_wiki_directory.html.haml
new file mode 100644
index 00000000000..0e5f32ed859
--- /dev/null
+++ b/app/views/projects/wikis/_wiki_directory.html.haml
@@ -0,0 +1,4 @@
+%li
+ = wiki_directory.slug
+ %ul
+ = render wiki_directory.pages, context: context
diff --git a/app/views/projects/wikis/_wiki_page.html.haml b/app/views/projects/wikis/_wiki_page.html.haml
new file mode 100644
index 00000000000..c84d06dad02
--- /dev/null
+++ b/app/views/projects/wikis/_wiki_page.html.haml
@@ -0,0 +1 @@
+= render "#{context}_wiki_page", wiki_page: wiki_page
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index e1eaffc6884..5fba2b1a5ae 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -13,11 +13,7 @@
= icon('cloud-download')
Clone repository
- %ul.content-list
- - @wiki_pages.each do |wiki_page|
- %li
- = 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)}
+ %ul.wiki-pages-list.content-list
+ = render @wiki_entries, context: 'pages'
+
= paginate @wiki_pages, theme: 'gitlab'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 1b6dceee241..3609461b721 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -6,9 +6,11 @@
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
+ .wiki-breadcrumb
+ %span= breadcrumb(@page.slug)
+
.nav-text
%h2.wiki-page-title= @page.title.capitalize
-
%span.wiki-last-edit-by
Last edited by
%strong
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index 9e8adc82583..7f1f807e2e7 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,7 +1,7 @@
- file_name, blob = blob
.blob-result
.file-holder
- .file-title
+ .js-file-title.file-title
- ref = @search_results.repository_ref
- blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(ref, file_name))
= link_to blob_link do
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 23ca6479414..f7808ea6aff 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -14,7 +14,7 @@
- snippet_path = reliable_snippet_path(snippet)
= link_to snippet_path do
.file-holder
- .file-title
+ .js-file-title.file-title
%i.fa.fa-file
%strong= snippet.file_name
- if markup?(snippet.file_name)
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index 648d0bd76cb..d87f9df2677 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -1,7 +1,7 @@
- wiki_blob = parse_search_result(wiki_blob)
.blob-result
.file-holder
- .file-title
+ .js-file-title.file-title
= link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.basename) do
%i.fa.fa-file
%strong
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index c196bc06b17..4b98ff88241 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -17,9 +17,9 @@
Try to keep the first line under 52 characters
and the others under 72.
- if descriptions.present?
- %p.hint.js-with-description-hint
+ .hint.js-with-description-hint
= link_to "#", class: "js-with-description-link" do
Include description in commit message
- %p.hint.js-without-description-hint.hide
+ .hint.js-without-description-hint.hide
= link_to "#", class: "js-without-description-link" do
Don't include description in commit message
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index 0bc851b4256..efb207b9916 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -1,3 +1,4 @@
+- parent = Group.find_by(id: params[:parent_id] || @group.parent_id)
- if @group.persisted?
.form-group
= f.label :name, class: 'control-label' do
@@ -11,11 +12,15 @@
.col-sm-10
.input-group.gl-field-error-anchor
.input-group-addon
- = root_url
+ %span>= root_url
+ - if parent
+ %strong= parent.full_path + '/'
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE,
title: 'Please choose a group name with no special characters.'
+ - if parent
+ = f.hidden_field :parent_id, value: parent.id
- if @group.persisted?
.alert.alert-warning.prepend-top-10
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
new file mode 100644
index 00000000000..1264e524d86
--- /dev/null
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -0,0 +1,19 @@
+- note_count = @issuable_meta_data[issuable.id].notes_count
+- issue_votes = @issuable_meta_data[issuable.id]
+- upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes
+- issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes')
+
+- if upvotes > 0
+ %li
+ = icon('thumbs-up')
+ = upvotes
+
+- if downvotes > 0
+ %li
+ = icon('thumbs-down')
+ = downvotes
+
+%li
+ = link_to issuable_url, class: ('no-comments' if note_count.zero?) do
+ = icon('comments')
+ = note_count
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
new file mode 100644
index 00000000000..b0778653d4e
--- /dev/null
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -0,0 +1,18 @@
+.stage-cell
+ - pipeline.stages.each do |stage|
+ - if stage.status
+ - detailed_status = stage.detailed_status(current_user)
+ - icon_status = "#{detailed_status.icon}_borderless"
+ - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
+
+ .stage-container.dropdown{ class: klass }
+ %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
+ = custom_icon(icon_status)
+ = icon('caret-down')
+
+ %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
+ .arrow-up
+ .js-builds-dropdown-list.scrollable-menu
+
+ .js-builds-dropdown-loading.builds-dropdown-loading.hidden
+ %span.fa.fa-spinner.fa-spin
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index b11257ee0e6..73efec88bb1 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -1,8 +1,11 @@
+- with_label = local_assigns.fetch(:with_label, true)
+
.form-group.project-visibility-level-holder
- = f.label :visibility_level, class: 'control-label' do
- Visibility Level
- = link_to icon('question-circle'), help_page_path("public_access/public_access")
- .col-sm-10
+ - if with_label
+ = f.label :visibility_level, class: 'control-label' do
+ Visibility Level
+ = link_to icon('question-circle'), help_page_path("public_access/public_access")
+ %div{ :class => ("col-sm-10" if with_label) }
- if can_change_visibility_level
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
- else
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index dd9e433491b..60ca23ef680 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -1,4 +1,5 @@
- group_member = local_assigns[:group_member]
+- full_name = true unless local_assigns[:full_name] == false
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" if group.description.blank?
@@ -28,7 +29,10 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group, class: 'group-name' do
- = group.full_name
+ - if full_name
+ = group.full_name
+ - else
+ = group.name
- if group_member
as
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 2ad06dcf25b..f17ae9f28eb 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -54,7 +54,7 @@
.issues_bulk_update.hide
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
- = 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
+ = 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]", default_label: "Status" } } ) do
%ul
%li
%a{ href: "#", data: { id: "reopen" } } Open
@@ -62,13 +62,13 @@
%a{ href: "#", data: {id: "close" } } Closed
.filter-item.inline
= 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]" } })
+ 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]", default_label: "Assignee" } })
.filter-item.inline
- = 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 } })
+ = 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]", default_label: "Milestone", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
- = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
+ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul
%li
%a{ href: "#", data: { id: "subscribe" } } Subscribe
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 55360dadbc4..6e417aa2251 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -11,7 +11,7 @@
class: "check_all_issues left"
.issues-other-filters.filtered-search-container
.filtered-search-input-container
- %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]) }
+ %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) }
= icon('filter')
%button.clear-search.hidden{ type: 'button' }
= icon('times')
@@ -101,7 +101,7 @@
.filter-item.inline
= 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 } })
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
%ul
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 77fc44fa5cc..3f7f1a86b9f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -2,7 +2,7 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('issuable')
-%aside.right-sidebar{ class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 659d4c905fc..239387fc9fa 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -19,9 +19,9 @@
%label.label.label-danger
%strong Blocked
- - if source.instance_of?(Group) && !@group
+ - if source.instance_of?(Group) && source != @group
&middot;
- = link_to source.name, source, class: "member-group-link"
+ = link_to source.full_name, source, class: "member-group-link"
.hidden-xs.cgray
- if member.request?
@@ -44,8 +44,9 @@
= link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at)
- if show_roles
+ - current_resource = @project || @group
.controls.member-controls
- - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project)
+ - if show_controls && member.source == current_resource
- if user != current_user
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
= f.hidden_field :access_level
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index 748b10a1298..ed94773ef89 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -10,6 +10,3 @@
.col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
%a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
-
-:javascript
- new gl.DueDateSelectors();
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 28935c8b598..4c7d69d40d5 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -5,7 +5,7 @@
- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
- can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable)
-%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'ui-sort-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) }
+%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable) }
%span
- if show_project_name
%strong #{project.name} &middot;
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index ac028f18e50..c19697802ce 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -1,6 +1,7 @@
- @sort ||= sort_value_recently_updated
- personal = params[:personal]
- archived = params[:archived]
+- shared = params[:shared]
- namespace_id = params[:namespace_id]
.dropdown
- toggle_text = projects_sort_options_hash[@sort]
@@ -28,3 +29,14 @@
%li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do
Owned by me
+ - if @group && @group.shared_projects.present?
+ %li.divider
+ %li
+ = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: nil), class: ("is-active" unless shared.present?) do
+ All projects
+ %li
+ = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 0), class: ("is-active" if shared == '0') do
+ Hide shared projects
+ %li
+ = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 1), class: ("is-active" if shared == '1') do
+ Hide group projects
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 56c0f7390a5..e7f7db73223 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -18,7 +18,7 @@
= f.label :file_name, "File", class: 'control-label'
.col-sm-10
.file-holder.snippet
- .file-title
+ .js-file-title.file-title
= f.text_field :file_name, placeholder: "Optionally name this file to add code highlighting, e.g. example.rb for Ruby.", class: 'form-control snippet-file-name'
.file-content.code
%pre#editor= @snippet.content
diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml
index 92151176fce..1a6e2542dc1 100644
--- a/app/views/sherlock/file_samples/show.html.haml
+++ b/app/views/sherlock/file_samples/show.html.haml
@@ -26,7 +26,7 @@
= @file_sample.events
%article.file-holder
- .file-title
+ .js-file-title.file-title
%i.fa.fa-file-text-o.fa-fw
%strong
= @file_sample.file
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 837a1a0cc8c..970afbe6b64 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -3,7 +3,7 @@
= render 'shared/snippets/header'
%article.file-holder.snippet-file-content
- .file-title
+ .js-file-title.file-title
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml
index 6228245d8d0..57b8845c55d 100644
--- a/app/views/users/calendar.html.haml
+++ b/app/views/users/calendar.html.haml
@@ -1,7 +1,7 @@
.clearfix.calendar
.js-contrib-calendar
.calendar-hint
- Summary of issues, merge requests, and push events
+ Summary of issues, merge requests, push events, and comments
:javascript
new Calendar(
#{@activity_dates.to_json},
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index b09782749f5..4afd31f788b 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -10,11 +10,17 @@
%i.fa.fa-clock-o
= event.created_at.to_s(:time)
- if event.push?
- #{event.action_name} #{event.ref_type} #{event.ref_name}
+ #{event.action_name} #{event.ref_type}
+ %strong
+ - commits_path = namespace_project_commits_path(event.project.namespace, event.project, event.ref_name)
+ = link_to_if event.project.repository.branch_exists?(event.ref_name), event.ref_name, commits_path
- else
= event_action_name(event)
- - if event.target
- %strong= link_to "#{event.target.to_reference}", [event.project.namespace.becomes(Namespace), event.project, event.target]
+ %strong
+ - if event.note?
+ = link_to event.note_target.to_reference, event_note_target_path(event)
+ - elsif event.target
+ = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target]
at
%strong
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 44254040e4e..dc2fea450bd 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -106,6 +106,8 @@
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
+ %h4.prepend-top-20
+ Most Recent Activity
.content_list{ data: { href: user_path } }
= spinner
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 6abbb5a5250..0e20df506a3 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -16,6 +16,6 @@ class AuthorizedProjectsWorker
def perform(user_id)
user = User.find_by(id: user_id)
- user.refresh_authorized_projects if user
+ user&.refresh_authorized_projects
end
end
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index 3194c389b3d..5483bbb210b 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -6,6 +6,6 @@ class DeleteUserWorker
delete_user = User.find(delete_user_id)
current_user = User.find(current_user_id)
- DeleteUserService.new(current_user).execute(delete_user, options.symbolize_keys)
+ Users::DestroyService.new(current_user).execute(delete_user, options.symbolize_keys)
end
end
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index a49a5fd0855..07e82767b06 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -11,6 +11,6 @@ class GroupDestroyWorker
user = User.find(user_id)
- DestroyGroupService.new(group, user).execute
+ Groups::DestroyService.new(group, user).execute
end
end