summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2017-04-03 17:22:18 +0100
committerFilipa Lacerda <filipa@gitlab.com>2017-04-03 17:22:18 +0100
commit1b85c5a73fb4e7b466d3d871be7d7eb4b889b3ce (patch)
tree7fd8628974752a279cbe27c61983f8755d92cc51 /app
parent16cca3a0ea7f4b95e99d7b3e8d4953334fa7bec7 (diff)
parentb2700e64cce7c9b258e117a995eda8de00a8a988 (diff)
downloadgitlab-ce-1b85c5a73fb4e7b466d3d871be7d7eb4b889b3ce.tar.gz
Merge branch 'master' into tc-fix-unplayable-build-action-404
* master: (525 commits) Introduce "polling_interval_multiplier" as application setting fix spelling CI_REPOSITORY_URL (line:355) gitab-ci-token to gitlab-ci-token. Pass Gitaly Repository messages to workhorse Use gitaly 0.5.0 Fix specs Improve specs examples Minor refactor Fix BrachFormatter for removed users Changelog Fix specs One more change to the branch names to preserve metadata Prefixes source branch name with short SHA to avoid collision Fix GitHub importer for PRs of deleted forked repositories Change order of specs Clean history after every test that changes history Clean history state after each test Fixes method not replacing URL parameters correctly Fix a transient failure caused by FFaker Remove unnecessary ORDER BY clause when updating todos Add a wait_for_ajax call to ensure Todos page cleans up properly ...
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/icon-merge-request-unmerged.svg2
-rw-r--r--app/assets/javascripts/activities.js3
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js8
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/spread_string.js50
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js4
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js42
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selector.js19
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selectors.js27
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js121
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js23
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js26
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js28
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js23
-rw-r--r--app/assets/javascripts/blob/notebook/index.js85
-rw-r--r--app/assets/javascripts/blob/notebook_viewer.js3
-rw-r--r--app/assets/javascripts/blob/template_selector.js101
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js9
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js9
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js9
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_license_selector.js13
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_license_selectors.js24
-rw-r--r--app/assets/javascripts/blob/template_selectors/template_selector.js92
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js32
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js15
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js177
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js9
-rw-r--r--app/assets/javascripts/boards/components/board.js2
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js4
-rw-r--r--app/assets/javascripts/boards/components/board_card.js5
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js3
-rw-r--r--app/assets/javascripts/boards/components/board_list.js4
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js3
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js8
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js7
-rw-r--r--app/assets/javascripts/boards/components/modal/filters.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js3
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js20
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.js3
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js5
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js6
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js2
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js3
-rw-r--r--app/assets/javascripts/boards/models/issue.js3
-rw-r--r--app/assets/javascripts/boards/models/list.js2
-rw-r--r--app/assets/javascripts/boards/services/board_service.js3
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js7
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js1
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js13
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_service.js44
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js160
-rw-r--r--app/assets/javascripts/commons/polyfills.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/limit_warning_component.js17
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_issue_component.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js7
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_production_component.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/total_time_component.js3
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js9
-rw-r--r--app/assets/javascripts/diff.js11
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js5
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js2
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js4
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js3
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js7
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js3
-rw-r--r--app/assets/javascripts/dispatcher.js8
-rw-r--r--app/assets/javascripts/droplab/droplab_filter.js2
-rw-r--r--app/assets/javascripts/due_date_select.js2
-rw-r--r--app/assets/javascripts/environments/components/environment.js14
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js11
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js13
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js27
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.js31
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js12
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js16
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js4
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.js15
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js3
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js1
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js28
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js20
-rw-r--r--app/assets/javascripts/group_name.js42
-rw-r--r--app/assets/javascripts/groups_select.js78
-rw-r--r--app/assets/javascripts/header.js2
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js2
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js3
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js3
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/help_state.js3
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js3
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js3
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/time_tracker.js2
-rw-r--r--app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js7
-rw-r--r--app/assets/javascripts/issuable_context.js3
-rw-r--r--app/assets/javascripts/issue.js8
-rw-r--r--app/assets/javascripts/labels_select.js2
-rw-r--r--app/assets/javascripts/layout_nav.js5
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js43
-rw-r--r--app/assets/javascripts/lib/utils/poll.js100
-rw-r--r--app/assets/javascripts/main.js13
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js3
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js3
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js3
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js5
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js4
-rw-r--r--app/assets/javascripts/merge_request_tabs.js40
-rw-r--r--app/assets/javascripts/merge_request_widget.js4
-rw-r--r--app/assets/javascripts/milestone_select.js7
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js6
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js48
-rw-r--r--app/assets/javascripts/notes.js4
-rw-r--r--app/assets/javascripts/profile/profile.js1
-rw-r--r--app/assets/javascripts/project.js3
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js8
-rw-r--r--app/assets/javascripts/render_gfm.js3
-rw-r--r--app/assets/javascripts/render_math.js4
-rw-r--r--app/assets/javascripts/right_sidebar.js54
-rw-r--r--app/assets/javascripts/shortcuts.js4
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js3
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js3
-rw-r--r--app/assets/javascripts/subscription.js2
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js14
-rw-r--r--app/assets/javascripts/user.js3
-rw-r--r--app/assets/javascripts/user_callout.js49
-rw-r--r--app/assets/javascripts/user_tabs.js21
-rw-r--r--app/assets/javascripts/users_select.js7
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/async_button.js93
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/empty_state.js33
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/error_state.js19
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/nav_controls.js52
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js72
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js56
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js71
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js33
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/stage.js116
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/status.js60
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/time_ago.js71
-rw-r--r--app/assets/javascripts/vue_pipelines_index/event_hub.js3
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js25
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js123
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_url.js63
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js291
-rw-r--r--app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js44
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js119
-rw-r--r--app/assets/javascripts/vue_pipelines_index/status.js64
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js31
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js (renamed from app/assets/javascripts/commit/pipelines/pipelines_store.js)37
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js78
-rw-r--r--app/assets/javascripts/vue_shared/common_vue.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js307
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js80
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js393
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js254
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js19
-rw-r--r--app/assets/stylesheets/framework/awards.scss9
-rw-r--r--app/assets/stylesheets/framework/buttons.scss37
-rw-r--r--app/assets/stylesheets/framework/common.scss6
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss78
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1
-rw-r--r--app/assets/stylesheets/framework/filters.scss41
-rw-r--r--app/assets/stylesheets/framework/forms.scss22
-rw-r--r--app/assets/stylesheets/framework/header.scss107
-rw-r--r--app/assets/stylesheets/framework/icons.scss12
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss24
-rw-r--r--app/assets/stylesheets/framework/lists.scss2
-rw-r--r--app/assets/stylesheets/framework/mixins.scss12
-rw-r--r--app/assets/stylesheets/framework/nav.scss58
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss4
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss7
-rw-r--r--app/assets/stylesheets/framework/typography.scss5
-rw-r--r--app/assets/stylesheets/framework/variables.scss221
-rw-r--r--app/assets/stylesheets/pages/boards.scss11
-rw-r--r--app/assets/stylesheets/pages/builds.scss6
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss15
-rw-r--r--app/assets/stylesheets/pages/environments.scss16
-rw-r--r--app/assets/stylesheets/pages/groups.scss17
-rw-r--r--app/assets/stylesheets/pages/issuable.scss37
-rw-r--r--app/assets/stylesheets/pages/issues.scss18
-rw-r--r--app/assets/stylesheets/pages/login.scss4
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss12
-rw-r--r--app/assets/stylesheets/pages/milestone.scss82
-rw-r--r--app/assets/stylesheets/pages/note_form.scss12
-rw-r--r--app/assets/stylesheets/pages/notes.scss37
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss88
-rw-r--r--app/assets/stylesheets/pages/profile.scss1
-rw-r--r--app/assets/stylesheets/pages/projects.scss102
-rw-r--r--app/assets/stylesheets/pages/sherlock.scss2
-rw-r--r--app/assets/stylesheets/pages/status.scss68
-rw-r--r--app/assets/stylesheets/pages/todos.scss49
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb1
-rw-r--r--app/controllers/admin/application_settings_controller.rb10
-rw-r--r--app/controllers/admin/background_jobs_controller.rb4
-rw-r--r--app/controllers/admin/labels_controller.rb9
-rw-r--r--app/controllers/admin/users_controller.rb12
-rw-r--r--app/controllers/application_controller.rb7
-rw-r--r--app/controllers/concerns/service_params.rb1
-rw-r--r--app/controllers/groups/group_members_controller.rb1
-rw-r--r--app/controllers/groups/labels_controller.rb6
-rw-r--r--app/controllers/import/base_controller.rb2
-rw-r--r--app/controllers/import/bitbucket_controller.rb8
-rw-r--r--app/controllers/profiles/accounts_controller.rb13
-rw-r--r--app/controllers/profiles/notifications_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb5
-rw-r--r--app/controllers/projects/blob_controller.rb5
-rw-r--r--app/controllers/projects/builds_controller.rb6
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb4
-rw-r--r--app/controllers/projects/issues_controller.rb20
-rw-r--r--app/controllers/projects/labels_controller.rb6
-rwxr-xr-x[-rw-r--r--]app/controllers/projects/merge_requests_controller.rb59
-rw-r--r--app/controllers/projects/milestones_controller.rb11
-rw-r--r--app/controllers/projects/pipelines_controller.rb6
-rw-r--r--app/controllers/projects/settings/members_controller.rb1
-rw-r--r--app/controllers/projects/tree_controller.rb1
-rw-r--r--app/controllers/projects/wikis_controller.rb5
-rw-r--r--app/controllers/registrations_controller.rb15
-rw-r--r--app/controllers/search_controller.rb40
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/controllers/users_controller.rb4
-rw-r--r--app/finders/group_finder.rb17
-rw-r--r--app/finders/labels_finder.rb13
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/auth_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/import_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb17
-rw-r--r--app/helpers/milestones_helper.rb6
-rw-r--r--app/helpers/namespaces_helper.rb8
-rw-r--r--app/helpers/nav_helper.rb10
-rw-r--r--app/helpers/sidekiq_helper.rb6
-rw-r--r--app/helpers/sorting_helper.rb28
-rw-r--r--app/helpers/users_helper.rb7
-rw-r--r--app/mailers/emails/builds.rb30
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/models/application_setting.rb16
-rw-r--r--app/models/blob.rb6
-rw-r--r--app/models/board.rb4
-rw-r--r--app/models/ci/build.rb21
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/ci/runner.rb4
-rw-r--r--app/models/ci/runner_project.rb6
-rw-r--r--app/models/ci/trigger.rb2
-rw-r--r--app/models/ci/variable.rb4
-rw-r--r--app/models/commit.rb9
-rw-r--r--app/models/commit_status.rb12
-rw-r--r--app/models/concerns/has_status.rb1
-rw-r--r--app/models/concerns/issuable.rb3
-rw-r--r--app/models/concerns/routable.rb10
-rw-r--r--app/models/concerns/spammable.rb2
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/issue.rb11
-rw-r--r--app/models/list.rb2
-rw-r--r--app/models/merge_request.rb6
-rw-r--r--app/models/milestone.rb15
-rw-r--r--app/models/namespace.rb20
-rw-r--r--app/models/note.rb5
-rw-r--r--app/models/project.rb37
-rw-r--r--app/models/project_services/builds_email_service.rb102
-rw-r--r--app/models/project_services/chat_message/build_message.rb102
-rw-r--r--app/models/project_services/chat_notification_service.rb41
-rw-r--r--app/models/project_services/hipchat_service.rb45
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/mattermost_service.rb17
-rw-r--r--app/models/project_services/prometheus_service.rb17
-rw-r--r--app/models/project_services/slack_service.rb17
-rw-r--r--app/models/project_team.rb3
-rw-r--r--app/models/project_wiki.rb7
-rw-r--r--app/models/repository.rb8
-rw-r--r--app/models/route.rb6
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/snippet.rb3
-rw-r--r--app/models/system_note_metadata.rb11
-rw-r--r--app/models/user.rb36
-rw-r--r--app/models/wiki_page.rb8
-rw-r--r--app/serializers/build_entity.rb7
-rw-r--r--app/serializers/build_serializer.rb8
-rw-r--r--app/serializers/environment_entity.rb7
-rw-r--r--app/serializers/pipeline_entity.rb11
-rw-r--r--app/serializers/pipeline_serializer.rb7
-rw-r--r--app/serializers/status_entity.rb2
-rw-r--r--app/services/boards/create_service.rb2
-rw-r--r--app/services/boards/issues/list_service.rb4
-rw-r--r--app/services/boards/issues/move_service.rb4
-rw-r--r--app/services/ci/register_job_service.rb6
-rw-r--r--app/services/ci/retry_pipeline_service.rb4
-rw-r--r--app/services/create_branch_service.rb6
-rw-r--r--app/services/groups/update_service.rb8
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/labels/base_service.rb161
-rw-r--r--app/services/labels/create_service.rb25
-rw-r--r--app/services/labels/find_or_create_service.rb4
-rw-r--r--app/services/labels/update_service.rb15
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/services/note_summary.rb20
-rw-r--r--app/services/notes/update_service.rb4
-rw-r--r--app/services/notification_recipient_service.rb293
-rw-r--r--app/services/notification_service.rb328
-rw-r--r--app/services/projects/import_service.rb3
-rw-r--r--app/services/search/global_service.rb4
-rw-r--r--app/services/search/project_service.rb4
-rw-r--r--app/services/search/snippet_service.rb4
-rw-r--r--app/services/search_service.rb63
-rw-r--r--app/services/spam_check_service.rb3
-rw-r--r--app/services/system_hooks_service.rb9
-rw-r--r--app/services/system_note_service.rb79
-rw-r--r--app/services/todo_service.rb40
-rw-r--r--app/services/users/create_service.rb110
-rw-r--r--app/services/users/destroy_service.rb4
-rw-r--r--app/validators/importable_url_validator.rb11
-rw-r--r--app/views/admin/appearances/_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_form.html.haml16
-rw-r--r--app/views/admin/dashboard/index.html.haml30
-rw-r--r--app/views/admin/projects/_projects.html.haml15
-rw-r--r--app/views/admin/users/_access_levels.html.haml2
-rw-r--r--app/views/award_emoji/_awards_block.html.haml6
-rw-r--r--app/views/ci/status/_graph_badge.html.haml4
-rw-r--r--app/views/dashboard/_projects_head.html.haml6
-rw-r--r--app/views/dashboard/milestones/show.html.haml2
-rw-r--r--app/views/dashboard/projects/index.html.haml4
-rw-r--r--app/views/dashboard/todos/index.html.haml16
-rw-r--r--app/views/devise/sessions/_new_base.html.haml2
-rw-r--r--app/views/discussions/_discussion.html.haml2
-rw-r--r--app/views/events/_event.atom.builder2
-rw-r--r--app/views/events/event/_note.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml21
-rw-r--r--app/views/groups/milestones/show.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml6
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/import/bitbucket/status.html.haml6
-rw-r--r--app/views/import/gitlab/status.html.haml2
-rw-r--r--app/views/import/google_code/new.html.haml2
-rw-r--r--app/views/import/google_code/status.html.haml6
-rw-r--r--app/views/issues/_issue.atom.builder4
-rw-r--r--app/views/koding/index.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml3
-rw-r--r--app/views/layouts/header/_default.html.haml38
-rw-r--r--app/views/notify/build_fail_email.html.haml24
-rw-r--r--app/views/notify/build_fail_email.text.erb11
-rw-r--r--app/views/notify/build_success_email.html.haml24
-rw-r--r--app/views/notify/build_success_email.text.erb11
-rw-r--r--app/views/notify/project_was_exported_email.html.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml8
-rw-r--r--app/views/profiles/notifications/show.html.haml5
-rw-r--r--app/views/profiles/preferences/show.html.haml10
-rw-r--r--app/views/profiles/show.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml2
-rw-r--r--app/views/projects/blob/_image.html.haml2
-rw-r--r--app/views/projects/blob/_notebook.html.haml5
-rw-r--r--app/views/projects/blob/_text.html.haml2
-rw-r--r--app/views/projects/blob/_upload.html.haml9
-rw-r--r--app/views/projects/blob/edit.html.haml4
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/boards/components/_board.html.haml4
-rw-r--r--app/views/projects/boards/components/_board_list.html.haml2
-rw-r--r--app/views/projects/builds/_sidebar.html.haml7
-rw-r--r--app/views/projects/builds/show.html.haml2
-rw-r--r--app/views/projects/buttons/_koding.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/commit/_pipelines_list.haml1
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/_overview.html.haml2
-rw-r--r--app/views/projects/diffs/_stats.html.haml2
-rw-r--r--app/views/projects/edit.html.haml4
-rw-r--r--app/views/projects/environments/_external_url.html.haml2
-rw-r--r--app/views/projects/environments/metrics.html.haml7
-rw-r--r--app/views/projects/environments/terminal.html.haml2
-rw-r--r--app/views/projects/issues/index.html.haml17
-rw-r--r--app/views/projects/issues/show.html.haml57
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml8
-rw-r--r--app/views/projects/merge_requests/merge.js.haml3
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml2
-rw-r--r--app/views/projects/milestones/index.html.haml1
-rw-r--r--app/views/projects/milestones/show.html.haml5
-rw-r--r--app/views/projects/new.html.haml4
-rw-r--r--app/views/projects/notes/_note.html.haml2
-rw-r--r--app/views/projects/pipelines/_head.html.haml6
-rw-r--r--app/views/projects/pipelines/_info.html.haml10
-rw-r--r--app/views/projects/pipelines/index.html.haml60
-rw-r--r--app/views/projects/protected_branches/_dropdown.html.haml2
-rw-r--r--app/views/projects/services/_index.html.haml2
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml4
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml2
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml4
-rw-r--r--app/views/projects/show.html.haml6
-rw-r--r--app/views/projects/stage/_in_stage_group.html.haml2
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml5
-rw-r--r--app/views/search/_category.html.haml141
-rw-r--r--app/views/shared/_group_form.html.haml6
-rw-r--r--app/views/shared/_milestones_filter.html.haml4
-rw-r--r--app/views/shared/_milestones_sort_dropdown.html.haml22
-rw-r--r--app/views/shared/_sort_dropdown.html.haml1
-rw-r--r--app/views/shared/_user_callout.html.haml14
-rw-r--r--app/views/shared/empty_states/_issues.html.haml5
-rw-r--r--app/views/shared/empty_states/icons/_pipelines_empty.svg1
-rw-r--r--app/views/shared/empty_states/icons/_pipelines_failed.svg1
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/icons/_mr_bold.svg1
-rw-r--r--app/views/shared/issuable/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml21
-rw-r--r--app/views/shared/issuable/_sidebar_todo.html.haml15
-rw-r--r--app/views/shared/members/_member.html.haml4
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml131
-rw-r--r--app/views/shared/milestones/_summary.html.haml45
-rw-r--r--app/views/shared/milestones/_tabs.html.haml47
-rw-r--r--app/views/shared/milestones/_top.html.haml3
-rw-r--r--app/views/shared/projects/_list.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml60
-rw-r--r--app/views/users/calendar_activities.html.haml4
-rw-r--r--app/views/users/show.html.haml46
-rw-r--r--app/workers/build_email_worker.rb20
-rw-r--r--app/workers/post_receive.rb12
434 files changed, 5617 insertions, 4029 deletions
diff --git a/app/assets/images/icon-merge-request-unmerged.svg b/app/assets/images/icon-merge-request-unmerged.svg
index c4d8e65122d..d53a7470243 100644
--- a/app/assets/images/icon-merge-request-unmerged.svg
+++ b/app/assets/images/icon-merge-request-unmerged.svg
@@ -1 +1 @@
-<svg width="12" height="15" viewBox="0 0 12 15" xmlns="http://www.w3.org/2000/svg"><path d="M10.267 11.028V5.167c-.028-.728-.318-1.372-.878-1.923-.56-.55-1.194-.85-1.922-.877h-.934V.5l-2.8 2.8 2.8 2.8V4.233h.934a.976.976 0 0 1 .644.29.88.88 0 0 1 .289.644v5.861a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472zM3.733 3.3a1.86 1.86 0 0 0-1.866-1.867 1.86 1.86 0 0 0-.934 3.472v6.123a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472V4.905c.55-.317.933-.914.933-1.605z" fill-rule="nonzero"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg> \ No newline at end of file
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index aebda7780e1..d816df831eb 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign, class-methods-use-this */
/* global Pager */
-/* global Cookies */
+
+import Cookies from 'js-cookie';
class Activities {
constructor() {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 9349918f7a0..c743dd551d7 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,4 +1,4 @@
-/* global Cookies */
+import Cookies from 'js-cookie';
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
index 5e3c45f7e92..20ab2d7e827 100644
--- a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
+++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
@@ -1,5 +1,3 @@
-import spreadString from './spread_string';
-
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
@@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) {
const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) {
- return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => {
+ return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => {
const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5;
});
@@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) {
// doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
- return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
+ return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
@@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false;
let hasZwj = false;
- spreadString(emojiUnicode).forEach((character) => {
+ Array.from(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0);
if (cp === zwj) {
hasZwj = true;
diff --git a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
deleted file mode 100644
index 327764ec6e9..00000000000
--- a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
+++ /dev/null
@@ -1,50 +0,0 @@
-// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known
-function knownCharCodeAt(givenString, index) {
- const str = `${givenString}`;
- const end = str.length;
-
- const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
- let idx = index;
- while ((surrogatePairs.exec(str)) != null) {
- const li = surrogatePairs.lastIndex;
- if (li - 2 < idx) {
- idx += 1;
- } else {
- break;
- }
- }
-
- if (idx >= end || idx < 0) {
- return NaN;
- }
-
- const code = str.charCodeAt(idx);
-
- let high;
- let low;
- if (code >= 0xD800 && code <= 0xDBFF) {
- high = code;
- low = str.charCodeAt(idx + 1);
- // Go one further, since one of the "characters" is part of a surrogate pair
- return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
- }
- return code;
-}
-
-// See http://stackoverflow.com/a/38901550/796832
-// ES5/PhantomJS compatible version of spreading a string
-//
-// [...'foo'] -> ['f', 'o', 'o']
-// [...'๐Ÿ–๐Ÿฟ'] -> ['๐Ÿ–', '๐Ÿฟ']
-function spreadString(str) {
- const arr = [];
- let i = 0;
- while (!isNaN(knownCharCodeAt(str, i))) {
- const codePoint = knownCharCodeAt(str, i);
- arr.push(String.fromCodePoint(codePoint));
- i += 1;
- }
- return arr;
-}
-
-export default spreadString;
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 92f3bb3ff52..576b8a0425f 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -18,13 +18,13 @@
// Button does not change visibility. If button has icon - it changes chevron style.
//
// %div.js-toggle-container
- // %a.js-toggle-button
+ // %button.js-toggle-button
// %div.js-toggle-content
//
$('body').on('click', '.js-toggle-button', function(e) {
toggleContainer($(this).closest('.js-toggle-container'));
- const targetTag = e.target.tagName.toLowerCase();
+ const targetTag = e.currentTarget.tagName.toLowerCase();
if (targetTag === 'a' || targetTag === 'button') {
e.preventDefault();
}
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js
deleted file mode 100644
index ec1c018424d..00000000000
--- a/app/assets/javascripts/blob/blob_ci_yaml.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/* eslint-disable no-param-reassign, comma-dangle */
-/* global Api */
-
-require('./template_selector');
-
-((global) => {
- class BlobCiYamlSelector extends gl.TemplateSelector {
- requestFile(query) {
- return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
- }
-
- requestFileSuccess(file) {
- return super.requestFileSuccess(file);
- }
- }
-
- global.BlobCiYamlSelector = BlobCiYamlSelector;
-
- class BlobCiYamlSelectors {
- constructor({ editor, $dropdowns } = {}) {
- this.editor = editor;
- this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
- this.initSelectors();
- }
-
- initSelectors() {
- const editor = this.editor;
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- return new BlobCiYamlSelector({
- editor,
- pattern: /(.gitlab-ci.yml)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
- dropdown: $dropdown
- });
- });
- }
- }
-
- global.BlobCiYamlSelectors = BlobCiYamlSelectors;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js b/app/assets/javascripts/blob/blob_dockerfile_selector.js
deleted file mode 100644
index d4f60cc6ecd..00000000000
--- a/app/assets/javascripts/blob/blob_dockerfile_selector.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/* global Api */
-
-require('./template_selector');
-
-(() => {
- const global = window.gl || (window.gl = {});
-
- class BlobDockerfileSelector extends gl.TemplateSelector {
- requestFile(query) {
- return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this));
- }
-
- requestFileSuccess(file) {
- return super.requestFileSuccess(file);
- }
- }
-
- global.BlobDockerfileSelector = BlobDockerfileSelector;
-})();
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/blob_dockerfile_selectors.js
deleted file mode 100644
index 9cee79fa5d5..00000000000
--- a/app/assets/javascripts/blob/blob_dockerfile_selectors.js
+++ /dev/null
@@ -1,27 +0,0 @@
-(() => {
- const global = window.gl || (window.gl = {});
-
- class BlobDockerfileSelectors {
- constructor({ editor, $dropdowns } = {}) {
- this.editor = editor;
- this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
- this.initSelectors();
- }
-
- initSelectors() {
- const editor = this.editor;
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- return new gl.BlobDockerfileSelector({
- editor,
- pattern: /(Dockerfile)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
- dropdown: $dropdown,
- });
- });
- }
- }
-
- global.BlobDockerfileSelectors = BlobDockerfileSelectors;
-})();
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 8f6bf162d6e..c9fe23aec75 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,66 +1,63 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, max-len */
+/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
/* global Dropzone */
-(function() {
- this.BlobFileDropzone = (function() {
- function BlobFileDropzone(form, method) {
- var dropzone, form_dropzone, submitButton;
- form_dropzone = form.find('.dropzone');
- Dropzone.autoDiscover = false;
- dropzone = form_dropzone.dropzone({
- autoDiscover: false,
- autoProcessQueue: false,
- url: form.attr('action'),
- // Rails uses a hidden input field for PUT
- // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
- method: method,
- clickable: true,
- uploadMultiple: false,
- paramName: "file",
- maxFilesize: gon.max_file_size || 10,
- parallelUploads: 1,
- maxFiles: 1,
- addRemoveLinks: true,
- previewsContainer: '.dropzone-previews',
- headers: {
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
- },
- init: function() {
- this.on('addedfile', function(file) {
- $('.dropzone-alerts').html('').hide();
- });
- this.on('success', function(header, response) {
- window.location.href = response.filePath;
- });
- this.on('maxfilesexceeded', function(file) {
- this.removeFile(file);
- });
- return this.on('sending', function(file, xhr, formData) {
- formData.append('target_branch', form.find('input[name="target_branch"]').val());
- formData.append('create_merge_request', form.find('.js-create-merge-request').val());
- formData.append('commit_message', form.find('.js-commit-message').val());
- });
- },
- // Override behavior of adding error underneath preview
- error: function(file, errorMessage) {
- var stripped;
- stripped = $("<div/>").html(errorMessage).text();
- $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show();
+export default class BlobFileDropzone {
+ constructor(form, method) {
+ const formDropzone = form.find('.dropzone');
+ Dropzone.autoDiscover = false;
+
+ const dropzone = formDropzone.dropzone({
+ autoDiscover: false,
+ autoProcessQueue: false,
+ url: form.attr('action'),
+ // Rails uses a hidden input field for PUT
+ // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
+ method: method,
+ clickable: true,
+ uploadMultiple: false,
+ paramName: 'file',
+ maxFilesize: gon.max_file_size || 10,
+ parallelUploads: 1,
+ maxFiles: 1,
+ addRemoveLinks: true,
+ previewsContainer: '.dropzone-previews',
+ headers: {
+ 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'),
+ },
+ init: function () {
+ this.on('addedfile', function () {
+ $('.dropzone-alerts').html('').hide();
+ });
+ this.on('success', function (header, response) {
+ window.location.href = response.filePath;
+ });
+ this.on('maxfilesexceeded', function (file) {
this.removeFile(file);
- }
- });
- submitButton = form.find('#submit-all')[0];
- submitButton.addEventListener('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
- if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
- alert("Please select a file");
- }
- dropzone[0].dropzone.processQueue();
- return false;
- });
- }
+ });
+ this.on('sending', function (file, xhr, formData) {
+ formData.append('target_branch', form.find('input[name="target_branch"]').val());
+ formData.append('create_merge_request', form.find('.js-create-merge-request').val());
+ formData.append('commit_message', form.find('.js-commit-message').val());
+ });
+ },
+ // Override behavior of adding error underneath preview
+ error: function (file, errorMessage) {
+ const stripped = $('<div/>').html(errorMessage).text();
+ $('.dropzone-alerts').html(`Error uploading file: "${stripped}"`).show();
+ this.removeFile(file);
+ },
+ });
- return BlobFileDropzone;
- })();
-}).call(window);
+ const submitButton = form.find('#submit-all')[0];
+ submitButton.addEventListener('click', function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
+ // eslint-disable-next-line no-alert
+ alert('Please select a file');
+ }
+ dropzone[0].dropzone.processQueue();
+ return false;
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
deleted file mode 100644
index de20eab9cd1..00000000000
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ /dev/null
@@ -1,23 +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, prefer-rest-params */
-/* global Api */
-
-require('./template_selector');
-
-(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.BlobGitignoreSelector = (function(superClass) {
- extend(BlobGitignoreSelector, superClass);
-
- function BlobGitignoreSelector() {
- return BlobGitignoreSelector.__super__.constructor.apply(this, arguments);
- }
-
- BlobGitignoreSelector.prototype.requestFile = function(query) {
- return Api.gitignoreText(query.name, this.requestFileSuccess.bind(this));
- };
-
- return BlobGitignoreSelector;
- })(gl.TemplateSelector);
-}).call(window);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
deleted file mode 100644
index 43e5c0a5641..00000000000
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, max-len */
-/* global BlobGitignoreSelector */
-
-(function() {
- this.BlobGitignoreSelectors = (function() {
- function BlobGitignoreSelectors(opts) {
- var ref;
- this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitignore-selector'), this.editor = opts.editor;
- this.$dropdowns.each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return new BlobGitignoreSelector({
- pattern: /(.gitignore)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
- dropdown: $dropdown,
- editor: _this.editor
- });
- };
- })(this));
- }
-
- return BlobGitignoreSelectors;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
deleted file mode 100644
index b582052a76e..00000000000
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ /dev/null
@@ -1,28 +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, prefer-rest-params, comma-dangle */
-/* global Api */
-
-require('./template_selector');
-
-(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.BlobLicenseSelector = (function(superClass) {
- extend(BlobLicenseSelector, superClass);
-
- function BlobLicenseSelector() {
- return BlobLicenseSelector.__super__.constructor.apply(this, arguments);
- }
-
- BlobLicenseSelector.prototype.requestFile = function(query) {
- var data;
- data = {
- project: this.dropdown.data('project'),
- fullname: this.dropdown.data('fullname')
- };
- return Api.licenseText(query.id, data, this.requestFileSuccess.bind(this));
- };
-
- return BlobLicenseSelector;
- })(gl.TemplateSelector);
-}).call(window);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js
deleted file mode 100644
index c5067b0feae..00000000000
--- a/app/assets/javascripts/blob/blob_license_selectors.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable no-unused-vars, no-param-reassign */
-/* global BlobLicenseSelector */
-
-((global) => {
- class BlobLicenseSelectors {
- constructor({ $dropdowns, editor }) {
- this.$dropdowns = $('.js-license-selector');
- this.editor = editor;
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- return new BlobLicenseSelector({
- editor,
- pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-license-selector-wrap'),
- dropdown: $dropdown,
- });
- });
- }
- }
-
- global.BlobLicenseSelectors = BlobLicenseSelectors;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js
new file mode 100644
index 00000000000..9b8bfbfc8c0
--- /dev/null
+++ b/app/assets/javascripts/blob/notebook/index.js
@@ -0,0 +1,85 @@
+/* eslint-disable no-new */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import NotebookLab from 'vendor/notebooklab';
+
+Vue.use(VueResource);
+Vue.use(NotebookLab);
+
+export default () => {
+ const el = document.getElementById('js-notebook-viewer');
+
+ new Vue({
+ el,
+ data() {
+ return {
+ error: false,
+ loadError: false,
+ loading: true,
+ json: {},
+ };
+ },
+ template: `
+ <div class="container-fluid md prepend-top-default append-bottom-default">
+ <div
+ class="text-center loading"
+ v-if="loading && !error">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"
+ aria-label="iPython notebook loading">
+ </i>
+ </div>
+ <notebook-lab
+ v-if="!loading && !error"
+ :notebook="json"
+ code-css-class="code white" />
+ <p
+ class="text-center"
+ v-if="error">
+ <span v-if="loadError">
+ An error occured whilst loading the file. Please try again later.
+ </span>
+ <span v-else>
+ An error occured whilst parsing the file.
+ </span>
+ </p>
+ </div>
+ `,
+ methods: {
+ loadFile() {
+ this.$http.get(el.dataset.endpoint)
+ .then((res) => {
+ this.json = res.json();
+ this.loading = false;
+ })
+ .catch((e) => {
+ if (e.status) {
+ this.loadError = true;
+ }
+
+ this.error = true;
+ });
+ },
+ },
+ mounted() {
+ if (gon.katex_css_url) {
+ const katexStyles = document.createElement('link');
+ katexStyles.setAttribute('rel', 'stylesheet');
+ katexStyles.setAttribute('href', gon.katex_css_url);
+ document.head.appendChild(katexStyles);
+ }
+
+ if (gon.katex_js_url) {
+ const katexScript = document.createElement('script');
+ katexScript.addEventListener('load', () => {
+ this.loadFile();
+ });
+ katexScript.setAttribute('src', gon.katex_js_url);
+ document.head.appendChild(katexScript);
+ } else {
+ this.loadFile();
+ }
+ },
+ });
+};
diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js
new file mode 100644
index 00000000000..b7a0a195a92
--- /dev/null
+++ b/app/assets/javascripts/blob/notebook_viewer.js
@@ -0,0 +1,3 @@
+import renderNotebook from './notebook';
+
+document.addEventListener('DOMContentLoaded', renderNotebook);
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
deleted file mode 100644
index 7e03ec3b391..00000000000
--- a/app/assets/javascripts/blob/template_selector.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, no-param-reassign, max-len */
-
-((global) => {
- class TemplateSelector {
- constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
- this.onClick = this.onClick.bind(this);
- this.dropdown = dropdown;
- this.data = data;
- this.pattern = pattern;
- this.wrapper = wrapper;
- this.editor = editor;
- this.fileEndpoint = fileEndpoint;
- this.$input = $input || $('#file_name');
- this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
- this.buildDropdown();
- this.bindEvents();
- this.onFilenameUpdate();
-
- this.autosizeUpdateEvent = document.createEvent('Event');
- this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
- }
-
- buildDropdown() {
- return this.dropdown.glDropdown({
- data: this.data,
- filterable: true,
- selectable: true,
- toggleLabel: this.toggleLabel,
- search: {
- fields: ['name']
- },
- clicked: this.onClick,
- text: function(item) {
- return item.name;
- }
- });
- }
-
- bindEvents() {
- return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
- }
-
- toggleLabel(item) {
- return item.name;
- }
-
- onFilenameUpdate() {
- var filenameMatches;
- if (!this.$input.length) {
- return;
- }
- filenameMatches = this.pattern.test(this.$input.val().trim());
- if (!filenameMatches) {
- this.wrapper.addClass('hidden');
- return;
- }
- return this.wrapper.removeClass('hidden');
- }
-
- onClick(item, el, e) {
- e.preventDefault();
- return this.requestFile(item);
- }
-
- requestFile(item) {
- // This `requestFile` method is an abstract method that should
- // be added by all subclasses.
- }
-
- // To be implemented on the extending class
- // e.g.
- // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
- requestFileSuccess(file, { skipFocus } = {}) {
- if (!file) return;
-
- const oldValue = this.editor.getValue();
- const newValue = file.content;
-
- this.editor.setValue(newValue, 1);
- if (!skipFocus) this.editor.focus();
-
- if (this.editor instanceof jQuery) {
- this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
- }
- }
-
- startLoadingSpinner() {
- this.dropdownIcon
- .addClass('fa-spinner fa-spin')
- .removeClass('fa-chevron-down');
- }
-
- stopLoadingSpinner() {
- this.dropdownIcon
- .addClass('fa-chevron-down')
- .removeClass('fa-spinner fa-spin');
- }
- }
-
- global.TemplateSelector = TemplateSelector;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js
new file mode 100644
index 00000000000..5a5954e7751
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js
@@ -0,0 +1,9 @@
+/* global Api */
+
+import TemplateSelector from './template_selector';
+
+export default class BlobCiYamlSelector extends TemplateSelector {
+ requestFile(query) {
+ return Api.gitlabCiYml(query.name, (file, config) => this.setEditorContent(file, config));
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js
new file mode 100644
index 00000000000..7a4d6a42a03
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js
@@ -0,0 +1,23 @@
+/* global Api */
+
+import BlobCiYamlSelector from './blob_ci_yaml_selector';
+
+export default class BlobCiYamlSelectors {
+ constructor({ editor, $dropdowns }) {
+ this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
+ this.initSelectors(editor);
+ }
+
+ initSelectors(editor) {
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ return new BlobCiYamlSelector({
+ editor,
+ pattern: /(.gitlab-ci.yml)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
+ dropdown: $dropdown,
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js
new file mode 100644
index 00000000000..19f8820a0cb
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js
@@ -0,0 +1,9 @@
+/* global Api */
+
+import TemplateSelector from './template_selector';
+
+export default class BlobDockerfileSelector extends TemplateSelector {
+ requestFile(query) {
+ return Api.dockerfileYml(query.name, (file, config) => this.setEditorContent(file, config));
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js
new file mode 100644
index 00000000000..da067035b43
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js
@@ -0,0 +1,23 @@
+import BlobDockerfileSelector from './blob_dockerfile_selector';
+
+export default class BlobDockerfileSelectors {
+ constructor({ editor, $dropdowns }) {
+ this.editor = editor;
+ this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
+ this.initSelectors();
+ }
+
+ initSelectors() {
+ const editor = this.editor;
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ return new BlobDockerfileSelector({
+ editor,
+ pattern: /(Dockerfile)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
+ dropdown: $dropdown,
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js
new file mode 100644
index 00000000000..0b6b02fc2b3
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js
@@ -0,0 +1,9 @@
+/* global Api */
+
+import TemplateSelector from './template_selector';
+
+export default class BlobGitignoreSelector extends TemplateSelector {
+ requestFile(query) {
+ return Api.gitignoreText(query.name, (file, config) => this.setEditorContent(file, config));
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js
new file mode 100644
index 00000000000..dc485d97677
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js
@@ -0,0 +1,23 @@
+import BlobGitignoreSelector from './blob_gitignore_selector';
+
+export default class BlobGitignoreSelectors {
+ constructor({ editor, $dropdowns }) {
+ this.$dropdowns = $dropdowns || $('.js-gitignore-selector');
+ this.editor = editor;
+ this.initSelectors();
+ }
+
+ initSelectors() {
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+
+ return new BlobGitignoreSelector({
+ pattern: /(.gitignore)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
+ dropdown: $dropdown,
+ editor: this.editor,
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selector.js b/app/assets/javascripts/blob/template_selectors/blob_license_selector.js
new file mode 100644
index 00000000000..e9cb31cc2dc
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_license_selector.js
@@ -0,0 +1,13 @@
+/* global Api */
+
+import TemplateSelector from './template_selector';
+
+export default class BlobLicenseSelector extends TemplateSelector {
+ requestFile(query) {
+ const data = {
+ project: this.dropdown.data('project'),
+ fullname: this.dropdown.data('fullname'),
+ };
+ return Api.licenseText(query.id, data, (file, config) => this.setEditorContent(file, config));
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js
new file mode 100644
index 00000000000..a44f4f78b2d
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js
@@ -0,0 +1,24 @@
+/* eslint-disable no-unused-vars, no-param-reassign */
+
+import BlobLicenseSelector from './blob_license_selector';
+
+export default class BlobLicenseSelectors {
+ constructor({ $dropdowns, editor }) {
+ this.$dropdowns = $dropdowns || $('.js-license-selector');
+ this.initSelectors(editor);
+ }
+
+ initSelectors(editor) {
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+
+ return new BlobLicenseSelector({
+ editor,
+ pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-license-selector-wrap'),
+ dropdown: $dropdown,
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/template_selector.js b/app/assets/javascripts/blob/template_selectors/template_selector.js
new file mode 100644
index 00000000000..d7c1c32efbd
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/template_selector.js
@@ -0,0 +1,92 @@
+/* eslint-disable class-methods-use-this, no-unused-vars */
+
+export default class TemplateSelector {
+ constructor({ dropdown, data, pattern, wrapper, editor, $input } = {}) {
+ this.pattern = pattern;
+ this.editor = editor;
+ this.dropdown = dropdown;
+ this.$dropdownContainer = wrapper;
+ this.$filenameInput = $input || $('#file_name');
+ this.$dropdownIcon = $('.fa-chevron-down', dropdown);
+
+ this.initDropdown(dropdown, data);
+ this.listenForFilenameInput();
+ this.renderMatchedDropdown();
+ this.initAutosizeUpdateEvent();
+ }
+
+ initDropdown(dropdown, data) {
+ return $(dropdown).glDropdown({
+ data,
+ filterable: true,
+ selectable: true,
+ toggleLabel: item => item.name,
+ search: {
+ fields: ['name'],
+ },
+ clicked: (item, el, e) => this.fetchFileTemplate(item, el, e),
+ text: item => item.name,
+ });
+ }
+
+ initAutosizeUpdateEvent() {
+ this.autosizeUpdateEvent = document.createEvent('Event');
+ this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
+ }
+
+ listenForFilenameInput() {
+ return this.$filenameInput.on('keyup blur', e => this.renderMatchedDropdown(e));
+ }
+
+ renderMatchedDropdown() {
+ if (!this.$filenameInput.length) {
+ return null;
+ }
+
+ const filenameMatches = this.pattern.test(this.$filenameInput.val().trim());
+
+ if (!filenameMatches) {
+ return this.$dropdownContainer.addClass('hidden');
+ }
+ return this.$dropdownContainer.removeClass('hidden');
+ }
+
+ fetchFileTemplate(item, el, e) {
+ e.preventDefault();
+ return this.requestFile(item);
+ }
+
+ requestFile(item) {
+ // This `requestFile` method is an abstract method that should
+ // be added by all subclasses.
+ }
+
+ // To be implemented on the extending class
+ // e.g. Api.gitlabCiYml(query.name, file => this.setEditorContent(file));
+
+ setEditorContent(file, { skipFocus } = {}) {
+ if (!file) return;
+
+ const newValue = file.content;
+
+ this.editor.setValue(newValue, 1);
+
+ if (!skipFocus) this.editor.focus();
+
+ if (this.editor instanceof jQuery) {
+ this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
+ }
+ }
+
+ startLoadingSpinner() {
+ this.$dropdownIcon
+ .addClass('fa-spinner fa-spin')
+ .removeClass('fa-chevron-down');
+ }
+
+ stopLoadingSpinner() {
+ this.$dropdownIcon
+ .addClass('fa-chevron-down')
+ .removeClass('fa-spinner fa-spin');
+ }
+}
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
new file mode 100644
index 00000000000..c5deccf631e
--- /dev/null
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -0,0 +1,32 @@
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
+/* global EditBlob */
+/* global NewCommitForm */
+
+import EditBlob from './edit_blob';
+import BlobFileDropzone from '../blob/blob_file_dropzone';
+
+$(() => {
+ const editBlobForm = $('.js-edit-blob-form');
+ const uploadBlobForm = $('.js-upload-blob-form');
+
+ if (editBlobForm.length) {
+ const urlRoot = editBlobForm.data('relative-url-root');
+ const assetsPath = editBlobForm.data('assets-prefix');
+ const blobLanguage = editBlobForm.data('blob-language');
+
+ new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage);
+ new NewCommitForm(editBlobForm);
+ }
+
+ if (uploadBlobForm.length) {
+ const method = uploadBlobForm.data('method');
+
+ new BlobFileDropzone(uploadBlobForm, method);
+ new NewCommitForm(uploadBlobForm);
+
+ window.gl.utils.disableButtonIfEmptyField(
+ uploadBlobForm.find('.js-commit-message'),
+ '.btn-upload-file',
+ );
+ }
+});
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
deleted file mode 100644
index 0436bbb0eaf..00000000000
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
-/* global EditBlob */
-/* global NewCommitForm */
-
-require('./edit_blob');
-
-(function() {
- $(function() {
- var url = $(".js-edit-blob-form").data("relative-url-root");
- url += $(".js-edit-blob-form").data("assets-prefix");
-
- var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
- new NewCommitForm($('.js-edit-blob-form'));
- });
-}).call(window);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index a1127b9e30e..d3560d5df3b 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,88 +1,99 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, max-len */
/* global ace */
-/* global BlobGitignoreSelectors */
-
-(function() {
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
-
- this.EditBlob = (function() {
- function EditBlob(assets_path, ace_mode) {
- if (ace_mode == null) {
- ace_mode = null;
- }
- this.editModeLinkClickHandler = bind(this.editModeLinkClickHandler, this);
- ace.config.set("modePath", assets_path + "/ace");
- ace.config.loadModule("ace/ext/searchbox");
- this.editor = ace.edit("editor");
- this.editor.focus();
- if (ace_mode) {
- this.editor.getSession().setMode("ace/mode/" + ace_mode);
- }
- $('form').submit((function(_this) {
- return function() {
- return $("#file-content").val(_this.editor.getValue());
- };
- // Before a form submission, move the content from the Ace editor into the
- // submitted textarea
- })(this));
- this.initModePanesAndLinks();
- this.initSoftWrap();
- new gl.BlobLicenseSelectors({
- editor: this.editor
- });
+
+import BlobLicenseSelectors from '../blob/template_selectors/blob_license_selectors';
+import BlobGitignoreSelectors from '../blob/template_selectors/blob_gitignore_selectors';
+import BlobCiYamlSelectors from '../blob/template_selectors/blob_ci_yaml_selectors';
+import BlobDockerfileSelectors from '../blob/template_selectors/blob_dockerfile_selectors';
+
+export default class EditBlob {
+ constructor(assetsPath, aceMode) {
+ this.configureAceEditor(aceMode, assetsPath);
+ this.prepFileContentForSubmit();
+ this.initModePanesAndLinks();
+ this.initSoftWrap();
+ this.initFileSelectors();
+ }
+
+ configureAceEditor(aceMode, assetsPath) {
+ ace.config.set('modePath', `${assetsPath}/ace`);
+ ace.config.loadModule('ace/ext/searchbox');
+
+ this.editor = ace.edit('editor');
+ this.editor.focus();
+
+ if (aceMode) {
+ this.editor.getSession().setMode(`ace/mode/${aceMode}`);
+ }
+ }
+
+ prepFileContentForSubmit() {
+ $('form').submit(() => {
+ $('#file-content').val(this.editor.getValue());
+ });
+ }
+
+ initFileSelectors() {
+ this.blobTemplateSelectors = [
+ new BlobLicenseSelectors({
+ editor: this.editor,
+ }),
new BlobGitignoreSelectors({
- editor: this.editor
- });
- new gl.BlobCiYamlSelectors({
- editor: this.editor
- });
- new gl.BlobDockerfileSelectors({
- editor: this.editor
+ editor: this.editor,
+ }),
+ new BlobCiYamlSelectors({
+ editor: this.editor,
+ }),
+ new BlobDockerfileSelectors({
+ editor: this.editor,
+ }),
+ ];
+ }
+
+ initModePanesAndLinks() {
+ this.$editModePanes = $('.js-edit-mode-pane');
+ this.$editModeLinks = $('.js-edit-mode a');
+ this.$editModeLinks.on('click', e => this.editModeLinkClickHandler(e));
+ }
+
+ editModeLinkClickHandler(e) {
+ e.preventDefault();
+
+ const currentLink = $(e.target);
+ const paneId = currentLink.attr('href');
+ const currentPane = this.$editModePanes.filter(paneId);
+
+ this.$editModeLinks.parent().removeClass('active hover');
+
+ currentLink.parent().addClass('active hover');
+
+ this.$editModePanes.hide();
+
+ currentPane.fadeIn(200);
+
+ if (paneId === '#preview') {
+ this.$toggleButton.hide();
+ return $.post(currentLink.data('preview-url'), {
+ content: this.editor.getValue(),
+ }, (response) => {
+ currentPane.empty().append(response);
+ return currentPane.renderGFM();
});
}
- EditBlob.prototype.initModePanesAndLinks = function() {
- this.$editModePanes = $(".js-edit-mode-pane");
- this.$editModeLinks = $(".js-edit-mode a");
- return this.$editModeLinks.click(this.editModeLinkClickHandler);
- };
-
- EditBlob.prototype.editModeLinkClickHandler = function(event) {
- var currentLink, currentPane, paneId;
- event.preventDefault();
- currentLink = $(event.target);
- paneId = currentLink.attr("href");
- currentPane = this.$editModePanes.filter(paneId);
- this.$editModeLinks.parent().removeClass("active hover");
- currentLink.parent().addClass("active hover");
- this.$editModePanes.hide();
- currentPane.fadeIn(200);
- if (paneId === "#preview") {
- this.$toggleButton.hide();
- return $.post(currentLink.data("preview-url"), {
- content: this.editor.getValue()
- }, function(response) {
- currentPane.empty().append(response);
- return currentPane.renderGFM();
- });
- } else {
- this.$toggleButton.show();
- return this.editor.focus();
- }
- };
-
- EditBlob.prototype.initSoftWrap = function() {
- this.isSoftWrapped = false;
- this.$toggleButton = $('.soft-wrap-toggle');
- this.$toggleButton.on('click', this.toggleSoftWrap.bind(this));
- };
-
- EditBlob.prototype.toggleSoftWrap = function(e) {
- this.isSoftWrapped = !this.isSoftWrapped;
- this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
- this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
- };
-
- return EditBlob;
- })();
-}).call(window);
+ this.$toggleButton.show();
+
+ return this.editor.focus();
+ }
+
+ initSoftWrap() {
+ this.isSoftWrapped = false;
+ this.$toggleButton = $('.soft-wrap-toggle');
+ this.$toggleButton.on('click', () => this.toggleSoftWrap());
+ }
+
+ toggleSoftWrap() {
+ this.isSoftWrapped = !this.isSoftWrapped;
+ this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
+ this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
+ }
+}
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index 3874c2819a5..e057ac8df02 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -1,12 +1,11 @@
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
-/* global Vue */
/* global BoardService */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
require('./models/issue');
require('./models/label');
require('./models/list');
@@ -24,6 +23,8 @@ require('./components/new_list_dropdown');
require('./components/modal/index');
require('../vue_shared/vue_resource_interceptor');
+Vue.use(VueResource);
+
$(() => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
@@ -78,7 +79,7 @@ $(() => {
resp.json().forEach((board) => {
const list = Store.addList(board);
- if (list.type === 'done') {
+ if (list.type === 'closed') {
list.position = Infinity;
}
});
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 67c0c419713..35b3205cca7 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
-/* global Vue */
/* global Sortable */
+import Vue from 'vue';
import boardBlankState from './board_blank_state';
require('./board_delete');
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js
index 52893d4642b..3fc68457961 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js
+++ b/app/assets/javascripts/boards/components/board_blank_state.js
@@ -1,5 +1,7 @@
/* global ListLabel */
-/* global Cookies */
+
+import Cookies from 'js-cookie';
+
const Store = gl.issueBoards.BoardsStore;
export default {
diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js
index 4b72090df31..f591134c548 100644
--- a/app/assets/javascripts/boards/components/board_card.js
+++ b/app/assets/javascripts/boards/components/board_card.js
@@ -1,4 +1,3 @@
-/* global Vue */
require('./issue_card_inner');
const Store = gl.issueBoards.BoardsStore;
@@ -51,9 +50,7 @@ export default {
this.showDetail = false;
},
showIssue(e) {
- const targetTagName = e.target.tagName.toLowerCase();
-
- if (targetTagName === 'a' || targetTagName === 'button') return;
+ if (e.target.classList.contains('js-no-trigger')) return;
if (this.showDetail) {
this.showDetail = false;
diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js
index 861600424a5..af621cfd57f 100644
--- a/app/assets/javascripts/boards/components/board_delete.js
+++ b/app/assets/javascripts/boards/components/board_delete.js
@@ -1,5 +1,6 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-alert */
-/* global Vue */
+
+import Vue from 'vue';
(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index 1330d4ae840..86e6c26e570 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, space-before-function-paren, max-len */
-/* global Vue */
/* global Sortable */
+import Vue from 'vue';
import boardNewIssue from './board_new_issue';
import boardCard from './board_card';
@@ -48,7 +48,7 @@ import boardCard from './board_card';
this.list.getIssues(false);
}
- if (this.scrollHeight() > this.listHeight()) {
+ if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index dfc6eed785c..3c080008244 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -1,10 +1,11 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
-/* global Vue */
/* global IssuableContext */
/* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */
+import Vue from 'vue';
+
require('./sidebar/remove_issue');
(() => {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index 69e30cec4c5..a4629b092bf 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -1,4 +1,4 @@
-/* global Vue */
+import Vue from 'vue';
import eventHub from '../eventhub';
(() => {
@@ -84,20 +84,20 @@ import eventHub from '../eventhub';
#{{ issue.id }}
</span>
<a
- class="card-assignee has-tooltip"
+ class="card-assignee has-tooltip js-no-trigger"
:href="rootPath + issue.assignee.username"
:title="'Assigned to ' + issue.assignee.name"
v-if="issue.assignee"
data-container="body">
<img
- class="avatar avatar-inline s20"
+ class="avatar avatar-inline s20 js-no-trigger"
:src="issue.assignee.avatar"
width="20"
height="20"
:alt="'Avatar for ' + issue.assignee.name" />
</a>
<button
- class="label color-label has-tooltip"
+ class="label color-label has-tooltip js-no-trigger"
v-for="label in issue.labels"
type="button"
v-if="showLabel(label)"
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
index 9538f5b69e9..823319df6e7 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
const ModalStore = gl.issueBoards.ModalStore;
@@ -30,7 +31,7 @@
if (this.activeTab === 'selected') {
obj.title = 'You haven\'t selected any issues yet';
obj.content = `
- Go back to <strong>All issues</strong> and select some issues
+ Go back to <strong>Open issues</strong> and select some issues
to add to your board.
`;
}
@@ -59,7 +60,7 @@
class="btn btn-default"
@click="changeTab('all')"
v-if="activeTab === 'selected'">
- All issues
+ Open issues
</button>
</div>
</div>
diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js
index bd394a2318c..b214b5a7199 100644
--- a/app/assets/javascripts/boards/components/modal/filters.js
+++ b/app/assets/javascripts/boards/components/modal/filters.js
@@ -14,8 +14,10 @@ export default {
this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.removeTokens();
+ this.filteredSearch.handleInputPlaceholder();
+ this.filteredSearch.toggleClearSearchButton();
},
- beforeDestroy() {
+ destroyed() {
this.filteredSearch.cleanup();
FilteredSearchContainer.container = document;
this.store.path = '';
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index 1cbc422c961..887ce373096 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -1,7 +1,8 @@
/* eslint-disable no-new */
-/* global Vue */
/* global Flash */
+import Vue from 'vue';
+
require('./lists_dropdown');
(() => {
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index 1b66c8b922d..91c08cde13a 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -1,5 +1,6 @@
-/* global Vue */
/* global ListIssue */
+
+import Vue from 'vue';
import queryData from '../../utils/query_data';
require('./header');
@@ -64,7 +65,15 @@ require('./empty_state');
},
filter: {
handler() {
- this.loadIssues(true);
+ if (this.$el.tagName) {
+ this.page = 1;
+ this.filterLoading = true;
+
+ this.loadIssues(true)
+ .then(() => {
+ this.filterLoading = false;
+ });
+ }
},
deep: true,
},
@@ -115,6 +124,9 @@ require('./empty_state');
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
},
+ created() {
+ this.page = 1;
+ },
components: {
'modal-header': gl.issueBoards.ModalHeader,
'modal-list': gl.issueBoards.ModalList,
@@ -135,14 +147,14 @@ require('./empty_state');
:image="blankStateImage"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
- v-if="!loading && showList"></modal-list>
+ v-if="!loading && showList && !filterLoading"></modal-list>
<empty-state
v-if="showEmptyState"
:image="blankStateImage"
:new-issue-path="newIssuePath"></empty-state>
<section
class="add-issues-list text-center"
- v-if="loading">
+ v-if="loading || filterLoading">
<div class="add-issues-list-loading">
<i class="fa fa-spinner fa-spin"></i>
</div>
diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js
index 3730c1ecaeb..aba56d4aa31 100644
--- a/app/assets/javascripts/boards/components/modal/list.js
+++ b/app/assets/javascripts/boards/components/modal/list.js
@@ -1,6 +1,8 @@
-/* global Vue */
/* global ListIssue */
/* global bp */
+
+import Vue from 'vue';
+
(() => {
const ModalStore = gl.issueBoards.ModalStore;
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
index 3c05120a2da..9e9ed46ab8d 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
const ModalStore = gl.issueBoards.ModalStore;
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
index e8cb43f3503..23cb1b13d11 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ b/app/assets/javascripts/boards/components/modal/tabs.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
const ModalStore = gl.issueBoards.ModalStore;
@@ -23,7 +24,7 @@
href="#"
role="button"
@click.prevent="changeTab('all')">
- All issues
+ Open issues
<span class="badge">
{{ issuesCount }}
</span>
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index e74935e1cb0..772ea4c5565 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -1,6 +1,8 @@
/* eslint-disable no-new */
-/* global Vue */
/* global Flash */
+
+import Vue from 'vue';
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -46,7 +48,7 @@
template: `
<div
class="block list"
- v-if="list.type !== 'done'">
+ v-if="list.type !== 'closed'">
<button
class="btn btn-default btn-block"
type="button"
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 101732309ea..1264280284c 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -28,6 +28,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
[].forEach.call(tokens, (el) => {
el.parentNode.removeChild(el);
});
+
+ this.filteredSearchInput.value = '';
}
updateTokens() {
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js b/app/assets/javascripts/boards/filters/due_date_filters.js
index 03425bb145b..70132dbfa6f 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js
@@ -1,6 +1,7 @@
-/* global Vue */
/* global dateFormat */
+import Vue from 'vue';
+
Vue.filter('due-date', (value) => {
const date = new Date(value);
return dateFormat(date, 'mmm d, yyyy', true);
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index ca5e6fa7e9d..d6175069e37 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -1,9 +1,10 @@
/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
-/* global Vue */
/* global ListLabel */
/* global ListMilestone */
/* global ListUser */
+import Vue from 'vue';
+
class ListIssue {
constructor (obj) {
this.globalId = obj.id;
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index f18ad2a0fac..91e5fb2a666 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -10,7 +10,7 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
- this.preset = ['done', 'blank'].indexOf(this.type) > -1;
+ this.preset = ['closed', 'blank'].indexOf(this.type) > -1;
this.page = 1;
this.loading = true;
this.loadingMore = false;
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index e54102814d6..db9bced2f89 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -1,5 +1,6 @@
/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */
-/* global Vue */
+
+import Vue from 'vue';
class BoardService {
constructor (root, bulkUpdatePath, boardId) {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 28ecb322df7..bcda70d0638 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -1,7 +1,8 @@
/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */
-/* global Cookies */
/* global List */
+import Cookies from 'js-cookie';
+
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@@ -44,7 +45,7 @@
},
shouldAddBlankState () {
// Decide whether to add the blank state
- return !(this.state.lists.filter(list => list.type !== 'done')[0]);
+ return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
@@ -97,7 +98,7 @@
issueTo.removeLabel(listFrom.label);
}
- if (listTo.type === 'done') {
+ if (listTo.type === 'closed') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index 7ee266a831f..9b009483a3c 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -15,6 +15,7 @@
searchTerm: '',
loading: false,
loadingNewPage: false,
+ filterLoading: false,
page: 1,
perPage: 50,
filter: {
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index b5a988df897..a92e068ca5a 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -1,8 +1,11 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
+/* eslint-disable no-param-reassign */
+
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import CommitPipelinesTable from './pipelines_table';
+
+Vue.use(VueResource);
-window.Vue = require('vue');
-require('./pipelines_table');
/**
* Commits View > Pipelines Tab > Pipelines Table.
* Merge Request View > Pipelines Tab > Pipelines Table.
@@ -21,7 +24,7 @@ $(() => {
}
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView();
+ gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js b/app/assets/javascripts/commit/pipelines/pipelines_service.js
deleted file mode 100644
index 8ae98f9bf97..00000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_service.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/* 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_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index 631ed34851c..4d5a857d705 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -1,13 +1,12 @@
-/* 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('./pipelines_service');
-const PipelineStore = require('./pipelines_store');
+import Vue from 'vue';
+import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
+import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
+import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
+import eventHub from '../../vue_pipelines_index/event_hub';
+import EmptyState from '../../vue_pipelines_index/components/empty_state';
+import ErrorState from '../../vue_pipelines_index/components/error_state';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
/**
*
@@ -20,48 +19,74 @@ const PipelineStore = require('./pipelines_store');
* 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 || {};
+export default Vue.component('pipelines-table', {
+ components: {
+ 'pipelines-table-component': PipelinesTableComponent,
+ 'error-state': ErrorState,
+ 'empty-state': EmptyState,
+ },
+
+ /**
+ * Accesses the DOM to provide the needed data.
+ * Returns the necessary props to render `pipelines-table-component` component.
+ *
+ * @return {Object}
+ */
+ data() {
+ const store = new PipelineStore();
- gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
+ return {
+ endpoint: null,
+ helpPagePath: null,
+ store,
+ state: store.state,
+ isLoading: false,
+ hasError: false,
+ };
+ },
- components: {
- 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
+ computed: {
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
},
- /**
- * 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 store = new PipelineStore();
-
- return {
- endpoint: pipelinesTableData.endpoint,
- store,
- state: store.state,
- isLoading: false,
- };
+ shouldRenderEmptyState() {
+ return !this.state.pipelines.length && !this.isLoading;
},
+ },
+
+ /**
+ * When the component is about to be mounted, tell the service to fetch the data
+ *
+ * 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.
+ *
+ */
+ beforeMount() {
+ this.endpoint = this.$el.dataset.endpoint;
+ this.helpPagePath = this.$el.dataset.helpPagePath;
+ this.service = new PipelinesService(this.endpoint);
+
+ this.fetchPipelines();
+
+ eventHub.$on('refreshPipelines', this.fetchPipelines);
+ },
- /**
- * When the component is about to be mounted, tell the service to fetch the data
- *
- * 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.
- *
- */
- beforeMount() {
- const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
+ beforeUpdate() {
+ if (this.state.pipelines.length && this.$children) {
+ this.store.startTimeAgoLoops.call(this, Vue);
+ }
+ },
+ beforeDestroyed() {
+ eventHub.$off('refreshPipelines');
+ },
+
+ methods: {
+ fetchPipelines() {
this.isLoading = true;
- return pipelinesService.all()
+ return this.service.getPipelines()
.then(response => response.json())
.then((json) => {
// depending of the endpoint the response can either bring a `pipelines` key or not.
@@ -70,35 +95,30 @@ const PipelineStore = require('./pipelines_store');
this.isLoading = false;
})
.catch(() => {
+ this.hasError = true;
this.isLoading = false;
- new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert');
});
},
+ },
- beforeUpdate() {
- if (this.state.pipelines.length && this.$children) {
- PipelineStore.startTimeAgoLoops.call(this, Vue);
- }
- },
+ template: `
+ <div class="content-list pipelines">
+ <div class="realtime-loading" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+
+ <empty-state
+ v-if="shouldRenderEmptyState"
+ :help-page-path="helpPagePath" />
+
+ <error-state v-if="shouldRenderErrorState" />
- template: `
- <div class="pipelines">
- <div class="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"/>
- </div>
+ <div class="table-holder"
+ v-if="!isLoading && state.pipelines.length > 0">
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service" />
</div>
- `,
- });
-})();
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index fbd0db64ca7..3253eebd9b5 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -1,9 +1,11 @@
// ECMAScript polyfills
import 'core-js/fn/array/find';
+import 'core-js/fn/array/from';
import 'core-js/fn/object/assign';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
+import 'core-js/fn/symbol';
// Browser polyfills
import './polyfills/custom_event';
diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
new file mode 100644
index 00000000000..abe48572347
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
@@ -0,0 +1,17 @@
+export default {
+ props: {
+ count: {
+ type: Number,
+ required: true,
+ },
+ },
+ template: `
+ <span v-if="count === 50" class="events-info pull-right">
+ <i class="fa fa-warning has-tooltip"
+ aria-hidden="true"
+ title="Limited to showing 50 events at most"
+ data-placement="top"></i>
+ Showing 50 events
+ </span>
+ `,
+};
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
index b83a4c63fad..3f419a96ff9 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -13,6 +14,7 @@
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
index cb1687dcc7a..7ffa38edd9e 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -13,6 +14,7 @@
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
index 42e1bbce744..d736c8b0c28 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
@@ -19,12 +19,7 @@ import iconCommit from '../svg/icon_commit.svg';
<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>
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
index 73f4205b578..698a79ca68c 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -13,6 +14,7 @@
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
index 501ffb1fac9..e63c41f2a57 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -13,6 +14,7 @@
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
index 8fa63734cf1..d51f7134e25 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
@@ -17,6 +17,7 @@ import iconBranch from '../svg/icon_branch.svg';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
index 0015249cfaa..17ae3a9ddc1 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
@@ -18,6 +18,7 @@ import iconBranch from '../svg/icon_branch.svg';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
index 0d85e1a4678..b4442ea5566 100644
--- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index beff293b587..b099b39e58f 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -1,9 +1,9 @@
-/* global Vue */
-/* global Cookies */
/* global Flash */
-window.Vue = require('vue');
-window.Cookies = require('js-cookie');
+import Vue from 'vue';
+import Cookies from 'js-cookie';
+import LimitWarningComponent from './components/limit_warning_component';
+
require('./components/stage_code_component');
require('./components/stage_issue_component');
require('./components/stage_plan_component');
@@ -131,5 +131,6 @@ $(() => {
});
// Register global components
+ Vue.component('limit-warning', LimitWarningComponent);
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
});
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index cfa60325fcc..88180149715 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -33,11 +33,7 @@ class Diff {
handleClickUnfold(e) {
const $target = $(e.target);
- // current babel config relies on iterators implementation, so we cannot simply do:
- // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
- const ref = this.lineNumbers($target.parent());
- const oldLineNumber = ref[0];
- const newLineNumber = ref[1];
+ const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
const offset = newLineNumber - oldLineNumber;
const bottom = $target.hasClass('js-unfold-bottom');
let since;
@@ -105,10 +101,11 @@ class Diff {
}
lineNumbers(line) {
- if (!line.children().length) {
+ const children = line.find('.diff-line-num').toArray();
+ if (children.length !== 2) {
return [0, 0];
}
- return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10));
+ return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0);
}
highlightSelectedLine() {
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
index d948dff58ec..fc2f20e3bcb 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
@@ -1,6 +1,7 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */
/* global CommentsStore */
-const Vue = require('vue');
+
+import Vue from 'vue';
(() => {
const CommentAndResolveBtn = Vue.extend({
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index 788daa96b3d..0297add94d5 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -1,4 +1,6 @@
-/* global CommentsStore Cookies notes */
+/* global CommentsStore */
+/* global notes */
+
import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg';
@@ -25,6 +27,7 @@ import collapseIcon from '../icons/collapse_icon.svg';
role="button"
data-container="body"
data-placement="top"
+ data-html="true"
:data-line-type="lineType"
:title="note.authorName + ': ' + note.noteTruncated"
:src="note.authorAvatar"
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index 283dc330cad..8edc45130fc 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -1,7 +1,8 @@
/* 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 DiscussionMixins */
/* global CommentsStore */
-const Vue = require('vue');
+
+import Vue from 'vue';
(() => {
const JumpToDiscussion = Vue.extend({
diff --git a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
index e86bef47172..8eb0e10b832 100644
--- a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
@@ -1,6 +1,7 @@
-/* global Vue */
/* global CommentsStore */
+import Vue from 'vue';
+
(() => {
const NewIssueForDiscussion = Vue.extend({
props: {
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index fbd980f0fce..312f38ce241 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -2,7 +2,8 @@
/* global CommentsStore */
/* global ResolveService */
/* global Flash */
-const Vue = require('vue');
+
+import Vue from 'vue';
(() => {
const ResolveBtn = Vue.extend({
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js
index de9367f2136..27147ac6b5c 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js
@@ -1,7 +1,8 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
/* global DiscussionMixins */
/* global CommentsStore */
-const Vue = require('vue');
+
+import Vue from 'vue';
((w) => {
w.ResolveCount = Vue.extend({
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
index 7c5fcd04d2d..a964b7d0c6b 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
@@ -2,7 +2,7 @@
/* global CommentsStore */
/* global ResolveService */
-const Vue = require('vue');
+import Vue from 'vue';
(() => {
const ResolveDiscussionBtn = Vue.extend({
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index 4f6b86a917c..b6b47e2da6f 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -1,8 +1,8 @@
/* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */
-/* global Vue */
/* global ResolveCount */
-const Vue = require('vue');
+import Vue from 'vue';
+
require('./models/discussion');
require('./models/note');
require('./stores/comments');
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js
index dce1a9b58bd..dc43e4b2cc7 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js
+++ b/app/assets/javascripts/diff_notes/models/discussion.js
@@ -1,7 +1,8 @@
/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */
-/* global Vue */
/* global NoteModel */
+import Vue from 'vue';
+
class DiscussionModel {
constructor (discussionId) {
this.id = discussionId;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index 090c454e9e4..bfa4fc9037a 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -2,10 +2,13 @@
/* global Flash */
/* global CommentsStore */
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
require('../../vue_shared/vue_resource_interceptor');
+Vue.use(VueResource);
+
(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js
index 69c4d7a8434..e6cbda56c91 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js
+++ b/app/assets/javascripts/diff_notes/stores/comments.js
@@ -1,7 +1,8 @@
/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
-/* global Vue */
/* global DiscussionModel */
+import Vue from 'vue';
+
((w) => {
w.CommentsStore = {
state: {},
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index db1a2848d8d..80490052389 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,4 +1,3 @@
-import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make this a bundle
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global UsernameValidator */
/* global ActiveTabMemoizer */
@@ -34,6 +33,8 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make
/* global ProjectShow */
/* global Labels */
/* global Shortcuts */
+/* global Sidebar */
+
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
@@ -42,9 +43,9 @@ import GroupsList from './groups_list';
import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
+import UserCallout from './user_callout';
const ShortcutsBlob = require('./shortcuts_blob');
-const UserCallout = require('./user_callout');
(function() {
var Dispatcher;
@@ -119,6 +120,7 @@ const UserCallout = require('./user_callout');
case 'groups:milestones:show':
case 'dashboard:milestones:show':
new Milestone();
+ new Sidebar();
break;
case 'dashboard:todos:index':
new gl.Todos();
@@ -329,8 +331,6 @@ const UserCallout = require('./user_callout');
case 'ci:lints:show':
new gl.CILintEditor();
break;
- case 'projects:environments:metrics':
- new PrometheusGraph();
case 'users:show':
new UserCallout();
break;
diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js
index 9b40a3f20a4..7f7d93f3e27 100644
--- a/app/assets/javascripts/droplab/droplab_filter.js
+++ b/app/assets/javascripts/droplab/droplab_filter.js
@@ -56,10 +56,12 @@ require('../window')(function(w){
this.hookInput = hookInput;
this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper);
+ this.hookInput.trigger.addEventListener('mousedown.dl', this.keydownWrapper);
},
destroy: function destroy(){
this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper);
+ this.hookInput.trigger.removeEventListener('mousedown.dl', this.keydownWrapper);
}
};
});
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index fdbb4644971..db10b383913 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -132,7 +132,7 @@ class DueDateSelect {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
- this.$loading.fadeIn();
+ this.$loading.removeClass('hidden').fadeIn();
if (isDropdown) {
this.$dropdown.trigger('loading.gl.dropdown');
diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js
index 0923ce6b550..51aab8460f6 100644
--- a/app/assets/javascripts/environments/components/environment.js
+++ b/app/assets/javascripts/environments/components/environment.js
@@ -1,21 +1,18 @@
-/* eslint-disable no-param-reassign, no-new */
+/* eslint-disable no-new */
/* global Flash */
+import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table';
import EnvironmentsStore from '../stores/environments_store';
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
import eventHub from '../event_hub';
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-
export default Vue.component('environment-component', {
components: {
'environment-table': EnvironmentTable,
- 'table-pagination': gl.VueGlPagination,
+ 'table-pagination': TablePaginationComponent,
},
data() {
@@ -59,7 +56,6 @@ export default Vue.component('environment-component', {
canCreateEnvironmentParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
},
-
},
/**
diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js
index 455a8819549..385085c03e2 100644
--- a/app/assets/javascripts/environments/components/environment_actions.js
+++ b/app/assets/javascripts/environments/components/environment_actions.js
@@ -25,6 +25,12 @@ export default {
};
},
+ computed: {
+ title() {
+ return 'Deploy to...';
+ },
+ },
+
methods: {
onClickAction(endpoint) {
this.isLoading = true;
@@ -44,8 +50,11 @@ export default {
template: `
<div class="btn-group" role="group">
<button
- class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
+ class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
+ data-container="body"
data-toggle="dropdown"
+ :title="title"
+ :aria-label="title"
:disabled="isLoading">
<span>
<span v-html="playIconSvg"></span>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js
index a554998f52c..d79b916c360 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.js
+++ b/app/assets/javascripts/environments/components/environment_external_url.js
@@ -9,12 +9,21 @@ export default {
},
},
+ computed: {
+ title() {
+ return 'Open';
+ },
+ },
+
template: `
<a
- class="btn external_url"
+ class="btn external-url has-tooltip"
+ data-container="body"
:href="externalUrl"
target="_blank"
- title="Environment external URL">
+ rel="noopener noreferrer nofollow"
+ :title="title"
+ :aria-label="title">
<i class="fa fa-external-link" aria-hidden="true"></i>
</a>
`,
diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js
index 9d753b4f808..fcae5a55120 100644
--- a/app/assets/javascripts/environments/components/environment_item.js
+++ b/app/assets/javascripts/environments/components/environment_item.js
@@ -1,29 +1,29 @@
import Timeago from 'timeago.js';
+import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions';
import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button';
-import '../../lib/utils/text_utility';
-import '../../vue_shared/components/commit';
+import MonitoringButtonComponent from './environment_monitoring';
+import CommitComponent from '../../vue_shared/components/commit';
/**
* Envrionment Item Component
*
* Renders a table row for each environment.
*/
-
const timeagoInstance = new Timeago();
export default {
-
components: {
- 'commit-component': gl.CommitComponent,
+ 'commit-component': CommitComponent,
'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent,
'stop-component': StopComponent,
'rollback-component': RollbackComponent,
'terminal-button-component': TerminalButtonComponent,
+ 'monitoring-button-component': MonitoringButtonComponent,
},
props: {
@@ -395,6 +395,14 @@ export default {
return '';
},
+ monitoringUrl() {
+ if (this.model && this.model.metrics_path) {
+ return this.model.metrics_path;
+ }
+
+ return '';
+ },
+
/**
* Constructs folder URL based on the current location and the folder id.
*
@@ -499,13 +507,16 @@ export default {
<external-url-component v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/>
- <stop-component v-if="hasStopAction && canCreateDeployment"
- :stop-url="model.stop_path"
- :service="service"/>
+ <monitoring-button-component v-if="monitoringUrl && canReadEnvironment"
+ :monitoring-url="monitoringUrl"/>
<terminal-button-component v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/>
+ <stop-component v-if="hasStopAction && canCreateDeployment"
+ :stop-url="model.stop_path"
+ :service="service"/>
+
<rollback-component v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.js b/app/assets/javascripts/environments/components/environment_monitoring.js
new file mode 100644
index 00000000000..064e2fc7434
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_monitoring.js
@@ -0,0 +1,31 @@
+/**
+ * Renders the Monitoring (Metrics) link in environments table.
+ */
+export default {
+ props: {
+ monitoringUrl: {
+ type: String,
+ default: '',
+ required: true,
+ },
+ },
+
+ computed: {
+ title() {
+ return 'Monitoring';
+ },
+ },
+
+ template: `
+ <a
+ class="btn monitoring-url has-tooltip"
+ data-container="body"
+ :href="monitoringUrl"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ :title="title"
+ :aria-label="title">
+ <i class="fa fa-area-chart" aria-hidden="true"></i>
+ </a>
+ `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js
index 5404d647745..47102692024 100644
--- a/app/assets/javascripts/environments/components/environment_stop.js
+++ b/app/assets/javascripts/environments/components/environment_stop.js
@@ -25,6 +25,12 @@ export default {
};
},
+ computed: {
+ title() {
+ return 'Stop';
+ },
+ },
+
methods: {
onClick() {
if (confirm('Are you sure you want to stop this environment?')) {
@@ -45,10 +51,12 @@ export default {
template: `
<button type="button"
- class="btn stop-env-link"
+ class="btn stop-env-link has-tooltip"
+ data-container="body"
@click="onClick"
:disabled="isLoading"
- title="Stop Environment">
+ :title="title"
+ :aria-label="title">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.js
index 66a71faa02f..092a50a0d6f 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.js
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.js
@@ -14,12 +14,22 @@ export default {
},
data() {
- return { terminalIconSvg };
+ return {
+ terminalIconSvg,
+ };
+ },
+
+ computed: {
+ title() {
+ return 'Terminal';
+ },
},
template: `
- <a class="btn terminal-button"
- title="Open web terminal"
+ <a class="btn terminal-button has-tooltip"
+ data-container="body"
+ :title="title"
+ :aria-label="title"
:href="terminalPath">
${terminalIconSvg}
</a>
diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js
index 5f07b612b91..338dff40bc9 100644
--- a/app/assets/javascripts/environments/components/environments_table.js
+++ b/app/assets/javascripts/environments/components/environments_table.js
@@ -1,11 +1,11 @@
/**
* Render environments table.
*/
-import EnvironmentItem from './environment_item';
+import EnvironmentTableRowComponent from './environment_item';
export default {
components: {
- 'environment-item': EnvironmentItem,
+ 'environment-item': EnvironmentTableRowComponent,
},
props: {
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js
index 7abcf6dbbea..8abbcf0c227 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js
@@ -1,20 +1,17 @@
-/* eslint-disable no-param-reassign, no-new */
+/* eslint-disable no-new */
/* global Flash */
+import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table';
import EnvironmentsStore from '../stores/environments_store';
-
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
export default Vue.component('environment-folder-view', {
-
components: {
'environment-table': EnvironmentTable,
- 'table-pagination': gl.VueGlPagination,
+ 'table-pagination': TablePaginationComponent,
},
data() {
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 76296c83d11..07040bf0d73 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -1,5 +1,8 @@
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
export default class EnvironmentsService {
constructor(endpoint) {
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index d3fe3872c56..3c3084f3b78 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -1,5 +1,4 @@
import '~/lib/utils/common_utils';
-
/**
* Environments Store.
*
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index 134bdc6ad80..e7bf530d343 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -38,6 +38,7 @@
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
+ this.resetFilters();
this.dismissDropdown();
this.dispatchInputEvent();
}
@@ -107,7 +108,7 @@
const hook = this.getCurrentHook();
if (hook) {
- const data = hook.list.data;
+ const data = hook.list.data || [];
const results = data.map((o) => {
const updated = o;
updated.droplab_hidden = false;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 7ace51748aa..22352950452 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -40,6 +40,8 @@ import FilteredSearchContainer from './container';
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this);
+ this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
+ this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
this.filteredSearchInputForm = this.filteredSearchInput.form;
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
@@ -51,11 +53,13 @@ import FilteredSearchContainer from './container';
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
+ this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper);
+ document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
}
@@ -69,11 +73,13 @@ import FilteredSearchContainer from './container';
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
+ this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper);
+ document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
}
@@ -124,6 +130,26 @@ import FilteredSearchContainer from './container';
}
}
+ addInputContainerFocus() {
+ const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
+
+ if (inputContainer) {
+ inputContainer.classList.add('focus');
+ }
+ }
+
+ removeInputContainerFocus(e) {
+ const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
+ const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+ const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+ const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
+
+ if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
+ !isElementInStaticFilterDropdown && inputContainer) {
+ inputContainer.classList.remove('focus');
+ }
+ }
+
static selectToken(e) {
const button = e.target.closest('.selectable');
@@ -358,7 +384,7 @@ import FilteredSearchContainer from './container';
paths.push(`search=${sanitized}`);
}
- const parameterizedUrl = `?scope=all&utf8=โœ“&${paths.join('&')}`;
+ const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
if (this.updateObject) {
this.updateObject(parameterizedUrl);
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
index 9bf1b1ced88..a2729dc0e95 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
@@ -8,21 +8,31 @@ require('./filtered_search_token_keys');
// Values that start with a double quote must end in a double quote (same for single)
const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
const tokens = [];
+ const tokenIndexes = []; // stores key+value for simple search
let lastToken = null;
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
let tokenValue = v1 || v2 || v3;
let tokenSymbol = symbol;
+ let tokenIndex = '';
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
tokenSymbol = tokenValue;
tokenValue = '';
}
- tokens.push({
- key,
- value: tokenValue || '',
- symbol: tokenSymbol || '',
- });
+ tokenIndex = `${key}:${tokenValue}`;
+
+ // Prevent adding duplicates
+ if (tokenIndexes.indexOf(tokenIndex) === -1) {
+ tokenIndexes.push(tokenIndex);
+
+ tokens.push({
+ key,
+ value: tokenValue || '',
+ symbol: tokenSymbol || '',
+ });
+ }
+
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js
index 6a028f299b1..62675d7e67e 100644
--- a/app/assets/javascripts/group_name.js
+++ b/app/assets/javascripts/group_name.js
@@ -1,40 +1,64 @@
-const GROUP_LIMIT = 2;
+
+import _ from 'underscore';
export default class GroupName {
constructor() {
- this.titleContainer = document.querySelector('.title');
- this.groups = document.querySelectorAll('.group-path');
+ this.titleContainer = document.querySelector('.title-container');
+ this.title = document.querySelector('.title');
+ this.titleWidth = this.title.offsetWidth;
this.groupTitle = document.querySelector('.group-title');
+ this.groups = document.querySelectorAll('.group-path');
this.toggle = null;
this.isHidden = false;
this.init();
}
init() {
- if (this.groups.length > GROUP_LIMIT) {
+ if (this.groups.length > 0) {
this.groups[this.groups.length - 1].classList.remove('hidable');
- this.addToggle();
+ this.toggleHandler();
+ window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100));
}
this.render();
}
- addToggle() {
- const header = document.querySelector('.header-content');
+ toggleHandler() {
+ if (this.titleWidth > this.titleContainer.offsetWidth) {
+ if (!this.toggle) this.createToggle();
+ this.showToggle();
+ } else if (this.toggle) {
+ this.hideToggle();
+ }
+ }
+
+ createToggle() {
this.toggle = document.createElement('button');
this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path');
this.toggle.innerHTML = '...';
this.toggle.addEventListener('click', this.toggleGroups.bind(this));
- header.insertBefore(this.toggle, this.titleContainer);
+ this.titleContainer.insertBefore(this.toggle, this.title);
this.toggleGroups();
}
+ showToggle() {
+ this.title.classList.add('wrap');
+ this.toggle.classList.remove('hidden');
+ if (this.isHidden) this.groupTitle.classList.add('is-hidden');
+ }
+
+ hideToggle() {
+ this.title.classList.remove('wrap');
+ this.toggle.classList.add('hidden');
+ if (this.isHidden) this.groupTitle.classList.remove('is-hidden');
+ }
+
toggleGroups() {
this.isHidden = !this.isHidden;
this.groupTitle.classList.toggle('is-hidden');
}
render() {
- this.titleContainer.classList.remove('initializing');
+ this.title.classList.remove('initializing');
}
}
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index e5dfa30edab..602a3b78189 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -6,23 +6,60 @@ var slice = [].slice;
window.GroupsSelect = (function() {
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
+ const self = _this;
+
return function(i, select) {
var all_available, skip_groups;
- all_available = $(select).data('all-available');
- skip_groups = $(select).data('skip-groups') || [];
- return $(select).select2({
+ const $select = $(select);
+ all_available = $select.data('all-available');
+ skip_groups = $select.data('skip-groups') || [];
+
+ $select.select2({
placeholder: "Search for a group",
- multiple: $(select).hasClass('multiselect'),
+ multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
- query: function(query) {
- var options = { all_available: all_available, skip_groups: skip_groups };
- return Api.groups(query.term, options, function(groups) {
- var data;
- data = {
- results: groups
+ ajax: {
+ url: Api.buildUrl(Api.groupsPath),
+ dataType: 'json',
+ quietMillis: 250,
+ transport: function (params) {
+ $.ajax(params).then((data, status, xhr) => {
+ const results = data || [];
+
+ const headers = gl.utils.normalizeCRLFHeaders(xhr.getAllResponseHeaders());
+ const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
+ const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
+ const more = currentPage < totalPages;
+
+ return {
+ results,
+ pagination: {
+ more,
+ },
+ };
+ }).then(params.success).fail(params.error);
+ },
+ data: function (search, page) {
+ return {
+ search,
+ page,
+ per_page: GroupsSelect.PER_PAGE,
+ all_available,
+ skip_groups,
+ };
+ },
+ results: function (data, page) {
+ if (data.length) return { results: [] };
+
+ const results = data.length ? data : data.results || [];
+ const more = data.pagination ? data.pagination.more : false;
+
+ return {
+ results,
+ page,
+ more,
};
- return query.callback(data);
- });
+ },
},
initSelection: function(element, callback) {
var id;
@@ -34,19 +71,23 @@ window.GroupsSelect = (function() {
formatResult: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.formatResult.apply(_this, args);
+ return self.formatResult.apply(self, args);
},
formatSelection: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.formatSelection.apply(_this, args);
+ return self.formatSelection.apply(self, args);
},
- dropdownCssClass: "ajax-groups-dropdown",
+ dropdownCssClass: "ajax-groups-dropdown select2-infinite",
// we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
});
+
+ self.dropdown = document.querySelector('.select2-infinite .select2-results');
+
+ $select.on('select2-loaded', self.forceOverflow.bind(self));
};
})(this));
}
@@ -65,5 +106,12 @@ window.GroupsSelect = (function() {
return group.full_name;
};
+ GroupsSelect.prototype.forceOverflow = function (e) {
+ const itemHeight = this.dropdown.querySelector('.select2-result:first-child').clientHeight;
+ this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight - (itemHeight * 0.9))}px`;
+ };
+
+ GroupsSelect.PER_PAGE = 20;
+
return GroupsSelect;
})();
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 34f44dad7a5..dc170c60456 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */
$(document).on('todo:toggle', function(e, count) {
- var $todoPendingCount = $('.todos-pending-count');
+ var $todoPendingCount = $('.todos-count');
$todoPendingCount.text(gl.text.highCountTrim(count));
$todoPendingCount.toggleClass('hidden', count === 0);
});
diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
index 357b3487ca9..aec13e78f42 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
@@ -1,4 +1,4 @@
-/* global Vue */
+import Vue from 'vue';
import stopwatchSvg from 'icons/_icon_stopwatch.svg';
require('../../../lib/utils/pretty_time');
diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
index 750468c679b..c55e263f6f4 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
require('../../../lib/utils/pretty_time');
(() => {
diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
index 309e9f2f9ef..a7fbd704c40 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
Vue.component('time-tracking-estimate-only-pane', {
name: 'time-tracking-estimate-only-pane',
diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js b/app/assets/javascripts/issuable/time_tracking/components/help_state.js
index d7ced6d7151..344b29ebea4 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/help_state.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
Vue.component('time-tracking-help-state', {
name: 'time-tracking-help-state',
diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
index 1d2ca643b5b..b081adf5e64 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
Vue.component('time-tracking-no-tracking-pane', {
name: 'time-tracking-no-tracking-pane',
diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
index ed283fec3c3..edb9169112f 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
@@ -1,4 +1,5 @@
-/* global Vue */
+import Vue from 'vue';
+
(() => {
Vue.component('time-tracking-spent-only-pane', {
name: 'time-tracking-spent-only-pane',
diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
index 1fae2d62b14..0213522f551 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
+++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
@@ -1,4 +1,4 @@
-/* global Vue */
+import Vue from 'vue';
require('./help_state');
require('./collapsed_state');
diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
index 0134b7cb6f3..1689a69e1ed 100644
--- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
+++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
@@ -1,11 +1,12 @@
-/* global Vue */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
require('./components/time_tracker');
require('../../smart_interval');
require('../../subbable_resource');
+Vue.use(VueResource);
+
(() => {
/* This Vue instance represents what will become the parent instance for the
* sidebar. It will be responsible for managing `issuable` state and propagating
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 115312d4b83..834b98e8601 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,8 +1,9 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
/* global UsersSelect */
-/* global Cookies */
/* global bp */
+import Cookies from 'js-cookie';
+
(function() {
this.IssuableContext = (function() {
function IssuableContext(currentUser) {
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index ef4029a8623..47e675f537e 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -2,6 +2,7 @@
/* global Flash */
require('./flash');
+require('~/lib/utils/text_utility');
require('vendor/jquery.waitforimages');
require('./task_list');
@@ -50,20 +51,21 @@ class Issue {
success: function(data, textStatus, jqXHR) {
if ('id' in data) {
$(document).trigger('issuable:change');
- const currentTotal = Number($('.issue_counter').text());
+ let total = Number($('.issue_counter').text().replace(/[^\d]/, ''));
if (isClose) {
$('a.btn-close').addClass('hidden');
$('a.btn-reopen').removeClass('hidden');
$('div.status-box-closed').removeClass('hidden');
$('div.status-box-open').addClass('hidden');
- $('.issue_counter').text(currentTotal - 1);
+ total -= 1;
} else {
$('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden');
- $('.issue_counter').text(currentTotal + 1);
+ total += 1;
}
+ $('.issue_counter').text(gl.text.addDelimiter(total));
} else {
new Flash(issueFailMessage, 'alert');
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index c648a0f076c..443fb3e0ca9 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -76,7 +76,7 @@
if (!selected.length) {
data[abilityName].label_ids = [''];
}
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 08ca9e4fa4d..a5f99bcdd8f 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -11,8 +11,9 @@
});
};
- $(function() {
- var $scrollingTabs = $('.scrolling-tabs');
+ $(document).on('init.scrolling-tabs', () => {
+ const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
+ $scrollingTabs.addClass('is-initialized');
hideEndFade($scrollingTabs);
$(window).off('resize.nav').on('resize.nav', function() {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index a1423b6fda5..46b80c04e20 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -232,6 +232,22 @@
};
/**
+ this will take in the getAllResponseHeaders result and normalize them
+ this way we don't run into production issues when nginx gives us lowercased header keys
+ */
+ w.gl.utils.normalizeCRLFHeaders = (headers) => {
+ const headersObject = {};
+ const headersArray = headers.split('\n');
+
+ headersArray.forEach((header) => {
+ const keyValue = header.split(': ');
+ headersObject[keyValue[0]] = keyValue[1];
+ });
+
+ return w.gl.utils.normalizeHeaders(headersObject);
+ };
+
+ /**
* Parses pagination object string values into numbers.
*
* @param {Object} paginationInformation
@@ -247,7 +263,7 @@
});
/**
- * Updates the search parameter of a URL given the parameter and values provided.
+ * Updates the search parameter of a URL given the parameter and value provided.
*
* If no search params are present we'll add it.
* If param for page is already present, we'll update it
@@ -262,17 +278,24 @@
let search;
const locationSearch = window.location.search;
- if (locationSearch.length === 0) {
- search = `?${param}=${value}`;
- }
+ if (locationSearch.length) {
+ const parameters = locationSearch.substring(1, locationSearch.length)
+ .split('&')
+ .reduce((acc, element) => {
+ const val = element.split('=');
+ acc[val[0]] = decodeURIComponent(val[1]);
+ return acc;
+ }, {});
- if (locationSearch.indexOf(param) !== -1) {
- const regex = new RegExp(param + '=\\d');
- search = locationSearch.replace(regex, `${param}=${value}`);
- }
+ parameters[param] = value;
- if (locationSearch.length && locationSearch.indexOf(param) === -1) {
- search = `${locationSearch}&${param}=${value}`;
+ const toString = Object.keys(parameters)
+ .map(val => `${val}=${encodeURIComponent(parameters[val])}`)
+ .join('&');
+
+ search = `?${toString}`;
+ } else {
+ search = `?${param}=${value}`;
}
return search;
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
new file mode 100644
index 00000000000..5c22aea51cd
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -0,0 +1,100 @@
+import httpStatusCodes from './http_status';
+
+/**
+ * Polling utility for handling realtime updates.
+ * Service for vue resouce and method need to be provided as props
+ *
+ * @example
+ * new Poll({
+ * resource: resource,
+ * method: 'name',
+ * data: {page: 1, scope: 'all'}, // optional
+ * successCallback: () => {},
+ * errorCallback: () => {},
+ * notificationCallback: () => {}, // optional
+ * }).makeRequest();
+ *
+ * Usage in pipelines table with visibility lib:
+ *
+ * const poll = new Poll({
+ * resource: this.service,
+ * method: 'getPipelines',
+ * data: { page: pageNumber, scope },
+ * successCallback: this.successCallback,
+ * errorCallback: this.errorCallback,
+ * notificationCallback: this.updateLoading,
+ * });
+ *
+ * if (!Visibility.hidden()) {
+ * poll.makeRequest();
+ * }
+ *
+ * Visibility.change(() => {
+ * if (!Visibility.hidden()) {
+ * poll.restart();
+ * } else {
+ * poll.stop();
+ * }
+* });
+ *
+ * 1. Checks for response and headers before start polling
+ * 2. Interval is provided by `Poll-Interval` header.
+ * 3. If `Poll-Interval` is -1, we stop polling
+ * 4. If HTTP response is 200, we poll.
+ * 5. If HTTP response is different from 200, we stop polling.
+ *
+ */
+export default class Poll {
+ constructor(options = {}) {
+ this.options = options;
+ this.options.data = options.data || {};
+ this.options.notificationCallback = options.notificationCallback ||
+ function notificationCallback() {};
+
+ this.intervalHeader = 'POLL-INTERVAL';
+ this.timeoutID = null;
+ this.canPoll = true;
+ }
+
+ checkConditions(response) {
+ const headers = gl.utils.normalizeHeaders(response.headers);
+ const pollInterval = parseInt(headers[this.intervalHeader], 10);
+
+ if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
+ this.timeoutID = setTimeout(() => {
+ this.makeRequest();
+ }, pollInterval);
+ }
+
+ this.options.successCallback(response);
+ }
+
+ makeRequest() {
+ const { resource, method, data, errorCallback, notificationCallback } = this.options;
+
+ // It's called everytime a new request is made. Useful to update the status.
+ notificationCallback(true);
+
+ return resource[method](data)
+ .then(response => this.checkConditions(response))
+ .catch(error => errorCallback(error));
+ }
+
+ /**
+ * Stops the polling recursive chain
+ * and guarantees if the timeout is already running it won't make another request by
+ * cancelling the previously established timeout.
+ */
+ stop() {
+ this.canPoll = false;
+ clearTimeout(this.timeoutID);
+ }
+
+ /**
+ * Restarts polling after it has been stoped
+ */
+ restart() {
+ this.canPoll = true;
+ this.makeRequest();
+ }
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 81d5748191d..665a59f3183 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,6 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
/* global bp */
-/* global Cookies */
/* global Flash */
/* global ConfirmDangerModal */
/* global Aside */
@@ -24,7 +23,6 @@ import './extensions/array';
window.jQuery = jQuery;
window.$ = jQuery;
window._ = _;
-window.Cookies = Cookies;
window.Pikaday = Pikaday;
window.Dropzone = Dropzone;
window.Sortable = Sortable;
@@ -49,15 +47,6 @@ import { installGlEmojiElement } from './behaviors/gl_emoji';
installGlEmojiElement();
// blob
-import './blob/blob_ci_yaml';
-import './blob/blob_dockerfile_selector';
-import './blob/blob_dockerfile_selectors';
-import './blob/blob_file_dropzone';
-import './blob/blob_gitignore_selector';
-import './blob/blob_gitignore_selectors';
-import './blob/blob_license_selector';
-import './blob/blob_license_selectors';
-import './blob/template_selector';
import './blob/create_branch_dropdown';
import './blob/target_branch_dropdown';
@@ -381,4 +370,6 @@ $(function () {
new Aside();
gl.utils.initTimeagoTimeout();
+
+ $(document).trigger('init.scrolling-tabs');
});
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
index c7e78fed8fe..645045fea88 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
@@ -1,8 +1,9 @@
/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */
-/* global Vue */
/* global ace */
/* global Flash */
+import Vue from 'vue';
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
index 240c8f98932..56d6678e1bd 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign, comma-dangle */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
index 97753c50b60..0fc4a13450a 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign, comma-dangle */
-/* global Vue */
+
+import Vue from 'vue';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index 74587df22c5..c4e379a4a0b 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -1,6 +1,7 @@
/* eslint-disable comma-dangle, object-shorthand, no-param-reassign, camelcase, no-nested-ternary, no-continue, max-len */
-/* global Cookies */
-/* global Vue */
+
+import Vue from 'vue';
+import Cookies from 'js-cookie';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 653e52fb6bf..15992460146 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -1,8 +1,8 @@
/* eslint-disable new-cap, comma-dangle, no-new */
-/* global Vue */
/* global Flash */
-window.Vue = require('vue');
+import Vue from 'vue';
+
require('./merge_conflict_store');
require('./merge_conflict_service');
require('./mixins/line_conflict_utils');
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 190336dbd20..3c4e6102469 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,11 +1,13 @@
/* eslint-disable no-new, class-methods-use-this */
/* global Breakpoints */
-/* global Cookies */
/* global Flash */
-require('./breakpoints');
-window.Cookies = require('js-cookie');
-require('./flash');
+import Cookies from 'js-cookie';
+
+import CommitPipelinesTable from './commit/pipelines/pipelines_table';
+
+import './breakpoints';
+import './flash';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -97,6 +99,13 @@ require('./flash');
.off('click', this.clickTab);
}
+ destroy() {
+ this.unbindEvents();
+ if (this.commitPipelinesTable) {
+ this.commitPipelinesTable.$destroy();
+ }
+ }
+
showTab(e) {
e.preventDefault();
this.activateTab($(e.target).data('action'));
@@ -127,16 +136,9 @@ require('./flash');
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
- $.scrollTo('.merge-request-details .merge-request-tabs', {
- offset: 0,
- });
} else if (action === 'pipelines') {
- if (this.pipelinesLoaded) {
- return;
- }
- const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
- this.pipelinesLoaded = true;
+ this.resetViewContainer();
+ this.loadPipelines();
} else {
this.expandView();
this.resetViewContainer();
@@ -225,6 +227,18 @@ require('./flash');
});
}
+ loadPipelines() {
+ if (this.pipelinesLoaded) {
+ return;
+ }
+ const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
+ // Could already be mounted from the `pipelines_bundle`
+ if (pipelineTableViewEl) {
+ this.commitPipelinesTable = new CommitPipelinesTable().$mount(pipelineTableViewEl);
+ }
+ this.pipelinesLoaded = true;
+ }
+
loadDiff(source) {
if (this.diffsLoaded) {
return;
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index 94a4f24f1d7..0e2af3df071 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -14,13 +14,13 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
<%= ci_success_icon %>
<span>
Deployed to
- <a href="<%- url %>" target="_blank" class="environment">
+ <a href="<%- url %>" target="_blank" rel="noopener noreferrer" class="environment">
<%- name %>
</a>
<span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>">
<%- deployed_at %>
</span>
- <a class="js-environment-link" href="<%- external_url %>" target="_blank">
+ <a class="js-environment-link" href="<%- external_url %>" target="_blank" rel="noopener noreferrer">
<i class="fa fa-external-link"></i>
View on <%- external_url_formatted %>
</a>
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 02ff6f5682c..ac4fad88fe5 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,8 +1,9 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
-/* global Vue */
/* global Issuable */
/* global ListMilestone */
+import Vue from 'vue';
+
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els) {
@@ -159,7 +160,7 @@
}
$dropdown.trigger('loading.gl.dropdown');
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
@@ -171,7 +172,7 @@
data = {};
data[abilityName] = {};
data[abilityName].milestone_id = selected != null ? selected : null;
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
new file mode 100644
index 00000000000..b3ce9310417
--- /dev/null
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -0,0 +1,6 @@
+import PrometheusGraph from './prometheus_graph';
+
+document.addEventListener('DOMContentLoaded', function onLoad() {
+ document.removeEventListener('DOMContentLoaded', onLoad, false);
+ return new PrometheusGraph();
+}, false);
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index 71eb746edac..844a0785bc9 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -1,11 +1,10 @@
-/* eslint-disable no-new */
+/* eslint-disable no-new*/
/* global Flash */
import d3 from 'd3';
-import _ from 'underscore';
import statusCodes from '~/lib/utils/http_status';
-import '~/lib/utils/common_utils';
-import '~/flash';
+import '../lib/utils/common_utils';
+import '../flash';
const prometheusGraphsContainer = '.prometheus-graph';
const metricsEndpoint = 'metrics.json';
@@ -31,22 +30,21 @@ class PrometheusGraph {
}
createGraph() {
- const self = this;
- _.each(this.data, (value, key) => {
- if (value.length > 0 && (key === 'cpu_values' || key === 'memory_values')) {
- self.plotValues(value, key);
+ Object.keys(this.data).forEach((key) => {
+ const value = this.data[key];
+ if (value.length > 0) {
+ this.plotValues(value, key);
}
});
}
init() {
- const self = this;
this.getData().then((metricsResponse) => {
- if (metricsResponse === {}) {
+ if (Object.keys(metricsResponse).length === 0) {
new Flash('Empty metrics', 'alert');
} else {
- self.transformData(metricsResponse);
- self.createGraph();
+ this.transformData(metricsResponse);
+ this.createGraph();
}
});
}
@@ -182,7 +180,7 @@ class PrometheusGraph {
// Metric Usage
axisLabelContainer.append('rect')
.attr('x', this.originalWidth - 170)
- .attr('y', (this.originalHeight / 2) - 80)
+ .attr('y', (this.originalHeight / 2) - 60)
.style('fill', graphSpecifics.area_fill_color)
.attr('width', 20)
.attr('height', 35);
@@ -190,13 +188,13 @@ class PrometheusGraph {
axisLabelContainer.append('text')
.attr('class', 'label-axis-text')
.attr('x', this.originalWidth - 140)
- .attr('y', (this.originalHeight / 2) - 65)
- .text(graphSpecifics.graph_legend_title);
+ .attr('y', (this.originalHeight / 2) - 50)
+ .text('Average');
axisLabelContainer.append('text')
.attr('class', 'text-metric-usage')
.attr('x', this.originalWidth - 140)
- .attr('y', (this.originalHeight / 2) - 50);
+ .attr('y', (this.originalHeight / 2) - 25);
}
handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) {
@@ -265,12 +263,12 @@ class PrometheusGraph {
cpu_values: {
area_fill_color: '#edf3fc',
line_color: '#5b99f7',
- graph_legend_title: 'CPU Usage (Cores)',
+ graph_legend_title: 'CPU utilization (%)',
},
memory_values: {
area_fill_color: '#fca326',
line_color: '#fc6d26',
- graph_legend_title: 'Memory Usage (MB)',
+ graph_legend_title: 'Memory usage (MB)',
},
};
@@ -321,12 +319,14 @@ class PrometheusGraph {
transformData(metricsResponse) {
const metricTypes = {};
- _.each(metricsResponse.metrics, (value, key) => {
- const metricValues = value[0].values;
- metricTypes[key] = _.map(metricValues, metric => ({
- time: new Date(metric[0] * 1000),
- value: metric[1],
- }));
+ Object.keys(metricsResponse.metrics).forEach((key) => {
+ if (key === 'cpu_values' || key === 'memory_values') {
+ const metricValues = (metricsResponse.metrics[key])[0];
+ metricTypes[key] = metricValues.values.map(metric => ({
+ time: new Date(metric[0] * 1000),
+ value: metric[1],
+ }));
+ }
});
this.data = metricTypes;
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 47cc34e7a20..1d563c63f39 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,14 +1,14 @@
/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */
/* global Flash */
/* global Autosave */
-/* global Cookies */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
+import Cookies from 'js-cookie';
+
require('./autosave');
window.autosize = require('vendor/autosize');
window.Dropzone = require('dropzone');
-window.Cookies = require('js-cookie');
require('./dropzone_input');
require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index c38bc762675..4ccea0624ee 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -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/project.js b/app/assets/javascripts/project.js
index db7ceaa2421..f944fcc5a58 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,7 +1,8 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
-/* global Cookies */
/* global ProjectSelect */
+import Cookies from 'js-cookie';
+
(function() {
this.Project = (function() {
function Project() {
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
index 5cf28aa7a73..1d4bb8a13d6 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
@@ -6,7 +6,7 @@ class ProtectedBranchDropdown {
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
- this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch');
+ this.$protectedBranch = this.$dropdownContainer.find('.js-create-new-protected-branch');
this.buildDropdown();
this.bindEvents();
@@ -46,7 +46,9 @@ class ProtectedBranchDropdown {
this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
}
- onClickCreateWildcard() {
+ onClickCreateWildcard(e) {
+ e.preventDefault();
+
// Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
@@ -69,7 +71,7 @@ class ProtectedBranchDropdown {
if (branchName) {
this.$dropdownContainer
- .find('.create-new-protected-branch code')
+ .find('.js-create-new-protected-branch code')
.text(branchName);
}
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
index 48cae8a4fa9..ea91aaa10a6 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/render_gfm.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */
+
// Render Gitlab flavoured Markdown
//
// Delegates to syntax highlight and render math
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
index 76c61c001ba..8b3fee49cb9 100644
--- a/app/assets/javascripts/render_math.js
+++ b/app/assets/javascripts/render_math.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len, no-console */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len, no-console */
+/* global katex */
+
// Renders math using KaTeX in any element with the
// `js-render-math` class
//
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 903862cac6b..a9b3de281e1 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */
-/* global Cookies */
+
+import Cookies from 'js-cookie';
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -55,14 +56,15 @@
Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget);
- $todoLoading = $('.js-issuable-todo-loading');
- $btnText = $('.js-issuable-todo-text', $this);
ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST';
if ($this.attr('data-delete-path')) {
url = "" + ($this.attr('data-delete-path'));
} else {
url = "" + ($this.data('url'));
}
+
+ $this.tooltip('hide');
+
return $.ajax({
url: url,
type: ajaxType,
@@ -73,34 +75,44 @@
},
beforeSend: (function(_this) {
return function() {
- return _this.beforeTodoSend($this, $todoLoading);
+ $('.js-issuable-todo').disable()
+ .addClass('is-loading');
};
})(this)
}).done((function(_this) {
return function(data) {
- return _this.todoUpdateDone(data, $this, $btnText, $todoLoading);
+ return _this.todoUpdateDone(data);
};
})(this));
};
- Sidebar.prototype.beforeTodoSend = function($btn, $todoLoading) {
- $btn.disable();
- return $todoLoading.removeClass('hidden');
- };
+ Sidebar.prototype.todoUpdateDone = function(data) {
+ const deletePath = data.delete_path ? data.delete_path : null;
+ const attrPrefix = deletePath ? 'mark' : 'todo';
+ const $todoBtns = $('.js-issuable-todo');
- Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) {
$(document).trigger('todo:toggle', data.count);
- $btn.enable();
- $todoLoading.addClass('hidden');
+ $todoBtns.each((i, el) => {
+ const $el = $(el);
+ const $elText = $el.find('.js-issuable-todo-inner');
- if (data.delete_path != null) {
- $btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path);
- return $btnText.text($btn.data('mark-text'));
- } else {
- $btn.attr('aria-label', $btn.data('todo-text')).removeAttr('data-delete-path');
- return $btnText.text($btn.data('todo-text'));
- }
+ $el.removeClass('is-loading')
+ .enable()
+ .attr('aria-label', $el.data(`${attrPrefix}-text`))
+ .attr('data-delete-path', deletePath)
+ .attr('title', $el.data(`${attrPrefix}-text`));
+
+ if ($el.hasClass('has-tooltip')) {
+ $el.tooltip('fixTitle');
+ }
+
+ if ($el.data(`${attrPrefix}-icon`)) {
+ $elText.html($el.data(`${attrPrefix}-icon`));
+ } else {
+ $elText.text($el.data(`${attrPrefix}-text`));
+ }
+ });
};
Sidebar.prototype.sidebarDropdownLoading = function(e) {
@@ -197,9 +209,9 @@
};
Sidebar.prototype.setSidebarHeight = function() {
- const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
+ const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + $('.sub-nav-scroll').outerHeight();
const $rightSidebar = $('.js-right-sidebar');
- const diff = $navHeight - $('body').scrollTop();
+ const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
$rightSidebar.outerHeight($(window).height() - diff);
} else {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 81766f4bd55..fd5097696ad 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -33,6 +33,10 @@
};
Shortcuts.prototype.toggleMarkdownPreview = function(e) {
+ // Check if short-cut was triggered while in Write Mode
+ if ($(e.target).hasClass('js-note-text')) {
+ $('.js-md-preview-button').focus();
+ }
return $(document).triggerHandler('markdown-preview:toggle', [e]);
};
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index e7baea894f6..4f1a19924a4 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -22,6 +22,9 @@ require('./shortcuts');
Mousetrap.bind('g m', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests');
});
+ Mousetrap.bind('g t', function() {
+ return ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-todos');
+ });
Mousetrap.bind('g p', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects');
});
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 09a58cad2b2..3f5d6724417 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -43,6 +43,9 @@ require('./shortcuts');
Mousetrap.bind('g m', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests');
});
+ Mousetrap.bind('g t', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-todos');
+ });
Mousetrap.bind('g w', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki');
});
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
index 62d1604fe9e..9c307915ec4 100644
--- a/app/assets/javascripts/subscription.js
+++ b/app/assets/javascripts/subscription.js
@@ -1,4 +1,4 @@
-/* global Vue */
+import Vue from 'vue';
(() => {
class Subscription {
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js
index e9e9aafd71a..32067ed1fee 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js
+++ b/app/assets/javascripts/templates/issuable_template_selector.js
@@ -1,15 +1,15 @@
/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
/* global Api */
-require('../blob/template_selector');
+import TemplateSelector from '../blob/template_selectors/template_selector';
((global) => {
- class IssuableTemplateSelector extends gl.TemplateSelector {
+ class IssuableTemplateSelector extends TemplateSelector {
constructor(...args) {
super(...args);
this.projectPath = this.dropdown.data('project-path');
this.namespacePath = this.dropdown.data('namespace-path');
- this.issuableType = this.wrapper.data('issuable-type');
+ this.issuableType = this.$dropdownContainer.data('issuable-type');
this.titleInput = $(`#${this.issuableType}_title`);
const initialQuery = {
@@ -41,16 +41,16 @@ require('../blob/template_selector');
}
setInputValueToTemplateContent() {
- // `this.requestFileSuccess` sets the value of the description input field
+ // `this.setEditorContent` sets the value of the description input field
// to the content of the template selected.
if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the
- // `skipFocus` option to `requestFileSuccess`.
- this.requestFileSuccess(this.currentTemplate, { skipFocus: true });
+ // `skipFocus` option to `setEditorContent`.
+ this.setEditorContent(this.currentTemplate, { skipFocus: true });
this.titleInput.focus();
} else {
- this.requestFileSuccess(this.currentTemplate, { skipFocus: false });
+ this.setEditorContent(this.currentTemplate, { skipFocus: false });
}
return;
}
diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js
index 059e6c628b3..19c9efe7fbd 100644
--- a/app/assets/javascripts/user.js
+++ b/app/assets/javascripts/user.js
@@ -1,5 +1,6 @@
/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */
-/* global Cookies */
+
+import Cookies from 'js-cookie';
((global) => {
global.User = class {
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
index 99419e85b20..fa078b48bf8 100644
--- a/app/assets/javascripts/user_callout.js
+++ b/app/assets/javascripts/user_callout.js
@@ -1,60 +1,27 @@
-/* global Cookies */
-
-const userCalloutElementName = '.user-callout';
-const closeButton = '.close-user-callout';
-const userCalloutBtn = '.user-callout-btn';
-const userCalloutSvgAttrName = 'callout-svg';
+import Cookies from 'js-cookie';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
-const USER_CALLOUT_TEMPLATE = `
- <div class="bordered-box landing content-block">
- <button class="btn btn-default close close-user-callout" type="button">
- <i class="fa fa-times dismiss-icon"></i>
- </button>
- <div class="row">
- <div class="col-sm-3 col-xs-12 svg-container">
- </div>
- <div class="col-sm-8 col-xs-12 inner-content">
- <h4>
- Customize your experience
- </h4>
- <p>
- Change syntax themes, default project pages, and more in preferences.
- </p>
- <a class="btn user-callout-btn" href="/profile/preferences">Check it out</a>
- </div>
- </div>
-</div>`;
-
-class UserCallout {
+export default class UserCallout {
constructor() {
this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
- this.userCalloutBody = $(userCalloutElementName);
- this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
- $(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
+ this.userCalloutBody = $('.user-callout');
this.init();
}
init() {
- const $template = $(USER_CALLOUT_TEMPLATE);
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
- $template.find('.svg-container').append(this.userCalloutSvg);
- this.userCalloutBody.append($template);
- $template.find(closeButton).on('click', e => this.dismissCallout(e));
- $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e));
- } else {
- this.userCalloutBody.remove();
+ $('.js-close-callout').on('click', e => this.dismissCallout(e));
}
}
dismissCallout(e) {
- Cookies.set(USER_CALLOUT_COOKIE, 'true');
const $currentTarget = $(e.currentTarget);
- if ($currentTarget.hasClass('close-user-callout')) {
+
+ Cookies.set(USER_CALLOUT_COOKIE, 'true');
+
+ if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove();
}
}
}
-
-module.exports = UserCallout;
diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js
index 465618e3d53..5db0d936ad8 100644
--- a/app/assets/javascripts/user_tabs.js
+++ b/app/assets/javascripts/user_tabs.js
@@ -1,4 +1,4 @@
-/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign */
+/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign, class-methods-use-this */
/*
UserTabs
@@ -82,8 +82,19 @@ content on the Users#show page.
}
bindEvents() {
- return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
+ this.changeProjectsPageWrapper = this.changeProjectsPage.bind(this);
+
+ this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
+
+ this.$parentEl.on('click', '.gl-pagination a', this.changeProjectsPageWrapper);
+ }
+
+ changeProjectsPage(e) {
+ e.preventDefault();
+
+ $('.tab-pane.active').empty();
+ this.loadTab($(e.target).attr('href'), this.getCurrentAction());
}
tabShown(event) {
@@ -119,7 +130,7 @@ content on the Users#show page.
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
- url: `${source}.json`,
+ url: source,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
@@ -153,6 +164,10 @@ content on the Users#show page.
}, document.title, new_state);
return new_state;
}
+
+ getCurrentAction() {
+ return this.$parentEl.find('.nav-links .active a').data('action');
+ }
}
global.UserTabs = UserTabs;
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index c7a57b47834..48e20cf501f 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,8 +1,9 @@
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
-/* global Vue */
/* global Issuable */
/* global ListUser */
+import Vue from 'vue';
+
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
slice = [].slice;
@@ -53,7 +54,7 @@
$loading = $block.find('.block-loading').fadeOut();
var updateIssueBoardsIssue = function () {
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
$loading.fadeOut();
@@ -90,7 +91,7 @@
data = {};
data[abilityName] = {};
data[abilityName].assignee_id = selected != null ? selected : null;
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.js b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
new file mode 100644
index 00000000000..58b8db4d519
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
@@ -0,0 +1,93 @@
+/* eslint-disable no-new, no-alert */
+/* global Flash */
+import '~/flash';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+
+ title: {
+ type: String,
+ required: true,
+ },
+
+ icon: {
+ type: String,
+ required: true,
+ },
+
+ cssClass: {
+ type: String,
+ required: true,
+ },
+
+ confirmActionMessage: {
+ type: String,
+ required: false,
+ },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+
+ computed: {
+ iconClass() {
+ return `fa fa-${this.icon}`;
+ },
+
+ buttonClass() {
+ return `btn has-tooltip ${this.cssClass}`;
+ },
+ },
+
+ methods: {
+ onClick() {
+ if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
+ this.makeRequest();
+ } else if (!this.confirmActionMessage) {
+ this.makeRequest();
+ }
+ },
+
+ makeRequest() {
+ this.isLoading = true;
+
+ this.service.postAction(this.endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshPipelines');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+ },
+
+ template: `
+ <button
+ type="button"
+ @click="onClick"
+ :class="buttonClass"
+ :title="title"
+ :aria-label="title"
+ data-container="body"
+ data-placement="top"
+ :disabled="isLoading">
+ <i :class="iconClass" aria-hidden="true"/>
+ <i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" />
+ </button>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/empty_state.js b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js
new file mode 100644
index 00000000000..56b4858f4b4
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js
@@ -0,0 +1,33 @@
+import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg';
+
+export default {
+ props: {
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="row empty-state">
+ <div class="col-xs-12">
+ <div class="svg-content">
+ ${pipelinesEmptyStateSVG}
+ </div>
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>Build with confidence</h4>
+ <p>
+ Continous Integration can help catch bugs by running your tests automatically,
+ while Continuous Deployment can help you deliver code to your product environment.
+ </p>
+ <a :href="helpPagePath" class="btn btn-info">
+ Get started with Pipelines
+ </a>
+ </div>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/error_state.js b/app/assets/javascripts/vue_pipelines_index/components/error_state.js
new file mode 100644
index 00000000000..e5d228bddf8
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/error_state.js
@@ -0,0 +1,19 @@
+import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg';
+
+export default {
+ template: `
+ <div class="row empty-state js-pipelines-error-state">
+ <div class="col-xs-12">
+ <div class="svg-content">
+ ${pipelinesErrorStateSVG}
+ </div>
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>The API failed to fetch the pipelines.</h4>
+ </div>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js b/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js
new file mode 100644
index 00000000000..6aa10531034
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js
@@ -0,0 +1,52 @@
+export default {
+ props: {
+ newPipelinePath: {
+ type: String,
+ required: true,
+ },
+
+ hasCiEnabled: {
+ type: Boolean,
+ required: true,
+ },
+
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+
+ ciLintPath: {
+ type: String,
+ required: true,
+ },
+
+ canCreatePipeline: {
+ type: Boolean,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="nav-controls">
+ <a
+ v-if="canCreatePipeline"
+ :href="newPipelinePath"
+ class="btn btn-create">
+ Run Pipeline
+ </a>
+
+ <a
+ v-if="!hasCiEnabled"
+ :href="helpPagePath"
+ class="btn btn-info">
+ Get started with Pipelines
+ </a>
+
+ <a
+ :href="ciLintPath"
+ class="btn btn-default">
+ CI Lint
+ </a>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
new file mode 100644
index 00000000000..1626ae17a30
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
@@ -0,0 +1,72 @@
+export default {
+ props: {
+ scope: {
+ type: String,
+ required: true,
+ },
+
+ count: {
+ type: Object,
+ required: true,
+ },
+
+ paths: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ mounted() {
+ $(document).trigger('init.scrolling-tabs');
+ },
+
+ template: `
+ <ul class="nav-links scrolling-tabs">
+ <li
+ class="js-pipelines-tab-all"
+ :class="{ 'active': scope === 'all'}">
+ <a :href="paths.allPath">
+ All
+ <span class="badge js-totalbuilds-count">
+ {{count.all}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-pending"
+ :class="{ 'active': scope === 'pending'}">
+ <a :href="paths.pendingPath">
+ Pending
+ <span class="badge">
+ {{count.pending}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-running"
+ :class="{ 'active': scope === 'running'}">
+ <a :href="paths.runningPath">
+ Running
+ <span class="badge">
+ {{count.running}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-finished"
+ :class="{ 'active': scope === 'finished'}">
+ <a :href="paths.finishedPath">
+ Finished
+ <span class="badge">
+ {{count.finished}}
+ </span>
+ </a>
+ </li>
+ <li class="js-pipelines-tab-branches"
+ :class="{ 'active': scope === 'branches'}">
+ <a :href="paths.branchesPath">Branches</a>
+ </li>
+ <li class="js-pipelines-tab-tags"
+ :class="{ 'active': scope === 'tags'}">
+ <a :href="paths.tagsPath">Tags</a>
+ </li>
+ </ul>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
new file mode 100644
index 00000000000..4e183d5c8ec
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
@@ -0,0 +1,56 @@
+export default {
+ props: [
+ 'pipeline',
+ ],
+ computed: {
+ user() {
+ return !!this.pipeline.user;
+ },
+ },
+ template: `
+ <td>
+ <a
+ :href="pipeline.path"
+ class="js-pipeline-url-link">
+ <span class="pipeline-id">#{{pipeline.id}}</span>
+ </a>
+ <span>by</span>
+ <a
+ class="js-pipeline-url-user"
+ v-if="user"
+ :href="pipeline.user.web_url">
+ <img
+ v-if="user"
+ class="avatar has-tooltip s20 "
+ :title="pipeline.user.name"
+ data-container="body"
+ :src="pipeline.user.avatar_url"
+ >
+ </a>
+ <span
+ v-if="!user"
+ class="js-pipeline-url-api api monospace">
+ API
+ </span>
+ <span
+ v-if="pipeline.flags.latest"
+ class="js-pipeline-url-lastest label label-success has-tooltip"
+ title="Latest pipeline for this branch"
+ data-original-title="Latest pipeline for this branch">
+ latest
+ </span>
+ <span
+ v-if="pipeline.flags.yaml_errors"
+ class="js-pipeline-url-yaml label label-danger has-tooltip"
+ :title="pipeline.yaml_errors"
+ :data-original-title="pipeline.yaml_errors">
+ yaml invalid
+ </span>
+ <span
+ v-if="pipeline.flags.stuck"
+ class="js-pipeline-url-stuck label label-warning">
+ stuck
+ </span>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
new file mode 100644
index 00000000000..4bb2b048884
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
@@ -0,0 +1,71 @@
+/* eslint-disable no-new */
+/* global Flash */
+import '~/flash';
+import playIconSvg from 'icons/_icon_play.svg';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ actions: {
+ type: Array,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ playIconSvg,
+ isLoading: false,
+ };
+ },
+
+ methods: {
+ onClickAction(endpoint) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshPipelines');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+ },
+
+ template: `
+ <div class="btn-group" v-if="actions">
+ <button
+ type="button"
+ class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
+ title="Manual job"
+ data-toggle="dropdown"
+ data-placement="top"
+ aria-label="Manual job"
+ :disabled="isLoading">
+ ${playIconSvg}
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </button>
+
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="action in actions">
+ <button
+ type="button"
+ class="js-pipeline-action-link no-btn"
+ @click="onClickAction(action.path)">
+ ${playIconSvg}
+ <span>{{action.name}}</span>
+ </button>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
new file mode 100644
index 00000000000..f18e2dfadaf
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
@@ -0,0 +1,33 @@
+export default {
+ props: {
+ artifacts: {
+ type: Array,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="btn-group" role="group">
+ <button
+ class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
+ title="Artifacts"
+ data-placement="top"
+ data-toggle="dropdown"
+ aria-label="Artifacts">
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="artifact in artifacts">
+ <a
+ rel="nofollow"
+ download
+ :href="artifact.path">
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <span>Download {{artifact.name}} artifacts</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/stage.js b/app/assets/javascripts/vue_pipelines_index/components/stage.js
new file mode 100644
index 00000000000..a2c29002707
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/stage.js
@@ -0,0 +1,116 @@
+/* global Flash */
+import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
+import createdSvg from 'icons/_icon_status_created_borderless.svg';
+import failedSvg from 'icons/_icon_status_failed_borderless.svg';
+import manualSvg from 'icons/_icon_status_manual_borderless.svg';
+import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
+import runningSvg from 'icons/_icon_status_running_borderless.svg';
+import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
+import successSvg from 'icons/_icon_status_success_borderless.svg';
+import warningSvg from 'icons/_icon_status_warning_borderless.svg';
+
+export default {
+ data() {
+ const svgsDictionary = {
+ icon_status_canceled: canceledSvg,
+ icon_status_created: createdSvg,
+ icon_status_failed: failedSvg,
+ icon_status_manual: manualSvg,
+ icon_status_pending: pendingSvg,
+ icon_status_running: runningSvg,
+ icon_status_skipped: skippedSvg,
+ icon_status_success: successSvg,
+ icon_status_warning: warningSvg,
+ };
+
+ return {
+ builds: '',
+ spinner: '<span class="fa fa-spinner fa-spin"></span>',
+ svg: svgsDictionary[this.stage.status.icon],
+ };
+ },
+
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ updated() {
+ if (this.builds) {
+ this.stopDropdownClickPropagation();
+ }
+ },
+
+ methods: {
+ fetchBuilds(e) {
+ const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
+
+ if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
+
+ return this.$http.get(this.stage.dropdown_path)
+ .then((response) => {
+ this.builds = JSON.parse(response.body).html;
+ }, () => {
+ const flash = new Flash('Something went wrong on our end.');
+ return flash;
+ });
+ },
+
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
+ e.stopPropagation();
+ });
+ },
+ },
+ computed: {
+ buildsOrSpinner() {
+ return this.builds ? this.builds : this.spinner;
+ },
+ dropdownClass() {
+ if (this.builds) return 'js-builds-dropdown-container';
+ return 'js-builds-dropdown-loading builds-dropdown-loading';
+ },
+ buildStatus() {
+ return `Build: ${this.stage.status.label}`;
+ },
+ tooltip() {
+ return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
+ },
+ triggerButtonClass() {
+ return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
+ },
+ },
+ template: `
+ <div>
+ <button
+ @click="fetchBuilds($event)"
+ :class="triggerButtonClass"
+ :title="stage.title"
+ data-placement="top"
+ data-toggle="dropdown"
+ type="button"
+ :aria-label="stage.title">
+ <span v-html="svg" aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+ <div class="arrow-up" aria-hidden="true"></div>
+ <div
+ :class="dropdownClass"
+ class="js-builds-dropdown-list scrollable-menu"
+ v-html="buildsOrSpinner">
+ </div>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/status.js b/app/assets/javascripts/vue_pipelines_index/components/status.js
new file mode 100644
index 00000000000..21a281af438
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/status.js
@@ -0,0 +1,60 @@
+import canceledSvg from 'icons/_icon_status_canceled.svg';
+import createdSvg from 'icons/_icon_status_created.svg';
+import failedSvg from 'icons/_icon_status_failed.svg';
+import manualSvg from 'icons/_icon_status_manual.svg';
+import pendingSvg from 'icons/_icon_status_pending.svg';
+import runningSvg from 'icons/_icon_status_running.svg';
+import skippedSvg from 'icons/_icon_status_skipped.svg';
+import successSvg from 'icons/_icon_status_success.svg';
+import warningSvg from 'icons/_icon_status_warning.svg';
+
+export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ const svgsDictionary = {
+ icon_status_canceled: canceledSvg,
+ icon_status_created: createdSvg,
+ icon_status_failed: failedSvg,
+ icon_status_manual: manualSvg,
+ icon_status_pending: pendingSvg,
+ icon_status_running: runningSvg,
+ icon_status_skipped: skippedSvg,
+ icon_status_success: successSvg,
+ icon_status_warning: warningSvg,
+ };
+
+ return {
+ svg: svgsDictionary[this.pipeline.details.status.icon],
+ };
+ },
+
+ computed: {
+ cssClasses() {
+ return `ci-status ci-${this.pipeline.details.status.group}`;
+ },
+
+ detailsPath() {
+ const { status } = this.pipeline.details;
+ return status.has_details ? status.details_path : false;
+ },
+
+ content() {
+ return `${this.svg} ${this.pipeline.details.status.text}`;
+ },
+ },
+ template: `
+ <td class="commit-link">
+ <a
+ :class="cssClasses"
+ :href="detailsPath"
+ v-html="content">
+ </a>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
new file mode 100644
index 00000000000..498d0715f54
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
@@ -0,0 +1,71 @@
+import iconTimerSvg from 'icons/_icon_timer.svg';
+import '../../lib/utils/datetime_utility';
+
+export default {
+ data() {
+ return {
+ currentTime: new Date(),
+ iconTimerSvg,
+ };
+ },
+ props: ['pipeline'],
+ computed: {
+ timeAgo() {
+ return gl.utils.getTimeago();
+ },
+ localTimeFinished() {
+ return gl.utils.formatDate(this.pipeline.details.finished_at);
+ },
+ timeStopped() {
+ const changeTime = this.currentTime;
+ const options = {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ };
+ options.timeZoneName = 'short';
+ const finished = this.pipeline.details.finished_at;
+ if (!finished && changeTime) return false;
+ return ({ words: this.timeAgo.format(finished) });
+ },
+ duration() {
+ const { duration } = this.pipeline.details;
+ const date = new Date(duration * 1000);
+
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
+
+ if (hh < 10) hh = `0${hh}`;
+ if (mm < 10) mm = `0${mm}`;
+ if (ss < 10) ss = `0${ss}`;
+
+ if (duration !== null) return `${hh}:${mm}:${ss}`;
+ return false;
+ },
+ },
+ methods: {
+ changeTime() {
+ this.currentTime = new Date();
+ },
+ },
+ template: `
+ <td class="pipelines-time-ago">
+ <p class="duration" v-if='duration'>
+ <span v-html="iconTimerSvg"></span>
+ {{duration}}
+ </p>
+ <p class="finished-at" v-if='timeStopped'>
+ <i class="fa fa-calendar"></i>
+ <time
+ data-toggle="tooltip"
+ data-placement="top"
+ data-container="body"
+ :data-original-title='localTimeFinished'>
+ {{timeStopped.words}}
+ </time>
+ </p>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/event_hub.js b/app/assets/javascripts/vue_pipelines_index/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js
index a90bd1518e9..48f9181a8d9 100644
--- a/app/assets/javascripts/vue_pipelines_index/index.js
+++ b/app/assets/javascripts/vue_pipelines_index/index.js
@@ -1,29 +1,22 @@
-/* eslint-disable no-param-reassign */
-/* global Vue, VueResource, gl */
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../lib/utils/common_utils');
-require('../vue_shared/vue_resource_interceptor');
-require('./pipelines');
+import Vue from 'vue';
+import PipelinesStore from './stores/pipelines_store';
+import PipelinesComponent from './pipelines';
+import '../vue_shared/vue_resource_interceptor';
$(() => new Vue({
- el: document.querySelector('.vue-pipelines-index'),
+ el: document.querySelector('#pipelines-list-vue'),
data() {
- const project = document.querySelector('.pipelines');
+ const store = new PipelinesStore();
return {
- scope: project.dataset.url,
- store: new gl.PipelineStore(),
+ store,
};
},
components: {
- 'vue-pipelines': gl.VuePipelines,
+ 'vue-pipelines': PipelinesComponent,
},
template: `
- <vue-pipelines
- :scope="scope"
- :store="store">
- </vue-pipelines>
+ <vue-pipelines :store="store" />
`,
}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js
deleted file mode 100644
index 583d6915a85..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js
+++ /dev/null
@@ -1,123 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign, no-alert */
-const playIconSvg = require('icons/_icon_play.svg');
-
-((gl) => {
- gl.VuePipelineActions = Vue.extend({
- props: ['pipeline'],
- computed: {
- actions() {
- return this.pipeline.details.manual_actions.length > 0;
- },
- artifacts() {
- return this.pipeline.details.artifacts.length > 0;
- },
- },
- methods: {
- download(name) {
- return `Download ${name} artifacts`;
- },
-
- /**
- * Shows a dialog when the user clicks in the cancel button.
- * We need to prevent the default behavior and stop propagation because the
- * link relies on UJS.
- *
- * @param {Event} event
- */
- confirmAction(event) {
- if (!confirm('Are you sure you want to cancel this pipeline?')) {
- event.preventDefault();
- event.stopPropagation();
- }
- },
- },
-
- data() {
- return { playIconSvg };
- },
-
- template: `
- <td class="pipeline-actions">
- <div class="pull-right">
- <div class="btn-group">
- <div class="btn-group" v-if="actions">
- <button
- class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
- data-toggle="dropdown"
- title="Manual job"
- data-placement="top"
- data-container="body"
- aria-label="Manual job">
- <span v-html="playIconSvg" aria-hidden="true"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for='action in pipeline.details.manual_actions'>
- <a
- rel="nofollow"
- data-method="post"
- :href="action.path" >
- <span v-html="playIconSvg" aria-hidden="true"></span>
- <span>{{action.name}}</span>
- </a>
- </li>
- </ul>
- </div>
-
- <div class="btn-group" v-if="artifacts">
- <button
- class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
- title="Artifacts"
- data-placement="top"
- data-container="body"
- data-toggle="dropdown"
- aria-label="Artifacts">
- <i class="fa fa-download" aria-hidden="true"></i>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for='artifact in pipeline.details.artifacts'>
- <a
- rel="nofollow"
- :href="artifact.path">
- <i class="fa fa-download" aria-hidden="true"></i>
- <span>{{download(artifact.name)}}</span>
- </a>
- </li>
- </ul>
- </div>
- <div class="btn-group" v-if="pipeline.flags.retryable">
- <a
- class="btn btn-default btn-retry has-tooltip"
- title="Retry"
- rel="nofollow"
- data-method="post"
- data-placement="top"
- data-container="body"
- data-toggle="dropdown"
- :href='pipeline.retry_path'
- aria-label="Retry">
- <i class="fa fa-repeat" aria-hidden="true"></i>
- </a>
- </div>
- <div class="btn-group" v-if="pipeline.flags.cancelable">
- <a
- class="btn btn-remove has-tooltip"
- title="Cancel"
- rel="nofollow"
- data-method="post"
- data-placement="top"
- data-container="body"
- data-toggle="dropdown"
- :href='pipeline.cancel_path'
- aria-label="Cancel">
- <i class="fa fa-remove" aria-hidden="true"></i>
- </a>
- </div>
- </div>
- </div>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js
deleted file mode 100644
index ae5649f0519..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VuePipelineUrl = Vue.extend({
- props: [
- 'pipeline',
- ],
- computed: {
- user() {
- return !!this.pipeline.user;
- },
- },
- template: `
- <td>
- <a :href='pipeline.path'>
- <span class="pipeline-id">#{{pipeline.id}}</span>
- </a>
- <span>by</span>
- <a
- v-if='user'
- :href='pipeline.user.web_url'
- >
- <img
- v-if='user'
- class="avatar has-tooltip s20 "
- :title='pipeline.user.name'
- data-container="body"
- :src='pipeline.user.avatar_url'
- >
- </a>
- <span
- v-if='!user'
- class="api monospace"
- >
- API
- </span>
- <span
- v-if='pipeline.flags.latest'
- class="label label-success has-tooltip"
- title="Latest pipeline for this branch"
- data-original-title="Latest pipeline for this branch"
- >
- latest
- </span>
- <span
- v-if='pipeline.flags.yaml_errors'
- class="label label-danger has-tooltip"
- :title='pipeline.yaml_errors'
- :data-original-title='pipeline.yaml_errors'
- >
- yaml invalid
- </span>
- <span
- v-if='pipeline.flags.stuck'
- class="label label-warning"
- >
- stuck
- </span>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js
index 601ef41e917..9bdc232b7da 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js
@@ -1,87 +1,246 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
+import Vue from 'vue';
+import PipelinesService from './services/pipelines_service';
+import eventHub from './event_hub';
+import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
+import TablePaginationComponent from '../vue_shared/components/table_pagination';
+import EmptyState from './components/empty_state';
+import ErrorState from './components/error_state';
+import NavigationTabs from './components/navigation_tabs';
+import NavigationControls from './components/nav_controls';
-window.Vue = require('vue');
-require('../vue_shared/components/table_pagination');
-require('./store');
-require('../vue_shared/components/pipelines_table');
-const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
+export default {
+ props: {
+ store: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ 'gl-pagination': TablePaginationComponent,
+ 'pipelines-table-component': PipelinesTableComponent,
+ 'empty-state': EmptyState,
+ 'error-state': ErrorState,
+ 'navigation-tabs': NavigationTabs,
+ 'navigation-controls': NavigationControls,
+ },
+
+ data() {
+ const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
+
+ return {
+ endpoint: pipelinesData.endpoint,
+ cssClass: pipelinesData.cssClass,
+ helpPagePath: pipelinesData.helpPagePath,
+ newPipelinePath: pipelinesData.newPipelinePath,
+ canCreatePipeline: pipelinesData.canCreatePipeline,
+ allPath: pipelinesData.allPath,
+ pendingPath: pipelinesData.pendingPath,
+ runningPath: pipelinesData.runningPath,
+ finishedPath: pipelinesData.finishedPath,
+ branchesPath: pipelinesData.branchesPath,
+ tagsPath: pipelinesData.tagsPath,
+ hasCi: pipelinesData.hasCi,
+ ciLintPath: pipelinesData.ciLintPath,
+ state: this.store.state,
+ apiScope: 'all',
+ pagenum: 1,
+ isLoading: false,
+ hasError: false,
+ };
+ },
+
+ computed: {
+ canCreatePipelineParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canCreatePipeline);
+ },
-((gl) => {
- gl.VuePipelines = Vue.extend({
+ scope() {
+ const scope = gl.utils.getParameterByName('scope');
+ return scope === null ? 'all' : scope;
+ },
+
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
+ },
- components: {
- 'gl-pagination': gl.VueGlPagination,
- 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
+ /**
+ * The empty state should only be rendered when the request is made to fetch all pipelines
+ * and none is returned.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderEmptyState() {
+ return !this.isLoading &&
+ !this.hasError &&
+ !this.state.pipelines.length &&
+ (this.scope === 'all' || this.scope === null);
},
- data() {
+ /**
+ * When a specific scope does not have pipelines we render a message.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderNoPipelinesMessage() {
+ return !this.isLoading &&
+ !this.hasError &&
+ !this.state.pipelines.length &&
+ this.scope !== 'all' &&
+ this.scope !== null;
+ },
+
+ shouldRenderTable() {
+ return !this.hasError &&
+ !this.isLoading && this.state.pipelines.length;
+ },
+
+ /**
+ * Pagination should only be rendered when there is more than one page.
+ *
+ * @return {Boolean}
+ */
+ shouldRenderPagination() {
+ return !this.isLoading &&
+ this.state.pipelines.length &&
+ this.state.pageInfo.total > this.state.pageInfo.perPage;
+ },
+
+ hasCiEnabled() {
+ return this.hasCi !== undefined;
+ },
+
+ paths() {
return {
- pipelines: [],
- timeLoopInterval: '',
- intervalId: '',
- apiScope: 'all',
- pageInfo: {},
- pagenum: 1,
- count: {},
- pageRequest: false,
+ allPath: this.allPath,
+ pendingPath: this.pendingPath,
+ finishedPath: this.finishedPath,
+ runningPath: this.runningPath,
+ branchesPath: this.branchesPath,
+ tagsPath: this.tagsPath,
};
},
- props: ['scope', 'store'],
- created() {
- const pagenum = gl.utils.getParameterByName('page');
- const scope = gl.utils.getParameterByName('scope');
- if (pagenum) this.pagenum = pagenum;
- if (scope) this.apiScope = scope;
+ },
- this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
- },
+ created() {
+ this.service = new PipelinesService(this.endpoint);
+
+ this.fetchPipelines();
+
+ eventHub.$on('refreshPipelines', this.fetchPipelines);
+ },
+
+ beforeUpdate() {
+ if (this.state.pipelines.length && this.$children) {
+ this.store.startTimeAgoLoops.call(this, Vue);
+ }
+ },
+
+ beforeDestroyed() {
+ eventHub.$off('refreshPipelines');
+ },
- beforeUpdate() {
- if (this.pipelines.length && this.$children) {
- CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
- }
+ methods: {
+ /**
+ * Will change the page number and update the URL.
+ *
+ * @param {Number} pageNumber desired page to go to.
+ */
+ change(pageNumber) {
+ const param = gl.utils.setParamInURL('page', pageNumber);
+
+ gl.utils.visitUrl(param);
+ return param;
},
- methods: {
- /**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
- */
- change(pageNumber) {
- const param = gl.utils.setParamInURL('page', pageNumber);
-
- gl.utils.visitUrl(param);
- return param;
- },
+ fetchPipelines() {
+ const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
+ const scope = gl.utils.getParameterByName('scope') || this.apiScope;
+
+ this.isLoading = true;
+ return this.service.getPipelines(scope, pageNumber)
+ .then(resp => ({
+ headers: resp.headers,
+ body: resp.json(),
+ }))
+ .then((response) => {
+ this.store.storeCount(response.body.count);
+ this.store.storePipelines(response.body.pipelines);
+ this.store.storePagination(response.headers);
+ })
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.hasError = true;
+ this.isLoading = false;
+ });
},
- template: `
- <div>
- <div class="pipelines realtime-loading" v-if='pageRequest'>
- <i class="fa fa-spinner fa-spin"></i>
+ },
+
+ template: `
+ <div :class="cssClass">
+
+ <div
+ class="top-area scrolling-tabs-container inner-page-scroll-tabs"
+ v-if="!isLoading && !shouldRenderEmptyState">
+ <div class="fade-left">
+ <i class="fa fa-angle-left" aria-hidden="true"></i>
</div>
+ <div class="fade-right">
+ <i class="fa fa-angle-right" aria-hidden="true"></i>
+ </div>
+ <navigation-tabs
+ :scope="scope"
+ :count="state.count"
+ :paths="paths" />
+
+ <navigation-controls
+ :new-pipeline-path="newPipelinePath"
+ :has-ci-enabled="hasCiEnabled"
+ :help-page-path="helpPagePath"
+ :ciLintPath="ciLintPath"
+ :can-create-pipeline="canCreatePipelineParsed " />
+ </div>
- <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 class="content-list pipelines">
+
+ <div
+ class="realtime-loading"
+ v-if="isLoading">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
</div>
- <div class="table-holder" v-if='!pageRequest && pipelines.length'>
- <pipelines-table-component :pipelines='pipelines'/>
+ <empty-state
+ v-if="shouldRenderEmptyState"
+ :help-page-path="helpPagePath" />
+
+ <error-state v-if="shouldRenderErrorState" />
+
+ <div
+ class="blank-state blank-state-no-icon"
+ v-if="shouldRenderNoPipelinesMessage">
+ <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ </div>
+
+ <div
+ class="table-holder"
+ v-if="shouldRenderTable">
+
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service"/>
</div>
<gl-pagination
- v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
- :pagenum='pagenum'
- :change='change'
- :count='count.all'
- :pageInfo='pageInfo'
- >
- </gl-pagination>
+ v-if="shouldRenderPagination"
+ :pagenum="pagenum"
+ :change="change"
+ :count="state.count.all"
+ :pageInfo="state.pageInfo"/>
</div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
new file mode 100644
index 00000000000..708f5068dd3
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
@@ -0,0 +1,44 @@
+/* eslint-disable class-methods-use-this */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class PipelinesService {
+
+ /**
+ * Commits and merge request endpoints need to be requested with `.json`.
+ *
+ * The url provided to request the pipelines in the new merge request
+ * page already has `.json`.
+ *
+ * @param {String} root
+ */
+ constructor(root) {
+ let endpoint;
+
+ if (root.indexOf('.json') === -1) {
+ endpoint = `${root}.json`;
+ } else {
+ endpoint = root;
+ }
+
+ this.pipelines = Vue.resource(endpoint);
+ }
+
+ getPipelines(scope, page) {
+ return this.pipelines.get({ scope, page });
+ }
+
+ /**
+ * Post request for all pipelines actions.
+ * Endpoint content type needs to be:
+ * `Content-Type:application/x-www-form-urlencoded`
+ *
+ * @param {String} endpoint
+ * @return {Promise}
+ */
+ postAction(endpoint) {
+ return Vue.http.post(endpoint, {}, { emulateJSON: true });
+ }
+}
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js b/app/assets/javascripts/vue_pipelines_index/stage.js
deleted file mode 100644
index ae4f0b4a53b..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/stage.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign */
-import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
-import createdSvg from 'icons/_icon_status_created_borderless.svg';
-import failedSvg from 'icons/_icon_status_failed_borderless.svg';
-import manualSvg from 'icons/_icon_status_manual_borderless.svg';
-import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
-import runningSvg from 'icons/_icon_status_running_borderless.svg';
-import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
-import successSvg from 'icons/_icon_status_success_borderless.svg';
-import warningSvg from 'icons/_icon_status_warning_borderless.svg';
-
-((gl) => {
- gl.VueStage = Vue.extend({
- data() {
- const svgsDictionary = {
- icon_status_canceled: canceledSvg,
- icon_status_created: createdSvg,
- icon_status_failed: failedSvg,
- icon_status_manual: manualSvg,
- icon_status_pending: pendingSvg,
- icon_status_running: runningSvg,
- icon_status_skipped: skippedSvg,
- icon_status_success: successSvg,
- icon_status_warning: warningSvg,
- };
-
- return {
- builds: '',
- spinner: '<span class="fa fa-spinner fa-spin"></span>',
- svg: svgsDictionary[this.stage.status.icon],
- };
- },
-
- props: {
- stage: {
- type: Object,
- required: true,
- },
- },
-
- updated() {
- if (this.builds) {
- this.stopDropdownClickPropagation();
- }
- },
-
- methods: {
- fetchBuilds(e) {
- const areaExpanded = e.currentTarget.attributes['aria-expanded'];
-
- if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
-
- return this.$http.get(this.stage.dropdown_path)
- .then((response) => {
- this.builds = JSON.parse(response.body).html;
- }, () => {
- const flash = new Flash('Something went wrong on our end.');
- return flash;
- });
- },
-
- /**
- * When the user right clicks or cmd/ctrl + click in the job name
- * the dropdown should not be closed and the link should open in another tab,
- * so we stop propagation of the click event inside the dropdown.
- *
- * Since this component is rendered multiple times per page we need to guarantee we only
- * target the click event of this component.
- */
- stopDropdownClickPropagation() {
- $(this.$el).on('click', '.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', (e) => {
- e.stopPropagation();
- });
- },
- },
- computed: {
- buildsOrSpinner() {
- return this.builds ? this.builds : this.spinner;
- },
- dropdownClass() {
- if (this.builds) return 'js-builds-dropdown-container';
- return 'js-builds-dropdown-loading builds-dropdown-loading';
- },
- buildStatus() {
- return `Build: ${this.stage.status.label}`;
- },
- tooltip() {
- return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
- },
- triggerButtonClass() {
- return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
- },
- },
- template: `
- <div>
- <button
- @click="fetchBuilds($event)"
- :class="triggerButtonClass"
- :title="stage.title"
- data-placement="top"
- data-toggle="dropdown"
- type="button"
- :aria-label="stage.title">
- <span v-html="svg" aria-hidden="true"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
- <div class="arrow-up" aria-hidden="true"></div>
- <div
- :class="dropdownClass"
- class="js-builds-dropdown-list scrollable-menu"
- v-html="buildsOrSpinner">
- </div>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/status.js b/app/assets/javascripts/vue_pipelines_index/status.js
deleted file mode 100644
index 8d9f83ac113..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/status.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-import canceledSvg from 'icons/_icon_status_canceled.svg';
-import createdSvg from 'icons/_icon_status_created.svg';
-import failedSvg from 'icons/_icon_status_failed.svg';
-import manualSvg from 'icons/_icon_status_manual.svg';
-import pendingSvg from 'icons/_icon_status_pending.svg';
-import runningSvg from 'icons/_icon_status_running.svg';
-import skippedSvg from 'icons/_icon_status_skipped.svg';
-import successSvg from 'icons/_icon_status_success.svg';
-import warningSvg from 'icons/_icon_status_warning.svg';
-
-((gl) => {
- gl.VueStatusScope = Vue.extend({
- props: [
- 'pipeline',
- ],
-
- data() {
- const svgsDictionary = {
- icon_status_canceled: canceledSvg,
- icon_status_created: createdSvg,
- icon_status_failed: failedSvg,
- icon_status_manual: manualSvg,
- icon_status_pending: pendingSvg,
- icon_status_running: runningSvg,
- icon_status_skipped: skippedSvg,
- icon_status_success: successSvg,
- icon_status_warning: warningSvg,
- };
-
- return {
- svg: svgsDictionary[this.pipeline.details.status.icon],
- };
- },
-
- computed: {
- cssClasses() {
- const cssObject = { 'ci-status': true };
- cssObject[`ci-${this.pipeline.details.status.group}`] = true;
- return cssObject;
- },
-
- detailsPath() {
- const { status } = this.pipeline.details;
- return status.has_details ? status.details_path : false;
- },
-
- content() {
- return `${this.svg} ${this.pipeline.details.status.text}`;
- },
- },
- template: `
- <td class="commit-link">
- <a
- :class="cssClasses"
- :href="detailsPath"
- v-html="content">
- </a>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js b/app/assets/javascripts/vue_pipelines_index/store.js
deleted file mode 100644
index 909007267b9..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/store.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/* global gl, Flash */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- const pageValues = (headers) => {
- const normalized = gl.utils.normalizeHeaders(headers);
- const paginationInfo = gl.utils.parseIntPagination(normalized);
- return paginationInfo;
- };
-
- gl.PipelineStore = class {
- fetchDataLoop(Vue, pageNum, url, apiScope) {
- this.pageRequest = true;
-
- return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
- .then((response) => {
- const pageInfo = pageValues(response.headers);
- this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
-
- const res = JSON.parse(response.body);
- this.count = Object.assign({}, this.count, res.count);
- this.pipelines = Object.assign([], this.pipelines, res.pipelines);
-
- this.pageRequest = false;
- }, () => {
- this.pageRequest = false;
- return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
- });
- }
- };
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
index f1b80e45444..7ac10086a55 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_store.js
+++ b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
@@ -1,31 +1,46 @@
/* eslint-disable no-underscore-dangle*/
-/**
- * Pipelines' Store for commits view.
- *
- * Used to store the Pipelines rendered in the commit view in the pipelines table.
- */
-require('../../vue_realtime_listener');
-
-class PipelinesStore {
+import '../../vue_realtime_listener';
+
+export default class PipelinesStore {
constructor() {
this.state = {};
+
this.state.pipelines = [];
+ this.state.count = {};
+ this.state.pageInfo = {};
}
storePipelines(pipelines = []) {
this.state.pipelines = pipelines;
+ }
- return pipelines;
+ storeCount(count = {}) {
+ this.state.count = count;
+ }
+
+ storePagination(pagination = {}) {
+ let paginationInfo;
+
+ if (Object.keys(pagination).length) {
+ const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
+ paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
+ } else {
+ paginationInfo = pagination;
+ }
+
+ this.state.pageInfo = paginationInfo;
}
/**
+ * FIXME: Move this inside the component.
+ *
* 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.
*
*/
- static startTimeAgoLoops() {
+ startTimeAgoLoops() {
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
@@ -44,5 +59,3 @@ class PipelinesStore {
gl.VueRealtimeListener(removeIntervals, startIntervals);
}
}
-
-module.exports = PipelinesStore;
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js b/app/assets/javascripts/vue_pipelines_index/time_ago.js
deleted file mode 100644
index a383570857d..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/time_ago.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-window.Vue = require('vue');
-require('../lib/utils/datetime_utility');
-
-const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
-
-((gl) => {
- gl.VueTimeAgo = Vue.extend({
- data() {
- return {
- currentTime: new Date(),
- iconTimerSvg,
- };
- },
- props: ['pipeline'],
- computed: {
- timeAgo() {
- return gl.utils.getTimeago();
- },
- localTimeFinished() {
- return gl.utils.formatDate(this.pipeline.details.finished_at);
- },
- timeStopped() {
- const changeTime = this.currentTime;
- const options = {
- weekday: 'long',
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- };
- options.timeZoneName = 'short';
- const finished = this.pipeline.details.finished_at;
- if (!finished && changeTime) return false;
- return ({ words: this.timeAgo.format(finished) });
- },
- duration() {
- const { duration } = this.pipeline.details;
- const date = new Date(duration * 1000);
-
- let hh = date.getUTCHours();
- let mm = date.getUTCMinutes();
- let ss = date.getSeconds();
-
- if (hh < 10) hh = `0${hh}`;
- if (mm < 10) mm = `0${mm}`;
- if (ss < 10) ss = `0${ss}`;
-
- if (duration !== null) return `${hh}:${mm}:${ss}`;
- return false;
- },
- },
- methods: {
- changeTime() {
- this.currentTime = new Date();
- },
- },
- template: `
- <td class="pipelines-time-ago">
- <p class="duration" v-if='duration'>
- <span v-html="iconTimerSvg"></span>
- {{duration}}
- </p>
- <p class="finished-at" v-if='timeStopped'>
- <i class="fa fa-calendar"></i>
- <time
- data-toggle="tooltip"
- data-placement="top"
- data-container="body"
- :data-original-title='localTimeFinished'>
- {{timeStopped.words}}
- </time>
- </p>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_shared/common_vue.js b/app/assets/javascripts/vue_shared/common_vue.js
new file mode 100644
index 00000000000..eb2a6071fda
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/common_vue.js
@@ -0,0 +1,6 @@
+import Vue from 'vue';
+import './vue_resource_interceptor';
+
+if (process.env.NODE_ENV !== 'production') {
+ Vue.config.productionTip = false;
+}
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
index 4381487b79e..fb68abd95a2 100644
--- a/app/assets/javascripts/vue_shared/components/commit.js
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -1,164 +1,157 @@
-/* global Vue */
-window.Vue = require('vue');
-const commitIconSvg = require('icons/_icon_commit.svg');
-
-(() => {
- window.gl = window.gl || {};
-
- window.gl.CommitComponent = Vue.component('commit-component', {
-
- props: {
- /**
- * Indicates the existance of a tag.
- * Used to render the correct icon, if true will render `fa-tag` icon,
- * if false will render `fa-code-fork` icon.
- */
- tag: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- /**
- * If provided is used to render the branch name and url.
- * Should contain the following properties:
- * name
- * ref_url
- */
- commitRef: {
- type: Object,
- required: false,
- default: () => ({}),
- },
-
- /**
- * Used to link to the commit sha.
- */
- commitUrl: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * Used to show the commit short sha that links to the commit url.
- */
- shortSha: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * If provided shows the commit tile.
- */
- title: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * If provided renders information about the author of the commit.
- * When provided should include:
- * `avatar_url` to render the avatar icon
- * `web_url` to link to user profile
- * `username` to render alt and title tags
- */
- author: {
- type: Object,
- required: false,
- default: () => ({}),
- },
+import commitIconSvg from 'icons/_icon_commit.svg';
+
+export default {
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render `fa-code-fork` icon.
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- computed: {
- /**
- * Used to verify if all the properties needed to render the commit
- * ref section were provided.
- *
- * TODO: Improve this! Use lodash _.has when we have it.
- *
- * @returns {Boolean}
- */
- hasCommitRef() {
- return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
- },
-
- /**
- * Used to verify if all the properties needed to render the commit
- * author section were provided.
- *
- * TODO: Improve this! Use lodash _.has when we have it.
- *
- * @returns {Boolean}
- */
- hasAuthor() {
- return this.author &&
- this.author.avatar_url &&
- this.author.web_url &&
- this.author.username;
- },
-
- /**
- * If information about the author is provided will return a string
- * to be rendered as the alt attribute of the img tag.
- *
- * @returns {String}
- */
- userImageAltDescription() {
- return this.author &&
- this.author.username ? `${this.author.username}'s avatar` : null;
- },
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ commitRef: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
- data() {
- return { commitIconSvg };
+ /**
+ * Used to link to the commit sha.
+ */
+ commitUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- template: `
- <div class="branch-commit">
-
- <div v-if="hasCommitRef" class="icon-container">
- <i v-if="tag" class="fa fa-tag"></i>
- <i v-if="!tag" class="fa fa-code-fork"></i>
- </div>
-
- <a v-if="hasCommitRef"
- class="monospace branch-name"
- :href="commitRef.ref_url">
- {{commitRef.name}}
- </a>
-
- <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
-
- <a class="commit-id monospace"
- :href="commitUrl">
- {{shortSha}}
- </a>
-
- <p class="commit-title">
- <span v-if="title">
- <a v-if="hasAuthor"
- class="avatar-image-container"
- :href="author.web_url">
- <img
- class="avatar has-tooltip s20"
- :src="author.avatar_url"
- :alt="userImageAltDescription"
- :title="author.username" />
- </a>
-
- <a class="commit-row-message"
- :href="commitUrl">
- {{title}}
- </a>
- </span>
- <span v-else>
- Cant find HEAD commit for this branch
- </span>
- </p>
+ /**
+ * Used to show the commit short sha that links to the commit url.
+ */
+ shortSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
+ },
+
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author &&
+ this.author.avatar_url &&
+ this.author.web_url &&
+ this.author.username;
+ },
+
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author &&
+ this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+
+ data() {
+ return { commitIconSvg };
+ },
+
+ template: `
+ <div class="branch-commit">
+
+ <div v-if="hasCommitRef" class="icon-container">
+ <i v-if="tag" class="fa fa-tag"></i>
+ <i v-if="!tag" class="fa fa-code-fork"></i>
</div>
- `,
- });
-})();
+
+ <a v-if="hasCommitRef"
+ class="monospace branch-name"
+ :href="commitRef.ref_url">
+ {{commitRef.name}}
+ </a>
+
+ <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
+
+ <a class="commit-id monospace"
+ :href="commitUrl">
+ {{shortSha}}
+ </a>
+
+ <p class="commit-title">
+ <span v-if="title">
+ <a v-if="hasAuthor"
+ class="avatar-image-container"
+ :href="author.web_url">
+ <img
+ class="avatar has-tooltip s20"
+ :src="author.avatar_url"
+ :alt="userImageAltDescription"
+ :title="author.username" />
+ </a>
+
+ <a class="commit-row-message"
+ :href="commitUrl">
+ {{title}}
+ </a>
+ </span>
+ <span v-else>
+ Cant find HEAD commit for this branch
+ </span>
+ </p>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js
index 0d8f85db965..afd8d7acf6b 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js
@@ -1,52 +1,48 @@
-/* eslint-disable no-param-reassign */
-/* global Vue */
+import PipelinesTableRowComponent from './pipelines_table_row';
-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: () => ([]),
- },
-
+export default {
+ props: {
+ pipelines: {
+ type: Array,
+ required: true,
+ default: () => ([]),
},
- components: {
- 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent,
+ service: {
+ type: Object,
+ required: true,
},
+ },
+
+ components: {
+ 'pipelines-table-row-component': 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"></th>
- </tr>
- </thead>
- <tbody>
- <template v-for="model in pipelines"
- v-bind:model="model">
- <tr is="pipelines-table-row-component"
- :pipeline="model"></tr>
- </template>
- </tbody>
- </table>
- `,
- });
-})();
+ 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"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in pipelines"
+ v-bind:model="model">
+ <tr is="pipelines-table-row-component"
+ :pipeline="model"
+ :service="service"></tr>
+ </template>
+ </tbody>
+ </table>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
index e5e88186a85..f5b3cb9214e 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -1,199 +1,228 @@
/* 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');
+
+import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button';
+import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
+import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
+import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
+import PipelinesStageComponent from '../../vue_pipelines_index/components/stage';
+import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url';
+import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago';
+import CommitComponent from './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: () => ({}),
- },
+export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ 'async-button-component': AsyncButtonComponent,
+ 'pipelines-actions-component': PipelinesActionsComponent,
+ 'pipelines-artifacts-component': PipelinesArtifactsComponent,
+ 'commit-component': CommitComponent,
+ 'dropdown-stage': PipelinesStageComponent,
+ 'pipeline-url': PipelinesUrlComponent,
+ 'status-scope': PipelinesStatusComponent,
+ 'time-ago': PipelinesTimeagoComponent,
+ },
+
+ 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;
},
- 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,
+ /**
+ * 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;
},
- 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,
- });
+ /**
+ * 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;
+ }, {});
+ }
- // 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 undefined;
+ },
- 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;
- }, {});
- }
+ /**
+ * 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;
+ },
- 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;
- },
+ /**
+ * 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;
},
- template: `
- <tr class="commit">
- <status-scope :pipeline="pipeline"/>
-
- <pipeline-url :pipeline="pipeline"></pipeline-url>
-
- <td>
- <commit-component
- :tag="commitTag"
- :commit-ref="commitRef"
- :commit-url="commitUrl"
- :short-sha="commitShortSha"
- :title="commitTitle"
- :author="commitAuthor"/>
- </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"/>
- </div>
- </td>
-
- <time-ago :pipeline="pipeline"/>
-
- <pipeline-actions :pipeline="pipeline" />
- </tr>
- `,
- });
-})();
+ /**
+ * 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;
+ },
+ },
+
+ template: `
+ <tr class="commit">
+ <status-scope :pipeline="pipeline"/>
+
+ <pipeline-url :pipeline="pipeline"></pipeline-url>
+
+ <td>
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"/>
+ </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"/>
+ </div>
+ </td>
+
+ <time-ago :pipeline="pipeline"/>
+
+ <td class="pipeline-actions">
+ <div class="pull-right btn-group">
+ <pipelines-actions-component
+ v-if="pipeline.details.manual_actions.length"
+ :actions="pipeline.details.manual_actions"
+ :service="service" />
+
+ <pipelines-artifacts-component
+ v-if="pipeline.details.artifacts.length"
+ :artifacts="pipeline.details.artifacts" />
+
+ <async-button-component
+ v-if="pipeline.flags.retryable"
+ :service="service"
+ :endpoint="pipeline.retry_path"
+ css-class="js-pipelines-retry-button btn-default btn-retry"
+ title="Retry"
+ icon="repeat" />
+
+ <async-button-component
+ v-if="pipeline.flags.cancelable"
+ :service="service"
+ :endpoint="pipeline.cancel_path"
+ css-class="js-pipelines-cancel-button btn-remove"
+ title="Cancel"
+ icon="remove"
+ confirm-action-message="Are you sure you want to cancel this pipeline?" />
+ </div>
+ </td>
+ </tr>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js b/app/assets/javascripts/vue_shared/components/table_pagination.js
index 8943b850a72..ebb14912b00 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.js
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js
@@ -1,147 +1,135 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign, no-plusplus */
-
-window.Vue = require('vue');
-
-((gl) => {
- const PAGINATION_UI_BUTTON_LIMIT = 4;
- const UI_LIMIT = 6;
- const SPREAD = '...';
- const PREV = 'Prev';
- const NEXT = 'Next';
- const FIRST = '<< First';
- const LAST = 'Last >>';
-
- gl.VueGlPagination = Vue.extend({
- props: {
-
- // TODO: Consider refactoring in light of turbolinks removal.
-
- /**
- This function will take the information given by the pagination component
-
- Here is an example `change` method:
-
- change(pagenum) {
- gl.utils.visitUrl(`?page=${pagenum}`);
- },
- */
-
- change: {
- type: Function,
- required: true,
+const PAGINATION_UI_BUTTON_LIMIT = 4;
+const UI_LIMIT = 6;
+const SPREAD = '...';
+const PREV = 'Prev';
+const NEXT = 'Next';
+const FIRST = 'ยซ First';
+const LAST = 'Last ยป';
+
+export default {
+ props: {
+ /**
+ This function will take the information given by the pagination component
+
+ Here is an example `change` method:
+
+ change(pagenum) {
+ gl.utils.visitUrl(`?page=${pagenum}`);
},
+ */
+ change: {
+ type: Function,
+ required: true,
+ },
- /**
- pageInfo will come from the headers of the API call
- in the `.then` clause of the VueResource API call
- there should be a function that contructs the pageInfo for this component
-
- This is an example:
-
- const pageInfo = headers => ({
- perPage: +headers['X-Per-Page'],
- page: +headers['X-Page'],
- total: +headers['X-Total'],
- totalPages: +headers['X-Total-Pages'],
- nextPage: +headers['X-Next-Page'],
- previousPage: +headers['X-Prev-Page'],
- });
- */
-
- pageInfo: {
- type: Object,
- required: true,
- },
+ /**
+ pageInfo will come from the headers of the API call
+ in the `.then` clause of the VueResource API call
+ there should be a function that contructs the pageInfo for this component
+
+ This is an example:
+
+ const pageInfo = headers => ({
+ perPage: +headers['X-Per-Page'],
+ page: +headers['X-Page'],
+ total: +headers['X-Total'],
+ totalPages: +headers['X-Total-Pages'],
+ nextPage: +headers['X-Next-Page'],
+ previousPage: +headers['X-Prev-Page'],
+ });
+ */
+ pageInfo: {
+ type: Object,
+ required: true,
},
- methods: {
- changePage(e) {
- const text = e.target.innerText;
- const { totalPages, nextPage, previousPage } = this.pageInfo;
-
- switch (text) {
- case SPREAD:
- break;
- case LAST:
- this.change(totalPages);
- break;
- case NEXT:
- this.change(nextPage);
- break;
- case PREV:
- this.change(previousPage);
- break;
- case FIRST:
- this.change(1);
- break;
- default:
- this.change(+text);
- break;
- }
- },
+ },
+ methods: {
+ changePage(e) {
+ const text = e.target.innerText;
+ const { totalPages, nextPage, previousPage } = this.pageInfo;
+
+ switch (text) {
+ case SPREAD:
+ break;
+ case LAST:
+ this.change(totalPages);
+ break;
+ case NEXT:
+ this.change(nextPage);
+ break;
+ case PREV:
+ this.change(previousPage);
+ break;
+ case FIRST:
+ this.change(1);
+ break;
+ default:
+ this.change(+text);
+ break;
+ }
},
- computed: {
- prev() {
- return this.pageInfo.previousPage;
- },
- next() {
- return this.pageInfo.nextPage;
- },
- getItems() {
- const total = this.pageInfo.totalPages;
- const page = this.pageInfo.page;
- const items = [];
+ },
+ computed: {
+ prev() {
+ return this.pageInfo.previousPage;
+ },
+ next() {
+ return this.pageInfo.nextPage;
+ },
+ getItems() {
+ const total = this.pageInfo.totalPages;
+ const page = this.pageInfo.page;
+ const items = [];
- if (page > 1) items.push({ title: FIRST });
+ if (page > 1) items.push({ title: FIRST });
- if (page > 1) {
- items.push({ title: PREV, prev: true });
- } else {
- items.push({ title: PREV, disabled: true, prev: true });
- }
+ if (page > 1) {
+ items.push({ title: PREV, prev: true });
+ } else {
+ items.push({ title: PREV, disabled: true, prev: true });
+ }
- if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
+ if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
- const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
- const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
+ const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
+ const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
- for (let i = start; i <= end; i++) {
- const isActive = i === page;
- items.push({ title: i, active: isActive, page: true });
- }
+ for (let i = start; i <= end; i += 1) {
+ const isActive = i === page;
+ items.push({ title: i, active: isActive, page: true });
+ }
- if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
- items.push({ title: SPREAD, separator: true, page: true });
- }
+ if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
+ items.push({ title: SPREAD, separator: true, page: true });
+ }
- if (page === total) {
- items.push({ title: NEXT, disabled: true, next: true });
- } else if (total - page >= 1) {
- items.push({ title: NEXT, next: true });
- }
+ if (page === total) {
+ items.push({ title: NEXT, disabled: true, next: true });
+ } else if (total - page >= 1) {
+ items.push({ title: NEXT, next: true });
+ }
- if (total - page >= 1) items.push({ title: LAST, last: true });
+ if (total - page >= 1) items.push({ title: LAST, last: true });
- return items;
- },
+ return items;
},
- template: `
- <div class="gl-pagination">
- <ul class="pagination clearfix">
- <li v-for='item in getItems'
- :class='{
- page: item.page,
- prev: item.prev,
- next: item.next,
- separator: item.separator,
- active: item.active,
- disabled: item.disabled
- }'
- >
- <a @click="changePage($event)">{{item.title}}</a>
- </li>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ },
+ template: `
+ <div class="gl-pagination">
+ <ul class="pagination clearfix">
+ <li v-for='item in getItems'
+ :class='{
+ page: item.page,
+ prev: item.prev,
+ next: item.next,
+ separator: item.separator,
+ active: item.active,
+ disabled: item.disabled
+ }'
+ >
+ <a @click="changePage($event)">{{item.title}}</a>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index 4157fefddc9..d5f87588c28 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -1,18 +1,23 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars,
-no-param-reassign, no-plusplus */
-/* global Vue */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+Vue.use(VueResource);
+
+// Maintain a global counter for active requests
+// see: spec/support/wait_for_vue_resource.rb
Vue.http.interceptors.push((request, next) => {
- Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+ window.activeVueResources = window.activeVueResources || 0;
+ window.activeVueResources += 1;
- next((response) => {
- Vue.activeResources--;
+ next(() => {
+ window.activeVueResources -= 1;
});
});
+// Inject CSRF token so we don't break any tests.
Vue.http.interceptors.push((request, next) => {
- // needed in order to not break the tests.
if ($.rails) {
+ // eslint-disable-next-line no-param-reassign
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
}
next();
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 546718ddaf8..1ae144fb471 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -92,6 +92,10 @@
.award-menu-holder {
display: inline-block;
position: relative;
+
+ .tooltip {
+ white-space: nowrap;
+ }
}
.award-control {
@@ -124,6 +128,10 @@
&:focus {
outline: 0;
}
+
+ .award-control-icon {
+ margin: 0;
+ }
}
&.is-loading {
@@ -153,6 +161,7 @@
.award-control-icon {
color: $border-gray-normal;
margin-top: 1px;
+ padding: 0 2px;
}
.award-control-text {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index cda46223492..4369ae78bde 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -68,23 +68,19 @@
}
@mixin btn-green {
- @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, $white-light);
+ @include btn-color($green-500, $green-600, $green-600, $green-700, $green-700, $green-800, $white-light);
}
@mixin btn-blue {
- @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, $white-light);
-}
-
-@mixin btn-blue-medium {
- @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, $white-light);
+ @include btn-color($blue-500, $blue-600, $blue-600, $blue-700, $blue-700, $blue-800, $white-light);
}
@mixin btn-orange {
- @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, $white-light);
+ @include btn-color($orange-500, $orange-600, $orange-600, $orange-700, $orange-700, $orange-800, $white-light);
}
@mixin btn-red {
- @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, $white-light);
+ @include btn-color($red-500, $red-600, $red-600, $red-700, $red-700, $red-800, $white-light);
}
@mixin btn-gray {
@@ -145,11 +141,11 @@
&.btn-new,
&.btn-create,
&.btn-save {
- @include btn-outline($white-light, $border-green-light, $border-green-light, $green-light, $white-light, $border-green-light, $green-normal, $border-green-normal);
+ @include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700);
}
&.btn-remove {
- @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal);
+ @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
}
}
@@ -157,11 +153,8 @@
@include btn-gray;
}
- &.btn-primary {
- @include btn-blue-medium;
- }
-
&.btn-info,
+ &.btn-primary,
&.btn-register {
@include btn-blue;
}
@@ -171,11 +164,11 @@
}
&.btn-close {
- @include btn-outline($white-light, $border-orange-light, $border-orange-light, $orange-light, $white-light, $border-orange-light, $orange-normal, $border-orange-normal);
+ @include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700);
}
&.btn-spam {
- @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal);
+ @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
}
&.btn-danger,
@@ -360,7 +353,7 @@
.btn-inverted {
&-secondary {
- @include btn-outline($white-light, $border-blue-light, $border-blue-light, $blue-light, $white-light, $border-blue-light, $blue-normal, $border-blue-normal);
+ @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700);
}
}
@@ -369,3 +362,13 @@
width: 100%;
}
}
+
+.btn-blank {
+ padding: 0;
+ background: transparent;
+ border: 0;
+
+ &:focus {
+ outline: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index a4b38723bbd..2c33b235980 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -429,3 +429,9 @@ table {
@include str-truncated(100%);
}
}
+
+.tooltip {
+ .tooltip-inner {
+ word-wrap: break-word;
+ }
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 186bb9ac616..2ede47e9de6 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -119,6 +119,46 @@
}
}
+@mixin dropdown-link {
+ display: block;
+ position: relative;
+ padding: 5px 8px;
+ color: $gl-text-color;
+ line-height: initial;
+ text-overflow: ellipsis;
+ border-radius: 2px;
+ white-space: nowrap;
+ overflow: hidden;
+
+ &:hover,
+ &:focus,
+ &.is-focused {
+ background-color: $dropdown-link-hover-bg;
+ text-decoration: none;
+
+ .badge {
+ background-color: darken($dropdown-link-hover-bg, 5%);
+ }
+ }
+
+ &.dropdown-menu-empty-link {
+ &.is-focused {
+ background-color: $dropdown-empty-row-bg;
+ }
+ }
+
+ &.dropdown-menu-user-link {
+ line-height: 16px;
+ }
+
+ .icon-play {
+ fill: $gl-text-color-secondary;
+ margin-right: 6px;
+ height: 12px;
+ width: 11px;
+ }
+}
+
.dropdown-menu,
.dropdown-menu-nav {
display: none;
@@ -178,43 +218,7 @@
}
a {
- display: block;
- position: relative;
- padding: 5px 8px;
- color: $gl-text-color;
- line-height: initial;
- text-overflow: ellipsis;
- border-radius: 2px;
- white-space: nowrap;
- overflow: hidden;
-
- &:hover,
- &:focus,
- &.is-focused {
- background-color: $dropdown-link-hover-bg;
- text-decoration: none;
-
- .badge {
- background-color: darken($row-hover, 5%);
- }
- }
-
- &.dropdown-menu-empty-link {
- &.is-focused {
- background-color: $dropdown-empty-row-bg;
- }
- }
-
- &.dropdown-menu-user-link {
- line-height: 16px;
- }
-
- .icon-play {
- fill: $gl-text-color-secondary;
- margin-right: 6px;
- height: 12px;
- width: 11px;
- }
+ @include dropdown-link;
}
.dropdown-header {
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 0a8bc95590e..d86ae57cd9a 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -2,5 +2,6 @@ gl-emoji {
display: inline-block;
display: inline-flex;
vertical-align: middle;
+ font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1.5em;
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 2ebeaf9a40d..51805c5d734 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -76,12 +76,14 @@
}
.input-token {
- flex: 1;
- -webkit-flex: 1;
+ max-width: 200px;
}
- .filtered-search-token + .input-token:not(:last-child) {
- max-width: 200px;
+ .input-token:only-child,
+ .input-token:last-child {
+ flex: 1;
+ -webkit-flex: 1;
+ max-width: initial;
}
}
@@ -158,8 +160,8 @@
background-color: $white-light;
@media (max-width: $screen-xs-min) {
- -webkit-flex: 1 1 100%;
- flex: 1 1 100%;
+ -webkit-flex: 1 1 auto;
+ flex: 1 1 auto;
margin-bottom: 10px;
.dropdown-menu {
@@ -171,17 +173,26 @@
}
}
+ &:hover {
+ @extend .form-control:hover;
+ }
+
+ &.focus,
+ &.focus:hover {
+ border-color: $dropdown-input-focus-border;
+ box-shadow: 0 0 4px $search-input-focus-shadow-color;
+ }
+
+ &.focus .fa-filter {
+ color: $common-gray-dark;
+ }
+
.form-control {
position: relative;
min-width: 200px;
- padding-left: 0;
- padding-right: 25px;
+ padding: 5px 25px 6px 0;
border-color: transparent;
- &:focus ~ .fa-filter {
- color: $common-gray-dark;
- }
-
&:focus,
&:hover {
outline: none;
@@ -221,6 +232,10 @@
.filter-dropdown-container {
display: -webkit-flex;
display: flex;
+
+ .dropdown-toggle {
+ line-height: 22px;
+ }
}
.dropdown-menu .filter-dropdown-item {
@@ -246,7 +261,9 @@
background-color: $white-light;
border-top: 0;
}
+}
+@media (max-width: $screen-xs) {
.filter-dropdown-container {
.dropdown-toggle,
.dropdown {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 25d6fbe465a..432024779fd 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -177,37 +177,45 @@ label {
}
.gl-field-error {
- color: $red-normal;
+ color: $red-500;
}
.gl-show-field-errors {
.gl-field-success-outline {
- border: 1px solid $green-normal;
+ border: 1px solid $green-600;
&:focus {
- box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-normal;
+ box-shadow: 0 0 0 1px $green-600 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-600;
border: 0 none;
}
}
.gl-field-error-outline {
- border: 1px solid $red-normal;
+ border: 1px solid $red-500;
&:focus {
- box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error;
+ box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error;
border: 0 none;
}
}
.gl-field-success-message {
- color: $green-normal;
+ color: $green-600;
}
.gl-field-error-message {
- color: $red-normal;
+ color: $red-500;
}
.gl-field-hint {
color: $gl-text-color;
}
}
+
+@media(max-width: $screen-xs-max) {
+ .remember-me {
+ .remember-me-checkbox {
+ margin-top: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 6660a022260..abb092623c0 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -26,7 +26,7 @@ header {
padding: 0 16px;
z-index: 100;
margin-bottom: 0;
- height: $header-height;
+ min-height: $header-height;
background-color: $gray-light;
border: none;
border-bottom: 1px solid $border-color;
@@ -48,10 +48,10 @@ header {
color: $gl-text-color-secondary;
font-size: 18px;
padding: 0;
- margin: ($header-height - 28) / 2 0;
+ margin: (($header-height - 28) / 2) 3px;
margin-left: 8px;
height: 28px;
- min-width: 28px;
+ min-width: 32px;
line-height: 28px;
text-align: center;
@@ -73,21 +73,29 @@ header {
background-color: $gray-light;
color: $gl-text-color;
- .todos-pending-count {
- background: darken($todo-alert-blue, 10%);
+ svg {
+ fill: $gl-text-color;
}
}
.fa-caret-down {
font-size: 14px;
}
+
+ svg {
+ position: relative;
+ top: 2px;
+ height: 17px;
+ // hack to get SVG to line up with FA icons
+ width: 23px;
+ fill: $gl-text-color-secondary;
+ }
}
.navbar-toggle {
color: $nav-toggle-gray;
- margin: 6px 0;
+ margin: 5px 0;
border-radius: 0;
- position: absolute;
right: -10px;
padding: 6px 10px;
@@ -135,14 +143,12 @@ header {
}
.header-content {
+ display: flex;
+ justify-content: space-between;
position: relative;
- height: $header-height;
+ min-height: $header-height;
padding-left: 30px;
- @media (min-width: $screen-sm-min) {
- padding-right: 0;
- }
-
.dropdown-menu {
margin-top: -5px;
}
@@ -165,8 +171,7 @@ header {
}
.group-name-toggle {
- margin: 0 5px;
- vertical-align: sub;
+ margin: 3px 5px;
}
.group-title {
@@ -177,39 +182,32 @@ header {
}
}
+ .title-container {
+ display: flex;
+ align-items: flex-start;
+ flex: 1 1 auto;
+ padding-top: (($header-height - 19) / 2);
+ overflow: hidden;
+ }
+
.title {
position: relative;
padding-right: 20px;
margin: 0;
font-size: 18px;
- max-width: 385px;
+ line-height: 22px;
display: inline-block;
- line-height: $header-height;
font-weight: normal;
color: $gl-text-color;
- overflow: hidden;
- text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
- &.initializing {
- display: none;
- }
-
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- max-width: 300px;
- }
-
- @media (max-width: $screen-xs-max) {
- max-width: 190px;
+ &.wrap {
+ white-space: normal;
}
- @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
- max-width: 428px;
- }
-
- @media (min-width: $screen-lg-min) {
- max-width: 685px;
+ &.initializing {
+ opacity: 0;
}
a {
@@ -226,10 +224,10 @@ header {
border: transparent;
background: transparent;
position: absolute;
+ top: 2px;
right: 3px;
width: 12px;
line-height: 19px;
- margin-top: (($header-height - 19) / 2);
padding: 0;
font-size: 10px;
text-align: center;
@@ -247,15 +245,12 @@ header {
}
.navbar-collapse {
- float: right;
+ flex: 0 0 auto;
border-top: none;
-
- @media (min-width: $screen-md-min) {
- padding: 0;
- }
+ padding: 0;
@media (max-width: $screen-xs-max) {
- float: none;
+ flex: 1 1 auto;
}
}
}
@@ -265,14 +260,34 @@ header {
}
.impersonation i {
- color: $red-normal;
+ color: $red-500;
}
}
-.page-sidebar-pinned.right-sidebar-expanded {
- @media (max-width: $screen-md-max) {
- .header-content .title {
- width: 300px;
+.navbar-nav {
+ li {
+ .badge {
+ position: inherit;
+ top: -3px;
+ font-weight: normal;
+ margin-left: -12px;
+ font-size: 11px;
+ color: $white-light;
+ padding: 1px 5px 2px;
+ border-radius: 7px;
+ box-shadow: 0 1px 0 rgba($gl-header-color, .2);
+
+ &.issues-count {
+ background-color: $green-500;
+ }
+
+ &.merge-requests-count {
+ background-color: $orange-600;
+ }
+
+ &.todos-count {
+ background-color: $blue-500;
+ }
}
}
}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index db8d231a82a..87667f39ab8 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -1,8 +1,8 @@
.ci-status-icon-success {
- color: $gl-success;
+ color: $green-500;
svg {
- fill: $gl-success;
+ fill: $green-500;
}
}
@@ -17,18 +17,18 @@
.ci-status-icon-pending,
.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings {
- color: $gl-warning;
+ color: $orange-500;
svg {
- fill: $gl-warning;
+ fill: $orange-500;
}
}
.ci-status-icon-running {
- color: $blue-normal;
+ color: $blue-400;
svg {
- fill: $blue-normal;
+ fill: $blue-400;
}
}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 46632f15f35..1537b0744cc 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -33,7 +33,7 @@
}
&.status-box-open {
- background-color: $green-light;
+ background-color: $green-500;
}
&.status-box-expired {
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 0a42b17c1f5..20c7bc93c28 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -23,6 +23,10 @@ body {
}
}
+.content-wrapper {
+ padding-bottom: 100px;
+}
+
.container {
padding-top: 0;
z-index: 5;
@@ -72,28 +76,28 @@ body {
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
.alert-warning {
transition: background-color 0.15s, border-color 0.15s;
- background-color: lighten($gl-warning, 4%);
- border-color: lighten($gl-warning, 4%);
+ background-color: $orange-500;
+ border-color: $orange-500;
}
.alert-warning + .alert-warning {
- background-color: $gl-warning;
- border-color: $gl-warning;
+ background-color: $orange-600;
+ border-color: $orange-600;
}
.alert-warning + .alert-warning + .alert-warning {
- background-color: darken($gl-warning, 4%);
- border-color: darken($gl-warning, 4%);
+ background-color: $orange-700;
+ border-color: $orange-700;
}
.alert-warning + .alert-warning + .alert-warning + .alert-warning {
- background-color: darken($gl-warning, 8%);
- border-color: darken($gl-warning, 8%);
+ background-color: $orange-800;
+ border-color: $orange-800;
}
.alert-warning:only-of-type {
- background-color: $gl-warning;
- border-color: $gl-warning;
+ background-color: $orange-500;
+ border-color: $orange-500;
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 7adbb0a4188..15dc0aa6a52 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -122,7 +122,7 @@ ul.content-list {
}
.member-group-link {
- color: $blue-normal;
+ color: $blue-600;
}
.description {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index df78bbdea51..b3340d41333 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -52,6 +52,18 @@
}
}
+@mixin basic-list-stats {
+ .stats {
+ float: right;
+ line-height: $list-text-height;
+ color: $gl-text-color;
+
+ span {
+ margin-right: 15px;
+ }
+ }
+}
+
@mixin bulleted-list {
> ul {
list-style-type: disc;
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index ea45aaa0253..e6d808717f3 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -138,7 +138,6 @@
.nav-links {
display: inline-block;
- width: 50%;
margin-bottom: 0;
border-bottom: none;
@@ -147,6 +146,10 @@
display: block;
}
+ &.scrolling-tabs {
+ float: left;
+ }
+
li a {
padding: 16px 15px 11px;
}
@@ -417,14 +420,16 @@
.page-with-layout-nav {
.right-sidebar {
- top: ($header-height * 2) + 2;
+ top: ($header-height + 1) * 2;
}
- .build-sidebar {
- top: ($header-height * 3) + 3;
+ &.page-with-sub-nav {
+ .right-sidebar {
+ top: ($header-height + 1) * 3;
- &.affix {
- top: 0;
+ &.affix {
+ top: 0;
+ }
}
}
}
@@ -475,3 +480,44 @@
}
}
}
+
+.inner-page-scroll-tabs {
+ position: relative;
+
+ .nav-links {
+ padding-bottom: 1px;
+ }
+
+ .fade-right {
+ @include fade(left, $white-light);
+ right: 0;
+ text-align: right;
+
+ .fa {
+ right: 5px;
+ }
+ }
+
+ .fade-left {
+ @include fade(right, $white-light);
+ left: 0;
+ text-align: left;
+
+ .fa {
+ left: 5px;
+ }
+ }
+
+ .fade-right,
+ .fade-left {
+ top: 16px;
+ bottom: auto;
+ }
+
+ &.is-smaller {
+ .fade-right,
+ .fade-left {
+ top: 11px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 40e93032f59..746c9c25620 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -33,7 +33,7 @@
padding-right: 0;
@media (min-width: $screen-sm-min) {
- .content-wrapper {
+ &:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
@@ -55,7 +55,7 @@
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- .content-wrapper {
+ &:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 12a86a64645..e54cc2866a7 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -176,6 +176,10 @@ summary {
&.panel-without-border {
border: 0;
}
+
+ &.panel-without-margin {
+ margin: 0;
+ }
}
.panel-succes .panel-heading,
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 0fc89d5976a..c9f345d24be 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -31,6 +31,7 @@ $border-radius-small: 3px !default;
//
$text-color: $gl-text-color;
$link-color: $gl-link-color;
+$link-hover-color: $gl-link-hover-color;
//== Typography
@@ -73,7 +74,7 @@ $pagination-hover-color: $gl-text-color;
$pagination-hover-bg: $row-hover;
$pagination-hover-border: $border-color;
-$pagination-active-color: $blue-dark;
+$pagination-active-color: $blue-600;
$pagination-active-bg: $white-light;
$pagination-active-border: $border-color;
@@ -135,8 +136,8 @@ $well-border: #eee;
//
//##
-$code-color: #c7254e;
-$code-bg: #f9f2f4;
+$code-color: $red-600;
+$code-bg: lighten($red-50, 2%);
$kbd-color: $white-light;
$kbd-bg: #333;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index db5e2c51fe7..c241816788b 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -306,6 +306,11 @@ a > code {
* Textareas intended for GFM
*
*/
+textarea.js-gfm-input {
+ font-family: $monospace_font;
+ font-size: 13px;
+}
+
.strikethrough {
text-decoration: line-through;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 6841adb637e..97794a47df8 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -26,27 +26,49 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
$gray-darkest: #c4c4c4;
-$green-light: #3cbd70;
-$green-normal: darken($green-light, $darken-normal-factor);
-$green-dark: darken($green-light, $darken-dark-factor);
-
-$blue-light: #2ea8e5;
-$blue-normal: darken($blue-light, $darken-normal-factor);
-$blue-dark: darken($blue-light, $darken-dark-factor);
-
-$blue-medium-light: #3498cb;
-$blue-medium: darken($blue-medium-light, $darken-normal-factor);
-$blue-medium-dark: darken($blue-medium-light, $darken-dark-factor);
-
-$blue-light-transparent: rgba(44, 159, 216, 0.05);
-
-$orange-light: #fc8a51;
-$orange-normal: darken($orange-light, $darken-normal-factor);
-$orange-dark: darken($orange-light, $darken-dark-factor);
-
-$red-light: #e52c5a;
-$red-normal: darken($red-light, $darken-normal-factor);
-$red-dark: darken($red-light, $darken-dark-factor);
+$green-50: #e4f5eb;
+$green-100: #bae6cc;
+$green-200: #8dd5aa;
+$green-300: #5fc488;
+$green-400: #3cb76f;
+$green-500: #1aaa55;
+$green-600: #168f48;
+$green-700: #12753a;
+$green-800: #0e5a2d;
+$green-900: #0a4020;
+
+$blue-50: #e4eff9;
+$blue-100: #bcd7f1;
+$blue-200: #8fbce8;
+$blue-300: #62a1df;
+$blue-400: #418cd8;
+$blue-500: #1f78d1;
+$blue-600: #1b69b6;
+$blue-700: #17599c;
+$blue-800: #134a81;
+$blue-900: #0f3b66;
+
+$orange-50: #fff2e1;
+$orange-100: #fedfb3;
+$orange-200: #feca81;
+$orange-300: #fdb44f;
+$orange-400: #fca429;
+$orange-500: #fc9403;
+$orange-600: #de7e00;
+$orange-700: #c26700;
+$orange-800: #a35100;
+$orange-900: #853b00;
+
+$red-50: #fbe7e4;
+$red-100: #f4c4bc;
+$red-200: #ed9d90;
+$red-300: #e67664;
+$red-400: #e05842;
+$red-500: #db3b21;
+$red-600: #c0341d;
+$red-700: #a62d19;
+$red-800: #8b2615;
+$red-900: #711e11;
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
@@ -58,33 +80,11 @@ $border-gray-light: darken($gray-light, $darken-border-factor);
$border-gray-normal: darken($gray-normal, $darken-border-factor);
$border-gray-dark: darken($white-normal, $darken-border-factor);
-$border-green-extra-light: #9adb84;
-$border-green-light: darken($green-light, $darken-border-factor);
-$border-green-normal: darken($green-normal, $darken-border-factor);
-$border-green-dark: darken($green-dark, $darken-border-factor);
-
-$border-blue-light: darken($blue-light, $darken-border-factor);
-$border-blue-normal: darken($blue-normal, $darken-border-factor);
-$border-blue-dark: darken($blue-dark, $darken-border-factor);
-
-$border-orange-light: darken($orange-light, $darken-border-factor);
-$border-orange-normal: darken($orange-normal, $darken-border-factor);
-$border-orange-dark: darken($orange-dark, $darken-border-factor);
-
-$border-red-light: darken($red-light, $darken-border-factor);
-$border-red-normal: darken($red-normal, $darken-border-factor);
-$border-red-dark: darken($red-dark, $darken-border-factor);
-
-$warning-message-bg: #fbf2d9;
-$warning-message-color: #9e8e60;
-$warning-message-border: #f0e2bb;
-
/*
* UI elements
*/
$border-color: #e5e5e5;
-$focus-border-color: #3aabf0;
-$sidebar-collapsed-icon-color: #999;
+$focus-border-color: $blue-300;
$well-expand-item: #e8f2f7;
$well-inner-border: #eef0f2;
$well-light-border: #f1f1f1;
@@ -97,10 +97,11 @@ $gl-font-size: 14px;
$gl-text-color: rgba(0, 0, 0, .85);
$gl-text-color-secondary: rgba(0, 0, 0, .55);
$gl-text-color-disabled: rgba(0, 0, 0, .35);
-$gl-text-green: #4a2;
-$gl-text-red: #d12f19;
-$gl-text-orange: #d90;
-$gl-link-color: #3777b0;
+$gl-text-green: $green-600;
+$gl-text-red: $red-500;
+$gl-text-orange: $orange-600;
+$gl-link-color: $blue-600;
+$gl-link-hover-color: $blue-800;
$gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
@@ -117,9 +118,9 @@ $list-text-disabled-color: $gl-text-color-disabled;
$list-border-light: #eee;
$list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px;
-$list-warning-row-bg: #fcf8e3;
-$list-warning-row-border: #faebcc;
-$list-warning-row-color: #8a6d3b;
+$list-warning-row-bg: $orange-50;
+$list-warning-row-border: $orange-100;
+$list-warning-row-color: $orange-700;
/*
* Markdown
@@ -146,24 +147,24 @@ $gl-sidebar-padding: 22px;
/*
* Misc
*/
-$row-hover: #f7faff;
-$row-hover-border: #b2d7ff;
+$row-hover: lighten($blue-50, 2%);
+$row-hover-border: $blue-100;
$progress-color: #c0392b;
$header-height: 50px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$gl-avatar-size: 40px;
-$error-exclamation-point: #e62958;
+$error-exclamation-point: $red-500;
$border-radius-default: 2px;
$settings-icon-size: 18px;
-$provider-btn-not-active-color: #4688f1;
-$link-underline-blue: #4a8bee;
-$active-item-blue: #4a8bee;
+$provider-btn-not-active-color: $blue-500;
+$link-underline-blue: $blue-500;
+$active-item-blue: $blue-500;
$layout-link-gray: #7e7c7c;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
-$issue-status-expired: #cea61b;
+$issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary;
$show-aside-bg: #eee;
$show-aside-color: #777;
@@ -192,10 +193,10 @@ $user-mention-color: #2fa0bb;
$time-color: #999;
$project-member-show-color: #aaa;
$gl-promo-color: #aaa;
-$error-bg: #c67;
-$warning-message-bg: #ffffe6;
-$warning-message-border: #ed9;
-$warning-message-color: #b90;
+$error-bg: $red-400;
+$warning-message-bg: $orange-50;
+$warning-message-border: $orange-100;
+$warning-message-color: $orange-700;
$control-group-descr-color: #666;
$table-permission-x-bg: #d9edf7;
$username-color: #666;
@@ -210,30 +211,30 @@ $tanuki-yellow: #fca326;
/*
* State colors:
*/
-$gl-primary: $blue-normal;
-$gl-success: $green-normal;
+$gl-primary: $blue-500;
+$gl-success: $green-500;
$gl-success-focus: rgba($gl-success, .4);
-$gl-info: $blue-normal;
-$gl-warning: $orange-normal;
-$gl-danger: $red-normal;
+$gl-info: $blue-500;
+$gl-warning: $orange-500;
+$gl-danger: $red-500;
$gl-btn-active-background: rgba(0, 0, 0, 0.16);
$gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background;
/*
* Commit Diff Colors
*/
-$added: #63c363;
-$deleted: #f77;
-$line-added: #ecfdf0;
-$line-added-dark: #c7f0d2;
-$line-removed: #fbe9eb;
-$line-removed-dark: #fac5cd;
-$line-number-old: #f9d7dc;
-$line-number-new: #ddfbe6;
-$line-number-select: #fbf2da;
-$line-target-blue: #f6faff;
-$line-select-yellow: #fcf8e7;
-$line-select-yellow-dark: #f0e2bd;
+$added: $green-300;
+$deleted: $red-300;
+$line-added: $green-50;
+$line-added-dark: $green-100;
+$line-removed: $red-50;
+$line-removed-dark: $red-100;
+$line-number-old: lighten($red-100, 5%);
+$line-number-new: lighten($green-100, 5%);
+$line-number-select: lighten($orange-100, 5%);
+$line-target-blue: $blue-50;
+$line-select-yellow: $orange-50;
+$line-select-yellow-dark: $orange-100;
$dark-diff-match-bg: rgba(255, 255, 255, 0.3);
$dark-diff-match-color: rgba(255, 255, 255, 0.1);
$file-mode-changed: #777;
@@ -273,7 +274,7 @@ $dropdown-toggle-active-border-color: darken($border-color, 14%);
/*
* Filtered Search
*/
-$dropdown-hover-color: #3b86ff;
+$dropdown-hover-color: $blue-400;
/*
* Buttons
@@ -296,10 +297,10 @@ $award-emoji-menu-shadow: rgba(0,0,0,.175);
/*
* Search Box
*/
-$search-input-border-color: rgba(#4688f1, .8);
+$search-input-border-color: rgba($blue-400, .8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
$search-input-width: 220px;
-$location-badge-active-bg: #4f91f8;
+$location-badge-active-bg: $blue-500;
$location-icon-color: #e7e9ed;
/*
@@ -361,18 +362,18 @@ $builds-trace-bg: #111;
/*
* Callout
*/
-$callout-danger-bg: #fdf7f7;
-$callout-danger-border: #eed3d7;
-$callout-danger-color: #b94a48;
-$callout-warning-bg: #faf8f0;
-$callout-warning-border: #faebcc;
-$callout-warning-color: #8a6d3b;
-$callout-info-bg: #f4f8fa;
-$callout-info-border: #bce8f1;
-$callout-info-color: #34789a;
-$callout-success-bg: #dff0d8;
-$callout-success-border: #5ca64d;
-$callout-success-color: #3c763d;
+$callout-danger-bg: $red-50;
+$callout-danger-border: $red-100;
+$callout-danger-color: $red-700;
+$callout-warning-bg: $orange-50;
+$callout-warning-border: $orange-100;
+$callout-warning-color: $orange-700;
+$callout-info-bg: $blue-50;
+$callout-info-border: $blue-100;
+$callout-info-color: $blue-700;
+$callout-success-bg: $green-50;
+$callout-success-border: $green-100;
+$callout-success-color: $green-700;
/*
* Commit Page
@@ -392,7 +393,7 @@ $common-green: $gl-text-green;
/*
* Editor
*/
-$editor-cancel-color: #b94a48;
+$editor-cancel-color: $red-600;
/*
* Events
@@ -416,10 +417,10 @@ $logs-p-color: #333;
* Forms
*/
$input-danger-bg: #f2dede;
-$input-danger-border: #d66;
+$input-danger-border: $red-400;
$input-group-addon-bg: #f7f8fa;
$gl-field-focus-shadow: rgba(0, 0, 0, 0.075);
-$gl-field-focus-shadow-error: rgba(210, 40, 82, 0.6);
+$gl-field-focus-shadow-error: rgba($red-500, 0.6);
/*
* Help
@@ -453,14 +454,14 @@ $label-border-radius: 100px;
/*
* Lint
*/
-$lint-incorrect-color: red;
-$lint-correct-color: #47a447;
+$lint-incorrect-color: $red-500;
+$lint-correct-color: $green-500;
/*
* Login
*/
$login-brand-holder-color: #888;
-$login-devise-error-color: #a00;
+$login-devise-error-color: $red-700;
/*
* Nav
@@ -474,33 +475,33 @@ $nav-toggle-gray: #666;
*/
$notify-details: #777;
$notify-footer: #777;
-$notify-new-file: #090;
-$notify-deleted-file: #b00;
+$notify-new-file: $green-600;
+$notify-deleted-file: $red-700;
/*
* Projects
*/
$project-option-descr-color: #54565b;
$project-breadcrumb-color: #999;
-$project-private-forks-notice-odd: #2aa056;
+$project-private-forks-notice-odd: $green-600;
$project-network-controls-color: #888;
/*
* Runners
*/
-$runner-state-shared-bg: #32b186;
-$runner-state-specific-bg: #3498db;
-$runner-status-online-color: $green-normal;
+$runner-state-shared-bg: $green-400;
+$runner-state-specific-bg: $blue-400;
+$runner-status-online-color: $green-600;
$runner-status-offline-color: $gray-darkest;
-$runner-status-paused-color: $red-normal;
+$runner-status-paused-color: $red-500;
/*
Stat Graph
*/
$stat-graph-common-bg: #f3f3f3;
-$stat-graph-area-fill: #1db34f;
+$stat-graph-area-fill: $green-500;
$stat-graph-axis-fill: #aaa;
-$stat-graph-orange-fill: #f17f49;
+$stat-graph-orange-fill: $orange-500;
$stat-graph-selection-fill: #333;
$stat-graph-selection-stroke: #333;
@@ -514,7 +515,7 @@ $select2-drop-shadow2: rgba(31, 37, 50, 0.317647);
/*
* Todo
*/
-$todo-alert-blue: #428bca;
+$todo-alert-blue: $blue-500;
$todo-body-pre-color: #777;
$todo-body-border: #ddd;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index f9ee33019cd..b6168a293e0 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -240,8 +240,13 @@
font-size: (14px / $issue-boards-font-size) * 1em;
}
+ .card-assignee {
+ margin-right: 5px;
+ }
+
.avatar {
margin-left: 0;
+ margin-right: 0;
}
}
@@ -296,7 +301,7 @@
}
}
-.issue-boards-sidebar {
+.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar {
&.right-sidebar {
top: 0;
bottom: 0;
@@ -487,9 +492,9 @@
right: -3px;
top: -3px;
width: 17px;
- background-color: $blue-light;
+ background-color: $blue-500;
color: $white-light;
- border: 1px solid $border-blue-light;
+ border: 1px solid $blue-600;
font-size: 9px;
line-height: 15px;
border-radius: 50%;
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index a24292a7c8c..969fc75c6eb 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -366,9 +366,3 @@
right: 0;
margin-top: -17px;
}
-
-@media (min-width: $screen-md-min) {
- .sub-nav.build {
- width: calc(100% + #{$gutter_width});
- }
-}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index da8410eca66..0dad91ba128 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -142,7 +142,9 @@
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
- line-height: 1;
+ font-size: $gl-font-size;
+ line-height: $gl-font-size;
+ outline: none;
&:hover {
background-color: darken($gray-light, 10%);
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index eab79c2a481..1aa1079903c 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -431,6 +431,21 @@
border-bottom: none;
}
+.diff-stats-summary-toggler {
+ padding: 0;
+ background-color: transparent;
+ border: 0;
+ color: $gl-link-color;
+ transition: color 0.1s linear;
+
+ &:hover,
+ &:focus {
+ outline: none;
+ text-decoration: underline;
+ color: $gl-link-hover-color;
+ }
+}
+
// Mobile
@media (max-width: 480px) {
.diff-title {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 73a5da715f2..3d91e0b22d8 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -18,12 +18,15 @@
.environments-container {
.table-holder {
width: 100%;
- overflow: auto;
+
+ @media (max-width: $screen-sm-max) {
+ overflow: auto;
+ }
}
.table.ci-table {
.environments-actions {
- min-width: 200px;
+ min-width: 300px;
}
.environments-commit,
@@ -219,3 +222,12 @@
stroke: $black;
stroke-width: 1;
}
+
+.environments-actions {
+ .external-url,
+ .monitoring-url,
+ .terminal-button,
+ .stop-env-link {
+ width: 38px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 84d21e48463..73a5889867a 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -9,16 +9,15 @@
}
}
-.group-row {
- .stats {
- float: right;
- line-height: $list-text-height;
- color: $gl-text-color;
+.group-root-path {
+ max-width: 40vw;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-wrap: nowrap;
+}
- span {
- margin-right: 15px;
- }
- }
+.group-row {
+ @include basic-list-stats;
}
.ldap-group-links {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 4426169ef5a..e84a05e3e9e 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -243,6 +243,10 @@
font-size: 13px;
font-weight: normal;
}
+
+ .hide-expanded {
+ display: none;
+ }
}
&.right-sidebar-collapsed {
@@ -282,10 +286,11 @@
display: block;
width: 100%;
text-align: center;
- padding-bottom: 10px;
+ margin-bottom: 10px;
color: $issuable-sidebar-color;
- &:hover {
+ &:hover,
+ &:hover .todo-undone {
color: $gl-text-color;
}
@@ -294,6 +299,10 @@
margin-top: 0;
}
+ .todo-undone {
+ color: $gl-link-color;
+ }
+
.author {
display: none;
}
@@ -498,7 +507,7 @@
svg {
width: 16px;
height: 16px;
- fill: $sidebar-collapsed-icon-color;
+ fill: $issuable-sidebar-color;
}
&:hover svg {
@@ -520,12 +529,12 @@
&.over_estimate {
.meter-fill {
- background: $red-light;
+ background: $red-500;
}
.time-remaining,
.compare-value.spent {
- color: $red-light;
+ color: $red-500;
}
}
}
@@ -582,3 +591,21 @@
opacity: 0;
}
}
+
+.issuable-todo-btn {
+ .fa-spinner {
+ display: none;
+ }
+
+ &.is-loading {
+ .fa-spinner {
+ display: inline-block;
+ }
+
+ &.sidebar-collapsed-icon {
+ .issuable-todo-inner {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index cb7ebd61504..b2f45625a2a 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -46,6 +46,10 @@ ul.related-merge-requests > li {
.merge-request-id {
flex-shrink: 0;
}
+
+ .merge-request-info {
+ margin-left: 5px;
+ }
}
.merge-requests-title,
@@ -58,10 +62,6 @@ ul.related-merge-requests > li {
display: inline-block;
}
-.merge-request-info {
- margin-left: 5px;
-}
-
.merge-request-status {
font-size: 13px;
padding: 0 5px;
@@ -69,21 +69,17 @@ ul.related-merge-requests > li {
height: 20px;
border-radius: 3px;
line-height: 18px;
- border: 1px solid;
&.merged {
- border-color: darken($blue-normal, 10%);
- background: $blue-normal;
+ background: $blue-500;
}
&.closed {
- border-color: darken($red-normal, 10%);
- background: $red-normal;
+ background: $red-500;
}
&.open {
- border: 1px solid darken($green-normal, 10%);
- background: $green-normal;
+ background: $green-500;
}
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 71ed5b1361a..8249e02b64a 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -85,11 +85,11 @@
}
.username .validation-success {
- color: $green-normal;
+ color: $green-600;
}
.username .validation-error {
- color: $red-normal;
+ color: $red-500;
}
}
}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 5a9f199fb34..35cefd449f1 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -255,7 +255,7 @@ $colors: (
&.saved {
.editor {
- border-top: solid 2px $border-green-extra-light;
+ border-top: solid 2px $green-200;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 7c3172421c1..566dcc64802 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -60,7 +60,17 @@
}
.modify-merge-commit-link {
+ padding: 0;
+
+ background-color: transparent;
+ border: 0;
+
color: $gl-text-color;
+
+ &:hover,
+ &:focus {
+ text-decoration: underline;
+ }
}
.merge-param-checkbox {
@@ -535,7 +545,7 @@
}
.fa-info-circle {
- color: $orange-normal;
+ color: $orange-500;
padding-right: 5px;
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 27c47d36818..335e587b8f4 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -52,66 +52,62 @@
}
}
-.milestone-summary {
- .milestone-stat {
- white-space: nowrap;
- margin-right: 10px;
+.milestone-sidebar {
+ .gutter-toggle {
+ margin-bottom: 10px;
+ }
- &.with-drilldown {
- margin-right: 2px;
+ .milestone-progress {
+ .title {
+ padding-top: 5px;
}
- }
- .remaining-days {
- color: $orange-light;
+ .progress {
+ height: 6px;
+ margin: 0;
+ }
}
- .milestone-stats-and-buttons {
- display: flex;
- justify-content: flex-start;
- flex-wrap: wrap;
+ .collapsed-milestone-date {
+ font-size: 12px;
+ }
- @media (min-width: $screen-xs-min) {
- justify-content: space-between;
- flex-wrap: nowrap;
- }
+ .milestone-date {
+ display: block;
}
- .milestone-progress-buttons {
- order: 1;
- margin-top: 10px;
+ .date-separator {
+ line-height: 5px;
+ }
- @media (min-width: $screen-xs-min) {
- order: 2;
- margin-top: 0;
- flex-shrink: 0;
- }
+ .remaining-days strong {
+ font-weight: normal;
+ }
- .btn {
- float: left;
- margin-right: $btn-side-margin;
+ .milestone-stat {
+ float: left;
+ margin-right: 14px;
+ }
- &:last-child {
- margin-right: 0;
- }
- }
+ .milestone-stat:last-child {
+ margin-right: 0;
}
- .milestone-stats {
- order: 2;
- width: 100%;
- padding: 7px 0;
- flex-shrink: 1;
+ .milestone-progress {
+ .sidebar-collapsed-icon {
+ clear: both;
+ padding: 15px 5px 5px;
- @media (min-width: $screen-xs-min) {
- // when displayed on one line stats go first, buttons second
- order: 1;
+ .progress {
+ margin: 5px 0;
+ }
}
}
- .progress {
- width: 100%;
- margin: 15px 0;
+ .right-sidebar-collapsed & {
+ .reference {
+ border-top: 1px solid $border-gray-normal;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c2156a5ac69..927bf9805ce 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -148,6 +148,18 @@
.error-alert > .alert {
margin-top: 5px;
margin-bottom: 5px;
+
+ &.alert-dismissable {
+ .close {
+ color: $white-light;
+ opacity: 0.85;
+ font-weight: normal;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
}
.discussion-body,
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index e238f0865f6..57cf8e136e2 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -243,22 +243,6 @@ ul.notes {
}
}
-.page-sidebar-pinned.right-sidebar-expanded {
- @media (max-width: $screen-md-max) {
- .note-header {
- .note-headline-light {
- display: block;
- }
-
- .note-actions {
- position: absolute;
- right: 0;
- top: 0;
- }
- }
- }
-}
-
// Diff code in discussion view
.discussion-body .diff-file {
.file-title {
@@ -426,8 +410,22 @@ ul.notes {
}
.discussion-toggle-button {
+ padding: 0;
+ background-color: transparent;
+ border: 0;
line-height: 20px;
font-size: 13px;
+ transition: color 0.1s linear;
+
+ &:hover {
+ color: $gl-link-color;
+ }
+
+ &:focus {
+ text-decoration: underline;
+ outline: none;
+ color: $gl-link-color;
+ }
.fa {
margin-right: 3px;
@@ -462,17 +460,18 @@ ul.notes {
background: $white-light;
padding: 1px 5px;
font-size: 12px;
- color: $gl-link-color;
+ color: $blue-500;
margin-left: -55px;
position: absolute;
z-index: 10;
width: 23px;
height: 23px;
- border: 1px solid $border-color;
+ border: 1px solid $blue-500;
transition: transform .1s ease-in-out;
&:hover {
- background: $gl-info;
+ background: $blue-500;
+ border-color: $blue-600;
color: $white-light;
transform: scale(1.15);
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 20eabc83142..a4fe652b52f 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -2,6 +2,7 @@
.realtime-loading {
font-size: 40px;
text-align: center;
+ margin: 0 auto;
}
.stage {
@@ -13,9 +14,16 @@
white-space: nowrap;
}
+ .empty-state {
+ margin: 5% auto 0;
+ }
+
.table-holder {
width: 100%;
- overflow: auto;
+
+ @media (max-width: $screen-sm-max) {
+ overflow: auto;
+ }
}
.commit-title {
@@ -72,11 +80,6 @@
color: $gl-text-color-secondary;
font-size: 14px;
}
-
- svg,
- .fa {
- margin-right: 0;
- }
}
.btn-group {
@@ -104,8 +107,6 @@
@media (max-width: $screen-md-max) {
.content-list {
- &.pipelines,
- &.environments-container,
&.builds-content-list {
width: 100%;
overflow: auto;
@@ -672,51 +673,71 @@
// Dropdown button animation in mini pipeline graph
&.ci-status-icon-success {
- border-color: $gl-success;
- color: $gl-success;
+ border-color: $green-500;
+ color: $green-500;
&:hover,
&:focus,
&:active {
- background-color: rgba($gl-success, 0.1);
- border-color: $gl-success;
+ background-color: $green-50;
+ border-color: $green-600;
+ color: $green-600;
+
+ svg {
+ fill: $green-600;
+ }
}
}
&.ci-status-icon-failed {
- border-color: $gl-danger;
- color: $gl-danger;
+ border-color: $red-500;
+ color: $red-500;
&:hover,
&:focus,
&:active {
- background-color: rgba($gl-danger, 0.1);
- border-color: $gl-danger;
+ background-color: $red-50;
+ border-color: $red-600;
+ color: $red-600;
+
+ svg {
+ fill: $red-600;
+ }
}
}
&.ci-status-icon-pending,
&.ci-status-icon-success_with_warnings {
- border-color: $gl-warning;
- color: $gl-warning;
+ border-color: $orange-500;
+ color: $orange-500;
&:hover,
&:focus,
&:active {
- background-color: rgba($gl-warning, 0.1);
- border-color: $gl-warning;
+ background-color: $orange-50;
+ border-color: $orange-600;
+ color: $orange-600;
+
+ svg {
+ fill: $orange-600;
+ }
}
}
&.ci-status-icon-running {
- border-color: $blue-normal;
- color: $blue-normal;
+ border-color: $blue-400;
+ color: $blue-400;
&:hover,
&:focus,
&:active {
- background-color: rgba($blue-normal, 0.1);
- border-color: $blue-normal;
+ background-color: $blue-50;
+ border-color: $blue-600;
+ color: $blue-600;
+
+ svg {
+ fill: $blue-600;
+ }
}
}
@@ -921,3 +942,22 @@
}
}
}
+
+/**
+ * Play button with icon in dropdowns
+ */
+.ci-table .no-btn {
+ border: none;
+ background: none;
+ outline: none;
+ width: 100%;
+ text-align: left;
+
+ .icon-play {
+ position: relative;
+ top: 2px;
+ margin-right: 5px;
+ height: 13px;
+ width: 12px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 1a983d8c9ef..703c5fc8869 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -74,7 +74,6 @@
display: inline;
a {
- color: $blue-dark;
text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index efa47be9a73..c2c2f371b87 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -477,20 +477,6 @@ a.deploy-project-label {
}
}
-.page-sidebar-pinned {
- .project-stats .nav > li.right {
- @media (min-width: $screen-lg-min) {
- float: none;
- }
- }
-
- .download-button {
- @media (min-width: $screen-lg-min) {
- margin-left: 0;
- }
- }
-}
-
.project-stats {
font-size: 0;
text-align: center;
@@ -582,54 +568,65 @@ pre.light-well {
/*
* Projects list rendered on dashboard and user page
*/
-
.projects-list {
@include basic-list;
+ display: flex;
+ flex-direction: column;
- .project-row {
- border-color: $white-normal;
-
- .project-full-name {
- @include str-truncated;
+ // Disable Flexbox for admin page
+ &.admin-projects {
+ display: block;
- @media (max-width: $screen-xs-max) {
- max-width: 50%;
- }
+ .project-row {
+ display: block;
}
+ }
- .controls {
- line-height: $list-text-height;
+ .project-row {
+ display: flex;
+ align-items: center;
+ @include basic-list-stats;
+ }
- .badge {
- @media (max-width: $screen-xs-max) {
- display: none;
- }
- }
+ h3 {
+ font-size: $gl-font-size;
+ }
- a:hover {
- text-decoration: none;
- }
+ a {
+ color: $gl-text-color;
+ }
- > span {
- margin-left: 10px;
- }
+ .avatar-container,
+ .controls {
+ flex: 0 0 auto;
+ }
- svg {
- position: relative;
- top: 2px;
- }
- }
+ .avatar-container {
+ align-self: flex-start;
+ }
- .description p {
- @media (max-width: $screen-xs-max) {
- max-width: 50%;
- }
+ .project-details {
+ min-width: 0;
+
+ p,
+ .commit-row-message {
+ @include str-truncated(100%);
+ margin-bottom: 0;
}
}
- .bottom {
- padding-top: $gl-padding;
- padding-bottom: 0;
+ .controls {
+ margin-left: auto;
+ }
+
+ .ci-status-link {
+ display: inline-block;
+ line-height: 17px;
+ vertical-align: middle;
+
+ &:hover {
+ text-decoration: none;
+ }
}
}
@@ -745,6 +742,15 @@ pre.light-well {
}
}
+.create-new-protected-branch-button {
+ @include dropdown-link;
+
+ width: 100%;
+ background-color: transparent;
+ border: 0;
+ text-align: left;
+}
+
.protected-branches-list {
margin-bottom: 30px;
diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss
index bed6470dbd3..23a9c2ada80 100644
--- a/app/assets/stylesheets/pages/sherlock.scss
+++ b/app/assets/stylesheets/pages/sherlock.scss
@@ -28,6 +28,6 @@ table .sherlock-code {
}
.sherlock-line-samples-table .slow {
- color: $red-light;
+ color: $red-500;
font-weight: bold;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 6f31d4ed789..4a284247143 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -21,42 +21,41 @@
&.ci-failed,
&.ci-failed_with_warnings {
- color: $gl-danger;
- border-color: $gl-danger;
+ color: $red-500;
+ border-color: $red-500;
&:not(span):hover {
- background-color: rgba($gl-danger, .07);
+ background-color: $red-50;
+ color: $red-600;
+ border-color: $red-600;
+
+ svg {
+ fill: $red-600;
+ }
}
svg {
- fill: $gl-danger;
+ fill: $red-500;
}
}
&.ci-success,
&.ci-success_with_warnings {
- color: $gl-success;
- border-color: $gl-success;
+ color: $green-600;
+ border-color: $green-500;
&:not(span):hover {
- background-color: rgba($gl-success, .07);
- }
-
- svg {
- fill: $gl-success;
- }
- }
-
- &.ci-info {
- color: $gl-info;
- border-color: $gl-info;
+ background-color: $green-50;
+ color: $green-700;
+ border-color: $green-600;
- &:not(span):hover {
- background-color: rgba($gl-info, .07);
+ svg {
+ fill: $green-600;
+ }
}
svg {
- fill: $gl-info;
+ fill: $green-500;
}
}
@@ -75,28 +74,41 @@
}
&.ci-pending {
- color: $gl-warning;
- border-color: $gl-warning;
+ color: $orange-600;
+ border-color: $orange-500;
&:not(span):hover {
- background-color: rgba($gl-warning, .07);
+ background-color: $orange-50;
+ color: $orange-700;
+ border-color: $orange-600;
+
+ svg {
+ fill: $orange-600;
+ }
}
svg {
- fill: $gl-warning;
+ fill: $orange-500;
}
}
+ &.ci-info,
&.ci-running {
- color: $blue-normal;
- border-color: $blue-normal;
+ color: $blue-500;
+ border-color: $blue-500;
&:not(span):hover {
- background-color: rgba($blue-normal, .07);
+ background-color: $blue-50;
+ color: $blue-600;
+ border-color: $blue-600;
+
+ svg {
+ fill: $blue-600;
+ }
}
svg {
- fill: $blue-normal;
+ fill: $blue-500;
}
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 5f0aede4f5e..a39815319f3 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -3,25 +3,6 @@
*
*/
-.navbar-nav {
- li {
- .badge.todos-pending-count {
- position: inherit;
- top: -6px;
- margin-top: -5px;
- font-weight: normal;
- background: $todo-alert-blue;
- margin-left: -17px;
- font-size: 11px;
- color: $white-light;
- padding: 3px;
- padding-top: 1px;
- padding-bottom: 1px;
- border-radius: 3px;
- }
- }
-}
-
.todos-list > .todo {
// workaround because we cannot use border-colapse
border-top: 1px solid transparent;
@@ -47,6 +28,7 @@
.todo-avatar,
.todo-actions {
+ @include transition(opacity);
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
@@ -67,21 +49,34 @@
flex: 0 1 100%;
min-width: 0;
}
-}
-.todos-list > .todo.todo-pending.done-reversible {
- background-color: $gray-light;
+ &.todo-pending.done-reversible {
+ background-color: $white-light;
- &:hover {
- border-color: $border-color;
- }
+ &:hover {
+ border-color: $white-dark;
+ background-color: $gray-light;
- .title {
- font-weight: normal;
+ .todo-avatar,
+ .todo-item {
+ opacity: .6;
+ }
+ }
+
+ .todo-avatar,
+ .todo-item {
+ opacity: .2;
+ }
+
+ .btn {
+ background-color: $gray-light;
+ }
}
}
.todo-item {
+ @include transition(opacity);
+
.todo-title {
display: flex;
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index 5055c318a5f..dc9a6df5f75 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -1,6 +1,7 @@
class Admin::AbuseReportsController < Admin::ApplicationController
def index
@abuse_reports = AbuseReport.order(id: :desc).page(params[:page])
+ @abuse_reports.includes(:reporter, :user)
end
def destroy
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8d831ffdd70..515d8e1523b 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -45,15 +45,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def application_setting_params
- restricted_levels = params[:application_setting][:restricted_visibility_levels]
- if restricted_levels.nil?
- params[:application_setting][:restricted_visibility_levels] = []
- else
- restricted_levels.map! do |level|
- level.to_i
- end
- end
-
import_sources = params[:application_setting][:import_sources]
if import_sources.nil?
params[:application_setting][:import_sources] = []
@@ -143,6 +134,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:unique_ips_limit_enabled,
:version_check_enabled,
:terminal_max_session_time,
+ :polling_interval_multiplier,
disabled_oauth_sign_in_sources: [],
import_sources: [],
diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb
index c09095b9849..5f90ad7137d 100644
--- a/app/controllers/admin/background_jobs_controller.rb
+++ b/app/controllers/admin/background_jobs_controller.rb
@@ -1,7 +1,7 @@
class Admin::BackgroundJobsController < Admin::ApplicationController
def show
- ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command))
- @sidekiq_processes = ps_output.split("\n").grep(/sidekiq/)
+ ps_output, _ = Gitlab::Popen.popen(%W(ps ww -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command))
+ @sidekiq_processes = ps_output.split("\n").grep(/sidekiq \d+\.\d+\.\d+/)
@concurrency = Sidekiq.options[:concurrency]
end
end
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index d496f08a598..4531657268c 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -16,10 +16,9 @@ class Admin::LabelsController < Admin::ApplicationController
end
def create
- @label = Label.new(label_params)
- @label.template = true
+ @label = Labels::CreateService.new(label_params).execute(template: true)
- if @label.save
+ if @label.persisted?
redirect_to admin_labels_url, notice: "Label was created"
else
render :new
@@ -27,7 +26,9 @@ class Admin::LabelsController < Admin::ApplicationController
end
def update
- if @label.update(label_params)
+ @label = Labels::UpdateService.new(label_params).execute(@label)
+
+ if @label.valid?
redirect_to admin_labels_path, notice: 'label was successfully updated.'
else
render :edit
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 24504685e48..563bcc65bd6 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -95,18 +95,14 @@ class Admin::UsersController < Admin::ApplicationController
def create
opts = {
- force_random_password: true,
- password_expires_at: nil
+ reset_password: true,
+ skip_confirmation: true
}
- @user = User.new(user_params.merge(opts))
- @user.created_by_id = current_user.id
- @user.generate_password
- @user.generate_reset_token
- @user.skip_confirmation!
+ @user = Users::CreateService.new(current_user, user_params.merge(opts)).execute
respond_to do |format|
- if @user.save
+ if @user.persisted?
format.html { redirect_to [:admin, @user], notice: 'User was successfully created.' }
format.json { render json: @user, status: :created, location: @user }
else
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b7ce081a5cd..6a6e335d314 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -64,8 +64,11 @@ class ApplicationController < ActionController::Base
# This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token!
- token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
- user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
+ token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
+
+ return unless token.present?
+
+ user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
if user && can?(user, :log_in)
# Notice we are passing store false, so the user is not
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index 2992568ae66..a8c0937569c 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -37,7 +37,6 @@ module ServiceParams
:namespace,
:new_issue_url,
:notify,
- :notify_only_broken_builds,
:notify_only_broken_pipelines,
:password,
:priority,
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 0cbf3eb58a3..00c50f9d0ad 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -14,6 +14,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort)
@members = @members.page(params[:page]).per(50)
+ @members.includes(:user)
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 587898a8634..facb25525b5 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -26,7 +26,7 @@ class Groups::LabelsController < Groups::ApplicationController
end
def create
- @label = @group.labels.create(label_params)
+ @label = Labels::CreateService.new(label_params).execute(group: group)
if @label.valid?
redirect_to group_labels_path(@group)
@@ -40,7 +40,9 @@ class Groups::LabelsController < Groups::ApplicationController
end
def update
- if @label.update_attributes(label_params)
+ @label = Labels::UpdateService.new(label_params).execute(@label)
+
+ if @label.valid?
redirect_back_or_group_labels_path
else
render :edit
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 256c41e6145..eeee027ef2d 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -11,7 +11,7 @@ class Import::BaseController < ApplicationController
namespace.add_owner(current_user)
namespace
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
- Namespace.find_by_path_or_name(name)
+ Namespace.find_by_full_path(name)
end
end
end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 8e42cdf415f..5ad1e116e4e 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -44,15 +44,15 @@ class Import::BitbucketController < Import::BaseController
repo_owner = repo.owner
repo_owner = current_user.username if repo_owner == bitbucket_client.user.username
- @target_namespace = params[:new_namespace].presence || repo_owner
+ namespace_path = params[:new_namespace].presence || repo_owner
- namespace = find_or_create_namespace(@target_namespace, current_user)
+ @target_namespace = find_or_create_namespace(namespace_path, current_user)
- if current_user.can?(:create_projects, namespace)
+ if current_user.can?(:create_projects, @target_namespace)
# The token in a session can be expired, we need to get most recent one because
# Bitbucket::Connection class refreshes it.
session[:bitbucket_token] = bitbucket_client.connection.token
- @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute
+ @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, credentials).execute
else
render 'unauthorized'
end
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index 69959fe3687..7d1aa8d1ce0 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -1,11 +1,22 @@
class Profiles::AccountsController < Profiles::ApplicationController
+ include AuthHelper
+
def show
@user = current_user
end
def unlink
provider = params[:provider]
- current_user.identities.find_by(provider: provider).destroy unless provider.to_s == 'saml'
+ identity = current_user.identities.find_by(provider: provider)
+
+ return render_404 unless identity
+
+ if unlink_allowed?(provider)
+ identity.destroy
+ else
+ flash[:alert] = "You are not allowed to unlink your primary login account"
+ end
+
redirect_to profile_account_path
end
end
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/application_controller.rb b/app/controllers/projects/application_controller.rb
index e2f81b09adc..f1a93ccb3ad 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -89,4 +89,9 @@ class Projects::ApplicationController < ApplicationController
def builds_enabled
return render_404 unless @project.feature_available?(:builds, current_user)
end
+
+ def update_ref
+ branch_exists = @repository.find_branch(@target_branch)
+ @ref = @target_branch if branch_exists
+ end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 52fc67d162c..80a95c6158b 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -89,11 +89,6 @@ class Projects::BlobController < Projects::ApplicationController
private
- def update_ref
- branch_exists = @repository.find_branch(@target_branch)
- @ref = @target_branch if branch_exists
- end
-
def blob
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 886934a3f67..3f3c90a49ab 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -1,7 +1,7 @@
class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play]
- before_action :authorize_update_build!, except: [:index, :show, :status, :raw]
+ before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace]
layout 'project'
def index
@@ -74,7 +74,9 @@ class Projects::BuildsController < Projects::ApplicationController
end
def status
- render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
+ render json: BuildSerializer
+ .new(project: @project, user: @current_user)
+ .represent_status(@build)
end
def erase
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 1502b734f37..d0c44e297e3 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -31,8 +31,10 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def disable
- @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy
+ deploy_key_project = @project.deploy_keys_projects.find_by(deploy_key_id: params[:id])
+ return render_404 unless deploy_key_project
+ deploy_key_project.destroy!
redirect_to_repository_settings(@project)
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index f2fee62ebd6..d984e6d3918 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -6,6 +6,8 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableCollections
include SpammableActions
+ prepend_before_action :authenticate_user!, only: [:new]
+
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
@@ -146,7 +148,14 @@ class Projects::IssuesController < Projects::ApplicationController
end
format.json do
- render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+ if @issue.valid?
+ render json: @issue.to_json(methods: [:task_status, :task_status_short],
+ include: { milestone: {},
+ assignee: { only: [:name, :username], methods: [:avatar_url] },
+ labels: { methods: :text_color } })
+ else
+ render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
+ end
end
end
@@ -251,4 +260,13 @@ class Projects::IssuesController < Projects::ApplicationController
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
)
end
+
+ def authenticate_user!
+ return if current_user
+
+ notice = "Please sign in to create the new issue."
+
+ store_location_for :user, request.fullpath
+ redirect_to new_user_session_path, notice: notice
+ end
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 1593b5c1afb..2f55ba4e700 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -29,7 +29,7 @@ class Projects::LabelsController < Projects::ApplicationController
end
def create
- @label = @project.labels.create(label_params)
+ @label = Labels::CreateService.new(label_params).execute(project: @project)
if @label.valid?
respond_to do |format|
@@ -48,7 +48,9 @@ class Projects::LabelsController < Projects::ApplicationController
end
def update
- if @label.update_attributes(label_params)
+ @label = Labels::UpdateService.new(label_params).execute(@label)
+
+ if @label.valid?
redirect_to namespace_project_labels_path(@project.namespace, @project)
else
render :edit
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 82f9b6e06db..37e3ac05916 100644..100755
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
- :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
+ :ci_status, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
@@ -39,6 +39,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@collection_type = "MergeRequest"
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
+ @merge_requests = @merge_requests.includes(merge_request_diff: :merge_request)
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
@@ -97,31 +98,31 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def diffs
apply_diff_view_cookie!
- @merge_request_diff =
- if params[:diff_id]
- @merge_request.merge_request_diffs.viewable.find(params[:diff_id])
- else
- @merge_request.merge_request_diff
- end
-
- @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
- @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
-
- if params[:start_sha].present?
- @start_sha = params[:start_sha]
- @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
-
- unless @start_version
- @start_sha = @merge_request_diff.head_commit_sha
- @start_version = @merge_request_diff
- end
- end
-
- @environment = @merge_request.environments_for(current_user).last
-
respond_to do |format|
format.html { define_discussion_vars }
format.json do
+ @merge_request_diff =
+ if params[:diff_id]
+ @merge_request.merge_request_diffs.viewable.find(params[:diff_id])
+ else
+ @merge_request.merge_request_diff
+ end
+
+ @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
+ @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
+
+ if params[:start_sha].present?
+ @start_sha = params[:start_sha]
+ @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
+
+ unless @start_version
+ @start_sha = @merge_request_diff.head_commit_sha
+ @start_version = @merge_request_diff
+ end
+ end
+
+ @environment = @merge_request.environments_for(current_user).last
+
if @start_sha
compared_diff_version
else
@@ -308,7 +309,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
format.json do
- render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+ render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
end
end
rescue ActiveRecord::StaleObjectError
@@ -402,7 +403,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if params[:ref].present?
@ref = params[:ref]
- @commit = @repository.commit(@ref)
+ @commit = @repository.commit("refs/heads/#{@ref}")
end
render layout: false
@@ -413,7 +414,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if params[:ref].present?
@ref = params[:ref]
- @commit = @target_project.commit(@ref)
+ @commit = @target_project.commit("refs/heads/#{@ref}")
end
render layout: false
@@ -473,6 +474,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response
end
+ def pipeline_status
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent_status(@merge_request.head_pipeline)
+ end
+
def ci_environments_status
environments =
begin
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index be52b0fa7cf..408c0c60cb0 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -13,14 +13,17 @@ class Projects::MilestonesController < Projects::ApplicationController
def index
@milestones =
case params[:state]
- when 'all' then @project.milestones.reorder(due_date: :desc, title: :asc)
- when 'closed' then @project.milestones.closed.reorder(due_date: :desc, title: :asc)
- else @project.milestones.active.reorder(due_date: :asc, title: :asc)
+ when 'all' then @project.milestones
+ when 'closed' then @project.milestones.closed
+ else @project.milestones.active
end
- @milestones = @milestones.includes(:project)
+ @sort = params[:sort] || 'due_date_asc'
+ @milestones = @milestones.sort(@sort)
+
respond_to do |format|
format.html do
+ @milestones = @milestones.includes(:project)
@milestones = @milestones.page(params[:page])
end
format.json do
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 718d9e86bea..43a1abaa662 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -72,6 +72,12 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
+ def status
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent_status(@pipeline)
+ end
+
def stage
@stage = pipeline.stage(params[:stage])
return not_found unless @stage
diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb
index cbfa2afa959..54f9dceddef 100644
--- a/app/controllers/projects/settings/members_controller.rb
+++ b/app/controllers/projects/settings/members_controller.rb
@@ -9,6 +9,7 @@ module Projects
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << @project.namespace_id unless @project.personal?
+ @skip_groups += @project.group.ancestors.pluck(:id) if @project.group
@project_members = MembersFinder.new(@project, current_user).execute
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 4f094146348..637b61504d8 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -34,6 +34,7 @@ class Projects::TreeController < Projects::ApplicationController
def create_dir
return render_404 unless @commit_params.values.all?
+ update_ref
create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)),
failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 8b6c83d4fed..c5e24b9e365 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -45,8 +45,9 @@ class Projects::WikisController < Projects::ApplicationController
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = @project_wiki.find_page(params[:id])
+ @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
- if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
+ if @page.valid?
redirect_to(
namespace_project_wiki_path(@project.namespace, @project, @page),
notice: 'Wiki was successfully updated.'
@@ -123,6 +124,6 @@ class Projects::WikisController < Projects::ApplicationController
end
def wiki_params
- params[:wiki].slice(:title, :content, :format, :message)
+ params.require(:wiki).permit(:title, :content, :format, :message)
end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index b44f38d4a0c..8109427a45f 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -1,5 +1,4 @@
class RegistrationsController < Devise::RegistrationsController
- before_action :signup_enabled?
include Recaptcha::Verify
def new
@@ -21,15 +20,17 @@ class RegistrationsController < Devise::RegistrationsController
flash.delete :recaptcha_error
render action: 'new'
end
+ rescue Gitlab::Access::AccessDeniedError
+ redirect_to(new_user_session_path)
end
def destroy
- Users::DestroyService.new(current_user).execute(current_user)
+ DeleteUserWorker.perform_async(current_user.id, current_user.id)
respond_to do |format|
format.html do
session.try(:destroy)
- redirect_to new_user_session_path, notice: "Account successfully removed."
+ redirect_to new_user_session_path, notice: "Account scheduled for removal."
end
end
end
@@ -50,12 +51,6 @@ class RegistrationsController < Devise::RegistrationsController
private
- def signup_enabled?
- unless current_application_settings.signup_enabled?
- redirect_to(new_user_session_path)
- end
- end
-
def sign_up_params
params.require(:user).permit(:username, :email, :email_confirmation, :name, :password)
end
@@ -65,7 +60,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def resource
- @resource ||= User.new(sign_up_params)
+ @resource ||= Users::CreateService.new(current_user, sign_up_params).build
end
def devise_mapping
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 612d69cf557..4a579601785 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -6,45 +6,19 @@ class SearchController < ApplicationController
layout 'search'
def show
- if params[:project_id].present?
- @project = Project.find_by(id: params[:project_id])
- @project = nil unless can?(current_user, :download_code, @project)
- end
+ search_service = SearchService.new(current_user, params)
- if params[:group_id].present?
- @group = Group.find_by(id: params[:group_id])
- @group = nil unless can?(current_user, :read_group, @group)
- end
+ @project = search_service.project
+ @group = search_service.group
return if params[:search].blank?
@search_term = params[:search]
- @scope = params[:scope]
- @show_snippets = params[:snippets].eql? 'true'
-
- @search_results =
- if @project
- unless %w(blobs notes issues merge_requests milestones wiki_blobs
- commits).include?(@scope)
- @scope = 'blobs'
- end
-
- Search::ProjectService.new(@project, current_user, params).execute
- elsif @show_snippets
- unless %w(snippet_blobs snippet_titles).include?(@scope)
- @scope = 'snippet_blobs'
- end
-
- Search::SnippetService.new(current_user, params).execute
- else
- unless %w(projects issues merge_requests milestones).include?(@scope)
- @scope = 'projects'
- end
- Search::GlobalService.new(current_user, params).execute
- end
-
- @search_objects = @search_results.objects(@scope, params[:page])
+ @scope = search_service.scope
+ @show_snippets = search_service.show_snippets?
+ @search_results = search_service.search_results
+ @search_objects = search_service.search_objects
check_single_commit_result
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 7d81c96262f..d8561871098 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -79,7 +79,7 @@ class SessionsController < Devise::SessionsController
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
referer_uri = URI(request.referer)
if referer_uri.host == Gitlab.config.gitlab.host
- referer_uri.path
+ referer_uri.request_uri
else
request.fullpath
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 6e29f1e8a65..2683614d2e8 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -39,7 +39,7 @@ class UsersController < ApplicationController
format.html { render 'show' }
format.json do
render json: {
- html: view_to_html_string("shared/projects/_list", projects: @projects, remote: true)
+ html: view_to_html_string("shared/projects/_list", projects: @projects)
}
end
end
@@ -65,7 +65,7 @@ class UsersController < ApplicationController
format.html { render 'show' }
format.json do
render json: {
- html: view_to_html_string("snippets/_snippets", collection: @snippets, remote: true)
+ html: view_to_html_string("snippets/_snippets", collection: @snippets)
}
end
end
diff --git a/app/finders/group_finder.rb b/app/finders/group_finder.rb
new file mode 100644
index 00000000000..24c84d2d1aa
--- /dev/null
+++ b/app/finders/group_finder.rb
@@ -0,0 +1,17 @@
+class GroupFinder
+ include Gitlab::Allowable
+
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute(*params)
+ group = Group.find_by(*params)
+
+ if can?(@current_user, :read_group, group)
+ group
+ else
+ nil
+ end
+ end
+end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index fa0e2a5e3d8..e52083f86e4 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -20,8 +20,17 @@ class LabelsFinder < UnionFinder
if project?
if project
- label_ids << project.group.labels if project.group.present?
- label_ids << project.labels
+ if project.group.present?
+ labels_table = Label.arel_table
+
+ label_ids << Label.where(
+ labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or(
+ labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id))
+ )
+ )
+ else
+ label_ids << project.labels
+ end
end
else
label_ids << Label.where(group_id: projects.group_ids)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index a3213581498..e5b811f3300 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -306,4 +306,8 @@ module ApplicationHelper
def active_when(condition)
'active' if condition
end
+
+ def show_user_callout?
+ cookies[:user_callout_dismissed] == 'true'
+ end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 1ee6c1d3afa..101fe579da2 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -76,5 +76,9 @@ module AuthHelper
(current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
end
+ def unlink_allowed?(provider)
+ %w(saml cas3).exclude?(provider.to_s)
+ end
+
extend self
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 0b0c6a07efd..8631bc54509 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -215,6 +215,6 @@ module BlobHelper
end
def open_raw_file_button(path)
- link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', title: 'Open raw', data: { container: 'body' }
+ link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' }
end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 8aad39e148b..cef624430da 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -211,7 +211,7 @@ module CommitsHelper
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
+ link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
icon('external-link')
end
end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index a0642a1894b..a57b5a8fea5 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -7,7 +7,7 @@ module ImportHelper
def provider_project_link(provider, path_with_namespace)
url = __send__("#{provider}_project_url", path_with_namespace)
- link_to path_with_namespace, url, target: '_blank'
+ link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer'
end
private
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index a777db2826b..ec57fec4f99 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -251,6 +251,21 @@ module IssuablesHelper
end
def selected_template(issuable)
- params[:issuable_template] if issuable_templates(issuable).include?(params[:issuable_template])
+ params[:issuable_template] if issuable_templates(issuable).any?{ |template| template[:name] == params[:issuable_template] }
+ end
+
+ def issuable_todo_button_data(issuable, todo, is_collapsed)
+ {
+ todo_text: "Add todo",
+ mark_text: "Mark done",
+ todo_icon: (is_collapsed ? icon('plus-square') : nil),
+ mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil),
+ issuable_id: issuable.id,
+ issuable_type: issuable.class.name.underscore,
+ url: namespace_project_todos_path(@project.namespace, @project),
+ delete_path: (dashboard_todo_path(todo) if todo),
+ placement: (is_collapsed ? 'left' : nil),
+ container: (is_collapsed ? 'body' : nil)
+ }
end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 5053b937c02..c9e70faa52e 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -19,8 +19,8 @@ module MilestonesHelper
end
end
- def milestones_browse_issuables_path(milestone, type:)
- opts = { milestone_title: milestone.title }
+ def milestones_browse_issuables_path(milestone, state: nil, type:)
+ opts = { milestone_title: milestone.title, state: state }
if @project
polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts)
@@ -89,10 +89,12 @@ module MilestonesHelper
content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
content.slice!("about ")
content << " remaining"
+ content.html_safe
elsif milestone.start_date && milestone.start_date.past?
days = milestone.elapsed_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} elapsed"
+ content.html_safe
end
end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 2e3a15bc1b9..7f656b8caae 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -6,7 +6,13 @@ module NamespacesHelper
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
groups = current_user.owned_groups + current_user.masters_groups
- groups << extra_group if extra_group && !Group.exists?(name: extra_group.name)
+ unless extra_group.nil? || extra_group.is_a?(Group)
+ extra_group = Group.find(extra_group) if Namespace.find(extra_group).kind == 'group'
+ end
+
+ if extra_group && extra_group.is_a?(Group) && (!Group.exists?(name: extra_group.name) || Ability.allowed?(current_user, :read_group, extra_group))
+ groups |= [extra_group]
+ end
users = [current_user.namespace]
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index c1523b4dabf..17bfd07e00f 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -6,7 +6,8 @@ module NavHelper
current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') ||
current_path?('merge_requests#pipelines') ||
- current_path?('issues#show')
+ current_path?('issues#show') ||
+ current_path?('milestones#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
else
@@ -16,6 +17,7 @@ module NavHelper
"page-gutter build-sidebar right-sidebar-expanded"
elsif current_path?('wikis#show') ||
current_path?('wikis#edit') ||
+ current_path?('wikis#update') ||
current_path?('wikis#history') ||
current_path?('wikis#git_access')
"page-gutter wiki-sidebar right-sidebar-expanded"
@@ -30,7 +32,11 @@ module NavHelper
end
def layout_nav_class
- "page-with-layout-nav" if defined?(nav) && nav
+ class_name = ''
+ class_name << " page-with-layout-nav" if defined?(nav) && nav
+ class_name << " page-with-sub-nav" if content_for?(:sub_nav)
+
+ class_name
end
def nav_control_class
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
index b5017080cfb..55f4da0ef85 100644
--- a/app/helpers/sidekiq_helper.rb
+++ b/app/helpers/sidekiq_helper.rb
@@ -3,9 +3,9 @@ module SidekiqHelper
(?<pid>\d+)\s+
(?<cpu>[\d\.,]+)\s+
(?<mem>[\d\.,]+)\s+
- (?<state>[DRSTWXZNLsl\+<]+)\s+
- (?<start>.+)\s+
- (?<command>sidekiq.*\])
+ (?<state>[DIEKNRSTVWXZNLpsl\+<>\/\d]+)\s+
+ (?<start>.+?)\s+
+ (?<command>(?:ruby\d+:\s+)?sidekiq.*\].*)
\z/x
def parse_sidekiq_ps(line)
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 959ee310867..5c89cbea3fc 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -2,6 +2,7 @@ module SortingHelper
def sort_options_hash
{
sort_value_name => sort_title_name,
+ sort_value_name_desc => sort_title_name_desc,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
@@ -50,6 +51,17 @@ module SortingHelper
}
end
+ def milestone_sort_options_hash
+ {
+ sort_value_name => sort_title_name_asc,
+ sort_value_name_desc => sort_title_name_desc,
+ sort_value_due_date_soon => sort_title_due_date_soon,
+ sort_value_due_date_later => sort_title_due_date_later,
+ sort_value_start_date_soon => sort_title_start_date_soon,
+ sort_value_start_date_later => sort_title_start_date_later,
+ }
+ end
+
def sort_title_priority
'Priority'
end
@@ -90,6 +102,14 @@ module SortingHelper
'Due later'
end
+ def sort_title_start_date_soon
+ 'Start soon'
+ end
+
+ def sort_title_start_date_later
+ 'Start later'
+ end
+
def sort_title_name
'Name'
end
@@ -202,6 +222,14 @@ module SortingHelper
'due_date_desc'
end
+ def sort_value_start_date_soon
+ 'start_date_asc'
+ end
+
+ def sort_value_start_date_later
+ 'start_date_desc'
+ end
+
def sort_value_name
'name_asc'
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
new file mode 100644
index 00000000000..9c623c9ba7c
--- /dev/null
+++ b/app/helpers/users_helper.rb
@@ -0,0 +1,7 @@
+module UsersHelper
+ def user_link(user)
+ link_to(user.name, user_path(user),
+ title: user.email,
+ class: 'has-tooltip commit-committer-link')
+ end
+end
diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb
deleted file mode 100644
index 3853af6201a..00000000000
--- a/app/mailers/emails/builds.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module Emails
- module Builds
- def build_fail_email(build_id, to)
- @build = Ci::Build.find(build_id)
- @project = @build.project
-
- add_project_headers
- add_build_headers('failed')
-
- mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
- end
-
- def build_success_email(build_id, to)
- @build = Ci::Build.find(build_id)
- @project = @build.project
-
- add_project_headers
- add_build_headers('success')
- mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
- end
-
- private
-
- def add_build_headers(status)
- headers['X-GitLab-Build-Id'] = @build.id
- headers['X-GitLab-Build-Ref'] = @build.ref
- headers['X-GitLab-Build-Status'] = status.to_s
- end
- end
-end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 5b9226a6b81..14df6f8f0a3 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -6,7 +6,6 @@ class Notify < BaseMailer
include Emails::Notes
include Emails::Projects
include Emails::Profile
- include Emails::Builds
include Emails::Pipelines
include Emails::Members
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index be632930895..2961e16f5e0 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -131,6 +131,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :polling_interval_multiplier,
+ presence: true,
+ numericality: { greater_than_or_equal_to: 0 }
+
validates_each :restricted_visibility_levels do |record, attr, value|
value&.each do |level|
unless Gitlab::VisibilityLevel.options.has_value?(level)
@@ -163,6 +167,8 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.current
+ ensure_cache_setup
+
Rails.cache.fetch(CACHE_KEY) do
ApplicationSetting.last
end
@@ -176,9 +182,16 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.cached
+ ensure_cache_setup
Rails.cache.fetch(CACHE_KEY)
end
+ def self.ensure_cache_setup
+ # This is a workaround for a Rails bug that causes attribute methods not
+ # to be loaded when read from cache: https://github.com/rails/rails/issues/27348
+ ApplicationSetting.define_attribute_methods
+ end
+
def self.defaults_ce
{
after_sign_up_text: nil,
@@ -224,7 +237,8 @@ class ApplicationSetting < ActiveRecord::Base
signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0,
two_factor_grace_period: 48,
- user_default_external: false
+ user_default_external: false,
+ polling_interval_multiplier: 1
}
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 1376b86fdad..95d2111a992 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -46,6 +46,10 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG'
end
+ def ipython_notebook?
+ text? && language&.name == 'Jupyter Notebook'
+ end
+
def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE
end
@@ -63,6 +67,8 @@ class Blob < SimpleDelegator
end
elsif image? || svg?
'image'
+ elsif ipython_notebook?
+ 'notebook'
elsif text?
'text'
else
diff --git a/app/models/board.rb b/app/models/board.rb
index 2780acc67c0..cf8317891b5 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -5,7 +5,7 @@ class Board < ActiveRecord::Base
validates :project, presence: true
- def done_list
- lists.merge(List.done).take
+ def closed_list
+ lists.merge(List.closed).take
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3722047251d..ad0be70c32a 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -15,7 +15,7 @@ module Ci
def persisted_environment
@persisted_environment ||= Environment.find_by(
name: expanded_environment_name,
- project_id: gl_project_id
+ project: project
)
end
@@ -223,7 +223,8 @@ module Ci
def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff)
- .where(source_branch: ref, source_project_id: pipeline.gl_project_id)
+ .where(source_branch: ref,
+ source_project: pipeline.project)
.reorder(iid: :asc)
merge_requests.find do |merge_request|
@@ -231,10 +232,6 @@ module Ci
end
end
- def project_id
- gl_project_id
- end
-
def repo_url
auth = "gitlab-ci-token:#{ensure_token!}@"
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
@@ -542,6 +539,16 @@ module Ci
Gitlab::Ci::Build::Credentials::Factory.new(self).create!
end
+ def dependencies
+ depended_jobs = depends_on_builds
+
+ return depended_jobs unless options[:dependencies].present?
+
+ depended_jobs.select do |job|
+ options[:dependencies].include?(job.name)
+ end
+ end
+
private
def update_artifacts_size
@@ -561,7 +568,7 @@ module Ci
end
def unscoped_project
- @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
+ @unscoped_project ||= Project.unscoped.find_by(id: project_id)
end
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d1009f88549..ad7e0b23ff4 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -5,9 +5,7 @@ module Ci
include Importable
include AfterCommitQueue
- self.table_name = 'ci_commits'
-
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
belongs_to :user
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
@@ -212,7 +210,7 @@ module Ci
end
def stuck?
- builds.pending.any?(&:stuck?)
+ builds.pending.includes(:project).any?(&:stuck?)
end
def retryable?
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index edd21f984c8..487ba61bc9c 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -9,7 +9,7 @@ module Ci
has_many :builds
has_many :runner_projects, dependent: :destroy
- has_many :projects, through: :runner_projects, foreign_key: :gl_project_id
+ has_many :projects, through: :runner_projects
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
@@ -24,7 +24,7 @@ module Ci
scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
- .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
+ .where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end
scope :assignable_for, ->(project) do
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 234376a7e4c..5f01a0daae9 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -1,10 +1,10 @@
module Ci
class RunnerProject < ActiveRecord::Base
extend Ci::Model
-
+
belongs_to :runner
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
- validates :runner_id, uniqueness: { scope: :gl_project_id }
+ validates :runner_id, uniqueness: { scope: :project_id }
end
end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 90473d41c04..cba1d81a861 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -4,7 +4,7 @@ module Ci
acts_as_paranoid
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
belongs_to :owner, class_name: "User"
has_many :trigger_requests, dependent: :destroy
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 2c8698d8b5d..6c6586110c5 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -2,11 +2,11 @@ module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
validates :key,
presence: true,
- uniqueness: { scope: :gl_project_id },
+ uniqueness: { scope: :project_id },
length: { maximum: 255 },
format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." }
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 6ea5b1ae51f..ce92cc369ad 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -321,7 +321,14 @@ class Commit
end
def raw_diffs(*args)
- raw.diffs(*args)
+ use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
+ deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
+
+ if use_gitaly && !deltas_only
+ Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
+ else
+ raw.diffs(*args)
+ end
end
def diffs(diff_options = nil)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 7e23e14794f..17b322b5ae3 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base
self.table_name = 'ci_builds'
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :user
@@ -105,6 +105,10 @@ class CommitStatus < ActiveRecord::Base
end
end
+ def locking_enabled?
+ status_changed?
+ end
+
def before_sha
pipeline.before_sha || Gitlab::Git::BLANK_SHA
end
@@ -133,6 +137,12 @@ class CommitStatus < ActiveRecord::Base
false
end
+ # Added in 9.0 to keep backward compatibility for projects exported in 8.17
+ # and prior.
+ def gl_project_id
+ 'dummy'
+ end
+
def detailed_status(current_user)
Gitlab::Ci::Status::Factory
.new(self, current_user)
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 5101cc7e687..0a1a65da05a 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -31,6 +31,7 @@ module HasStatus
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})>0 THEN 'running'
WHEN (#{manual})>0 THEN 'manual'
+ WHEN (#{created})>0 THEN 'running'
ELSE 'failed'
END)"
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 91f4eb13ecc..4d54426b79e 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -48,11 +48,14 @@ module Issuable
delegate :name,
:email,
+ :public_email,
to: :author,
+ allow_nil: true,
prefix: true
delegate :name,
:email,
+ :public_email,
to: :assignee,
allow_nil: true,
prefix: true
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 9f6d215ceb3..529fb5ce988 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -51,11 +51,13 @@ module Routable
paths.each do |path|
path = connection.quote(path)
- where = "(routes.path = #{path})"
- if cast_lower
- where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))"
- end
+ where =
+ if cast_lower
+ "(LOWER(routes.path) = LOWER(#{path}))"
+ else
+ "(routes.path = #{path})"
+ end
wheres << where
end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 107e6764ba2..647a6cad3d7 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -41,7 +41,7 @@ module Spammable
def check_for_spam
error_msg = if Gitlab::Recaptcha.enabled?
"Your #{spammable_entity_type} has been recognized as spam. "\
- "You can still submit it by solving Captcha."
+ "Please, change the content or solve the reCAPTCHA to proceed."
else
"Your #{spammable_entity_type} has been recognized as spam and has been discarded."
end
diff --git a/app/models/event.rb b/app/models/event.rb
index d7ca8e3c599..5c34844b5d3 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
- delegate :name, :email, to: :author, prefix: true, allow_nil: true
+ delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true
delegate :title, to: :note, prefix: true, allow_nil: true
diff --git a/app/models/group.rb b/app/models/group.rb
index bd0ecae3da4..60274386103 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -207,7 +207,7 @@ class Group < Namespace
end
def members_with_parents
- GroupMember.non_request.where(source_id: ancestors.map(&:id).push(id))
+ GroupMember.non_request.where(source_id: ancestors.pluck(:id).push(id))
end
def users_with_parents
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 1427fdc31a4..10a5d9d2a24 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -55,6 +55,14 @@ class Issue < ActiveRecord::Base
state :opened
state :reopened
state :closed
+
+ before_transition any => :closed do |issue|
+ issue.closed_at = Time.zone.now
+ end
+
+ before_transition closed: any do |issue|
+ issue.closed_at = nil
+ end
end
def hook_attrs
@@ -203,9 +211,8 @@ class Issue < ActiveRecord::Base
due_date.try(:past?) || false
end
- # Only issues on public projects should be checked for spam
def check_for_spam?
- project.public?
+ project.public? && (title_changed? || description_changed?)
end
def as_json(options = {})
diff --git a/app/models/list.rb b/app/models/list.rb
index 1e5da7f4dd4..fbd19acd1f5 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
- enum list_type: { label: 1, done: 2 }
+ enum list_type: { label: 1, closed: 2 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 4759829a15c..5ff83944d8c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -154,8 +154,10 @@ class MergeRequest < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def self.in_projects(relation)
- source = where(source_project_id: relation).select(:id)
- target = where(target_project_id: relation).select(:id)
+ # unscoping unnecessary conditions that'll be applied
+ # when executing `where("merge_requests.id IN (#{union.to_sql})")`
+ source = unscoped.where(source_project_id: relation).select(:id)
+ target = unscoped.where(target_project_id: relation).select(:id)
union = Gitlab::SQL::Union.new([source, target])
where("merge_requests.id IN (#{union.to_sql})")
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index c0deb59ec4c..e85d5709624 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -107,6 +107,21 @@ class Milestone < ActiveRecord::Base
end
end
+ def self.sort(method)
+ case method.to_s
+ when 'due_date_asc'
+ reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC'))
+ when 'due_date_desc'
+ reorder(Gitlab::Database.nulls_last_order('due_date', 'DESC'))
+ when 'start_date_asc'
+ reorder(Gitlab::Database.nulls_last_order('start_date', 'ASC'))
+ when 'start_date_desc'
+ reorder(Gitlab::Database.nulls_last_order('start_date', 'DESC'))
+ else
+ order_by(method)
+ end
+ end
+
##
# Returns the String necessary to reference this Milestone in Markdown
#
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index d350f1d6770..1d4b1f7d590 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -120,10 +120,10 @@ class Namespace < ActiveRecord::Base
# Move the namespace directory in all storages paths used by member projects
repository_storage_paths.each do |repository_storage_path|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage_path, path_was)
+ gitlab_shell.add_namespace(repository_storage_path, full_path_was)
- unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
- Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+ unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path)
+ Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
@@ -131,8 +131,8 @@ class Namespace < ActiveRecord::Base
end
end
- Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
- Gitlab::PagesTransfer.new.rename_namespace(path_was, path)
+ Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path)
+ Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path)
remove_exports!
@@ -155,7 +155,7 @@ class Namespace < ActiveRecord::Base
def send_update_instructions
projects.each do |project|
- project.send_move_instructions("#{path_was}/#{project.path}")
+ project.send_move_instructions("#{full_path_was}/#{project.path}")
end
end
@@ -195,7 +195,7 @@ class Namespace < ActiveRecord::Base
# Scopes the model on direct and indirect children of the record
def descendants
- self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
+ self.class.joins(:route).merge(Route.inside_path(route.path)).reorder('routes.path ASC')
end
def user_ids_for_project_authorizations
@@ -230,10 +230,10 @@ class Namespace < ActiveRecord::Base
old_repository_storage_paths.each do |repository_storage_path|
# Move namespace directory into trash.
# We will remove it later async
- new_path = "#{path}+#{id}+deleted"
+ new_path = "#{full_path}+#{id}+deleted"
- if gitlab_shell.mv_namespace(repository_storage_path, path, new_path)
- message = "Namespace directory \"#{path}\" moved to \"#{new_path}\""
+ if gitlab_shell.mv_namespace(repository_storage_path, full_path, new_path)
+ message = "Namespace directory \"#{full_path}\" moved to \"#{new_path}\""
Gitlab::AppLogger.info message
# Remove namespace directroy async with delay so
diff --git a/app/models/note.rb b/app/models/note.rb
index e22e96aec6f..16d66cb1427 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -37,6 +37,7 @@ class Note < ActiveRecord::Base
has_many :todos, dependent: :destroy
has_many :events, as: :target, dependent: :destroy
+ has_one :system_note_metadata
delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true
@@ -70,7 +71,9 @@ class Note < ActiveRecord::Base
scope :fresh, ->{ order(created_at: :asc, id: :asc) }
scope :inc_author_project, ->{ includes(:project, :author) }
scope :inc_author, ->{ includes(:author) }
- scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) }
+ scope :inc_relations_for_view, -> do
+ includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata)
+ end
scope :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) }
scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
diff --git a/app/models/project.rb b/app/models/project.rb
index 2ffaaac93f3..f1bba56d32c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -89,7 +89,6 @@ class Project < ActiveRecord::Base
has_one :campfire_service, dependent: :destroy
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
- has_one :builds_email_service, dependent: :destroy
has_one :pipelines_email_service, dependent: :destroy
has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
@@ -159,13 +158,13 @@ class Project < ActiveRecord::Base
has_one :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
- has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id
- has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
- has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses
- has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id
+ has_many :commit_statuses, dependent: :destroy
+ has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline'
+ has_many :builds, class_name: 'Ci::Build' # the builds are created from the commit_statuses
+ has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
- has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id
- has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id
+ has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
+ has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
has_many :environments, dependent: :destroy
has_many :deployments, dependent: :destroy
@@ -197,6 +196,7 @@ class Project < ActiveRecord::Base
validates :name, uniqueness: { scope: :namespace_id }
validates :path, uniqueness: { scope: :namespace_id }
validates :import_url, addressable_url: true, if: :external_import?
+ validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
validate :avatar_type,
@@ -238,7 +238,7 @@ class Project < ActiveRecord::Base
# 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}/%")
+ where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
end
# "enabled" here means "not disabled". It includes private features!
@@ -314,20 +314,15 @@ class Project < ActiveRecord::Base
ntable = Namespace.arel_table
pattern = "%#{query}%"
- projects = select(:id).where(
+ # unscoping unnecessary conditions that'll be applied
+ # when executing `where("projects.id IN (#{union.to_sql})")`
+ projects = unscoped.select(:id).where(
ptable[:path].matches(pattern).
or(ptable[:name].matches(pattern)).
or(ptable[:description].matches(pattern))
)
- # We explicitly remove any eager loading clauses as they're:
- #
- # 1. Not needed by this query
- # 2. Combined with .joins(:namespace) lead to all columns from the
- # projects & namespaces tables being selected, leading to a SQL error
- # due to the columns of all UNION'd queries no longer being the same.
- namespaces = select(:id).
- except(:includes).
+ namespaces = unscoped.select(:id).
joins(:namespace).
where(ntable[:name].matches(pattern))
@@ -881,13 +876,9 @@ class Project < ActiveRecord::Base
end
def http_url_to_repo(user = nil)
- url = web_url
+ credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
- if user
- url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" }
- end
-
- "#{url}.git"
+ Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url
end
# Check if current branch name is marked as protected in the system
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index ebd21e37189..0c526b53d72 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -1,107 +1,11 @@
+# This class is to be removed with 9.1
+# We should also by then remove BuildsEmailService from database
class BuildsEmailService < Service
- prop_accessor :recipients
- boolean_accessor :add_pusher
- boolean_accessor :notify_only_broken_builds
- validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? }
-
- def initialize_properties
- if properties.nil?
- self.properties = {}
- self.notify_only_broken_builds = true
- end
- end
-
- def title
- 'Builds emails'
- end
-
- def description
- 'Email the builds status to a list of recipients.'
- end
-
def self.to_param
'builds_email'
end
def self.supported_events
- %w(build)
- end
-
- def execute(push_data)
- return unless supported_events.include?(push_data[:object_kind])
- return unless should_build_be_notified?(push_data)
-
- recipients = all_recipients(push_data)
-
- if recipients.any?
- BuildEmailWorker.perform_async(
- push_data[:build_id],
- recipients,
- push_data
- )
- end
- end
-
- def can_test?
- project.builds.any?
- end
-
- def disabled_title
- "Please setup a build on your repository."
- end
-
- def test_data(project = nil, user = nil)
- Gitlab::DataBuilder::Build.build(project.builds.last)
- end
-
- def fields
- [
- { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' },
- { type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
- ]
- end
-
- def test(data)
- begin
- # bypass build status verification when testing
- data[:build_status] = "failed"
- data[:build_allow_failure] = false
-
- result = execute(data)
- rescue StandardError => error
- return { success: false, result: error }
- end
-
- { success: true, result: result }
- end
-
- def should_build_be_notified?(data)
- case data[:build_status]
- when 'success'
- !notify_only_broken_builds?
- when 'failed'
- !allow_failure?(data)
- else
- false
- end
- end
-
- def allow_failure?(data)
- data[:build_allow_failure] == true
- end
-
- def all_recipients(data)
- all_recipients = []
-
- unless recipients.blank?
- all_recipients += recipients.split(',').compact.reject(&:blank?)
- end
-
- if add_pusher? && data[:user][:email]
- all_recipients << data[:user][:email]
- end
-
- all_recipients
+ %w[]
end
end
diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb
deleted file mode 100644
index c776e0a20c4..00000000000
--- a/app/models/project_services/chat_message/build_message.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-module ChatMessage
- class BuildMessage < BaseMessage
- attr_reader :sha
- attr_reader :ref_type
- attr_reader :ref
- attr_reader :status
- attr_reader :project_name
- attr_reader :project_url
- attr_reader :user_name
- attr_reader :user_url
- attr_reader :duration
- attr_reader :stage
- attr_reader :build_id
- attr_reader :build_name
-
- def initialize(params)
- @sha = params[:sha]
- @ref_type = params[:tag] ? 'tag' : 'branch'
- @ref = params[:ref]
- @project_name = params[:project_name]
- @project_url = params[:project_url]
- @status = params[:commit][:status]
- @user_name = params[:commit][:author_name]
- @user_url = params[:commit][:author_url]
- @duration = params[:commit][:duration]
- @stage = params[:build_stage]
- @build_name = params[:build_name]
- @build_id = params[:build_id]
- end
-
- def pretext
- ''
- end
-
- def fallback
- format(message)
- end
-
- def attachments
- [{ text: format(message), color: attachment_color }]
- end
-
- private
-
- def message
- "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_link} #{humanized_status} on build #{build_link} of stage #{stage} in #{duration} #{'second'.pluralize(duration)}"
- end
-
- def build_url
- "#{project_url}/builds/#{build_id}"
- end
-
- def build_link
- link(build_name, build_url)
- end
-
- def user_link
- link(user_name, user_url)
- end
-
- def format(string)
- Slack::Notifier::LinkFormatter.format(string)
- end
-
- def humanized_status
- case status
- when 'success'
- 'passed'
- else
- status
- end
- end
-
- def attachment_color
- if status == 'success'
- 'good'
- else
- 'danger'
- end
- end
-
- def branch_url
- "#{project_url}/commits/#{ref}"
- end
-
- def branch_link
- link(ref, branch_url)
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def commit_url
- "#{project_url}/commit/#{sha}/builds"
- end
-
- def commit_link
- link(Commit.truncate_sha(sha), commit_url)
- end
- end
-end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 8468934425f..75834103db5 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -6,7 +6,7 @@ class ChatNotificationService < Service
default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel
- boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
+ boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :webhook, presence: true, url: true, if: :activated?
@@ -16,8 +16,8 @@ class ChatNotificationService < Service
if properties.nil?
self.properties = {}
- self.notify_only_broken_builds = true
self.notify_only_broken_pipelines = true
+ self.notify_only_default_branch = true
end
end
@@ -27,7 +27,20 @@ class ChatNotificationService < Service
def self.supported_events
%w[push issue confidential_issue merge_request note tag_push
- build pipeline wiki_page]
+ pipeline wiki_page]
+ end
+
+ def fields
+ default_fields + build_event_channels
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
+ { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ { type: 'checkbox', name: 'notify_only_default_branch' },
+ ]
end
def execute(data)
@@ -89,8 +102,6 @@ class ChatNotificationService < Service
ChatMessage::MergeMessage.new(data) unless is_update?(data)
when "note"
ChatMessage::NoteMessage.new(data)
- when "build"
- ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline"
ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
@@ -125,18 +136,18 @@ class ChatNotificationService < Service
data[:object_attributes][:action] == 'update'
end
- def should_build_be_notified?(data)
- case data[:commit][:status]
- when 'success'
- !notify_only_broken_builds?
- when 'failed'
- true
- else
- false
- end
+ def should_pipeline_be_notified?(data)
+ notify_for_ref?(data) && notify_for_pipeline?(data)
end
- def should_pipeline_be_notified?(data)
+ def notify_for_ref?(data)
+ return true if data[:object_attributes][:tag]
+ return true unless notify_only_default_branch
+
+ data[:object_attributes][:ref] == project.default_branch
+ end
+
+ def notify_for_pipeline?(data)
case data[:object_attributes][:status]
when 'success'
!notify_only_broken_pipelines?
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index c4142c38b2f..8b181221bb0 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -9,13 +9,13 @@ class HipchatService < Service
].freeze
prop_accessor :token, :room, :server, :color, :api_version
- boolean_accessor :notify_only_broken_builds, :notify
+ boolean_accessor :notify_only_broken_pipelines, :notify
validates :token, presence: true, if: :activated?
def initialize_properties
if properties.nil?
self.properties = {}
- self.notify_only_broken_builds = true
+ self.notify_only_broken_pipelines = true
end
end
@@ -41,12 +41,12 @@ class HipchatService < Service
placeholder: 'Leave blank for default (v2)' },
{ type: 'text', name: 'server',
placeholder: 'Leave blank for default. https://hipchat.example.com' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def self.supported_events
- %w(push issue confidential_issue merge_request note tag_push build)
+ %w(push issue confidential_issue merge_request note tag_push pipeline)
end
def execute(data)
@@ -90,8 +90,8 @@ class HipchatService < Service
create_merge_request_message(data) unless is_update?(data)
when "note"
create_note_message(data)
- when "build"
- create_build_message(data) if should_build_be_notified?(data)
+ when "pipeline"
+ create_pipeline_message(data) if should_pipeline_be_notified?(data)
end
end
@@ -240,28 +240,29 @@ class HipchatService < Service
message
end
- def create_build_message(data)
- ref_type = data[:tag] ? 'tag' : 'branch'
- ref = data[:ref]
- sha = data[:sha]
- user_name = data[:commit][:author_name]
- status = data[:commit][:status]
- duration = data[:commit][:duration]
+ def create_pipeline_message(data)
+ pipeline_attributes = data[:object_attributes]
+ pipeline_id = pipeline_attributes[:id]
+ ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ ref = pipeline_attributes[:ref]
+ user_name = (data[:user] && data[:user][:name]) || 'API'
+ status = pipeline_attributes[:status]
+ duration = pipeline_attributes[:duration]
branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"
- commit_link = "<a href=\"#{project_url}/commit/#{CGI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
+ pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>"
- "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
+ "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
end
def message_color(data)
- build_status_color(data) || color || 'yellow'
+ pipeline_status_color(data) || color || 'yellow'
end
- def build_status_color(data)
- return unless data && data[:object_kind] == 'build'
+ def pipeline_status_color(data)
+ return unless data && data[:object_kind] == 'pipeline'
- case data[:commit][:status]
+ case data[:object_attributes][:status]
when 'success'
'green'
else
@@ -294,10 +295,10 @@ class HipchatService < Service
end
end
- def should_build_be_notified?(data)
- case data[:commit][:status]
+ def should_pipeline_be_notified?(data)
+ case data[:object_attributes][:status]
when 'success'
- !notify_only_broken_builds?
+ !notify_only_broken_pipelines?
when 'failed'
true
else
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index eef403dba92..3b90fd1c2c7 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -62,7 +62,7 @@ class JiraService < IssueTrackerService
def help
"You need to configure JIRA before enabling this service. For more details
read the
- [JIRA service documentation](#{help_page_url('project_services/jira')})."
+ [JIRA service documentation](#{help_page_url('user/project/integrations/jira')})."
end
def title
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
index c13538e9fea..0362ed172c7 100644
--- a/app/models/project_services/mattermost_service.rb
+++ b/app/models/project_services/mattermost_service.rb
@@ -22,20 +22,11 @@ class MattermostService < ChatNotificationService
</ol>'
end
- def fields
- default_fields + build_event_channels
- end
-
- def default_fields
- [
- { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/โ€ฆ' },
- { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- ]
- end
-
def default_channel_placeholder
"Channel handle (e.g. town-square)"
end
+
+ def webhook_placeholder
+ 'http://mattermost.example.com/hooks/โ€ฆ'
+ end
end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 375966b9efc..6854d2243d7 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -30,7 +30,14 @@ class PrometheusService < MonitoringService
end
def help
- 'Retrieves `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. An `environment` label is required on each metric to identify the Environment.'
+ <<-MD.strip_heredoc
+ Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total`
+ and `container_memory_usage_bytes` from the configured Prometheus server.
+
+ If you are not using [Auto-Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html)
+ or have set up your own Prometheus server, an `environment` label is required on each metric to
+ [identify the Environment](https://docs.gitlab.com/ce/user/project/integrations/prometheus.html#metrics-and-labels).
+ MD
end
def self.to_param
@@ -67,16 +74,16 @@ class PrometheusService < MonitoringService
def calculate_reactive_cache(environment_slug)
return unless active? && project && !project.pending_delete?
- memory_query = %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024}
- cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))}
+ memory_query = %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024}
+ cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100}
{
success: true,
metrics: {
- # Memory used in MB
+ # Average Memory used in MB
memory_values: client.query_range(memory_query, start: 8.hours.ago),
memory_current: client.query(memory_query),
- # CPU Usage rate in cores.
+ # Average CPU Utilization
cpu_values: client.query_range(cpu_query, start: 8.hours.ago),
cpu_current: client.query(cpu_query)
},
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index da7496573ef..71da0af75f6 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -21,20 +21,11 @@ class SlackService < ChatNotificationService
</ol>'
end
- def fields
- default_fields + build_event_channels
- end
-
- def default_fields
- [
- { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/โ€ฆ' },
- { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- ]
- end
-
def default_channel_placeholder
"Channel name (e.g. general)"
end
+
+ def webhook_placeholder
+ 'https://hooks.slack.com/services/โ€ฆ'
+ end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 8a53e974b6f..6d6644053f8 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -169,6 +169,9 @@ class ProjectTeam
# Lookup only the IDs we need
user_ids = user_ids - access.keys
+
+ return access if user_ids.empty?
+
users_access = project.project_authorizations.
where(user: user_ids).
group(:user_id).
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 539b31780b3..70eef359cdd 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -42,8 +42,11 @@ class ProjectWiki
url_to_repo
end
- def http_url_to_repo
- [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
+ def http_url_to_repo(user = nil)
+ url = "#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git"
+ credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
+
+ Gitlab::UrlSanitizer.new(url, credentials: credentials).full_url
end
def wiki_base_path
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 6ab04440ca8..596650353fc 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -981,7 +981,13 @@ class Repository
end
def is_ancestor?(ancestor_id, descendant_id)
- merge_base(ancestor_id, descendant_id) == ancestor_id
+ Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
+ if is_enabled
+ raw_repository.is_ancestor?(ancestor_id, descendant_id)
+ else
+ merge_base_commit(ancestor_id, descendant_id) == ancestor_id
+ end
+ end
end
def empty_repo?
diff --git a/app/models/route.rb b/app/models/route.rb
index 73574a6206b..4b3efab5c3c 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -10,9 +10,11 @@ class Route < ActiveRecord::Base
after_update :rename_descendants
+ scope :inside_path, -> (path) { where('routes.path LIKE ?', "#{sanitize_sql_like(path)}/%") }
+
def rename_descendants
if path_changed? || name_changed?
- descendants = Route.where('path LIKE ?', "#{path_was}/%")
+ descendants = self.class.inside_path(path_was)
descendants.each do |route|
attributes = {}
@@ -21,7 +23,7 @@ class Route < ActiveRecord::Base
attributes[:path] = route.path.sub(path_was, path)
end
- if name_changed? && route.name.present?
+ if name_changed? && name_was.present? && route.name.present?
attributes[:name] = route.name.sub(name_was, name)
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 2f75a2e4e7f..e73f7e5d1a3 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -215,7 +215,6 @@ class Service < ActiveRecord::Base
assembla
bamboo
buildkite
- builds_email
bugzilla
campfire
custom_issue_tracker
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index dbd564e5e7d..30aca62499c 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -132,7 +132,8 @@ class Snippet < ActiveRecord::Base
end
def check_for_spam?
- public?
+ visibility_level_changed?(to: Snippet::PUBLIC) ||
+ (public? && (title_changed? || content_changed?))
end
def spammable_entity_type
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
new file mode 100644
index 00000000000..5cc66574941
--- /dev/null
+++ b/app/models/system_note_metadata.rb
@@ -0,0 +1,11 @@
+class SystemNoteMetadata < ActiveRecord::Base
+ ICON_TYPES = %w[
+ commit merge confidentiality status label assignee cross_reference
+ title time_tracking branch milestone discussion task moved
+ ].freeze
+
+ validates :note, presence: true
+ validates :action, inclusion: ICON_TYPES, allow_nil: true
+
+ belongs_to :note
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 39c1281179b..95a766f2ede 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -22,6 +22,7 @@ class User < ActiveRecord::Base
default_value_for :hide_no_ssh_key, false
default_value_for :hide_no_password, false
default_value_for :project_view, :files
+ default_value_for :notified_of_own_activity, false
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
@@ -115,7 +116,9 @@ class User < ActiveRecord::Base
validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
validates :bio, length: { maximum: 255 }, allow_blank: true
- validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
+ validates :projects_limit,
+ presence: true,
+ numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
namespace: true,
presence: true,
@@ -126,10 +129,9 @@ class User < ActiveRecord::Base
validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
validate :owns_public_email, if: ->(user) { user.public_email_changed? }
+ validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
- before_validation :generate_password, 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? }
@@ -139,8 +141,6 @@ class User < ActiveRecord::Base
before_save :ensure_external_user_rights
after_save :ensure_namespace_correct
after_initialize :set_projects_limit
- before_create :check_confirmation_email
- after_create :post_create_hook
after_destroy :post_destroy_hook
# User's Layout preference
@@ -324,6 +324,8 @@ class User < ActiveRecord::Base
end
def find_by_personal_access_token(token_string)
+ return unless token_string
+
PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
end
@@ -382,10 +384,8 @@ class User < ActiveRecord::Base
"#{self.class.reference_prefix}#{username}"
end
- def generate_password
- if force_random_password
- self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min)
- end
+ def skip_confirmation=(bool)
+ skip_confirmation! if bool
end
def generate_reset_token
@@ -397,10 +397,6 @@ class User < ActiveRecord::Base
@reset_token
end
- def check_confirmation_email
- skip_confirmation! unless current_application_settings.send_user_confirmation_email
- end
-
def recently_sent_password_reset?
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end
@@ -639,8 +635,10 @@ class User < ActiveRecord::Base
end
def fork_of(project)
- links = ForkedProjectLink.where(forked_from_project_id: project, forked_to_project_id: personal_projects)
-
+ links = ForkedProjectLink.where(
+ forked_from_project_id: project,
+ forked_to_project_id: personal_projects.unscope(:order)
+ )
if links.any?
links.first.forked_to_project
else
@@ -795,12 +793,6 @@ class User < ActiveRecord::Base
end
end
- def post_create_hook
- log_info("User \"#{name}\" (#{email}) was created")
- notification_service.new_user(self, @reset_token) if created_by_id
- system_hook_service.execute_hooks_for(self, :create)
- end
-
def post_destroy_hook
log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy)
@@ -877,7 +869,7 @@ class User < ActiveRecord::Base
def ci_authorized_runners
@ci_authorized_runners ||= begin
runner_ids = Ci::RunnerProject.
- where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})").
+ where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})").
select(:runner_id)
Ci::Runner.specific.where(id: runner_ids)
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 465c4d903ac..c771c22f46a 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -155,7 +155,7 @@ class WikiPage
end
# Returns boolean True or False if this instance
- # has been fully saved to disk or not.
+ # has been fully created on disk or not.
def persisted?
@persisted == true
end
@@ -226,6 +226,8 @@ class WikiPage
end
def save(method, *args)
+ saved = false
+
project_wiki = wiki
if valid? && project_wiki.send(method, *args)
@@ -243,10 +245,10 @@ class WikiPage
set_attributes
@persisted = true
+ saved = true
else
errors.add(:base, project_wiki.error_message) if project_wiki.error_message
- @persisted = false
end
- @persisted
+ saved
end
end
diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb
index 2c116102888..b804d6d0e8a 100644
--- a/app/serializers/build_entity.rb
+++ b/app/serializers/build_entity.rb
@@ -19,10 +19,17 @@ class BuildEntity < Grape::Entity
expose :playable?, as: :playable
expose :created_at
expose :updated_at
+ expose :detailed_status, as: :status, with: StatusEntity
private
+ alias_method :build, :object
+
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build)
end
+
+ def detailed_status
+ build.detailed_status(request.user)
+ end
end
diff --git a/app/serializers/build_serializer.rb b/app/serializers/build_serializer.rb
new file mode 100644
index 00000000000..79b67001199
--- /dev/null
+++ b/app/serializers/build_serializer.rb
@@ -0,0 +1,8 @@
+class BuildSerializer < BaseSerializer
+ entity BuildEntity
+
+ def represent_status(resource)
+ data = represent(resource, { only: [:status] })
+ data.fetch(:status, {})
+ end
+end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 4c017960628..4ff15a78115 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -9,6 +9,13 @@ class EnvironmentEntity < Grape::Entity
expose :last_deployment, using: DeploymentEntity
expose :stop_action?
+ expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment|
+ metrics_namespace_project_environment_path(
+ environment.project.namespace,
+ environment.project,
+ environment)
+ end
+
expose :environment_path do |environment|
namespace_project_environment_path(
environment.project.namespace,
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 61f0f11d7d2..3f16dd66d54 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -12,12 +12,7 @@ class PipelineEntity < Grape::Entity
end
expose :details do
- expose :status do |pipeline, options|
- StatusEntity.represent(
- pipeline.detailed_status(request.user),
- options)
- end
-
+ expose :detailed_status, as: :status, with: StatusEntity
expose :duration
expose :finished_at
expose :stages, using: StageEntity
@@ -82,4 +77,8 @@ class PipelineEntity < Grape::Entity
pipeline.cancelable? &&
can?(request.user, :update_pipeline, pipeline)
end
+
+ def detailed_status
+ pipeline.detailed_status(request.user)
+ end
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index ab2d3d5a3ec..7829df9fada 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -22,4 +22,11 @@ class PipelineSerializer < BaseSerializer
super(resource, opts)
end
end
+
+ def represent_status(resource)
+ return {} unless resource.present?
+
+ data = represent(resource, { only: [{ details: [:status] }] })
+ data.dig(:details, :status) || {}
+ end
end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index 47066bebfb1..dfd9d1584a1 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -1,7 +1,7 @@
class StatusEntity < Grape::Entity
include RequestAwareEntity
- expose :icon, :text, :label, :group
+ expose :icon, :favicon, :text, :label, :group
expose :has_details?, as: :has_details
expose :details_path
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
index f6275a63109..fd9ff115eab 100644
--- a/app/services/boards/create_service.rb
+++ b/app/services/boards/create_service.rb
@@ -12,7 +12,7 @@ module Boards
def create_board!
board = project.boards.create
- board.lists.create(list_type: :done)
+ board.lists.create(list_type: :closed)
board
end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 83f51947bd4..533e6787855 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -3,7 +3,7 @@ module Boards
class ListService < BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
- issues = without_board_labels(issues) unless movable_list?
+ issues = without_board_labels(issues) unless list
issues = with_list_label(issues) if movable_list?
issues.order_by_position_and_priority
end
@@ -41,7 +41,7 @@ module Boards
end
def set_state
- params[:state] = list && list.done? ? 'closed' : 'opened'
+ params[:state] = list && list.closed? ? 'closed' : 'opened'
end
def board_label_ids
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 2a9981ab884..d5735f13c1e 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -48,8 +48,8 @@ module Boards
end
def issue_state
- return 'reopen' if moving_from_list.done?
- return 'close' if moving_to_list.done?
+ return 'reopen' if moving_from_list.closed?
+ return 'close' if moving_to_list.closed?
end
def add_label_ids
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 0ab9042bf24..d6a4280ce4c 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -55,13 +55,13 @@ module Ci
new_builds.
# don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true }).
- joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
+ joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id').
where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
# Implement fair scheduling
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
- joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
+ joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id").
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
end
@@ -71,7 +71,7 @@ module Ci
def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.shared).
- group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
+ group(:project_id).select(:project_id, 'count(*) AS running_builds')
end
def new_builds
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index 574561adc4c..f72ddbf690c 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -7,14 +7,14 @@ module Ci
raise Gitlab::Access::AccessDeniedError
end
- pipeline.builds.failed_or_canceled.find_each do |build|
+ pipeline.builds.latest.failed_or_canceled.find_each do |build|
next unless build.retryable?
Ci::RetryBuildService.new(project, current_user)
.reprocess(build)
end
- pipeline.builds.skipped.find_each do |skipped|
+ pipeline.builds.latest.skipped.find_each do |skipped|
retry_optimistic_lock(skipped) { |build| build.process }
end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index b07338d500a..673ed02f952 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -25,12 +25,12 @@ class CreateBranchService < BaseService
private
def create_master_branch
- project.repository.commit_file(
+ project.repository.create_file(
current_user,
'/README.md',
'',
message: 'Add README.md',
- branch_name: 'master',
- update: false)
+ branch_name: 'master'
+ )
end
end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 4e878ec556a..1d65c76d282 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -1,6 +1,8 @@
module Groups
class UpdateService < Groups::BaseService
def execute
+ reject_parent_id!
+
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != group.visibility_level
@@ -22,5 +24,11 @@ module Groups
false
end
end
+
+ private
+
+ def reject_parent_id!
+ params.except!(:parent_id)
+ end
end
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index a444c78b609..b7fe5cb168b 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -19,7 +19,7 @@ module Issues
if issue.previous_changes.include?('title') ||
issue.previous_changes.include?('description')
- todo_service.update_issue(issue, current_user)
+ todo_service.update_issue(issue, current_user, old_mentioned_users)
end
if issue.previous_changes.include?('milestone_id')
diff --git a/app/services/labels/base_service.rb b/app/services/labels/base_service.rb
new file mode 100644
index 00000000000..91d72a57b4e
--- /dev/null
+++ b/app/services/labels/base_service.rb
@@ -0,0 +1,161 @@
+module Labels
+ class BaseService < ::BaseService
+ COLOR_NAME_TO_HEX = {
+ black: '#000000',
+ silver: '#C0C0C0',
+ gray: '#808080',
+ white: '#FFFFFF',
+ maroon: '#800000',
+ red: '#FF0000',
+ purple: '#800080',
+ fuchsia: '#FF00FF',
+ green: '#008000',
+ lime: '#00FF00',
+ olive: '#808000',
+ yellow: '#FFFF00',
+ navy: '#000080',
+ blue: '#0000FF',
+ teal: '#008080',
+ aqua: '#00FFFF',
+ orange: '#FFA500',
+ aliceblue: '#F0F8FF',
+ antiquewhite: '#FAEBD7',
+ aquamarine: '#7FFFD4',
+ azure: '#F0FFFF',
+ beige: '#F5F5DC',
+ bisque: '#FFE4C4',
+ blanchedalmond: '#FFEBCD',
+ blueviolet: '#8A2BE2',
+ brown: '#A52A2A',
+ burlywood: '#DEB887',
+ cadetblue: '#5F9EA0',
+ chartreuse: '#7FFF00',
+ chocolate: '#D2691E',
+ coral: '#FF7F50',
+ cornflowerblue: '#6495ED',
+ cornsilk: '#FFF8DC',
+ crimson: '#DC143C',
+ darkblue: '#00008B',
+ darkcyan: '#008B8B',
+ darkgoldenrod: '#B8860B',
+ darkgray: '#A9A9A9',
+ darkgreen: '#006400',
+ darkgrey: '#A9A9A9',
+ darkkhaki: '#BDB76B',
+ darkmagenta: '#8B008B',
+ darkolivegreen: '#556B2F',
+ darkorange: '#FF8C00',
+ darkorchid: '#9932CC',
+ darkred: '#8B0000',
+ darksalmon: '#E9967A',
+ darkseagreen: '#8FBC8F',
+ darkslateblue: '#483D8B',
+ darkslategray: '#2F4F4F',
+ darkslategrey: '#2F4F4F',
+ darkturquoise: '#00CED1',
+ darkviolet: '#9400D3',
+ deeppink: '#FF1493',
+ deepskyblue: '#00BFFF',
+ dimgray: '#696969',
+ dimgrey: '#696969',
+ dodgerblue: '#1E90FF',
+ firebrick: '#B22222',
+ floralwhite: '#FFFAF0',
+ forestgreen: '#228B22',
+ gainsboro: '#DCDCDC',
+ ghostwhite: '#F8F8FF',
+ gold: '#FFD700',
+ goldenrod: '#DAA520',
+ greenyellow: '#ADFF2F',
+ grey: '#808080',
+ honeydew: '#F0FFF0',
+ hotpink: '#FF69B4',
+ indianred: '#CD5C5C',
+ indigo: '#4B0082',
+ ivory: '#FFFFF0',
+ khaki: '#F0E68C',
+ lavender: '#E6E6FA',
+ lavenderblush: '#FFF0F5',
+ lawngreen: '#7CFC00',
+ lemonchiffon: '#FFFACD',
+ lightblue: '#ADD8E6',
+ lightcoral: '#F08080',
+ lightcyan: '#E0FFFF',
+ lightgoldenrodyellow: '#FAFAD2',
+ lightgray: '#D3D3D3',
+ lightgreen: '#90EE90',
+ lightgrey: '#D3D3D3',
+ lightpink: '#FFB6C1',
+ lightsalmon: '#FFA07A',
+ lightseagreen: '#20B2AA',
+ lightskyblue: '#87CEFA',
+ lightslategray: '#778899',
+ lightslategrey: '#778899',
+ lightsteelblue: '#B0C4DE',
+ lightyellow: '#FFFFE0',
+ limegreen: '#32CD32',
+ linen: '#FAF0E6',
+ mediumaquamarine: '#66CDAA',
+ mediumblue: '#0000CD',
+ mediumorchid: '#BA55D3',
+ mediumpurple: '#9370DB',
+ mediumseagreen: '#3CB371',
+ mediumslateblue: '#7B68EE',
+ mediumspringgreen: '#00FA9A',
+ mediumturquoise: '#48D1CC',
+ mediumvioletred: '#C71585',
+ midnightblue: '#191970',
+ mintcream: '#F5FFFA',
+ mistyrose: '#FFE4E1',
+ moccasin: '#FFE4B5',
+ navajowhite: '#FFDEAD',
+ oldlace: '#FDF5E6',
+ olivedrab: '#6B8E23',
+ orangered: '#FF4500',
+ orchid: '#DA70D6',
+ palegoldenrod: '#EEE8AA',
+ palegreen: '#98FB98',
+ paleturquoise: '#AFEEEE',
+ palevioletred: '#DB7093',
+ papayawhip: '#FFEFD5',
+ peachpuff: '#FFDAB9',
+ peru: '#CD853F',
+ pink: '#FFC0CB',
+ plum: '#DDA0DD',
+ powderblue: '#B0E0E6',
+ rosybrown: '#BC8F8F',
+ royalblue: '#4169E1',
+ saddlebrown: '#8B4513',
+ salmon: '#FA8072',
+ sandybrown: '#F4A460',
+ seagreen: '#2E8B57',
+ seashell: '#FFF5EE',
+ sienna: '#A0522D',
+ skyblue: '#87CEEB',
+ slateblue: '#6A5ACD',
+ slategray: '#708090',
+ slategrey: '#708090',
+ snow: '#FFFAFA',
+ springgreen: '#00FF7F',
+ steelblue: '#4682B4',
+ tan: '#D2B48C',
+ thistle: '#D8BFD8',
+ tomato: '#FF6347',
+ turquoise: '#40E0D0',
+ violet: '#EE82EE',
+ wheat: '#F5DEB3',
+ whitesmoke: '#F5F5F5',
+ yellowgreen: '#9ACD32',
+ rebeccapurple: '#663399'
+ }.freeze
+
+ def convert_color_name_to_hex
+ color = params[:color]
+ color_name = color.strip.downcase
+
+ return color if color_name.start_with?('#')
+
+ COLOR_NAME_TO_HEX[color_name.to_sym] || color
+ end
+ end
+end
diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb
new file mode 100644
index 00000000000..6c399c92377
--- /dev/null
+++ b/app/services/labels/create_service.rb
@@ -0,0 +1,25 @@
+module Labels
+ class CreateService < Labels::BaseService
+ def initialize(params = {})
+ @params = params.dup.with_indifferent_access
+ end
+
+ # returns the created label
+ def execute(target_params)
+ params[:color] = convert_color_name_to_hex if params[:color].present?
+
+ project_or_group = target_params[:project] || target_params[:group]
+
+ if project_or_group.present?
+ project_or_group.labels.create(params)
+ elsif target_params[:template]
+ label = Label.new(params)
+ label.template = true
+ label.save
+ label
+ else
+ Rails.logger.warn("target_params should contain :project or :group or :template, actual value: #{target_params}")
+ end
+ end
+ end
+end
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
index cf4f7606c94..940c8b333d3 100644
--- a/app/services/labels/find_or_create_service.rb
+++ b/app/services/labels/find_or_create_service.rb
@@ -3,7 +3,7 @@ module Labels
def initialize(current_user, project, params = {})
@current_user = current_user
@project = project
- @params = params.dup
+ @params = params.dup.with_indifferent_access
end
def execute(skip_authorization: false)
@@ -28,7 +28,7 @@ module Labels
new_label = available_labels.find_by(title: title)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project))
- new_label = project.labels.create(params)
+ new_label = Labels::CreateService.new(params).execute(project: project)
end
new_label
diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb
new file mode 100644
index 00000000000..28dcabf9541
--- /dev/null
+++ b/app/services/labels/update_service.rb
@@ -0,0 +1,15 @@
+module Labels
+ class UpdateService < Labels::BaseService
+ def initialize(params = {})
+ @params = params.dup.with_indifferent_access
+ end
+
+ # returns the updated label
+ def execute(label)
+ params[:color] = convert_color_name_to_hex if params[:color].present?
+
+ label.update(params)
+ label
+ end
+ end
+end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 9d4739e37bb..fdce542bd9e 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -6,7 +6,7 @@ module MergeRequests
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
merge_request.target_branch = find_target_branch
- merge_request.can_be_created = branches_valid? && source_branch_specified? && target_branch_specified?
+ merge_request.can_be_created = branches_valid?
compare_branches if branches_present?
assign_title_and_description if merge_request.can_be_created
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 3cb9aae83f6..ab7fcf3b6e2 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -28,7 +28,7 @@ module MergeRequests
if merge_request.previous_changes.include?('title') ||
merge_request.previous_changes.include?('description')
- todo_service.update_merge_request(merge_request, current_user)
+ todo_service.update_merge_request(merge_request, current_user, old_mentioned_users)
end
if merge_request.previous_changes.include?('target_branch')
diff --git a/app/services/note_summary.rb b/app/services/note_summary.rb
new file mode 100644
index 00000000000..a6f6320d573
--- /dev/null
+++ b/app/services/note_summary.rb
@@ -0,0 +1,20 @@
+class NoteSummary
+ attr_reader :note
+ attr_reader :metadata
+
+ def initialize(noteable, project, author, body, action: nil, commit_count: nil)
+ @note = { noteable: noteable, project: project, author: author, note: body }
+ @metadata = { action: action, commit_count: commit_count }.compact
+
+ set_commit_params if note[:noteable].is_a?(Commit)
+ end
+
+ def metadata?
+ metadata.present?
+ end
+
+ def set_commit_params
+ note.merge!(noteable_type: 'Commit', commit_id: note[:noteable].id)
+ note[:noteable] = nil
+ end
+end
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 75a4b3ed826..75fd08ea0a9 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -3,11 +3,13 @@ module Notes
def execute(note)
return note unless note.editable?
+ old_mentioned_users = note.mentioned_users.to_a
+
note.update_attributes(params.merge(updated_by: current_user))
note.create_new_cross_references!(current_user)
if note.previous_changes.include?('note')
- TodoService.new.update_note(note, current_user)
+ TodoService.new.update_note(note, current_user, old_mentioned_users)
end
note
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
new file mode 100644
index 00000000000..940e850600f
--- /dev/null
+++ b/app/services/notification_recipient_service.rb
@@ -0,0 +1,293 @@
+#
+# Used by NotificationService to determine who should receive notification
+#
+class NotificationRecipientService
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
+ custom_action = build_custom_key(action, target)
+
+ recipients = target.participants(current_user)
+
+ unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
+ recipients = add_project_watchers(recipients)
+ end
+
+ recipients = add_custom_notifications(recipients, custom_action)
+ recipients = reject_mention_users(recipients)
+
+ # Re-assign is considered as a mention of the new assignee so we add the
+ # new assignee to the list of recipients after we rejected users with
+ # the "on mention" notification level
+ if [:reassign_merge_request, :reassign_issue].include?(custom_action)
+ recipients << previous_assignee if previous_assignee
+ recipients << target.assignee
+ end
+
+ recipients = reject_muted_users(recipients)
+ recipients = add_subscribed_users(recipients, target)
+
+ if [:new_issue, :new_merge_request].include?(custom_action)
+ recipients = add_labels_subscribers(recipients, target)
+ end
+
+ recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
+
+ recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity?
+
+ recipients.uniq
+ end
+
+ def build_relabeled_recipients(target, current_user, labels:)
+ recipients = add_labels_subscribers([], target, labels: labels)
+ recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
+ recipients.delete(current_user) unless current_user.notified_of_own_activity?
+ recipients.uniq
+ end
+
+ def build_new_note_recipients(note)
+ target = note.noteable
+
+ ability, subject = if note.for_personal_snippet?
+ [:read_personal_snippet, note.noteable]
+ else
+ [:read_project, note.project]
+ end
+
+ mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) }
+
+ # Add all users participating in the thread (author, assignee, comment authors)
+ recipients =
+ if target.respond_to?(:participants)
+ target.participants(note.author)
+ else
+ mentioned_users
+ end
+
+ unless note.for_personal_snippet?
+ # Merge project watchers
+ recipients = add_project_watchers(recipients)
+
+ # Merge project with custom notification
+ recipients = add_custom_notifications(recipients, :new_note)
+ end
+
+ # Reject users with Mention notification level, except those mentioned in _this_ note.
+ recipients = reject_mention_users(recipients - mentioned_users)
+ recipients = recipients + mentioned_users
+
+ recipients = reject_muted_users(recipients)
+
+ recipients = add_subscribed_users(recipients, note.noteable)
+ recipients = reject_unsubscribed_users(recipients, note.noteable)
+ recipients = reject_users_without_access(recipients, note.noteable)
+
+ recipients.delete(note.author) unless note.author.notified_of_own_activity?
+ recipients.uniq
+ end
+
+ # Remove users with disabled notifications from array
+ # Also remove duplications and nil recipients
+ def reject_muted_users(users)
+ reject_users(users, :disabled)
+ end
+
+ protected
+
+ # Get project/group users with CUSTOM notification level
+ def add_custom_notifications(recipients, action)
+ user_ids = []
+
+ # Users with a notification setting on group or project
+ user_ids += user_ids_notifiable_on(project, :custom, action)
+ user_ids += user_ids_notifiable_on(project.group, :custom, action)
+
+ # Users with global level custom
+ user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
+ user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global)
+
+ global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
+ user_ids += user_ids_with_global_level_custom(global_users_ids, action)
+
+ recipients.concat(User.find(user_ids))
+ end
+
+ def add_project_watchers(recipients)
+ recipients.concat(project_watchers).compact
+ end
+
+ # Get project users with WATCH notification level
+ def project_watchers
+ project_members_ids = user_ids_notifiable_on(project)
+
+ user_ids_with_project_global = user_ids_notifiable_on(project, :global)
+ user_ids_with_group_global = user_ids_notifiable_on(project.group, :global)
+
+ user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq)
+
+ user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids)
+ user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids)
+
+ User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a
+ end
+
+ # Remove users with notification level 'Mentioned'
+ def reject_mention_users(users)
+ reject_users(users, :mention)
+ end
+
+ def add_subscribed_users(recipients, target)
+ return recipients unless target.respond_to? :subscribers
+
+ recipients + target.subscribers(project)
+ end
+
+ def user_ids_notifiable_on(resource, notification_level = nil, action = nil)
+ return [] unless resource
+
+ if notification_level
+ settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
+ settings = settings.select { |setting| setting.events[action] } if action.present?
+ settings.map(&:user_id)
+ else
+ resource.notification_settings.pluck(:user_id)
+ end
+ end
+
+ # Build a list of user_ids based on project notification settings
+ def select_project_members_ids(project, global_setting, user_ids_global_level_watch)
+ user_ids = user_ids_notifiable_on(project, :watch)
+
+ # If project setting is global, add to watch list if global setting is watch
+ global_setting.each do |user_id|
+ if user_ids_global_level_watch.include?(user_id)
+ user_ids << user_id
+ end
+ end
+
+ user_ids
+ end
+
+ # Build a list of user_ids based on group notification settings
+ def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch)
+ uids = user_ids_notifiable_on(group, :watch)
+
+ # Group setting is watch, add to user_ids list if user is not project member
+ user_ids = []
+ uids.each do |user_id|
+ if project_members.exclude?(user_id)
+ user_ids << user_id
+ end
+ end
+
+ # Group setting is global, add to user_ids list if global setting is watch
+ global_setting.each do |user_id|
+ if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id)
+ user_ids << user_id
+ end
+ end
+
+ user_ids
+ end
+
+ def user_ids_with_global_level_watch(ids)
+ settings_with_global_level_of(:watch, ids).pluck(:user_id)
+ end
+
+ def user_ids_with_global_level_custom(ids, action)
+ settings = settings_with_global_level_of(:custom, ids)
+ settings = settings.select { |setting| setting.events[action] }
+ settings.map(&:user_id)
+ end
+
+ def settings_with_global_level_of(level, ids)
+ NotificationSetting.where(
+ user_id: ids,
+ source_type: nil,
+ level: NotificationSetting.levels[level]
+ )
+ end
+
+ # Reject users which has certain notification level
+ #
+ # Example:
+ # reject_users(users, :watch, project)
+ #
+ def reject_users(users, level)
+ level = level.to_s
+
+ unless NotificationSetting.levels.keys.include?(level)
+ raise 'Invalid notification level'
+ end
+
+ users = users.to_a.compact.uniq
+ users = users.select { |u| u.can?(:receive_notifications) }
+
+ users.reject do |user|
+ global_notification_setting = user.global_notification_setting
+
+ next global_notification_setting.level == level unless project
+
+ setting = user.notification_settings_for(project)
+
+ if project.group && (setting.nil? || setting.global?)
+ setting = user.notification_settings_for(project.group)
+ end
+
+ # reject users who globally set mention notification and has no setting per project/group
+ next global_notification_setting.level == level unless setting
+
+ # reject users who set mention notification in project
+ next true if setting.level == level
+
+ # reject users who have mention level in project and disabled in global settings
+ setting.global? && global_notification_setting.level == level
+ end
+ end
+
+ def reject_unsubscribed_users(recipients, target)
+ return recipients unless target.respond_to? :subscriptions
+
+ recipients.reject do |user|
+ subscription = target.subscriptions.find_by_user_id(user.id)
+ subscription && !subscription.subscribed
+ end
+ end
+
+ def reject_users_without_access(recipients, target)
+ ability = case target
+ when Issuable
+ :"read_#{target.to_ability_name}"
+ when Ci::Pipeline
+ :read_build # We have build trace in pipeline emails
+ end
+
+ return recipients unless ability
+
+ recipients.select do |user|
+ user.can?(ability, target)
+ end
+ end
+
+ def add_labels_subscribers(recipients, target, labels: nil)
+ return recipients unless target.respond_to? :labels
+
+ (labels || target.labels).each do |label|
+ recipients += label.subscribers(project)
+ end
+
+ recipients
+ end
+
+ # Build event key to search on custom notification level
+ # Check NotificationSetting::EMAIL_EVENTS
+ def build_custom_key(action, object)
+ "#{action}_#{object.class.model_name.name.underscore}".to_sym
+ end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index fdaba9b95fb..2c6f849259e 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -150,7 +150,10 @@ class NotificationService
end
def resolve_all_discussions(merge_request, current_user)
- recipients = build_recipients(merge_request, merge_request.target_project, current_user, action: "resolve_all_discussions")
+ recipients = NotificationRecipientService.new(merge_request.target_project).build_recipients(
+ merge_request,
+ current_user,
+ action: "resolve_all_discussions")
recipients.each do |recipient|
mailer.resolved_all_discussions_email(recipient.id, merge_request.id, current_user.id).deliver_later
@@ -164,64 +167,15 @@ class NotificationService
end
# Notify users on new note in system
- #
- # TODO: split on methods and refactor
- #
def new_note(note)
return true unless note.noteable_type.present?
# ignore gitlab service messages
return true if note.cross_reference? && note.system?
- target = note.noteable
-
- recipients = []
-
- mentioned_users = note.mentioned_users
-
- ability, subject = if note.for_personal_snippet?
- [:read_personal_snippet, note.noteable]
- else
- [:read_project, note.project]
- end
-
- mentioned_users.select! do |user|
- user.can?(ability, subject)
- end
-
- # Add all users participating in the thread (author, assignee, comment authors)
- participants =
- if target.respond_to?(:participants)
- target.participants(note.author)
- else
- mentioned_users
- end
-
- recipients = recipients.concat(participants)
-
- unless note.for_personal_snippet?
- # Merge project watchers
- recipients = add_project_watchers(recipients, note.project)
-
- # Merge project with custom notification
- recipients = add_custom_notifications(recipients, note.project, :new_note)
- end
-
- # Reject users with Mention notification level, except those mentioned in _this_ note.
- recipients = reject_mention_users(recipients - mentioned_users, note.project)
- recipients = recipients + mentioned_users
-
- recipients = reject_muted_users(recipients, note.project)
-
- recipients = add_subscribed_users(recipients, note.project, note.noteable)
- recipients = reject_unsubscribed_users(recipients, note.noteable)
- recipients = reject_users_without_access(recipients, note.noteable)
-
- recipients.delete(note.author)
- recipients = recipients.uniq
-
notify_method = "note_#{note.to_ability_name}_email".to_sym
+ recipients = NotificationRecipientService.new(note.project).build_new_note_recipients(note)
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
end
@@ -290,7 +244,7 @@ class NotificationService
def project_was_moved(project, old_path_with_namespace)
recipients = project.team.members
- recipients = reject_muted_users(recipients, project)
+ recipients = NotificationRecipientService.new(project).reject_muted_users(recipients)
recipients.each do |recipient|
mailer.project_was_moved_email(
@@ -302,7 +256,7 @@ class NotificationService
end
def issue_moved(issue, new_issue, current_user)
- recipients = build_recipients(issue, issue.project, current_user)
+ recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user)
recipients.map do |recipient|
email = mailer.issue_moved_email(recipient, issue, new_issue, current_user)
@@ -324,11 +278,11 @@ class NotificationService
return unless mailer.respond_to?(email_template)
- recipients ||= build_recipients(
+ recipients ||= NotificationRecipientService.new(pipeline.project).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
@@ -337,199 +291,8 @@ class NotificationService
protected
- # Get project/group users with CUSTOM notification level
- def add_custom_notifications(recipients, project, action)
- user_ids = []
-
- # Users with a notification setting on group or project
- user_ids += notification_settings_for(project, :custom, action)
- user_ids += notification_settings_for(project.group, :custom, action)
-
- # Users with global level custom
- users_with_project_level_global = notification_settings_for(project, :global)
- users_with_group_level_global = notification_settings_for(project.group, :global)
-
- global_users_ids = users_with_project_level_global.concat(users_with_group_level_global)
- user_ids += users_with_global_level_custom(global_users_ids, action)
-
- recipients.concat(User.find(user_ids))
- end
-
- # Get project users with WATCH notification level
- def project_watchers(project)
- project_members = notification_settings_for(project)
-
- users_with_project_level_global = notification_settings_for(project, :global)
- users_with_group_level_global = notification_settings_for(project.group, :global)
-
- users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
-
- users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
- users_with_group_setting = select_group_member_setting(project.group, project_members, users_with_group_level_global, users)
-
- User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
- end
-
- def notification_settings_for(resource, notification_level = nil, action = nil)
- return [] unless resource
-
- if notification_level
- settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
- settings = settings.select { |setting| setting.events[action] } if action.present?
- settings.map(&:user_id)
- else
- resource.notification_settings.pluck(:user_id)
- end
- end
-
- def users_with_global_level_watch(ids)
- settings_with_global_level_of(:watch, ids).pluck(:user_id)
- end
-
- def users_with_global_level_custom(ids, action)
- settings = settings_with_global_level_of(:custom, ids)
- settings = settings.select { |setting| setting.events[action] }
- settings.map(&:user_id)
- end
-
- def settings_with_global_level_of(level, ids)
- NotificationSetting.where(
- user_id: ids,
- source_type: nil,
- level: NotificationSetting.levels[level]
- )
- end
-
- # Build a list of users based on project notification settings
- def select_project_member_setting(project, global_setting, users_global_level_watch)
- users = notification_settings_for(project, :watch)
-
- # If project setting is global, add to watch list if global setting is watch
- global_setting.each do |user_id|
- if users_global_level_watch.include?(user_id)
- users << user_id
- end
- end
-
- users
- end
-
- # Build a list of users based on group notification settings
- def select_group_member_setting(group, project_members, global_setting, users_global_level_watch)
- uids = notification_settings_for(group, :watch)
-
- # Group setting is watch, add to users list if user is not project member
- users = []
- uids.each do |user_id|
- if project_members.exclude?(user_id)
- users << user_id
- end
- end
-
- # Group setting is global, add to users list if global setting is watch
- global_setting.each do |user_id|
- if project_members.exclude?(user_id) && users_global_level_watch.include?(user_id)
- users << user_id
- end
- end
-
- users
- end
-
- def add_project_watchers(recipients, project)
- recipients.concat(project_watchers(project)).compact
- end
-
- # Remove users with disabled notifications from array
- # Also remove duplications and nil recipients
- def reject_muted_users(users, project = nil)
- reject_users(users, :disabled, project)
- end
-
- # Remove users with notification level 'Mentioned'
- def reject_mention_users(users, project = nil)
- reject_users(users, :mention, project)
- end
-
- # Reject users which has certain notification level
- #
- # Example:
- # reject_users(users, :watch, project)
- #
- def reject_users(users, level, project = nil)
- level = level.to_s
-
- unless NotificationSetting.levels.keys.include?(level)
- raise 'Invalid notification level'
- end
-
- users = users.to_a.compact.uniq
- users = users.select { |u| u.can?(:receive_notifications) }
-
- users.reject do |user|
- global_notification_setting = user.global_notification_setting
-
- next global_notification_setting.level == level unless project
-
- setting = user.notification_settings_for(project)
-
- if project.group && (setting.nil? || setting.global?)
- setting = user.notification_settings_for(project.group)
- end
-
- # reject users who globally set mention notification and has no setting per project/group
- next global_notification_setting.level == level unless setting
-
- # reject users who set mention notification in project
- next true if setting.level == level
-
- # reject users who have mention level in project and disabled in global settings
- setting.global? && global_notification_setting.level == level
- end
- end
-
- def reject_unsubscribed_users(recipients, target)
- return recipients unless target.respond_to? :subscriptions
-
- recipients.reject do |user|
- subscription = target.subscriptions.find_by_user_id(user.id)
- subscription && !subscription.subscribed
- end
- end
-
- def reject_users_without_access(recipients, target)
- ability = case target
- when Issuable
- :"read_#{target.to_ability_name}"
- when Ci::Pipeline
- :read_build # We have build trace in pipeline emails
- end
-
- return recipients unless ability
-
- recipients.select do |user|
- user.can?(ability, target)
- end
- end
-
- def add_subscribed_users(recipients, project, target)
- return recipients unless target.respond_to? :subscribers
-
- recipients + target.subscribers(project)
- end
-
- def add_labels_subscribers(recipients, project, target, labels: nil)
- return recipients unless target.respond_to? :labels
-
- (labels || target.labels).each do |label|
- recipients += label.subscribers(project)
- end
-
- recipients
- end
-
def new_resource_email(target, project, method)
- recipients = build_recipients(target, project, target.author, action: "new")
+ recipients = NotificationRecipientService.new(project).build_recipients(target, target.author, action: "new")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later
@@ -537,7 +300,7 @@ class NotificationService
end
def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method)
- recipients = build_recipients(target, project, current_user, action: "new")
+ recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "new")
recipients = recipients & new_mentioned_users
recipients.each do |recipient|
@@ -548,9 +311,8 @@ class NotificationService
def close_resource_email(target, project, current_user, method, skip_current_user: true)
action = method == :merged_merge_request_email ? "merge" : "close"
- recipients = build_recipients(
+ recipients = NotificationRecipientService.new(project).build_recipients(
target,
- project,
current_user,
action: action,
skip_current_user: skip_current_user
@@ -565,7 +327,12 @@ class NotificationService
previous_assignee_id = previous_record(target, 'assignee_id')
previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- recipients = build_recipients(target, project, current_user, action: "reassign", previous_assignee: previous_assignee)
+ recipients = NotificationRecipientService.new(project).build_recipients(
+ target,
+ current_user,
+ action: "reassign",
+ previous_assignee: previous_assignee
+ )
recipients.each do |recipient|
mailer.send(
@@ -579,7 +346,7 @@ class NotificationService
end
def relabeled_resource_email(target, project, labels, current_user, method)
- recipients = build_relabeled_recipients(target, project, current_user, labels: labels)
+ recipients = NotificationRecipientService.new(project).build_relabeled_recipients(target, current_user, labels: labels)
label_names = labels.map(&:name)
recipients.each do |recipient|
@@ -588,58 +355,13 @@ class NotificationService
end
def reopen_resource_email(target, project, current_user, method, status)
- recipients = build_recipients(target, project, current_user, action: "reopen")
+ recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "reopen")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later
end
end
- def build_recipients(target, project, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
- custom_action = build_custom_key(action, target)
-
- recipients = target.participants(current_user)
-
- unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
- recipients = add_project_watchers(recipients, project)
- end
-
- recipients = add_custom_notifications(recipients, project, custom_action)
- recipients = reject_mention_users(recipients, project)
-
- recipients = recipients.uniq
-
- # Re-assign is considered as a mention of the new assignee so we add the
- # new assignee to the list of recipients after we rejected users with
- # the "on mention" notification level
- if [:reassign_merge_request, :reassign_issue].include?(custom_action)
- recipients << previous_assignee if previous_assignee
- recipients << target.assignee
- end
-
- recipients = reject_muted_users(recipients, project)
- recipients = add_subscribed_users(recipients, project, target)
-
- if [:new_issue, :new_merge_request].include?(custom_action)
- recipients = add_labels_subscribers(recipients, project, target)
- end
-
- recipients = reject_unsubscribed_users(recipients, target)
- recipients = reject_users_without_access(recipients, target)
-
- recipients.delete(current_user) if skip_current_user
-
- recipients.uniq
- end
-
- def build_relabeled_recipients(target, project, current_user, labels:)
- 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.uniq
- end
-
def mailer
Notify
end
@@ -651,10 +373,4 @@ class NotificationService
end
end
end
-
- # Build event key to search on custom notification level
- # Check NotificationSetting::EMAIL_EVENTS
- def build_custom_key(action, object)
- "#{action}_#{object.class.model_name.name.underscore}".to_sym
- end
end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 1c5a549feb9..d484a96f785 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -33,6 +33,7 @@ module Projects
def import_repository
begin
+ raise Error, "Blocked import URL." if Gitlab::UrlBlocker.blocked_url?(project.import_url)
gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
rescue => e
# Expire cache to prevent scenarios such as:
@@ -40,7 +41,7 @@ module Projects
# 2. Retried import, repo is broken or not imported but +exists?+ still returns true
project.repository.before_import if project.repository_exists?
- raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
+ raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
end
end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index 781cd13b44b..a3c655493a5 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -16,5 +16,9 @@ module Search
Gitlab::SearchResults.new(current_user, projects, params[:search])
end
+
+ def scope
+ @scope ||= %w[issues merge_requests milestones].delete(params[:scope]) { 'projects' }
+ end
end
end
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index 4b500914cfb..9a22abae635 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -12,5 +12,9 @@ module Search
params[:search],
params[:repository_ref])
end
+
+ def scope
+ @scope ||= %w[notes issues merge_requests milestones wiki_blobs commits].delete(params[:scope]) { 'blobs' }
+ end
end
end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 0b3e713e220..4f161beea4d 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -11,5 +11,9 @@ module Search
Gitlab::SnippetSearchResults.new(snippets, params[:search])
end
+
+ def scope
+ @scope ||= %w[snippet_titles].delete(params[:scope]) { 'snippet_blobs' }
+ end
end
end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
new file mode 100644
index 00000000000..8d46a8dab3e
--- /dev/null
+++ b/app/services/search_service.rb
@@ -0,0 +1,63 @@
+class SearchService
+ include Gitlab::Allowable
+
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params.dup
+ end
+
+ def project
+ return @project if defined?(@project)
+
+ @project =
+ if params[:project_id].present?
+ the_project = Project.find_by(id: params[:project_id])
+ can?(current_user, :download_code, the_project) ? the_project : nil
+ else
+ nil
+ end
+ end
+
+ def group
+ return @group if defined?(@group)
+
+ @group =
+ if params[:group_id].present?
+ the_group = Group.find_by(id: params[:group_id])
+ can?(current_user, :read_group, the_group) ? the_group : nil
+ else
+ nil
+ end
+ end
+
+ def show_snippets?
+ return @show_snippets if defined?(@show_snippets)
+
+ @show_snippets = params[:snippets] == 'true'
+ end
+
+ delegate :scope, to: :search_service
+
+ def search_results
+ @search_results ||= search_service.execute
+ end
+
+ def search_objects
+ @search_objects ||= search_results.objects(scope, params[:page])
+ end
+
+ private
+
+ def search_service
+ @search_service ||=
+ if project
+ Search::ProjectService.new(project, current_user, params)
+ elsif show_snippets?
+ Search::SnippetService.new(current_user, params)
+ else
+ Search::GlobalService.new(current_user, params)
+ end
+ end
+
+ attr_reader :current_user, :params
+end
diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb
index 023e0824e85..11030bee8f1 100644
--- a/app/services/spam_check_service.rb
+++ b/app/services/spam_check_service.rb
@@ -14,6 +14,9 @@ module SpamCheckService
@spam_log_id = params.delete(:spam_log_id)
end
+ # In order to be proceed to the spam check process, @spammable has to be
+ # a dirty instance, which means it should be already assigned with the new
+ # attribute values.
def spam_check(spammable, user)
spam_service = SpamService.new(spammable, @request)
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 868fa7b3f21..af0ddbe5934 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -24,10 +24,9 @@ class SystemHooksService
key: model.key,
id: model.id
)
+
if model.user
- data.merge!(
- username: model.user.username
- )
+ data[:username] = model.user.username
end
when Project
data.merge!(project_data(model))
@@ -35,8 +34,6 @@ class SystemHooksService
if event == :rename || event == :transfer
data[:old_path_with_namespace] = model.old_path_with_namespace
end
-
- data
when User
data.merge!({
name: model.name,
@@ -59,6 +56,8 @@ class SystemHooksService
when GroupMember
data.merge!(group_member_data(model))
end
+
+ data
end
def build_event_name(model, event)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 8e02fe3741a..d3e502b66dd 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -26,7 +26,7 @@ module SystemNoteService
body << new_commit_summary(new_commits).join("\n")
body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})"
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count))
end
# Called when the assignee of a Noteable is changed or removed
@@ -46,7 +46,7 @@ module SystemNoteService
def change_assignee(noteable, project, author, assignee)
body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}"
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
end
# Called when one or more labels on a Noteable are added and/or removed
@@ -86,7 +86,7 @@ module SystemNoteService
body << ' ' << 'label'.pluralize(labels_count)
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'label'))
end
# Called when the milestone of a Noteable is changed
@@ -106,7 +106,7 @@ module SystemNoteService
def change_milestone(noteable, project, author, milestone)
body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project)}"
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
end
# Called when the estimated time of a Noteable is changed
@@ -132,7 +132,7 @@ module SystemNoteService
"changed time estimate to #{parsed_time}"
end
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
end
# Called when the spent time of a Noteable is changed
@@ -161,7 +161,7 @@ module SystemNoteService
body = "#{action} #{parsed_time} of time spent"
end
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
end
# Called when the status of a Noteable is changed
@@ -183,53 +183,57 @@ module SystemNoteService
body = status.dup
body << " via #{source.gfm_reference(project)}" if source
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'status'))
end
# Called when 'merge when pipeline succeeds' is executed
def merge_when_pipeline_succeeds(noteable, project, author, last_commit)
body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds"
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end
# Called when 'merge when pipeline succeeds' is canceled
def cancel_merge_when_pipeline_succeeds(noteable, project, author)
body = 'canceled the automatic merge'
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end
def remove_merge_request_wip(noteable, project, author)
body = 'unmarked as a **Work In Progress**'
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
def add_merge_request_wip(noteable, project, author)
body = 'marked as a **Work In Progress**'
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
def add_merge_request_wip_from_commit(noteable, project, author, commit)
body = "marked as a **Work In Progress** from #{commit.to_reference(project)}"
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
def self.resolve_all_discussions(merge_request, project, author)
body = "resolved all discussions"
- create_note(noteable: merge_request, project: project, author: author, note: body)
+ create_note(NoteSummary.new(merge_request, project, author, body, action: 'discussion'))
end
def discussion_continued_in_issue(discussion, project, author, issue)
body = "created #{issue.to_reference} to continue this discussion"
- note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
- note_attributes[:type] = note_attributes.delete(:note_type)
- create_note(note_attributes)
+ note_params = discussion.reply_attributes.merge(project: project, author: author, note: body)
+ note_params[:type] = note_params.delete(:note_type)
+
+ note = Note.create(note_params.merge(system: true))
+ note.system_note_metadata = SystemNoteMetadata.new({ action: 'discussion' })
+
+ note
end
# Called when the title of a Noteable is changed
@@ -253,7 +257,8 @@ module SystemNoteService
marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true)
body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"
- create_note(noteable: noteable, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
# Called when the confidentiality changes
@@ -269,7 +274,8 @@ module SystemNoteService
# Returns the created Note object
def change_issue_confidentiality(issue, project, author)
body = issue.confidential ? 'made the issue confidential' : 'made the issue visible to everyone'
- create_note(noteable: issue, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(issue, project, author, body, action: 'confidentiality'))
end
# Called when a branch in Noteable is changed
@@ -288,7 +294,8 @@ module SystemNoteService
# Returns the created Note object
def change_branch(noteable, project, author, branch_type, old_branch, new_branch)
body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`"
- create_note(noteable: noteable, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
end
# Called when a branch in Noteable is added or deleted
@@ -314,7 +321,8 @@ module SystemNoteService
end
body = "#{verb} #{branch_type} branch `#{branch}`"
- create_note(noteable: noteable, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
end
# Called when a branch is created from the 'new branch' button on a issue
@@ -325,7 +333,8 @@ module SystemNoteService
link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
body = "created branch [`#{branch}`](#{link})"
- create_note(noteable: issue, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(issue, project, author, body, action: 'branch'))
end
# Called when a Mentionable references a Noteable
@@ -349,23 +358,12 @@ module SystemNoteService
return if cross_reference_disallowed?(noteable, mentioner)
gfm_reference = mentioner.gfm_reference(noteable.project)
-
- note_options = {
- project: noteable.project,
- author: author,
- note: cross_reference_note_content(gfm_reference)
- }
-
- if noteable.is_a?(Commit)
- note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id)
- else
- note_options[:noteable] = noteable
- end
+ body = cross_reference_note_content(gfm_reference)
if noteable.is_a?(ExternalIssue)
noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author)
else
- create_note(note_options)
+ create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference'))
end
end
@@ -444,7 +442,8 @@ module SystemNoteService
def change_task_status(noteable, project, author, new_task)
status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
body = "marked the task **#{new_task.source}** as #{status_label}"
- create_note(noteable: noteable, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'task'))
end
# Called when noteable has been moved to another project
@@ -466,7 +465,8 @@ module SystemNoteService
cross_reference = noteable_ref.to_reference(project)
body = "moved #{direction} #{cross_reference}"
- create_note(noteable: noteable, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
end
private
@@ -482,8 +482,11 @@ module SystemNoteService
end
end
- def create_note(args = {})
- Note.create(args.merge(system: true))
+ def create_note(note_summary)
+ note = Note.create(note_summary.note.merge(system: true))
+ note.system_note_metadata = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata?
+
+ note
end
def cross_reference_note_prefix
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index bf7e76ec59e..b6e88b0280f 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -19,8 +19,8 @@ class TodoService
#
# * mark all pending todos related to the issue for the current user as done
#
- def update_issue(issue, current_user)
- update_issuable(issue, current_user)
+ def update_issue(issue, current_user, skip_users = [])
+ update_issuable(issue, current_user, skip_users)
end
# When close an issue we should:
@@ -60,8 +60,8 @@ class TodoService
#
# * create a todo for each mentioned user on merge request
#
- def update_merge_request(merge_request, current_user)
- update_issuable(merge_request, current_user)
+ def update_merge_request(merge_request, current_user, skip_users = [])
+ update_issuable(merge_request, current_user, skip_users)
end
# When close a merge request we should:
@@ -123,7 +123,7 @@ class TodoService
mark_pending_todos_as_done(merge_request, merge_request.author)
mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
end
-
+
# When a merge request could not be automatically merged due to its unmergeable state we should:
#
# * create a todo for a merge_user
@@ -131,7 +131,7 @@ class TodoService
def merge_request_became_unmergeable(merge_request)
create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
end
-
+
# When create a note we should:
#
# * mark all pending todos related to the noteable for the note author as done
@@ -146,8 +146,8 @@ class TodoService
# * mark all pending todos related to the noteable for the current user as done
# * create a todo for each new user mentioned on note
#
- def update_note(note, current_user)
- handle_note(note, current_user)
+ def update_note(note, current_user, skip_users = [])
+ handle_note(note, current_user, skip_users)
end
# When an emoji is awarded we should:
@@ -204,7 +204,7 @@ class TodoService
# Only update those that are not really on that state
todos = todos.where.not(state: state)
todos_ids = todos.pluck(:id)
- todos.update_all(state: state)
+ todos.unscope(:order).update_all(state: state)
current_user.update_todos_count_cache
todos_ids
end
@@ -223,11 +223,11 @@ class TodoService
create_mention_todos(issuable.project, issuable, author)
end
- def update_issuable(issuable, author)
+ def update_issuable(issuable, author, skip_users = [])
# Skip toggling a task list item in a description
return if toggling_tasks?(issuable)
- create_mention_todos(issuable.project, issuable, author)
+ create_mention_todos(issuable.project, issuable, author, nil, skip_users)
end
def destroy_issuable(issuable, user)
@@ -239,7 +239,7 @@ class TodoService
issuable.tasks? && issuable.updated_tasks.any?
end
- def handle_note(note, author)
+ def handle_note(note, author, skip_users = [])
# Skip system notes, and notes on project snippet
return if note.system? || note.for_snippet?
@@ -247,7 +247,7 @@ class TodoService
target = note.noteable
mark_pending_todos_as_done(target, author)
- create_mention_todos(project, target, author, note)
+ create_mention_todos(project, target, author, note, skip_users)
end
def create_assignment_todo(issuable, author)
@@ -257,14 +257,14 @@ class TodoService
end
end
- def create_mention_todos(project, target, author, note = nil)
+ def create_mention_todos(project, target, author, note = nil, skip_users = [])
# Create Todos for directly addressed users
- directly_addressed_users = filter_directly_addressed_users(project, note || target, author)
+ directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users)
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)
+ mentioned_users = filter_mentioned_users(project, note || target, author, skip_users)
attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
create_todos(mentioned_users, attributes)
end
@@ -307,13 +307,13 @@ class TodoService
reject_users_without_access(users, project, target).uniq
end
- def filter_mentioned_users(project, target, author)
- mentioned_users = target.mentioned_users(author)
+ def filter_mentioned_users(project, target, author, skip_users = [])
+ mentioned_users = target.mentioned_users(author) - skip_users
filter_todo_users(mentioned_users, project, target)
end
- def filter_directly_addressed_users(project, target, author)
- directly_addressed_users = target.directly_addressed_users(author)
+ def filter_directly_addressed_users(project, target, author, skip_users = [])
+ directly_addressed_users = target.directly_addressed_users(author) - skip_users
filter_todo_users(directly_addressed_users, project, target)
end
diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb
new file mode 100644
index 00000000000..193fcd85896
--- /dev/null
+++ b/app/services/users/create_service.rb
@@ -0,0 +1,110 @@
+module Users
+ # Service for creating a new user.
+ class CreateService < BaseService
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params.dup
+ end
+
+ def build
+ raise Gitlab::Access::AccessDeniedError unless can_create_user?
+
+ user = User.new(build_user_params)
+
+ if current_user&.is_admin?
+ if params[:reset_password]
+ @reset_token = user.generate_reset_token
+ params[:force_random_password] = true
+ end
+
+ if params[:force_random_password]
+ random_password = Devise.friendly_token.first(Devise.password_length.min)
+ user.password = user.password_confirmation = random_password
+ end
+ end
+
+ identity_attrs = params.slice(:extern_uid, :provider)
+
+ if identity_attrs.any?
+ user.identities.build(identity_attrs)
+ end
+
+ user
+ end
+
+ def execute
+ user = build
+
+ if user.save
+ log_info("User \"#{user.name}\" (#{user.email}) was created")
+ notification_service.new_user(user, @reset_token) if @reset_token
+ system_hook_service.execute_hooks_for(user, :create)
+ end
+
+ user
+ end
+
+ private
+
+ def can_create_user?
+ (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.is_admin?
+ end
+
+ # Allowed params for creating a user (admins only)
+ def admin_create_params
+ [
+ :access_level,
+ :admin,
+ :avatar,
+ :bio,
+ :can_create_group,
+ :color_scheme_id,
+ :email,
+ :external,
+ :force_random_password,
+ :hide_no_password,
+ :hide_no_ssh_key,
+ :key_id,
+ :linkedin,
+ :name,
+ :password,
+ :password_expires_at,
+ :projects_limit,
+ :remember_me,
+ :skip_confirmation,
+ :skype,
+ :theme_id,
+ :twitter,
+ :username,
+ :website_url
+ ]
+ end
+
+ # Allowed params for user signup
+ def signup_params
+ [
+ :email,
+ :email_confirmation,
+ :name,
+ :password,
+ :username
+ ]
+ end
+
+ def build_user_params
+ if current_user&.is_admin?
+ user_params = params.slice(*admin_create_params)
+ user_params[:created_by_id] = current_user&.id
+
+ if params[:reset_password]
+ user_params.merge!(force_random_password: true, password_expires_at: nil)
+ end
+ else
+ user_params = params.slice(*signup_params)
+ user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email
+ end
+
+ user_params
+ end
+ end
+end
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 833da5bc5d1..a3b32a71a64 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -20,10 +20,10 @@ module Users
Groups::DestroyService.new(group, current_user).execute
end
- user.personal_projects.each do |project|
+ user.personal_projects.with_deleted.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
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end
move_issues_to_ghost_user(user)
diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb
new file mode 100644
index 00000000000..37a314adee6
--- /dev/null
+++ b/app/validators/importable_url_validator.rb
@@ -0,0 +1,11 @@
+# ImportableUrlValidator
+#
+# This validator blocks projects from using dangerous import_urls to help
+# protect against Server-side Request Forgery (SSRF).
+class ImportableUrlValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if Gitlab::UrlBlocker.blocked_url?(value)
+ record.errors.add(attribute, "imports are not allowed from that URL")
+ end
+ end
+end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 9175b3d3f96..e403a9da616 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -48,7 +48,7 @@
.form-actions
= f.submit 'Save', class: 'btn btn-save append-right-10'
- if @appearance.persisted?
- = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank'
+ = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- if @appearance.updated_at
%span.pull-right
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 00366b0a8c9..5d51a2b5cbc 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -404,7 +404,7 @@
Enable Sentry
.help-block
Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
- %a{ href: 'https://getsentry.com', target: '_blank' } https://getsentry.com
+ %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
.form-group
= f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
@@ -558,5 +558,19 @@
Maximum time for web terminal websocket connection (in seconds).
0 for unlimited.
+ %fieldset
+ %legend Real-time features
+ .form-group
+ = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :polling_interval_multiplier, class: 'form-control'
+ .help-block
+ Change this value to influence how frequently the GitLab UI polls for updates.
+ If you set the value to 2 all polling intervals are multiplied
+ by 2, which means that polling happens half as frequently.
+ The multiplier can also have a decimal value.
+ The default value (1) is a reasonable choice for the majority of GitLab
+ installations. Set to 0 to completely disable polling.
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e67ad663720..ebca9beb035 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -43,28 +43,34 @@
%h4
Features
%hr
- %p
- Sign up
+ - sign_up = "Sign up"
+ %p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") }
+ = sign_up
%span.light.pull-right
= boolean_to_icon signup_enabled?
- %p
- LDAP
+ - ldap = "LDAP"
+ %p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") }
+ = ldap
%span.light.pull-right
= boolean_to_icon Gitlab.config.ldap.enabled
- %p
- Gravatar
+ - gravatar = "Gravatar"
+ %p{ "aria-label" => "#{gravatar}: status " + (gravatar_enabled? ? "on" : "off") }
+ = gravatar
%span.light.pull-right
= boolean_to_icon gravatar_enabled?
- %p
- OmniAuth
+ - omniauth = "OmniAuth"
+ %p{ "aria-label" => "#{omniauth}: status " + (Gitlab.config.omniauth.enabled ? "on" : "off") }
+ = omniauth
%span.light.pull-right
= boolean_to_icon Gitlab.config.omniauth.enabled
- %p
- Reply by email
+ - reply_email = "Reply by email"
+ %p{ "aria-label" => "#{reply_email}: status " + (Gitlab::IncomingEmail.enabled? ? "on" : "off") }
+ = reply_email
%span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled?
- %p
- Container Registry
+ - container_reg = "Container Registry"
+ %p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") }
+ = container_reg
%span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index c1a9f8d6ddd..596f367a00d 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -1,15 +1,16 @@
.js-projects-list-holder
- if @projects.any?
- %ul.projects-list.content-list
+ %ul.projects-list.content-list.admin-projects
- @projects.each_with_index do |project|
- %li.project-row
+ %li.project-row{ class: ('no-description' if project.description.blank?) }
.controls
- - if project.archived
- %span.label.label-warning archived
- %span.badge
- = storage_counter(project.statistics.storage_size)
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
+ .stats
+ %span.badge
+ = storage_counter(project.statistics.storage_size)
+ - if project.archived
+ %span.label.label-warning archived
.title
= link_to [:admin, project.namespace.becomes(Namespace), project] do
.dash-project-avatar
@@ -20,7 +21,7 @@
- if project.namespace
= project.namespace.human_name
\/
- %span.project-name.filter-title
+ %span.project-name
= project.name
- if project.description.present?
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index 7855239dfe5..794aaec89bd 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -2,7 +2,7 @@
%legend Access
.form-group
= f.label :projects_limit, class: 'control-label'
- .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control'
+ .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control'
.form-group
= f.label :can_create_group, class: 'control-label'
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index a1ef34dc588..5aae410a63f 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -10,8 +10,8 @@
- if current_user
.award-menu-holder.js-award-holder
- %button.btn.award-control.js-add-award{ type: "button" }
+ %button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
+ 'aria-label': 'Add emoji',
+ data: { title: 'Add emoji', placement: "bottom" } }
= icon('smile-o', class: "award-control-icon award-control-icon-normal")
= icon('spinner spin', class: "award-control-icon award-control-icon-loading")
- %span.award-control-text
- Add
diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml
index 0530d21a7e2..128b418090f 100644
--- a/app/views/ci/status/_graph_badge.html.haml
+++ b/app/views/ci/status/_graph_badge.html.haml
@@ -6,7 +6,7 @@
- tooltip = "#{subject.name} - #{status.label}"
- if status.has_details?
- = link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip } do
+ = link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
%span{ class: klass }= custom_icon(status.icon)
.ci-status-text= subject.name
- else
@@ -15,6 +15,6 @@
.ci-status-text= subject.name
- if status.has_action?
- = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
+ = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
%i.ci-action-icon-wrapper{ class: "js-#{status.action_icon.dasherize}" }
= custom_icon(status.action_icon)
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 600ee63a5c0..4679b9549d1 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,7 +1,9 @@
= content_for :flash_message do
= render 'shared/project_limit'
-.top-area
- %ul.nav-links
+.top-area.scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.nav-links.scrolling-tabs
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects
diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml
index 60c84a26420..2129920afd2 100644
--- a/app/views/dashboard/milestones/show.html.haml
+++ b/app/views/dashboard/milestones/show.html.haml
@@ -1,5 +1,5 @@
- header_title "Milestones", dashboard_milestones_path
= render 'shared/milestones/top', milestone: @milestone
-= render 'shared/milestones/summary', milestone: @milestone
= render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true
+= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 51
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index eef794dbd51..596499230f9 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -4,7 +4,9 @@
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
-.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
+- unless show_user_callout?
+ = render 'shared/user_callout'
+
- if @projects.any? || params[:name]
= render 'dashboard/projects_head'
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index d31ced004a0..52d6ebd8a14 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -19,12 +19,13 @@
.nav-controls
- if @todos.any?(&:pending?)
- = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
- Mark all as done
- = icon('spinner spin')
- = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do
- Undo mark all as done
- = icon('spinner spin')
+ .append-right-default
+ = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
+ Mark all as done
+ = icon('spinner spin')
+ = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do
+ Undo mark all as done
+ = icon('spinner spin')
.todos-filters
.row-content-block.second-block
@@ -67,12 +68,11 @@
= link_to todos_filter_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created
-
.js-todos-all
- if @todos.any?
.js-todos-list-container
.js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
- .panel.panel-default.panel-small.panel-without-border
+ .panel.panel-default.panel-without-border.panel-without-margin
%ul.content-list.todos-list
= render @todos
= paginate @todos, theme: "gitlab"
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 5d359538efe..21c751a23f8 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -8,7 +8,7 @@
- if devise_mapping.rememberable?
.remember-me.checkbox
%label{ for: "user_remember_me" }
- = f.check_box :remember_me
+ = f.check_box :remember_me, class: 'remember-me-checkbox'
%span Remember me
.pull-right.forgot-password
= link_to "Forgot your password?", new_password_path(resource_name)
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 6f5d4bf2a2f..2d78c55211e 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -8,7 +8,7 @@
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header
.discussion-actions
- = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
+ %button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button" }
- if expanded
= icon("chevron-up")
- else
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index 43a52cf3002..158061579f6 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -9,7 +9,7 @@ xml.entry do
xml.author do
xml.name event.author_name
- xml.email event.author_email
+ xml.email event.author_public_email
end
xml.summary(type: "xhtml") do |summary|
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index f08c96df309..64b5a733b77 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -15,6 +15,6 @@
= link_to note.attachment.url, target: '_blank' do
= image_tag note.attachment.url, class: 'note-image-attach'
- else
- = link_to note.attachment.url, target: "_blank", class: 'note-file-attach' do
+ = link_to note.attachment.url, target: '_blank', class: 'note-file-attach' do
%i.fa.fa-paperclip
= note.attachment_identifier
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 56f463572bb..f630f1effdc 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -17,24 +17,3 @@
= link_to filter_projects_path(visibility_level: level) do
= visibility_level_icon(level)
= visibility_level_label(level)
-
-- if @tags.present?
- .dropdown
- %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
- = icon('tags')
- %span.light Tags:
- - if params[:tag].present?
- = params[:tag]
- - else
- Any
- = icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- %li
- = link_to filter_projects_path(tag: nil) do
- Any
-
- - @tags.each do |tag|
- %li{ class: active_when(tag.name == params[:tag]) || 'light' }
- = link_to filter_projects_path(tag: tag.name) do
- = icon('tag')
- = tag.name
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index e66a8e0a3b3..8e83b2002b2 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -4,5 +4,5 @@
= 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
+= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 2684f16c373..8e6da3fad90 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -118,6 +118,12 @@
.key m
%td
Go to merge requests
+ %tr
+ %td.shortcut
+ .key g
+ .key t
+ %td
+ Go to todos
%tbody
%tr
%th
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 31631887317..f93b6b63426 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -17,7 +17,7 @@
%br
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br
- Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
+ Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
- if current_application_settings.help_page_text.present?
%hr
= markdown_field(current_application_settings, :help_page_text)
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index e18bd47798b..e6058617ac9 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -33,7 +33,7 @@
- @already_added_projects.each do |project|
%tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
- = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank'
+ = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank', rel: 'noopener noreferrer'
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
@@ -50,7 +50,7 @@
- @repos.each do |repo|
%tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
%td
- = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank"
+ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer'
%td.import-target
%fieldset.row
.input-group
@@ -70,7 +70,7 @@
- @incompatible_repos.each do |repo|
%tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
%td
- = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank'
+ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer'
%td.import-target
%td.import-actions-job-status
= label_tag 'Incompatible Project', nil, class: 'label label-danger'
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index d5b88709a34..7456799ca0e 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -43,7 +43,7 @@
- @repos.each do |repo|
%tr{ id: "repo_#{repo["id"]}" }
%td
- = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
+ = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank", rel: 'noopener noreferrer'
%td.import-target
= import_project_target(repo['namespace']['path'], repo['name'])
%td.import-actions.job-status
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index 336becd229e..c5800a1cca0 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -13,7 +13,7 @@
%li
%p
Go to
- #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: "_blank"}.
+ #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: '_blank', rel: 'noopener noreferrer'}.
%li
%p
Make sure you're logged into the account that owns the projects you'd like to import.
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index 5e01af008be..60de6bfe816 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -36,7 +36,7 @@
- @already_added_projects.each do |project|
%tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
- = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank"
+ = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank", rel: 'noopener noreferrer'
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
@@ -53,7 +53,7 @@
- @repos.each do |repo|
%tr{ id: "repo_#{repo.id}" }
%td
- = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+ = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer'
%td.import-target
#{current_user.username}/#{repo.name}
%td.import-actions.job-status
@@ -63,7 +63,7 @@
- @incompatible_repos.each do |repo|
%tr{ id: "repo_#{repo.id}" }
%td
- = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+ = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer'
%td.import-target
%td.import-actions-job-status
= label_tag "Incompatible Project", nil, class: "label label-danger"
diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder
index fcd30c8c765..23a88448055 100644
--- a/app/views/issues/_issue.atom.builder
+++ b/app/views/issues/_issue.atom.builder
@@ -7,7 +7,7 @@ xml.entry do
xml.author do
xml.name issue.author_name
- xml.email issue.author_email
+ xml.email issue.author_public_email
end
xml.summary issue.title
@@ -26,7 +26,7 @@ xml.entry do
if issue.assignee
xml.assignee do
xml.name issue.assignee.name
- xml.email issue.assignee.email
+ xml.email issue.assignee_public_email
end
end
end
diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml
index 65887aacbaf..04e2d4b63e6 100644
--- a/app/views/koding/index.html.haml
+++ b/app/views/koding/index.html.haml
@@ -2,5 +2,5 @@
%p
= icon('circle', class: 'cgreen')
Integration is active for
- = link_to koding_project_url, target: '_blank' do
+ = link_to koding_project_url, target: '_blank', rel: 'noopener noreferrer' do
#{current_application_settings.koding_url}
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index a35a918d501..b7df11681d3 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -3,8 +3,9 @@
.layout-nav
.container-fluid
= render "layouts/nav/#{nav}"
- .content-wrapper{ class: "#{layout_nav_class}" }
+ - if content_for?(:sub_nav)
= yield :sub_nav
+ .content-wrapper{ class: layout_nav_class }
.alert-wrapper
= render "layouts/broadcast"
= render "layouts/flash"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 5fde5c2613e..23abf6897d4 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -11,9 +11,13 @@
= render 'layouts/nav/dashboard'
- else
= render 'layouts/nav/explore'
- %button.navbar-toggle{ type: 'button' }
- %span.sr-only Toggle navigation
- = icon('ellipsis-v')
+
+ .header-logo
+ = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
+ = brand_header_logo
+
+ .title-container
+ %h1.title{ class: ('initializing' if @has_group_title) }= title
.navbar-collapse.collapse
%ul.nav.navbar-nav
@@ -31,11 +35,6 @@
%li
= link_to admin_root_path, title: 'Admin Area', aria: { label: "Admin Area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw')
- %li
- = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('bell fw')
- %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
- = todos_count_format(todos_pending_count)
- if current_user.can_create_project?
%li
= link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
@@ -45,6 +44,21 @@
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
+ %li
+ = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('hashtag fw')
+ %span.badge.issues-count
+ = number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
+ %li
+ = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = custom_icon('mr_bold')
+ %span.badge.merge-requests-count
+ = number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
+ %li
+ = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('check-circle fw')
+ %span.badge.todos-count
+ = todos_count_format(todos_pending_count)
%li.header-user.dropdown
= link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
@@ -63,11 +77,9 @@
%div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
- .header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
- = brand_header_logo
-
- %h1.title{ class: ('initializing' if @has_group_title) }= title
+ %button.navbar-toggle{ type: 'button' }
+ %span.sr-only Toggle navigation
+ = icon('ellipsis-v')
= yield :header_content
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
deleted file mode 100644
index 060b50ffc69..00000000000
--- a/app/views/notify/build_fail_email.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header do
- %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
- GitLab (job failed)
-
-%h3
- Project:
- = link_to namespace_project_url(@project.namespace, @project) do
- = @project.name
-
-%p
- Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
-%p
- Author: #{@build.pipeline.git_author_name}
-%p
- Branch: #{@build.ref}
-%p
- Stage: #{@build.stage}
-%p
- Job: #{@build.name}
-%p
- Message: #{@build.pipeline.git_commit_message}
-
-%p
- Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb
deleted file mode 100644
index 2a94688a6b0..00000000000
--- a/app/views/notify/build_fail_email.text.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-Job failed for <%= @project.name %>
-
-Status: <%= @build.status %>
-Commit: <%= @build.pipeline.short_sha %>
-Author: <%= @build.pipeline.git_author_name %>
-Branch: <%= @build.ref %>
-Stage: <%= @build.stage %>
-Job: <%= @build.name %>
-Message: <%= @build.pipeline.git_commit_message %>
-
-Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
deleted file mode 100644
index ca0eaa96a9d..00000000000
--- a/app/views/notify/build_success_email.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header do
- %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
- GitLab (job successful)
-
-%h3
- Project:
- = link_to namespace_project_url(@project.namespace, @project) do
- = @project.name
-
-%p
- Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
-%p
- Author: #{@build.pipeline.git_author_name}
-%p
- Branch: #{@build.ref}
-%p
- Stage: #{@build.stage}
-%p
- Job: #{@build.name}
-%p
- Message: #{@build.pipeline.git_commit_message}
-
-%p
- Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb
deleted file mode 100644
index 445cd46e64f..00000000000
--- a/app/views/notify/build_success_email.text.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-Job successful for <%= @project.name %>
-
-Status: <%= @build.status %>
-Commit: <%= @build.pipeline.short_sha %>
-Author: <%= @build.pipeline.git_author_name %>
-Branch: <%= @build.ref %>
-Stage: <%= @build.stage %>
-Job: <%= @build.name %>
-Message: <%= @build.pipeline.git_commit_message %>
-
-Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml
index b28fea35ad5..76440926a2b 100644
--- a/app/views/notify/project_was_exported_email.html.haml
+++ b/app/views/notify/project_was_exported_email.html.haml
@@ -2,7 +2,7 @@
Project #{@project.name} was exported successfully.
%p
The project export can be downloaded from:
- = link_to download_export_namespace_project_url(@project.namespace, @project) do
+ = link_to download_export_namespace_project_url(@project.namespace, @project), rel: 'nofollow', download: '', do
= @project.name_with_namespace + " export"
%p
The download link will expire in 24 hours.
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 8a994f6d600..5ce2220c907 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -75,12 +75,12 @@
.provider-btn-image
= provider_image_tag(provider)
- if auth_active?(provider)
- - if provider.to_s == 'saml'
- %a.provider-btn
- Active
- - else
+ - if unlink_allowed?(provider)
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
Disconnect
+ - else
+ %a.provider-btn
+ Active
- else
= link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do
Connect
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/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index df0a0212f3d..99690e6b98a 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -6,7 +6,9 @@
%h4.prepend-top-0
Syntax highlighting theme
%p
- This setting allow you to customize the appearance of the syntax.
+ This setting allows you to customize the appearance of the syntax.
+ = succeed '.' do
+ = link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank'
.col-lg-9.syntax-theme
- Gitlab::ColorSchemes.each do |scheme|
= label_tag do
@@ -20,6 +22,8 @@
Behavior
%p
This setting allows you to customize the behavior of the system layout and default views.
+ = succeed '.' do
+ = link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank'
.col-lg-9
.form-group
= f.label :layout, class: 'label-light' do
@@ -29,13 +33,11 @@
Choose between fixed (max. 1200px) and fluid (100%) application layout.
.form-group
= f.label :dashboard, class: 'label-light' do
- Default Dashboard
- = link_to('(?)', help_page_path('profile/preferences') + '#default-dashboard', target: '_blank')
+ Default dashboard
= f.select :dashboard, dashboard_choices, {}, class: 'form-control'
.form-group
= f.label :project_view, class: 'label-light' do
Project view
- = link_to('(?)', help_page_path('profile/preferences') + '#default-project-view', target: '_blank')
= f.select :project_view, project_view_choices, {}, class: 'form-control'
.help-block
Choose what content you want to see on a project's home page.
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index d551754a2e5..c74b3249a13 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -18,7 +18,7 @@
or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
.col-lg-9
.clearfix.avatar-image.append-bottom-default
- = link_to avatar_icon(@user, 400), target: '_blank' do
+ = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
%h5.prepend-top-0
Upload new avatar
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 79a0dc1b959..0fd19780570 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,6 +1,6 @@
- empty_repo = @project.empty_repo?
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
- %div{ class: container_class }
+ .limit-container-width{ class: container_class }
.avatar-container.s70.project-avatar
= project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile')
%h1.project-title
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index edf55d59f28..de8c173f26f 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -3,7 +3,7 @@
.top-block.row-content-block.clearfix
.pull-right
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
- class: 'btn btn-default download' do
+ rel: 'nofollow', download: '', class: 'btn btn-default download' do
= icon('download')
Download artifacts archive
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index f864702d862..ea3cecb86a9 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -9,7 +9,7 @@
- else
.nothing-here-block
The SVG could not be displayed as it is too large, you can
- #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')}
+ #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer')}
instead.
- else
%img{ src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path)), alt: "#{blob.name}" }
diff --git a/app/views/projects/blob/_notebook.html.haml b/app/views/projects/blob/_notebook.html.haml
new file mode 100644
index 00000000000..ab1cf933944
--- /dev/null
+++ b/app/views/projects/blob/_notebook.html.haml
@@ -0,0 +1,5 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('notebook_viewer')
+
+.file-content#js-notebook-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml
index b1e1be49de9..7b16d266982 100644
--- a/app/views/projects/blob/_text.html.haml
+++ b/app/views/projects/blob/_text.html.haml
@@ -3,7 +3,7 @@
.nothing-here-block
File too large, you can
= succeed '.' do
- = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank'
+ = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer'
- else
- blob.load_all_data!(@repository)
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 4924c73cf8e..e14885f264b 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -5,7 +5,7 @@
%a.close{ href: "#", "data-dismiss" => "modal" } ร—
%h3.page-title= title
.modal-body
- = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do
+ = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal', data: { method: method } do
.dropzone
.dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light
@@ -24,8 +24,5 @@
.inline.prepend-left-10
= commit_in_fork_help
-
-:javascript
- gl.utils.disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file');
- new BlobFileDropzone($('.js-upload-blob-form'), '#{method}');
- new NewCommitForm($('.js-upload-blob-form'))
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('blob')
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 8853801016b..afe0b5dba45 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -2,14 +2,14 @@
- page_title "Edit", @blob.path, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_bundle_tag('blob_edit')
+ = page_specific_javascript_bundle_tag('blob')
= render "projects/commits/head"
%div{ class: container_class }
- if @conflict
.alert.alert-danger
Someone edited the file the same time you did. Please check out
- = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank"
+ = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs.
.file-editor
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index e0ce8cc9601..4c449e040ee 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,7 +1,7 @@
- page_title "New File", @path.presence, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_bundle_tag('blob_edit')
+ = page_specific_javascript_bundle_tag('blob')
%h3.page-title
New File
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index 0bca6a786cb..5a4eaf92b16 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -7,12 +7,12 @@
data: { container: "body", placement: "bottom" } }
{{ list.title }}
.board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
- %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" && !disabled }' }
+ %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_issue, @project)
%button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
"@click" => "showNewIssueForm",
- "v-if" => 'list.type !== "done"',
+ "v-if" => 'list.type !== "closed"',
"aria-label" => "Add an issue",
"title" => "Add an issue",
data: { placement: "top", container: "body" } }
diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml
index 4a4dd84d5d2..4a0b2110601 100644
--- a/app/views/projects/boards/components/_board_list.html.haml
+++ b/app/views/projects/boards/components/_board_list.html.haml
@@ -3,7 +3,7 @@
= icon("spinner spin")
- if can? current_user, :create_issue, @project
%board-new-issue{ ":list" => "list",
- "v-if" => 'list.type !== "done" && showIssueForm' }
+ "v-if" => 'list.type !== "closed" && showIssueForm' }
%ul.board-list{ "ref" => "list",
"v-show" => "!loading",
":data-board" => "list.id",
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 78720d88e4e..6f45d5b0689 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.js-right-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } }
+%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "153", "spy" => "affix" } }
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
Job
%strong ##{@build.id}
@@ -33,7 +33,7 @@
= link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep
- = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do
Download
- if @build.artifacts_metadata?
@@ -137,3 +137,6 @@
= build.id
- if build.retried?
%i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
+
+:javascript
+ new Sidebar();
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 307010edb58..d5fe771613c 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,6 +1,6 @@
- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Jobs"
-= render "projects/pipelines/head", build_subnav: true
+= render "projects/pipelines/head"
%div{ class: container_class }
.build-page
diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml
index 5d9a776da89..a5a9e4d0621 100644
--- a/app/views/projects/buttons/_koding.html.haml
+++ b/app/views/projects/buttons/_koding.html.haml
@@ -1,3 +1,3 @@
- if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch)
- = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank' do
+ = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank', rel: 'noopener noreferrer' do
Run in IDE (Koding)
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 09286a1b3c6..aeed293a724 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -94,7 +94,7 @@
%td
.pull-right
- if can?(current_user, :read_build, build) && build.artifacts?
- = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do
+ = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do
= icon('download')
- if can?(current_user, :update_build, build)
- if build.active?
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index da5a676274f..09e3a775d1c 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -1,6 +1,7 @@
- disable_initialization = local_assigns.fetch(:disable_initialization, false)
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint,
+ "help-page-path" => help_page_path('ci/quick_start/README'),
} }
- content_for :page_specific_javascripts do
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 6ab9a80e083..4b1ff75541a 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -24,7 +24,7 @@
.visible-xs-inline
= render_commit_status(commit, ref: ref)
- if commit.description?
- %a.text-expander.hidden-xs.js-toggle-button ...
+ %button.text-expander.hidden-xs.js-toggle-button{ type: "button" } ...
- if commit.description?
%pre.commit-row-description.js-toggle-content
diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml
index c8f0b547f80..9007f2c24ba 100644
--- a/app/views/projects/cycle_analytics/_overview.html.haml
+++ b/app/views/projects/cycle_analytics/_overview.html.haml
@@ -9,7 +9,7 @@
Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
To set up CA, you must first define a production environment by setting up your CI and then deploy to production.
%p
- %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more
+ %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: '_blank' } Read more
.col-md-6.overview-image
%span.overview-icon
= custom_icon ('icon_cycle_analytics_overview')
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index 8e24e28765f..fd4f3c8d3cc 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -1,7 +1,7 @@
.js-toggle-container
.commit-stat-summary
Showing
- = link_to '#', class: 'js-toggle-button' do
+ %button.diff-stats-summary-toggler.js-toggle-button{ type: "button" }
%strong= pluralize(diff_files.size, "changed file")
with
%strong.cgreen #{diff_files.sum(&:added_lines)} additions
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 2802a4eca7b..b78de092a60 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -31,7 +31,7 @@
= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
.form-group
= f.label :tag_list, "Tags", class: 'label-light'
- = f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control"
+ = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control"
%p.help-block Separate tags with commas.
%hr
%fieldset
@@ -163,7 +163,7 @@
- if @project.export_project_path
= link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project),
- method: :get, class: "btn btn-default"
+ rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
= link_to 'Generate new export', generate_new_export_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-default"
- else
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
index 4c8fe1c271b..bf0f1819073 100644
--- a/app/views/projects/environments/_external_url.html.haml
+++ b/app/views/projects/environments/_external_url.html.haml
@@ -1,3 +1,3 @@
- if environment.external_url && can?(current_user, :read_environment, environment)
- = link_to environment.external_url, target: '_blank', class: 'btn external-url' do
+ = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do
= icon('external-link')
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index f8e94ca98ae..3b45162df52 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -1,5 +1,8 @@
- @no_container = true
- page_title "Metrics for environment", @environment.name
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_d3')
+ = page_specific_javascript_bundle_tag('monitoring')
= render "projects/pipelines/head"
%div{ class: container_class }
@@ -15,7 +18,11 @@
= render 'projects/deployments/actions', deployment: @environment.last_deployment
.row
.col-sm-12
+ %h4
+ CPU utilization
%svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
.row
.col-sm-12
+ %h4
+ Memory usage
%svg.prometheus-graph{ 'graph-type' => 'memory_values' }
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index ef0dd0eda3c..c8363087d6a 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -16,7 +16,7 @@
.col-sm-6
.nav-controls
- = link_to @environment.external_url, class: 'btn btn-default' do
+ = link_to @environment.external_url, class: 'btn btn-default', target: '_blank', rel: 'noopener noreferrer nofollow' do
= icon('external-link')
= render 'projects/deployments/actions', deployment: @environment.last_deployment
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 7b7d7b1e00e..f3a429d12d9 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -19,15 +19,14 @@
.nav-controls
= link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- - if can? current_user, :create_issue, @project
- = link_to new_namespace_project_issue_path(@project.namespace,
- @project,
- issue: { assignee_id: issues_finder.assignee.try(:id),
- milestone_id: issues_finder.milestones.first.try(:id) }),
- class: "btn btn-new",
- title: "New Issue",
- id: "new_issue_link" do
- New Issue
+ = link_to new_namespace_project_issue_path(@project.namespace,
+ @project,
+ issue: { assignee_id: issues_finder.assignee.try(:id),
+ milestone_id: issues_finder.milestones.first.try(:id) }),
+ class: "btn btn-new",
+ title: "New Issue",
+ id: "new_issue_link" do
+ New Issue
= render 'shared/issuable/search_bar', type: :issues
.issues-holder
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index d39f36e94c7..6ac05bf3afe 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -20,37 +20,34 @@
= confidential_icon(@issue)
= issuable_meta(@issue, @project, "Issue")
- - if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue)
- .issuable-actions
- .clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
- Options
- = icon('caret-down')
- .dropdown-menu.dropdown-menu-align-right.hidden-lg
- %ul
- - if can?(current_user, :create_issue, @project)
- %li
- = 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 }, 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 }, 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_by?(current_user)
- %li
- = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
-
- - if can?(current_user, :create_issue, @project)
- = 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 }, 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'
+ .issuable-actions
+ .clearfix.issue-btn-group.dropdown
+ %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
+ Options
+ = icon('caret-down')
+ .dropdown-menu.dropdown-menu-align-right.hidden-lg
+ %ul
+ %li
+ = 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 }, 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 }, 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_by?(current_user)
- = 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'
+ %li
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
+
+ = 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 }, 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_by?(current_user)
+ = 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'
.issue-details.issuable-details
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index ad14b4e583e..8d134aaac67 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -21,7 +21,7 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle local_assigns.fetch(f.object.source_branch, "Select source branch"), { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
+ = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-branch
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index c8f097c69da..881ee9fd596 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -16,7 +16,7 @@
.pull-right
- if @merge_request.source_branch_exists?
- if koding_enabled? && @repository.koding_yml
- = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do
+ = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank', rel: 'noopener noreferrer' do
Run in IDE (Koding)
= link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
@@ -52,8 +52,10 @@
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
- .merge-request-tabs-container
- %ul.merge-request-tabs.nav-links.no-top.no-bottom
+ .merge-request-tabs-container.scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.merge-request-tabs.nav-links.scrolling-tabs
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
Discussion
diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml
index f0a23bec5e7..e632fc681cf 100644
--- a/app/views/projects/merge_requests/merge.js.haml
+++ b/app/views/projects/merge_requests/merge.js.haml
@@ -1,7 +1,8 @@
- case @status
- when :success
+ - remove_source_branch = params[:should_remove_source_branch] == '1' || @merge_request.remove_source_branch?
:plain
- merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'});
+ merge_request_widget.mergeInProgress(#{remove_source_branch});
- when :merge_when_pipeline_succeeds
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}");
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index 93ed4b68e0e..cde0ce08e14 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -49,7 +49,7 @@
%strong Tip:
= succeed '.' do
You can also checkout merge requests locally by
- = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank'
+ = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
:javascript
$(function(){
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index c94c7944c0b..e5ec151a61d 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -37,7 +37,7 @@
= check_box_tag :should_remove_source_branch
Remove source branch
.accept-control
- = link_to "#", class: "modify-merge-commit-link js-toggle-button" do
+ %button.modify-merge-commit-link.js-toggle-button{ type: "button" }
= icon('edit')
Modify commit message
.js-toggle-content.hide.prepend-top-default
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 918f5d161bb..b6340a00b29 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -7,6 +7,7 @@
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
.nav-controls
+ = render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project)
= link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do
New Milestone
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index d16f49bd33a..f612b5c7d6b 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -36,6 +36,9 @@
= link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
Delete
+ %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+
.detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) }
%h2.title
= markdown_field(@milestone, :title)
@@ -53,5 +56,5 @@
.alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close this milestone now.
- = render 'shared/milestones/summary', milestone: @milestone, project: @project
= render 'shared/milestones/tabs', milestone: @milestone
+ = render 'shared/milestones/sidebar', milestone: @milestone, project: @project, affix_offset: 153
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index d129da943f8..09ac1fd6794 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -23,7 +23,7 @@
- if current_user.can_select_namespace?
.input-group-addon
= root_url
- = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1}
+ = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1}
- else
.input-group-addon.static-namespace
@@ -76,7 +76,7 @@
Gitea
%div
- if git_import_enabled?
- = link_to "#", class: 'btn js-toggle-button import_git' do
+ %button.btn.js-toggle-button.import_git{ type: "button" }
= icon('git', text: 'Repo by URL')
.import_gitlab_project
- if gitlab_project_import_enabled?
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 5552086bc50..6c0e6d48d6c 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -37,7 +37,7 @@
":can-resolve" => can_resolve,
":author-name" => "'#{j(note.author.name)}'",
"author-avatar" => note.author.avatar_url,
- ":note-truncated" => "'#{truncate(note.note, length: 17)}'",
+ ":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index a5acb7ac4a5..bc57f7f1c46 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -1,7 +1,7 @@
= content_for :sub_nav do
.scrolling-tabs-container.sub-nav-scroll
= render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs{ class: ('build' if local_assigns.fetch(:build_subnav, false)) }
+ .nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) }
- if project_nav_tab? :pipelines
= nav_link(path: 'pipelines#index', controller: :pipelines) do
@@ -10,13 +10,13 @@
Pipelines
- if project_nav_tab? :builds
- = nav_link(path: 'builds#index', controller: :builds) do
+ = nav_link(controller: :builds) do
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
Jobs
- if project_nav_tab? :environments
- = nav_link(path: 'environments#index', controller: :environments) do
+ = nav_link(controller: :environments) do
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span
Environments
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 0605af4fcd3..4be9a1371ec 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,10 +1,12 @@
.page-content-header
.header-main-content
= render 'ci/status/badge', status: @pipeline.detailed_status(current_user)
- %strong Pipeline ##{@commit.pipelines.last.id}
- triggered #{time_ago_with_tooltip(@commit.authored_date)} by
- = author_avatar(@commit, size: 24)
- = commit_author_link(@commit)
+ %strong Pipeline ##{@pipeline.id}
+ triggered #{time_ago_with_tooltip(@pipeline.created_at)}
+ - if @pipeline.user
+ by
+ = user_avatar(user: @pipeline.user, size: 24)
+ = user_link(@pipeline.user)
.header-action-buttons
- if can?(current_user, :update_pipeline, @pipeline.project)
- if @pipeline.retryable?
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 5d59ce06612..3d73284699f 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -2,53 +2,19 @@
- page_title "Pipelines"
= render "projects/pipelines/head"
-%div{ class: container_class }
- .top-area
- %ul.nav-links
- %li.js-pipelines-tab-all{ class: active_when(@scope.nil?) }>
- = link_to project_pipelines_path(@project) do
- All
- %span.badge.js-totalbuilds-count
- = number_with_delimiter(@pipelines_count)
-
- %li.js-pipelines-tab-pending{ class: active_when(@scope == 'pending') }>
- = link_to project_pipelines_path(@project, scope: :pending) do
- Pending
- %span.badge
- = number_with_delimiter(@pending_count)
-
- %li.js-pipelines-tab-running{ class: active_when(@scope == 'running') }>
- = link_to project_pipelines_path(@project, scope: :running) do
- Running
- %span.badge.js-running-count
- = number_with_delimiter(@running_count)
-
- %li.js-pipelines-tab-finished{ class: active_when(@scope == 'finished') }>
- = link_to project_pipelines_path(@project, scope: :finished) do
- Finished
- %span.badge
- = number_with_delimiter(@finished_count)
-
- %li.js-pipelines-tab-branches{ class: active_when(@scope == 'branches') }>
- = link_to project_pipelines_path(@project, scope: :branches) do
- Branches
-
- %li.js-pipelines-tab-tags{ class: active_when(@scope == 'tags') }>
- = link_to project_pipelines_path(@project, scope: :tags) do
- Tags
-
- .nav-controls
- - if can? current_user, :create_pipeline, @project
- = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do
- Run pipeline
-
- - unless @repository.gitlab_ci_yml
- = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
-
- = 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) } }
- .vue-pipelines-index
+#pipelines-list-vue{ data: { endpoint: namespace_project_pipelines_path(@project.namespace, @project, format: :json),
+ "css-class" => container_class,
+ "help-page-path" => help_page_path('ci/quick_start/README'),
+ "new-pipeline-path" => new_namespace_project_pipeline_path(@project.namespace, @project),
+ "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
+ "all-path" => project_pipelines_path(@project),
+ "pending-path" => project_pipelines_path(@project, scope: :pending),
+ "running-path" => project_pipelines_path(@project, scope: :running),
+ "finished-path" => project_pipelines_path(@project, scope: :finished),
+ "branches-path" => project_pipelines_path(@project, scope: :branches),
+ "tags-path" => project_pipelines_path(@project, scope: :tags),
+ "has-ci" => @repository.gitlab_ci_yml,
+ "ci-lint-path" => ci_lint_path } }
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('vue_pipelines')
diff --git a/app/views/projects/protected_branches/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml
index a9e27df5a87..5af0cc7a2f3 100644
--- a/app/views/projects/protected_branches/_dropdown.html.haml
+++ b/app/views/projects/protected_branches/_dropdown.html.haml
@@ -10,6 +10,6 @@
%ul.dropdown-footer-list
%li
- = link_to '#', title: "New Protected Branch", class: "create-new-protected-branch" do
+ %button{ class: "create-new-protected-branch-button js-create-new-protected-branch", title: "New Protected Branch" }
Create wildcard
%code
diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml
index 964133504e6..86d5a0ec7b8 100644
--- a/app/views/projects/services/_index.html.haml
+++ b/app/views/projects/services/_index.html.haml
@@ -18,7 +18,7 @@
%th Last edit
- @services.sort_by(&:title).each do |service|
%tr
- %td
+ %td{ "aria-label" => "#{service.title}: status " + (service.activated? ? "on" : "off") }
= boolean_to_icon service.activated?
%td
= link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do
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 3a323d94cc2..2fb88297fb3 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
@@ -4,13 +4,13 @@
%ul.list-unstyled.indent-list
%li
1.
- = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noopener noreferrer nofollow' do
Enable custom slash commands
= icon('external-link')
on your Mattermost installation
%li
2.
- = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noopener noreferrer nofollow' do
Add a slash command
= icon('external-link')
in your Mattermost team with these options:
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 a04fd5035a6..2a1b9d4c465 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -4,7 +4,7 @@
%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
+ = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
View documentation
= icon('external-link')
%p.inline
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 0d973a20d4c..078b7be6865 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -5,7 +5,7 @@
%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
+ = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
View documentation
= icon('external-link')
%p.inline
@@ -57,7 +57,7 @@
= label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
= image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
- = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank')
+ = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
.form-group
= label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index de1229d58aa..edfe6da1816 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -13,7 +13,7 @@
= render "home_panel"
- if current_user && can?(current_user, :download_code, @project)
- %nav.project-stats{ class: container_class }
+ %nav.project-stats.limit-container-width{ class: container_class }
%ul.nav
%li
= link_to project_files_path(@project) do
@@ -74,11 +74,11 @@
Set up auto deploy
- if @repository.commit
- %div{ class: container_class }
+ .limit-container-width{ class: container_class }
.project-last-commit
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
-%div{ class: container_class }
+.limit-container-width{ class: container_class }
- if @project.archived?
.text-warning.center.prepend-top-20
%p
diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml
index 9c5eb501174..671a3ef481c 100644
--- a/app/views/projects/stage/_in_stage_group.html.haml
+++ b/app/views/projects/stage/_in_stage_group.html.haml
@@ -1,5 +1,5 @@
- group_status = CommitStatus.where(id: subject).status
-%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } }
+%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}", container: 'body' } }
%span{ class: "ci-status-icon ci-status-icon-#{group_status}" }
= ci_icon_for_status(group_status)
%span.ci-status-text
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 8c582f747b3..713b758727e 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.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
+%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "102", "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')
@@ -19,3 +19,6 @@
More Pages
= render 'projects/wikis/new'
+
+:javascript
+ new Sidebar();
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 5afb95ac430..059a0d1ac78 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,71 +1,74 @@
-%ul.nav-links.search-filter
- - if @project
- %li{ class: active_when(@scope == 'blobs') }
- = link_to search_filter_path(scope: 'blobs') do
- Code
- %span.badge
- = @search_results.blobs_count
- %li{ class: active_when(@scope == 'issues') }
- = link_to search_filter_path(scope: 'issues') do
- Issues
- %span.badge
- = @search_results.issues_count
- %li{ class: active_when(@scope == 'merge_requests') }
- = link_to search_filter_path(scope: 'merge_requests') do
- Merge requests
- %span.badge
- = @search_results.merge_requests_count
- %li{ class: active_when(@scope == 'milestones') }
- = link_to search_filter_path(scope: 'milestones') do
- Milestones
- %span.badge
- = @search_results.milestones_count
- %li{ class: active_when(@scope == 'notes') }
- = link_to search_filter_path(scope: 'notes') do
- Comments
- %span.badge
- = @search_results.notes_count
- %li{ class: active_when(@scope == 'wiki_blobs') }
- = link_to search_filter_path(scope: 'wiki_blobs') do
- Wiki
- %span.badge
- = @search_results.wiki_blobs_count
- %li{ class: active_when(@scope == 'commits') }
- = link_to search_filter_path(scope: 'commits') do
- Commits
- %span.badge
- = @search_results.commits_count
+.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.nav-links.search-filter.scrolling-tabs
+ - if @project
+ %li{ class: active_when(@scope == 'blobs') }
+ = link_to search_filter_path(scope: 'blobs') do
+ Code
+ %span.badge
+ = @search_results.blobs_count
+ %li{ class: active_when(@scope == 'issues') }
+ = link_to search_filter_path(scope: 'issues') do
+ Issues
+ %span.badge
+ = @search_results.issues_count
+ %li{ class: active_when(@scope == 'merge_requests') }
+ = link_to search_filter_path(scope: 'merge_requests') do
+ Merge requests
+ %span.badge
+ = @search_results.merge_requests_count
+ %li{ class: active_when(@scope == 'milestones') }
+ = link_to search_filter_path(scope: 'milestones') do
+ Milestones
+ %span.badge
+ = @search_results.milestones_count
+ %li{ class: active_when(@scope == 'notes') }
+ = link_to search_filter_path(scope: 'notes') do
+ Comments
+ %span.badge
+ = @search_results.notes_count
+ %li{ class: active_when(@scope == 'wiki_blobs') }
+ = link_to search_filter_path(scope: 'wiki_blobs') do
+ Wiki
+ %span.badge
+ = @search_results.wiki_blobs_count
+ %li{ class: active_when(@scope == 'commits') }
+ = link_to search_filter_path(scope: 'commits') do
+ Commits
+ %span.badge
+ = @search_results.commits_count
- - elsif @show_snippets
- %li{ class: active_when(@scope == 'snippet_blobs') }
- = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
- Snippet Contents
- %span.badge
- = @search_results.snippet_blobs_count
- %li{ class: active_when(@scope == 'snippet_titles') }
- = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
- Titles and Filenames
- %span.badge
- = @search_results.snippet_titles_count
+ - elsif @show_snippets
+ %li{ class: active_when(@scope == 'snippet_blobs') }
+ = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
+ Snippet Contents
+ %span.badge
+ = @search_results.snippet_blobs_count
+ %li{ class: active_when(@scope == 'snippet_titles') }
+ = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
+ Titles and Filenames
+ %span.badge
+ = @search_results.snippet_titles_count
- - else
- %li{ class: active_when(@scope == 'projects') }
- = link_to search_filter_path(scope: 'projects') do
- Projects
- %span.badge
- = @search_results.projects_count
- %li{ class: active_when(@scope == 'issues') }
- = link_to search_filter_path(scope: 'issues') do
- Issues
- %span.badge
- = @search_results.issues_count
- %li{ class: active_when(@scope == 'merge_requests') }
- = link_to search_filter_path(scope: 'merge_requests') do
- Merge requests
- %span.badge
- = @search_results.merge_requests_count
- %li{ class: active_when(@scope == 'milestones') }
- = link_to search_filter_path(scope: 'milestones') do
- Milestones
- %span.badge
- = @search_results.milestones_count
+ - else
+ %li{ class: active_when(@scope == 'projects') }
+ = link_to search_filter_path(scope: 'projects') do
+ Projects
+ %span.badge
+ = @search_results.projects_count
+ %li{ class: active_when(@scope == 'issues') }
+ = link_to search_filter_path(scope: 'issues') do
+ Issues
+ %span.badge
+ = @search_results.issues_count
+ %li{ class: active_when(@scope == 'merge_requests') }
+ = link_to search_filter_path(scope: 'merge_requests') do
+ Merge requests
+ %span.badge
+ = @search_results.merge_requests_count
+ %li{ class: active_when(@scope == 'milestones') }
+ = link_to search_filter_path(scope: 'milestones') do
+ Milestones
+ %span.badge
+ = @search_results.milestones_count
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index c2d9ac87b20..8869d510aef 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -1,4 +1,6 @@
-- parent = Group.find_by(id: params[:parent_id] || @group.parent_id)
+- parent = GroupFinder.new(current_user).execute(id: params[:parent_id] || @group.parent_id)
+- group_path = root_url
+- group_path << parent.full_path + '/' if parent
- if @group.persisted?
.form-group
= f.label :name, class: 'control-label' do
@@ -11,7 +13,7 @@
Group path
.col-sm-10
.input-group.gl-field-error-anchor
- .input-group-addon
+ .group-root-path.input-group-addon.has-tooltip{ title: group_path, :'data-placement' => 'bottom' }
%span>= root_url
- if parent
%strong= parent.full_path + '/'
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 57a0eaa919e..db2ac1e1d12 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -4,10 +4,10 @@
Open
%span.badge= counts[:opened]
%li{ class: milestone_class_for_state(params[:state], 'closed') }>
- = link_to milestones_filter_path(state: 'closed') do
+ = link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc') do
Closed
%span.badge= counts[:closed]
%li{ class: milestone_class_for_state(params[:state], 'all') }>
- = link_to milestones_filter_path(state: 'all') do
+ = link_to milestones_filter_path(state: 'all', sort: 'due_date_desc') do
All
%span.badge= counts[:all]
diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml
new file mode 100644
index 00000000000..9b2f2fdcc93
--- /dev/null
+++ b/app/views/shared/_milestones_sort_dropdown.html.haml
@@ -0,0 +1,22 @@
+.dropdown.inline.prepend-left-10
+ %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %span.light
+ - if @sort.present?
+ = milestone_sort_options_hash[@sort]
+ - else
+ = sort_title_due_date_soon
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+ %li
+ = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do
+ = sort_title_due_date_soon
+ = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do
+ = sort_title_due_date_later
+ = link_to page_filter_path(sort: sort_value_start_date_soon, label: true) do
+ = sort_title_start_date_soon
+ = link_to page_filter_path(sort: sort_value_start_date_later, label: true) do
+ = sort_title_start_date_later
+ = link_to page_filter_path(sort: sort_value_name, label: true) do
+ = sort_title_name_asc
+ = link_to page_filter_path(sort: sort_value_name_desc, label: true) do
+ = sort_title_name_desc
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 367aa550a78..a212c714826 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,6 +1,5 @@
.dropdown.inline.prepend-left-10
%button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } }
- %span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
diff --git a/app/views/shared/_user_callout.html.haml b/app/views/shared/_user_callout.html.haml
new file mode 100644
index 00000000000..8f1293adcb1
--- /dev/null
+++ b/app/views/shared/_user_callout.html.haml
@@ -0,0 +1,14 @@
+.user-callout
+ .bordered-box.landing.content-block
+ %button.btn.btn-default.close.js-close-callout{ type: 'button',
+ 'aria-label' => 'Dismiss customize experience box' }
+ = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
+ .row
+ .col-sm-3.col-xs-12.svg-container
+ = custom_icon('icon_customization')
+ .col-sm-8.col-xs-12.inner-content
+ %h4
+ Customize your experience
+ %p
+ Change syntax themes, default project pages, and more in preferences.
+ = link_to 'Check it out', profile_preferences_path, class: 'btn btn-default js-close-callout'
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index e2033654018..7a7e3d46796 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -16,7 +16,6 @@
Also, issues are searchable and filterable.
- if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
- - else
- = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
- else
- %h4.text-center There are no issues to show.
+ %h4 There are no issues to show.
+ = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
diff --git a/app/views/shared/empty_states/icons/_pipelines_empty.svg b/app/views/shared/empty_states/icons/_pipelines_empty.svg
new file mode 100644
index 00000000000..8119d5bebe0
--- /dev/null
+++ b/app/views/shared/empty_states/icons/_pipelines_empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 150"><g fill="none" fill-rule="evenodd" transform="translate(0-3)"><g transform="translate(0 105)"><g fill="#e5e5e5"><rect width="78" height="4" x="34" y="21" opacity=".5" rx="2"/><path d="m152 23c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2"/></g><g transform="translate(0 4)"><path fill="#98d7b2" fill-rule="nonzero" d="m19 38c-10.493 0-19-8.507-19-19 0-10.493 8.507-19 19-19 10.493 0 19 8.507 19 19 0 10.493-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path fill="#31af64" d="m17.07 21.02l-2.829-2.829c-.786-.786-2.047-.781-2.828 0-.786.786-.781 2.047 0 2.828l4.243 4.243c.392.392.902.587 1.412.588.512.002 1.021-.193 1.41-.582l7.79-7.79c.777-.777.775-2.042-.006-2.823-.786-.786-2.045-.784-2.823-.006l-6.37 6.37"/></g><g fill="#e52c5a" transform="translate(102)"><path fill-rule="nonzero" d="m24 47.5c-12.979 0-23.5-10.521-23.5-23.5 0-12.979 10.521-23.5 23.5-23.5 12.979 0 23.5 10.521 23.5 23.5 0 12.979-10.521 23.5-23.5 23.5m0-5c10.217 0 18.5-8.283 18.5-18.5 0-10.217-8.283-18.5-18.5-18.5-10.217 0-18.5 8.283-18.5 18.5 0 10.217 8.283 18.5 18.5 18.5"/><path d="m28.24 24l2.833-2.833c1.167-1.167 1.167-3.067-.004-4.239-1.169-1.169-3.069-1.173-4.239-.004l-2.833 2.833-2.833-2.833c-1.167-1.167-3.067-1.167-4.239.004-1.169 1.169-1.173 3.069-.004 4.239l2.833 2.833-2.833 2.833c-1.167 1.167-1.167 3.067.004 4.239 1.169 1.169 3.069 1.173 4.239.004l2.833-2.833 2.833 2.833c1.167 1.167 3.067 1.167 4.239-.004 1.169-1.169 1.173-3.069.004-4.239l-2.833-2.833"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="m236 37c-7.732 0-14-6.268-14-14 0-7.732 6.268-14 14-14 7.732 0 14 6.268 14 14 0 7.732-6.268 14-14 14m0-4c5.523 0 10-4.477 10-10 0-5.523-4.477-10-10-10-5.523 0-10 4.477-10 10 0 5.523 4.477 10 10 10"/></g><g transform="translate(69 3)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 11.99v60.02c0 4.413 3.583 7.99 8 7.99h89.991c4.419 0 8-3.579 8-7.99v-60.02c0-4.413-3.583-7.99-8-7.99h-89.991c-4.419 0-8 3.579-8 7.99m-4 0c0-6.622 5.378-11.99 12-11.99h89.991c6.629 0 12 5.367 12 11.99v60.02c0 6.622-5.378 11.99-12 11.99h-89.991c-6.629 0-12-5.367-12-11.99v-60.02m52.874 80.3l-13.253-15.292h34.76l-13.253 15.292c-2.237 2.582-6.01 2.585-8.253 0m3.02-2.62c.644.743 1.564.743 2.207 0l7.516-8.673h-17.24l7.516 8.673"/><rect width="18" height="6" x="15" y="23" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="39" y="39" fill="#e52c5a" rx="3"/><rect width="18" height="6" x="33" y="55" fill="#e5e5e5" rx="3"/><rect width="12" height="6" x="39" y="23" fill="#fde5d8" rx="3"/><rect width="12" height="6" x="57" y="55" fill="#e52c5a" rx="3"/><rect width="12" height="6" x="15" y="55" fill="#b5a7dd" rx="3"/><rect width="18" height="6" x="81" y="23" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="15" y="39" fill="#fde5d8" rx="3"/><rect width="6" height="6" x="57" y="23" fill="#e52c5a" rx="3"/><g fill="#fde5d8"><rect width="6" height="6" x="69" y="23" rx="3"/><rect width="6" height="6" x="75" y="39" rx="3"/></g><rect width="6" height="6" x="63" y="39" fill="#e52c5a" rx="3"/></g><g transform="matrix(.70711-.70711.70711.70711 84.34 52.5)"><path fill="#6b4fbb" fill-rule="nonzero" d="m28.02 67.48c-15.927-2.825-28.02-16.738-28.02-33.476 0-18.778 15.222-34 34-34 18.778 0 34 15.222 34 34 0 16.738-12.1 30.652-28.02 33.476.015.173.023.347.023.524v21.999c0 3.314-2.693 6-6 6-3.314 0-6-2.682-6-6v-21.999c0-.177.008-.351.023-.524m5.977-7.476c14.359 0 26-11.641 26-26 0-14.359-11.641-26-26-26-14.359 0-26 11.641-26 26 0 14.359 11.641 26 26 26"/><path fill="#fff" fill-opacity=".3" stroke="#6b4fbb" stroke-width="8" d="m31 71c16.569 0 30-13.431 30-30 0-16.569-13.431-30-30-30" transform="matrix(.86603.5-.5.86603 26.663-17.507)"/></g></g></svg> \ No newline at end of file
diff --git a/app/views/shared/empty_states/icons/_pipelines_failed.svg b/app/views/shared/empty_states/icons/_pipelines_failed.svg
new file mode 100644
index 00000000000..7dbabf7e4ef
--- /dev/null
+++ b/app/views/shared/empty_states/icons/_pipelines_failed.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 446 249" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m260.03 114h23.972v-.013c19.972-.53 36-16.887 36-36.987 0-20.435-16.565-37-37-37-.993 0-1.977.039-2.95.116-4.95-14.605-18.773-25.12-35.05-25.12-5.464 0-10.652 1.185-15.32 3.311-6.649-9.841-17.909-16.311-30.68-16.311-20.435 0-37 16.565-37 37 0 .701.019 1.397.058 2.088-16.11 3.999-28.06 18.561-28.06 35.912 0 20.435 16.565 37 37 37 .324 0 .646-.004.968-.012"/><ellipse id="2" cx="41" cy="41" rx="41" ry="41"/><mask id="1" width="186" height="112" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="82" height="82" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="matrix(.86603.5-.5.86603 228.11 137.43)"><path stroke="#b5a7dd" stroke-width="4" d="m.445.161c15.89 10.636 34.998 16.839 55.55 16.839"/><g transform="translate(56 4)"><path fill="#fb722e" d="m16 8c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2m0 10c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2"/><path fill="#fde5d8" fill-rule="nonzero" d="m4 22h6c3.315 0 6-2.685 6-5.997v-6.01c0-3.315-2.684-5.997-6-5.997h-6v18m-4-18.992c0-1.661 1.343-3.01 2.994-3.01h7.01c5.523 0 10 4.47 10 9.997v6.01c0 5.521-4.476 9.997-10 9.997h-7.01c-1.654 0-2.994-1.343-2.994-3.01v-19.984"/></g></g><g fill-rule="nonzero" transform="translate(257)"><path fill="#e5e5e5" d="m3.597 18.747c5.611-9.09 15.519-14.747 26.403-14.747 17.12 0 31 13.879 31 31 0 7.02-2.34 13.685-6.58 19.1l3.149 2.466c4.786-6.111 7.431-13.639 7.431-21.565 0-19.33-15.67-35-35-35-12.286 0-23.476 6.384-29.808 16.647l3.404 2.1"/><g transform="matrix(.96593.25882-.25882.96593 15.98 9.578)"><path fill="#b5a7dd" d="m12.426 11.592l-2.142 1.768-3.664-2.116c-.186-.107-.43-.042-.543.154l-1.229 2.129c-.116.2-.052.438.138.547l3.658 2.112-.455 2.735c-.109.657-.165 1.327-.165 2.01 0 .678.055 1.348.165 2.01l.455 2.735-3.658 2.112c-.186.107-.251.351-.138.547l1.229 2.129c.116.2.353.264.543.154l3.664-2.116 2.142 1.768c1.036.855 2.205 1.533 3.462 2l2.6.972v4.225c0 .215.179.393.405.393h2.458c.231 0 .405-.174.405-.393v-4.225l2.6-.972c1.257-.47 2.426-1.147 3.462-2l2.142-1.768 3.664 2.116c.186.107.43.042.543-.154l1.229-2.129c.116-.2.052-.438-.138-.547l-3.658-2.112.455-2.735c.109-.657.165-1.327.165-2.01 0-.678-.055-1.348-.165-2.01l-.455-2.735 3.658-2.112c.186-.107.251-.351.138-.547l-1.229-2.129c-.116-.2-.353-.264-.543-.154l-3.664 2.116-2.142-1.768c-1.036-.855-2.205-1.533-3.462-2l-2.6-.972v-4.225c0-.215-.179-.393-.405-.393h-2.458c-.231 0-.405.174-.405.393v4.225l-2.6.972c-1.257.47-2.426 1.147-3.462 2m2.062-5.749v-1.45c0-2.426 1.963-4.393 4.405-4.393h2.458c2.433 0 4.405 1.967 4.405 4.393v1.45c1.689.631 3.243 1.538 4.608 2.665l1.259-.727c2.101-1.213 4.786-.497 6.01 1.618l1.229 2.129c1.216 2.107.499 4.798-1.602 6.01l-1.257.726c.144.866.219 1.755.219 2.662 0 .907-.075 1.796-.219 2.662l1.257.726c2.101 1.213 2.823 3.896 1.602 6.01l-1.229 2.129c-1.216 2.107-3.906 2.832-6.01 1.618l-1.259-.727c-1.365 1.127-2.92 2.034-4.608 2.665v1.45c0 2.426-1.963 4.393-4.405 4.393h-2.458c-2.433 0-4.405-1.967-4.405-4.393v-1.45c-1.689-.631-3.243-1.538-4.608-2.665l-1.259.727c-2.101 1.213-4.786.497-6.01-1.618l-1.229-2.129c-1.216-2.107-.499-4.798 1.602-6.01l1.257-.726c-.144-.866-.219-1.755-.219-2.662 0-.907.075-1.796.219-2.662l-1.257-.726c-2.101-1.213-2.823-3.896-1.602-6.01l1.229-2.129c1.216-2.107 3.906-2.832 6.01-1.618l1.259.727c1.365-1.127 2.92-2.034 4.608-2.665"/><path fill="#6b4fbb" d="m20.12 23.366c1.347 0 2.439-1.092 2.439-2.439 0-1.347-1.092-2.439-2.439-2.439-1.347 0-2.439 1.092-2.439 2.439 0 1.347 1.092 2.439 2.439 2.439m0 4c-3.556 0-6.439-2.883-6.439-6.439 0-3.556 2.883-6.439 6.439-6.439 3.556 0 6.439 2.883 6.439 6.439 0 3.556-2.883 6.439-6.439 6.439"/></g></g><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#1)" stroke-linejoin="round" xlink:href="#0"/><g transform="translate(175 58)"><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#3)" xlink:href="#2"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="m41 78c20.435 0 37-16.565 37-37 0-20.435-16.565-37-37-37-20.435 0-37 16.565-37 37 0 20.435 16.565 37 37 37m0 4c-22.644 0-41-18.356-41-41 0-22.644 18.356-41 41-41 22.644 0 41 18.356 41 41 0 22.644-18.356 41-41 41"/><g transform="matrix(.96593.25882-.25882.96593 23.581 9.415)"><path fill="#b5a7dd" d="m14.821 13.655l-2.142 1.768-3.933-2.271c-.72-.416-1.634-.171-2.046.543l-1.507 2.61c-.409.708-.161 1.631.553 2.043l3.926 2.267-.455 2.735c-.145.869-.218 1.754-.218 2.65 0 .896.073 1.782.218 2.65l.455 2.735-3.926 2.267c-.72.416-.965 1.329-.553 2.043l1.507 2.61c.409.708 1.332.955 2.046.543l3.933-2.271 2.142 1.768c1.369 1.131 2.916 2.027 4.579 2.648l2.6.972v4.534c0 .831.669 1.5 1.493 1.5h3.01c.817 0 1.493-.676 1.493-1.5v-4.534l2.6-.972c1.663-.621 3.21-1.518 4.579-2.648l2.142-1.768 3.933 2.271c.72.416 1.634.171 2.046-.543l1.507-2.61c.409-.708.161-1.631-.553-2.043l-3.926-2.267.455-2.735c.145-.869.218-1.754.218-2.65 0-.896-.073-1.782-.218-2.65l-.455-2.735 3.926-2.267c.72-.416.965-1.329.553-2.043l-1.507-2.61c-.409-.708-1.332-.955-2.046-.543l-3.933 2.271-2.142-1.768c-1.369-1.131-2.916-2.027-4.579-2.648l-2.6-.972v-4.534c0-.831-.669-1.5-1.493-1.5h-3.01c-.817 0-1.493.676-1.493 1.5v4.534l-2.6.972c-1.663.621-3.21 1.518-4.579 2.648m3.179-6.395v-1.759c0-3.038 2.471-5.5 5.493-5.5h3.01c3.034 0 5.493 2.46 5.493 5.5v1.759c2.098.784 4.03 1.91 5.725 3.311l1.528-.882c2.631-1.519 5.999-.61 7.51 2.01l1.507 2.61c1.517 2.627.616 5.987-2.02 7.507l-1.525.881c.179 1.076.272 2.18.272 3.307 0 1.127-.093 2.231-.272 3.307l1.525.881c2.631 1.519 3.528 4.89 2.02 7.507l-1.507 2.61c-1.517 2.627-4.877 3.527-7.51 2.01l-1.528-.882c-1.696 1.401-3.627 2.527-5.725 3.311v1.759c0 3.038-2.471 5.5-5.493 5.5h-3.01c-3.034 0-5.493-2.46-5.493-5.5v-1.759c-2.098-.784-4.03-1.91-5.725-3.311l-1.528.882c-2.631 1.519-5.999.61-7.51-2.01l-1.507-2.61c-1.517-2.627-.616-5.987 2.02-7.507l1.525-.881c-.179-1.076-.272-2.18-.272-3.307 0-1.127.093-2.231.272-3.307l-1.525-.881c-2.631-1.519-3.528-4.89-2.02-7.507l1.507-2.61c1.517-2.627 4.877-3.527 7.51-2.01l1.528.882c1.696-1.401 3.627-2.527 5.725-3.311"/><path fill="#6b4fbb" d="m25 30c2.209 0 4-1.791 4-4 0-2.209-1.791-4-4-4-2.209 0-4 1.791-4 4 0 2.209 1.791 4 4 4m0 4c-4.418 0-8-3.582-8-8 0-4.418 3.582-8 8-8 4.418 0 8 3.582 8 8 0 4.418-3.582 8-8 8"/></g></g></g><g transform="translate(140 161)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 8.541v30.01c0 2.202 1.793 3.995 4 3.995h20c2.209 0 4-1.789 4-3.995v-30.01c0-2.202-1.793-3.995-4-3.995h-20c-2.209 0-4 1.789-4 3.995m-4 0c0-4.416 3.583-7.995 8-7.995h20c4.416 0 8 3.584 8 7.995v30.01c0 4.416-3.583 7.995-8 7.995h-20c-4.416 0-8-3.584-8-7.995v-30.01"/><g fill="#fb722e"><rect width="4" height="11" x="10" y="18.545" rx="2"/><rect width="4" height="11" x="21" y="18.545" rx="2"/></g></g><path fill="#e5e5e5" fill-rule="nonzero" d="m445.16 245.34c-16.874-11.778-110.62-20.336-222.14-20.336-111.61 0-205.4 8.571-222.18 20.364-.904.635-1.121 1.883-.486 2.786.635.904 1.883 1.121 2.786.486 15.756-11.07 109.46-19.636 219.88-19.636 110.34 0 203.99 8.55 219.85 19.617.906.632 2.153.41 2.785-.495.632-.906.41-2.153-.495-2.785"/></g></svg> \ No newline at end of file
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index a95020a9be8..09f946f1d88 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -17,7 +17,7 @@
.stats
%span
= icon('bookmark')
- = number_with_delimiter(group.projects.count)
+ = number_with_delimiter(group.projects.non_archived.count)
%span
= icon('users')
diff --git a/app/views/shared/icons/_mr_bold.svg b/app/views/shared/icons/_mr_bold.svg
new file mode 100644
index 00000000000..2daa55a8652
--- /dev/null
+++ b/app/views/shared/icons/_mr_bold.svg
@@ -0,0 +1 @@
+<svg width="15" height="20" viewBox="0 0 12 14" xmlns="http://www.w3.org/2000/svg"><path d="M1 4.967a2.15 2.15 0 1 1 2.3 0v5.066a2.15 2.15 0 1 1-2.3 0V4.967zm7.85 5.17V5.496c0-.745-.603-1.346-1.35-1.346V6l-3-3 3-3v1.85c2.016 0 3.65 1.63 3.65 3.646v4.45a2.15 2.15 0 1 1-2.3.191z" fill-rule="nonzero"/></svg>
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 0b0f2c9cd1a..17107f55a2d 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -8,7 +8,7 @@
.alert.alert-danger
Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
Please check out
- = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank"
+ = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs
.form-group
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index b58640c3ef0..330fa8a5b10 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -25,7 +25,7 @@
%button.btn.btn-link
= icon('search')
%span
- Keep typing and press Enter
+ Press Enter or click to search
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 048fc488207..92d2d93a732 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -3,7 +3,7 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('issuable')
-%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "102", "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
@@ -13,15 +13,12 @@
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
= sidebar_gutter_toggle_icon
- if current_user
- %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", "aria-label" => (todo.nil? ? "Add todo" : "Mark done"), data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
- %span.js-issuable-todo-text
- - if todo
- Mark done
- - else
- Add todo
- = icon('spin spinner', class: 'hidden js-issuable-todo-loading', 'aria-hidden': 'true')
+ = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
+ - if current_user
+ .block.todo.hide-expanded
+ = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true
.block.assignee
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) }
- if issuable.assignee
@@ -30,7 +27,7 @@
= icon('user', 'aria-hidden': 'true')
.title.hide-collapsed
Assignee
- = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -64,7 +61,7 @@
None
.title.hide-collapsed
Milestone
- = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -91,7 +88,7 @@
= issuable.due_date.try(:to_s, :medium) || 'None'
.title.hide-collapsed
Due date
- = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -126,7 +123,7 @@
= selected_labels.size
.title.hide-collapsed
Labels
- = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml
new file mode 100644
index 00000000000..574e2958ae8
--- /dev/null
+++ b/app/views/shared/issuable/_sidebar_todo.html.haml
@@ -0,0 +1,15 @@
+- is_collapsed = local_assigns.fetch(:is_collapsed, false)
+- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : 'Mark done'
+- todo_content = is_collapsed ? icon('plus-square') : 'Add todo'
+
+%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
+ class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'),
+ title: (todo.nil? ? 'Add todo' : 'Mark done'),
+ 'aria-label' => (todo.nil? ? 'Add todo' : 'Mark done'),
+ data: issuable_todo_button_data(issuable, todo, is_collapsed) }
+ %span.issuable-todo-inner.js-issuable-todo-inner<
+ - if todo
+ = mark_content
+ - else
+ = todo_content
+ = icon('spin spinner', 'aria-hidden': 'true')
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 8e721c9c8dd..a5aa768b1b2 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -31,7 +31,7 @@
Joined #{time_ago_with_tooltip(member.created_at)}
- if member.expires?
ยท
- %span{ class: ('text-warning' if member.expires_soon?) }
+ %span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) }
Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else
@@ -47,7 +47,7 @@
- current_resource = @project || @group
.controls.member-controls
- if show_controls && member.source == current_resource
- - if user != current_user
+ - if user != current_user && can_admin_member
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
= f.hidden_field :access_level
.member-form-control.dropdown.append-right-5
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
new file mode 100644
index 00000000000..2810f1377b2
--- /dev/null
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -0,0 +1,131 @@
+- affix_offset = local_assigns.fetch(:affix_offset, "102")
+- project = local_assigns[:project]
+
+%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+ .issuable-sidebar.milestone-sidebar
+ .block.milestone-progress.issuable-sidebar-header
+ %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
+ = sidebar_gutter_toggle_icon
+
+ .sidebar-collapsed-icon
+ %span== #{milestone.percent_complete(current_user)}%
+ = milestone_progress_bar(milestone)
+ .title.hide-collapsed
+ %strong.bold== #{milestone.percent_complete(current_user)}%
+ %span.hide-collapsed
+ complete
+ .value.hide-collapsed
+ = milestone_progress_bar(milestone)
+
+ .block.start_date.hide-collapsed
+ .title
+ Start date
+ - if @project && can?(current_user, :admin_milestone, @project)
+ = link_to 'Edit', edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: 'edit-link pull-right'
+ .value
+ %span.value-content
+ - if milestone.start_date
+ %span.bold= milestone.start_date.to_s(:medium)
+ - else
+ %span.no-value No start date
+
+ .block.due_date
+ .sidebar-collapsed-icon
+ = icon('calendar', 'aria-hidden': 'true')
+ %span.collapsed-milestone-date
+ - if milestone.start_date && milestone.due_date
+ - if milestone.start_date.year == milestone.due_date.year
+ .milestone-date= milestone.start_date.strftime('%b %-d')
+ - else
+ .milestone-date= milestone.start_date.strftime('%b %-d %Y')
+ .date-separator -
+ .due_date= milestone.due_date.strftime('%b %-d %Y')
+ - elsif milestone.start_date
+ From
+ .milestone-date= milestone.start_date.strftime('%b %-d %Y')
+ - elsif milestone.due_date
+ Until
+ .milestone-date= milestone.due_date.strftime('%b %-d %Y')
+ - else
+ None
+ .title.hide-collapsed
+ Due date
+ - if @project && can?(current_user, :admin_milestone, @project)
+ = link_to 'Edit', edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: 'edit-link pull-right'
+ .value.hide-collapsed
+ %span.value-content
+ - if milestone.due_date
+ %span.bold= milestone.due_date.to_s(:medium)
+ - else
+ %span.no-value No due date
+ - remaining_days = milestone_remaining_days(milestone)
+ - if remaining_days.present?
+ = surround '(', ')' do
+ %span.remaining-days= remaining_days
+
+ - if !project || can?(current_user, :read_issue, project)
+ .block
+ .sidebar-collapsed-icon
+ %strong
+ = icon('hashtag', 'aria-hidden': 'true')
+ %span= milestone.issues_visible_to_user(current_user).count
+ .title.hide-collapsed
+ Issues
+ %span.badge= milestone.issues_visible_to_user(current_user).count
+ - if project && can?(current_user, :create_issue, project)
+ = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "pull-right", title: "New Issue" do
+ New issue
+ .value.hide-collapsed.bold
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :issues) do
+ Open:
+ = milestone.issues_visible_to_user(current_user).opened.count
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :issues, state: 'closed') do
+ Closed:
+ = milestone.issues_visible_to_user(current_user).closed.count
+
+ .block
+ .sidebar-collapsed-icon
+ %strong
+ = icon('exclamation', 'aria-hidden': 'true')
+ %span= milestone.issues_visible_to_user(current_user).count
+ .title.hide-collapsed
+ Merge requests
+ %span.badge= milestone.merge_requests.count
+ .value.hide-collapsed.bold
+ - if !project || can?(current_user, :read_merge_request, project)
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :merge_requests) do
+ Open:
+ = milestone.merge_requests.opened.count
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'closed') do
+ Closed:
+ = milestone.merge_requests.closed.count
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'merged') do
+ Merged:
+ = milestone.merge_requests.merged.count
+ - else
+ %span.milestone-stat
+ Open:
+ = milestone.merge_requests.opened.count
+ %span.milestone-stat
+ Closed:
+ = milestone.merge_requests.closed.count
+ %span.milestone-stat
+ Merged:
+ = milestone.merge_requests.merged.count
+
+ - milestone_ref = milestone.try(:to_reference, full: true)
+ - if milestone_ref.present?
+ .block.reference
+ .sidebar-collapsed-icon.dont-change-state
+ = clipboard_button(clipboard_text: milestone_ref, title: "Copy reference to clipboard", placement: "left")
+ .cross-project-reference.hide-collapsed
+ %span
+ Reference:
+ %cite{ title: milestone_ref }
+ = milestone_ref
+ = clipboard_button(clipboard_text: milestone_ref, title: "Copy reference to clipboard", placement: "left")
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
deleted file mode 100644
index 78079f633d5..00000000000
--- a/app/views/shared/milestones/_summary.html.haml
+++ /dev/null
@@ -1,45 +0,0 @@
-- project = local_assigns[:project]
-
-.context.prepend-top-default
- .milestone-summary
- %h4 Progress
-
- .milestone-stats-and-buttons
- .milestone-stats
- - if !project || can?(current_user, :read_issue, project)
- %span.milestone-stat.with-drilldown
- %strong= milestone.issues_visible_to_user(current_user).size
- issues:
- %span.milestone-stat
- %strong= milestone.issues_visible_to_user(current_user).opened.size
- open and
- %strong= milestone.issues_visible_to_user(current_user).closed.size
- closed
- %span.milestone-stat.with-drilldown
- %strong= milestone.merge_requests.size
- merge requests:
- %span.milestone-stat
- %strong= milestone.merge_requests.opened.size
- open and
- %strong= milestone.merge_requests.merged.size
- merged
- %span.milestone-stat
- %strong== #{milestone.percent_complete(current_user)}%
- complete
- - remaining_days = milestone_remaining_days(milestone)
- - if remaining_days.present?
- %span.milestone-stat
- %span.remaining-days= remaining_days
-
- .milestone-progress-buttons
- %span.tab-issues-buttons
- - if project
- - if can?(current_user, :create_issue, project)
- = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do
- New Issue
- - if can?(current_user, :read_issue, project)
- = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn"
- %span.tab-merge-requests-buttons.hidden
- = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn"
-
- = milestone_progress_bar(milestone)
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index a0e9ec46220..9a4502873ef 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -1,26 +1,29 @@
-%ul.nav-links.no-top.no-bottom
- - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
- Issues
- %span.badge= milestone.issues_visible_to_user(current_user).size
+.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.nav-links.scrolling-tabs
+ - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
+ %li.active
+ = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
+ Issues
+ %span.badge= milestone.issues_visible_to_user(current_user).size
+ %li
+ = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
+ Merge Requests
+ %span.badge= milestone.merge_requests.size
+ - else
+ %li.active
+ = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
+ Merge Requests
+ %span.badge= milestone.merge_requests.size
%li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
- Merge Requests
- %span.badge= milestone.merge_requests.size
- - else
- %li.active
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
- Merge Requests
- %span.badge= milestone.merge_requests.size
- %li
- = link_to '#tab-participants', 'data-toggle' => 'tab' do
- Participants
- %span.badge= milestone.participants.count
- %li
- = link_to '#tab-labels', 'data-toggle' => 'tab' do
- Labels
- %span.badge= milestone.labels.count
+ = link_to '#tab-participants', 'data-toggle' => 'tab' do
+ Participants
+ %span.badge= milestone.participants.count
+ %li
+ = link_to '#tab-labels', 'data-toggle' => 'tab' do
+ Labels
+ %span.badge= milestone.labels.count
- show_project_name = local_assigns.fetch(:show_project_name, false)
- show_full_project_name = local_assigns.fetch(:show_full_project_name, false)
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 497446c1ef3..2562f085338 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -3,6 +3,9 @@
- group = local_assigns[:group]
.detail-page-header
+ %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+
.status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" }
- if milestone.closed?
Closed
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index c57282c5742..c0699b13719 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -10,7 +10,7 @@
.js-projects-list-holder
- if projects.any?
- %ul.projects-list.content-list
+ %ul.projects-list
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) ? 'hide' : nil
= render "shared/projects/project", project: project, skip_namespace: skip_namespace,
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index df21857e1ad..761f0b606b5 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -10,44 +10,44 @@
%li.project-row{ class: css_class }
= cache(cache_key) do
+ - if avatar
+ .avatar-container.s40
+ - if use_creator_avatar
+ = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
+ - else
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ .project-details
+ %h3.prepend-top-0.append-bottom-0
+ = link_to project_path(project), class: dom_class(project) do
+ %span.project-full-name
+ %span.namespace-name
+ - if project.namespace && !skip_namespace
+ = project.namespace.human_name
+ \/
+ %span.project-name
+ = project.name
+
+ - if show_last_commit_as_description
+ .description.prepend-top-5
+ = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit),
+ class: "commit-row-message"
+ - elsif project.description.present?
+ .description.prepend-top-5
+ = markdown_field(project, :description)
+
.controls
- if project.archived
- %span.label.label-warning archived
+ %span.prepend-left-10.label.label-warning archived
- if project.pipeline_status.has_status?
- %span
+ %span.prepend-left-10
= render_project_pipeline_status(project.pipeline_status)
- if forks
- %span
+ %span.prepend-left-10
= icon('code-fork')
= number_with_delimiter(project.forks_count)
- if stars
- %span
+ %span.prepend-left-10
= icon('star')
= number_with_delimiter(project.star_count)
- %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) }
+ %span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) }
= visibility_level_icon(project.visibility_level, fw: true)
-
- .title
- = link_to project_path(project), class: dom_class(project) do
- - if avatar
- .dash-project-avatar
- .avatar-container.s40
- - if use_creator_avatar
- = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- - else
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
- %span.project-full-name
- %span.namespace-name
- - if project.namespace && !skip_namespace
- = project.namespace.human_name
- \/
- %span.project-name.filter-title
- = project.name
-
- - if show_last_commit_as_description
- .description
- = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit),
- class: "commit-row-message"
- - elsif project.description.present?
- .description
- = markdown_field(project, :description)
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 4afd31f788b..d1e88274878 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -18,9 +18,9 @@
= event_action_name(event)
%strong
- if event.note?
- = link_to event.note_target.to_reference, event_note_target_path(event)
+ = link_to event.note_target.to_reference, event_note_target_path(event), class: 'has-tooltip', title: event.target_title
- elsif event.target
- = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target]
+ = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
at
%strong
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 76cd330e80a..969ea7ab9e6 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -13,7 +13,7 @@
.cover-block.user-cover-block
.cover-controls
- if @user == current_user
- = link_to profile_path, class: 'btn btn-gray' do
+ = link_to profile_path, class: 'btn btn-gray has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
= icon('pencil')
- elsif current_user
- if @user.abuse_report
@@ -24,7 +24,7 @@
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle')
- = link_to user_path(@user, rss_url_options), class: 'btn btn-gray' do
+ = link_to user_path(@user, rss_url_options), class: 'btn btn-gray has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do
= icon('rss')
- if current_user && current_user.admin?
= link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
@@ -33,7 +33,7 @@
.profile-header
.avatar-holder
- = link_to avatar_icon(@user, 400), target: '_blank' do
+ = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
.user-info
@@ -44,7 +44,7 @@
%span.middle-dot-divider
@#{@user.username}
%span.middle-dot-divider
- Member since #{@user.created_at.to_s(:medium)}
+ Member since #{@user.created_at.to_date.to_s(:long)}
.cover-desc
- unless @user.public_email.blank?
@@ -79,25 +79,29 @@
%p.profile-user-bio
= @user.bio
- %ul.nav-links.center.user-profile-nav
- %li.js-activity-tab
- = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
- Activity
- %li.js-groups-tab
- = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
- Groups
- %li.js-contributed-tab
- = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
- Contributed projects
- %li.js-projects-tab
- = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
- Personal projects
- %li.js-snippets-tab
- = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do
- Snippets
+ .scrolling-tabs-container
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.nav-links.center.user-profile-nav.scrolling-tabs
+ %li.js-activity-tab
+ = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
+ Activity
+ %li.js-groups-tab
+ = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
+ Groups
+ %li.js-contributed-tab
+ = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
+ Contributed projects
+ %li.js-projects-tab
+ = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
+ Personal projects
+ %li.js-snippets-tab
+ = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do
+ Snippets
%div{ class: container_class }
- .user-callout{ 'callout-svg' => custom_icon('icon_customization') }
+ - if @user == current_user && !show_user_callout?
+ = render 'shared/user_callout'
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb
deleted file mode 100644
index 5fdb1f2baa0..00000000000
--- a/app/workers/build_email_worker.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class BuildEmailWorker
- include Sidekiq::Worker
- include BuildQueue
-
- def perform(build_id, recipients, push_data)
- recipients.each do |recipient|
- begin
- case push_data['build_status']
- when 'success'
- Notify.build_success_email(build_id, recipient).deliver_now
- when 'failed'
- Notify.build_fail_email(build_id, recipient).deliver_now
- end
- # These are input errors and won't be corrected even if Sidekiq retries
- rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
- logger.info("Failed to send e-mail for project '#{push_data['project_name']}' to #{recipient}: #{e}")
- end
- end
- end
-end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 2cd87895c55..015a41b6e82 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -3,20 +3,16 @@ class PostReceive
include DedicatedSidekiqQueue
def perform(repo_path, identifier, changes)
- if repository_storage = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1]['path'].to_s) }
- repo_path.gsub!(repository_storage[1]['path'].to_s, "")
- else
- log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"")
- end
+ repo_relative_path = Gitlab::RepoPath.strip_storage_path(repo_path)
changes = Base64.decode64(changes) unless changes.include?(' ')
# Use Sidekiq.logger so arguments can be correlated with execution
# time and thread ID's.
Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS']
- post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes)
+ post_received = Gitlab::GitPostReceive.new(repo_relative_path, identifier, changes)
if post_received.project.nil?
- log("Triggered hook for non-existing project with full path \"#{repo_path}\"")
+ log("Triggered hook for non-existing project with full path \"#{repo_relative_path}\"")
return false
end
@@ -25,7 +21,7 @@ class PostReceive
elsif post_received.regular_project?
process_project_changes(post_received)
else
- log("Triggered hook for unidentifiable repository type with full path \"#{repo_path}\"")
+ log("Triggered hook for unidentifiable repository type with full path \"#{repo_relative_path}\"")
false
end
end