From 7b533ef7f140450783485b01cdf0434b77a9f90e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 23 May 2017 12:36:41 -0500 Subject: Change from double click to single click to open dropdown --- .codeclimate.yml | 38 ++ .gitlab-ci.yml | 21 +- .gitlab/issue_templates/Bug.md | 6 + .rubocop.yml | 9 + .rubocop_todo.yml | 7 - .scss-lint.yml | 68 +-- CHANGELOG.md | 5 + GITLAB_SHELL_VERSION | 2 +- Gemfile | 9 +- Gemfile.lock | 29 +- app/assets/images/i2p-step.svg | 4 + app/assets/javascripts/blob/viewer/index.js | 2 +- app/assets/javascripts/boards/boards_bundle.js | 1 + .../javascripts/boards/components/modal/filters.js | 1 + .../javascripts/boards/filtered_search_boards.js | 8 +- app/assets/javascripts/build.js | 336 ++++++------- .../commit/pipelines/pipelines_table.js | 2 +- app/assets/javascripts/copy_as_gfm.js | 128 +++-- .../diff_notes/components/jump_to_discussion.js | 7 + app/assets/javascripts/dispatcher.js | 10 +- app/assets/javascripts/droplab/keyboard.js | 2 +- .../javascripts/droplab/plugins/ajax_filter.js | 48 +- app/assets/javascripts/dropzone_input.js | 5 + .../environments/components/environment.vue | 130 ++++-- .../folder/environments_folder_view.vue | 125 +++-- .../environments/mixins/environments_mixin.js | 17 + .../environments/services/environments_service.js | 3 +- .../environments/stores/environments_store.js | 6 + .../components/recent_searches_dropdown_content.js | 6 +- .../javascripts/filtered_search/dropdown_hint.js | 29 +- .../filtered_search/dropdown_non_user.js | 2 +- .../javascripts/filtered_search/dropdown_user.js | 13 +- .../javascripts/filtered_search/dropdown_utils.js | 11 +- .../filtered_search/filtered_search_bundle.js | 2 +- .../filtered_search_dropdown_manager.js | 10 +- .../filtered_search/filtered_search_manager.js | 79 ++-- .../filtered_search/filtered_search_token_keys.js | 8 + .../filtered_search/filtered_search_tokenizer.js | 3 +- .../filtered_search_visual_tokens.js | 97 +++- .../filtered_search/recent_searches_root.js | 1 + .../stores/recent_searches_store.js | 3 +- app/assets/javascripts/flash.js | 34 +- app/assets/javascripts/gfm_auto_complete.js | 15 +- app/assets/javascripts/gl_dropdown.js | 10 +- app/assets/javascripts/gl_field_errors.js | 10 +- app/assets/javascripts/integrations/index.js | 7 + .../integrations/integration_settings_form.js | 123 +++++ .../javascripts/issue_show/components/app.vue | 267 ++++++++++- .../issue_show/components/description.vue | 79 +++- .../issue_show/components/edit_actions.vue | 87 ++++ .../javascripts/issue_show/components/edited.vue | 56 +++ .../components/fields/confidential_checkbox.vue | 23 + .../issue_show/components/fields/description.vue | 72 +++ .../components/fields/description_template.vue | 111 +++++ .../issue_show/components/fields/project_move.vue | 83 ++++ .../issue_show/components/fields/title.vue | 94 ++++ .../javascripts/issue_show/components/form.vue | 135 ++++++ .../issue_show/components/locked_warning.vue | 20 + .../javascripts/issue_show/components/title.vue | 36 +- app/assets/javascripts/issue_show/event_hub.js | 3 + app/assets/javascripts/issue_show/index.js | 83 ++++ .../javascripts/issue_show/mixins/animate.js | 2 +- app/assets/javascripts/issue_show/mixins/update.js | 10 + .../javascripts/issue_show/services/index.js | 17 +- app/assets/javascripts/issue_show/stores/index.js | 54 ++- app/assets/javascripts/lib/utils/ajax_cache.js | 4 +- app/assets/javascripts/lib/utils/common_utils.js | 6 +- app/assets/javascripts/lib/utils/notify.js | 85 ++-- app/assets/javascripts/lib/utils/number_utils.js | 10 + app/assets/javascripts/lib/utils/text_utility.js | 2 +- app/assets/javascripts/lib/utils/url_utility.js | 3 +- app/assets/javascripts/main.js | 1 - app/assets/javascripts/merge_request_tabs.js | 2 +- app/assets/javascripts/notebook/cells/markdown.vue | 24 +- app/assets/javascripts/notes.js | 324 ++++++++----- .../pipelines/components/graph/graph_component.vue | 68 +-- .../pipelines/components/header_component.vue | 97 ++++ .../pipelines/components/pipeline_url.js | 56 --- .../pipelines/components/pipeline_url.vue | 65 +++ app/assets/javascripts/pipelines/graph_bundle.js | 10 - .../pipelines/pipeline_details_bundle.js | 70 +++ .../pipelines/pipeline_details_mediatior.js | 59 +++ app/assets/javascripts/pipelines/pipelines.js | 2 +- .../pipelines/services/pipeline_service.js | 5 + .../pipelines/services/pipelines_service.js | 2 - .../javascripts/pipelines/stores/pipeline_store.js | 6 +- app/assets/javascripts/project_select.js | 9 +- app/assets/javascripts/raven/index.js | 4 + app/assets/javascripts/raven/raven_config.js | 5 +- app/assets/javascripts/shortcuts_issuable.js | 12 +- .../components/assignees/sidebar_assignees.js | 3 +- .../javascripts/sidebar/stores/sidebar_store.js | 4 + app/assets/javascripts/single_file_diff.js | 2 +- app/assets/javascripts/task_list.js | 2 +- app/assets/javascripts/user_callout.js | 11 +- app/assets/javascripts/users_select.js | 34 +- .../components/mr_widget_memory_usage.js | 44 +- .../components/states/mr_widget_ready_to_merge.js | 8 +- .../vue_merge_request_widget/dependencies.js | 1 + .../javascripts/vue_merge_request_widget/index.js | 2 + .../vue_merge_request_widget/mr_widget_options.js | 12 + .../stores/get_state_key.js | 8 +- .../stores/mr_widget_store.js | 4 +- .../javascripts/vue_shared/components/commit.js | 4 +- .../vue_shared/components/header_ci_component.vue | 133 ++++++ .../vue_shared/components/markdown/field.vue | 107 +++++ .../vue_shared/components/markdown/header.vue | 113 +++++ .../vue_shared/components/markdown/toolbar.vue | 33 ++ .../components/markdown/toolbar_button.vue | 58 +++ .../vue_shared/components/pipelines_table_row.js | 4 +- .../vue_shared/components/time_ago_tooltip.vue | 58 +++ .../components/user_avatar/user_avatar_image.vue | 8 +- .../javascripts/vue_shared/mixins/timeago.js | 18 + .../javascripts/vue_shared/mixins/tooltip.js | 4 + .../vue_shared/vue_resource_interceptor.js | 2 +- app/assets/stylesheets/framework.scss | 1 + app/assets/stylesheets/framework/awards.scss | 7 +- app/assets/stylesheets/framework/blocks.scss | 1 - app/assets/stylesheets/framework/emojis.scss | 1 - app/assets/stylesheets/framework/files.scss | 4 +- app/assets/stylesheets/framework/filters.scss | 44 +- app/assets/stylesheets/framework/flash.scss | 20 + app/assets/stylesheets/framework/header.scss | 22 +- app/assets/stylesheets/framework/lists.scss | 1 - app/assets/stylesheets/framework/mobile.scss | 5 - app/assets/stylesheets/framework/notes.scss | 14 + app/assets/stylesheets/framework/selects.scss | 1 - app/assets/stylesheets/framework/sidebar.scss | 4 + app/assets/stylesheets/framework/timeline.scss | 35 +- app/assets/stylesheets/framework/typography.scss | 6 +- app/assets/stylesheets/framework/variables.scss | 10 +- app/assets/stylesheets/pages/boards.scss | 2 + app/assets/stylesheets/pages/builds.scss | 227 +++++---- app/assets/stylesheets/pages/ci_projects.scss | 1 - app/assets/stylesheets/pages/convdev_index.scss | 255 ++++++++++ app/assets/stylesheets/pages/diff.scss | 7 +- app/assets/stylesheets/pages/environments.scss | 4 + app/assets/stylesheets/pages/issuable.scss | 4 +- app/assets/stylesheets/pages/issues.scss | 1 - app/assets/stylesheets/pages/members.scss | 2 +- app/assets/stylesheets/pages/merge_requests.scss | 8 +- app/assets/stylesheets/pages/note_form.scss | 35 +- app/assets/stylesheets/pages/notes.scss | 137 +++--- app/assets/stylesheets/pages/pipelines.scss | 12 + app/assets/stylesheets/pages/profile.scss | 25 +- app/assets/stylesheets/pages/projects.scss | 5 - app/controllers/admin/builds_controller.rb | 25 - .../conversational_development_index_controller.rb | 5 + app/controllers/admin/hook_logs_controller.rb | 29 ++ app/controllers/admin/hooks_controller.rb | 32 +- app/controllers/admin/jobs_controller.rb | 25 + app/controllers/admin/keys_controller.rb | 4 +- app/controllers/application_controller.rb | 38 +- app/controllers/autocomplete_controller.rb | 2 +- app/controllers/concerns/diff_for_path.rb | 13 +- app/controllers/concerns/hooks_execution.rb | 15 + app/controllers/concerns/issuable_actions.rb | 11 +- app/controllers/concerns/renders_blob.rb | 4 +- app/controllers/dashboard/projects_controller.rb | 3 +- app/controllers/dashboard_controller.rb | 4 +- app/controllers/groups_controller.rb | 3 +- app/controllers/profiles_controller.rb | 8 + app/controllers/projects/artifacts_controller.rb | 6 +- app/controllers/projects/blob_controller.rb | 2 +- .../projects/build_artifacts_controller.rb | 55 +++ app/controllers/projects/builds_controller.rb | 122 +---- app/controllers/projects/compare_controller.rb | 6 +- .../projects/environments_controller.rb | 2 + app/controllers/projects/hook_logs_controller.rb | 33 ++ app/controllers/projects/hooks_controller.rb | 17 +- app/controllers/projects/issues_controller.rb | 18 +- app/controllers/projects/jobs_controller.rb | 142 ++++++ .../projects/merge_requests_controller.rb | 9 - app/controllers/projects/pipelines_controller.rb | 2 +- app/controllers/projects/refs_controller.rb | 2 +- app/controllers/projects/services_controller.rb | 39 +- app/controllers/projects/snippets_controller.rb | 2 +- app/controllers/projects/variables_controller.rb | 3 +- app/controllers/sessions_controller.rb | 6 +- app/controllers/snippets_controller.rb | 2 +- app/finders/projects_finder.rb | 33 +- app/finders/todos_finder.rb | 2 +- app/helpers/application_helper.rb | 6 +- app/helpers/avatars_helper.rb | 20 +- app/helpers/blob_helper.rb | 16 +- app/helpers/builds_helper.rb | 8 +- app/helpers/button_helper.rb | 2 +- app/helpers/commits_helper.rb | 10 - .../conversational_development_index_helper.rb | 16 + app/helpers/diff_helper.rb | 30 +- app/helpers/dropdowns_helper.rb | 16 +- app/helpers/gitlab_routing_helper.rb | 16 +- app/helpers/issuables_helper.rb | 51 ++ app/helpers/labels_helper.rb | 5 +- app/helpers/notes_helper.rb | 2 +- app/helpers/projects_helper.rb | 36 +- app/helpers/rss_helper.rb | 2 +- app/helpers/selects_helper.rb | 8 + app/helpers/submodule_helper.rb | 1 + app/helpers/system_note_helper.rb | 3 +- app/mailers/base_mailer.rb | 6 + app/models/application_setting.rb | 18 +- app/models/audit_event.rb | 2 +- app/models/blob.rb | 12 +- app/models/blob_viewer/auxiliary.rb | 4 +- app/models/blob_viewer/base.rb | 26 +- app/models/blob_viewer/client_side.rb | 4 +- app/models/blob_viewer/server_side.rb | 4 +- app/models/blob_viewer/text.rb | 4 +- app/models/ci/build.rb | 134 ++++-- app/models/ci/pipeline.rb | 15 +- app/models/ci/pipeline_schedule.rb | 14 +- app/models/ci/trigger_request.rb | 2 +- app/models/ci/variable.rb | 5 + app/models/commit.rb | 15 +- app/models/commit_status.rb | 11 +- app/models/concerns/discussion_on_diff.rb | 8 + app/models/concerns/editable.rb | 7 + app/models/concerns/issuable.rb | 1 + app/models/concerns/note_on_diff.rb | 10 - app/models/concerns/noteable.rb | 7 +- app/models/concerns/routable.rb | 83 ---- .../concerns/select_for_project_authorization.rb | 6 +- .../conversational_development_index/card.rb | 26 ++ .../idea_to_production_step.rb | 19 + .../conversational_development_index/metric.rb | 21 + app/models/deployment.rb | 5 + app/models/diff_discussion.rb | 17 +- app/models/diff_note.rb | 31 +- app/models/discussion.rb | 9 +- app/models/environment.rb | 28 ++ app/models/event.rb | 2 +- app/models/group.rb | 16 +- app/models/hooks/service_hook.rb | 2 +- app/models/hooks/system_hook.rb | 4 - app/models/hooks/web_hook.rb | 43 +- app/models/hooks/web_hook_log.rb | 13 + app/models/issue.rb | 4 +- app/models/key.rb | 9 +- app/models/label.rb | 6 +- app/models/legacy_diff_note.rb | 4 +- app/models/lfs_objects_project.rb | 3 +- app/models/list.rb | 2 +- app/models/member.rb | 4 + app/models/members/group_member.rb | 4 - app/models/members/project_member.rb | 4 - app/models/merge_request.rb | 80 ++-- app/models/merge_request_diff.rb | 34 +- app/models/milestone.rb | 2 +- app/models/namespace.rb | 29 +- app/models/note.rb | 14 +- app/models/personal_access_token.rb | 2 +- app/models/project.rb | 45 +- app/models/project_authorization.rb | 6 + app/models/project_import_data.rb | 2 +- app/models/project_services/asana_service.rb | 3 +- app/models/project_services/assembla_service.rb | 2 +- app/models/project_services/bamboo_service.rb | 4 +- app/models/project_services/buildkite_service.rb | 4 +- app/models/project_services/campfire_service.rb | 4 +- .../project_services/chat_notification_service.rb | 6 +- .../custom_issue_tracker_service.rb | 6 +- app/models/project_services/deployment_service.rb | 4 + app/models/project_services/drone_ci_service.rb | 4 +- .../project_services/external_wiki_service.rb | 2 +- app/models/project_services/flowdock_service.rb | 2 +- app/models/project_services/gemnasium_service.rb | 4 +- app/models/project_services/hipchat_service.rb | 2 +- app/models/project_services/irker_service.rb | 2 +- .../project_services/issue_tracker_service.rb | 8 +- app/models/project_services/jira_service.rb | 62 ++- app/models/project_services/kubernetes_service.rb | 24 +- app/models/project_services/mock_ci_service.rb | 7 +- .../project_services/mock_monitoring_service.rb | 4 + .../project_services/pipelines_email_service.rb | 3 +- .../project_services/pivotaltracker_service.rb | 3 +- app/models/project_services/prometheus_service.rb | 3 +- app/models/project_services/pushover_service.rb | 6 +- app/models/project_services/teamcity_service.rb | 4 +- app/models/project_team.rb | 9 +- app/models/project_wiki.rb | 7 +- app/models/sent_notification.rb | 2 +- app/models/service.rb | 2 +- app/models/snippet.rb | 1 + app/models/system_note_metadata.rb | 1 + app/models/user.rb | 65 ++- app/policies/ci/build_policy.rb | 2 +- app/policies/group_policy.rb | 17 +- .../metric_presenter.rb | 144 ++++++ app/serializers/analytics_build_entity.rb | 2 +- app/serializers/build_action_entity.rb | 2 +- app/serializers/build_artifact_entity.rb | 39 +- app/serializers/build_details_entity.rb | 50 ++ app/serializers/build_entity.rb | 8 +- app/serializers/entity_date_helper.rb | 2 +- app/serializers/issue_entity.rb | 6 + app/serializers/merge_request_entity.rb | 3 +- app/serializers/pipeline_details_entity.rb | 7 + app/serializers/pipeline_entity.rb | 23 +- app/serializers/pipeline_serializer.rb | 2 +- app/serializers/runner_entity.rb | 18 + app/serializers/user_entity.rb | 5 + app/services/ci/create_pipeline_service.rb | 15 +- app/services/ci/create_trigger_request_service.rb | 2 +- app/services/create_deployment_service.rb | 76 ++- .../discussions/update_diff_position_service.rb | 41 ++ app/services/git_push_service.rb | 2 +- app/services/git_tag_push_service.rb | 2 +- app/services/gravatar_service.rb | 21 +- app/services/issuable_base_service.rb | 2 +- app/services/issues/close_service.rb | 1 + app/services/issues/reopen_service.rb | 1 + app/services/merge_requests/close_service.rb | 1 + .../merge_requests/conflicts/resolve_service.rb | 12 +- app/services/merge_requests/create_service.rb | 15 + app/services/merge_requests/post_merge_service.rb | 1 + app/services/merge_requests/refresh_service.rb | 4 +- app/services/merge_requests/reopen_service.rb | 3 +- app/services/notes/diff_position_update_service.rb | 30 -- app/services/projects/destroy_service.rb | 14 +- app/services/search_service.rb | 2 +- app/services/submit_usage_ping_service.rb | 41 ++ app/services/system_note_service.rb | 24 +- .../users/refresh_authorized_projects_service.rb | 40 +- app/services/web_hook_service.rb | 120 +++++ app/uploaders/artifact_uploader.rb | 28 +- app/uploaders/gitlab_uploader.rb | 6 +- app/uploaders/lfs_object_uploader.rb | 16 + app/validators/dynamic_path_validator.rb | 218 ++------- app/views/admin/background_jobs/_head.html.haml | 25 - app/views/admin/background_jobs/show.html.haml | 2 +- app/views/admin/builds/index.html.haml | 18 - .../_callout.html.haml | 13 + .../_card.html.haml | 25 + .../_disabled.html.haml | 9 + .../_no_data.html.haml | 7 + .../show.html.haml | 35 ++ app/views/admin/dashboard/_head.html.haml | 2 +- app/views/admin/dashboard/index.html.haml | 6 + app/views/admin/health_check/show.html.haml | 2 +- app/views/admin/hook_logs/_index.html.haml | 37 ++ app/views/admin/hook_logs/show.html.haml | 10 + app/views/admin/hooks/edit.html.haml | 6 + app/views/admin/jobs/index.html.haml | 18 + app/views/admin/logs/show.html.haml | 2 +- app/views/admin/monitoring/_head.html.haml | 29 ++ app/views/admin/requests_profiles/index.html.haml | 4 +- app/views/admin/runners/show.html.haml | 2 +- app/views/admin/system_info/show.html.haml | 7 +- app/views/admin/users/_user.html.haml | 2 +- app/views/dashboard/_activities.html.haml | 3 - app/views/dashboard/activity.html.haml | 12 +- app/views/dashboard/issues.html.haml | 2 +- app/views/dashboard/merge_requests.html.haml | 2 +- app/views/dashboard/projects/index.html.haml | 22 +- app/views/dashboard/projects/starred.html.haml | 18 +- app/views/devise/shared/_signup_box.html.haml | 2 +- app/views/discussions/_diff_with_notes.html.haml | 2 +- app/views/discussions/_discussion.html.haml | 7 +- app/views/discussions/_jump_to_next.html.haml | 4 +- app/views/discussions/_notes.html.haml | 14 +- app/views/events/_event_last_push.html.haml | 14 - app/views/events/event/_push.html.haml | 2 +- app/views/groups/_activities.html.haml | 3 - app/views/groups/_head.html.haml | 3 + app/views/groups/_show_nav.html.haml | 7 +- app/views/groups/show.html.haml | 1 - app/views/layouts/nav/_admin.html.haml | 8 +- app/views/layouts/nav/_project.html.haml | 2 +- app/views/notify/links/ci/builds/_build.html.haml | 2 +- app/views/notify/links/ci/builds/_build.text.erb | 2 +- app/views/notify/repository_push_email.html.haml | 28 +- app/views/notify/repository_push_email.text.haml | 20 +- app/views/profiles/accounts/_reset_token.html.haml | 11 + app/views/profiles/accounts/show.html.haml | 34 +- app/views/profiles/preferences/show.html.haml | 4 +- app/views/projects/_activity.html.haml | 2 - app/views/projects/_last_push.html.haml | 34 +- app/views/projects/activity.html.haml | 2 + .../projects/artifacts/_tree_directory.html.haml | 2 +- app/views/projects/artifacts/_tree_file.html.haml | 2 +- app/views/projects/artifacts/browse.html.haml | 8 +- app/views/projects/artifacts/file.html.haml | 8 +- app/views/projects/blame/show.html.haml | 2 +- app/views/projects/blob/_breadcrumb.html.haml | 2 +- app/views/projects/blob/show.html.haml | 5 +- .../boards/components/sidebar/_assignee.html.haml | 5 +- app/views/projects/builds/_header.html.haml | 31 -- app/views/projects/builds/_sidebar.html.haml | 142 ------ app/views/projects/builds/_table.html.haml | 25 - app/views/projects/builds/_user.html.haml | 7 - app/views/projects/builds/index.html.haml | 23 - app/views/projects/builds/show.html.haml | 86 ---- app/views/projects/ci/builds/_build.html.haml | 12 +- app/views/projects/commit/show.html.haml | 2 +- app/views/projects/deployments/_actions.haml | 1 + app/views/projects/diffs/_content.html.haml | 23 +- app/views/projects/diffs/_diffs.html.haml | 14 +- app/views/projects/diffs/_file.html.haml | 12 +- app/views/projects/diffs/_file_header.html.haml | 16 +- app/views/projects/diffs/_image.html.haml | 69 --- app/views/projects/diffs/_line.html.haml | 2 +- app/views/projects/diffs/_parallel_view.html.haml | 2 +- app/views/projects/diffs/_stats.html.haml | 6 +- app/views/projects/diffs/_text_file.html.haml | 4 +- app/views/projects/diffs/viewers/_image.html.haml | 68 +++ app/views/projects/diffs/viewers/_text.html.haml | 8 + app/views/projects/environments/show.html.haml | 2 +- app/views/projects/hook_logs/_index.html.haml | 37 ++ app/views/projects/hook_logs/show.html.haml | 11 + app/views/projects/hooks/edit.html.haml | 8 + app/views/projects/issues/_discussion.html.haml | 6 +- app/views/projects/issues/show.html.haml | 50 +- app/views/projects/jobs/_header.html.haml | 31 ++ app/views/projects/jobs/_sidebar.html.haml | 132 ++++++ app/views/projects/jobs/_table.html.haml | 25 + app/views/projects/jobs/_user.html.haml | 7 + app/views/projects/jobs/index.html.haml | 23 + app/views/projects/jobs/show.html.haml | 98 ++++ .../projects/merge_requests/_discussion.html.haml | 2 +- app/views/projects/merge_requests/index.html.haml | 4 +- .../merge_requests/show/_versions.html.haml | 2 +- .../_pipeline_schedule.html.haml | 5 +- app/views/projects/pipelines/_head.html.haml | 2 +- app/views/projects/pipelines/_info.html.haml | 16 +- app/views/projects/pipelines/_with_tabs.html.haml | 8 +- app/views/projects/pipelines/charts/_overall.haml | 6 +- app/views/projects/pipelines/show.html.haml | 6 + .../projects/pipelines_settings/_show.html.haml | 2 +- .../projects/registry/repositories/index.html.haml | 72 ++- app/views/projects/services/_form.html.haml | 13 +- app/views/projects/settings/_head.html.haml | 2 +- app/views/projects/snippets/show.html.haml | 2 +- app/views/projects/tree/show.html.haml | 1 + app/views/projects/variables/_content.html.haml | 5 +- app/views/projects/variables/_form.html.haml | 9 + app/views/projects/variables/_table.html.haml | 3 + app/views/search/_category.html.haml | 77 +-- app/views/shared/_field.html.haml | 7 +- app/views/shared/_group_form.html.haml | 2 +- .../shared/_new_project_item_select.html.haml | 2 +- app/views/shared/_user_callout.html.haml | 2 +- app/views/shared/hook_logs/_content.html.haml | 44 ++ app/views/shared/hook_logs/_status_label.html.haml | 3 + app/views/shared/icons/_convdev_no_data.svg | 40 ++ app/views/shared/icons/_convdev_no_index.svg | 67 +++ app/views/shared/icons/_convdev_overview.svg | 64 +++ app/views/shared/icons/_i2p_step_1.svg | 12 + app/views/shared/icons/_i2p_step_10.svg | 12 + app/views/shared/icons/_i2p_step_2.svg | 5 + app/views/shared/icons/_i2p_step_3.svg | 12 + app/views/shared/icons/_i2p_step_4.svg | 6 + app/views/shared/icons/_i2p_step_5.svg | 5 + app/views/shared/icons/_i2p_step_6.svg | 15 + app/views/shared/icons/_i2p_step_7.svg | 7 + app/views/shared/icons/_i2p_step_8.svg | 4 + app/views/shared/icons/_i2p_step_9.svg | 4 + app/views/shared/icons/_icon_status_skipped.svg | 2 +- .../icons/_icon_status_skipped_borderless.svg | 2 +- app/views/shared/icons/_scroll_down.svg | 6 +- .../shared/icons/_scroll_down_hover_active.svg | 3 - app/views/shared/icons/_scroll_up.svg | 4 +- app/views/shared/icons/_scroll_up_hover_active.svg | 3 - .../shared/issuable/_label_dropdown.html.haml | 2 +- app/views/shared/issuable/_search_bar.html.haml | 44 +- .../shared/issuable/_sidebar_assignees.html.haml | 5 +- .../shared/issuable/_user_dropdown_item.html.haml | 11 + .../shared/issuable/form/_merge_params.html.haml | 10 + .../form/_metadata_issue_assignee.html.haml | 2 +- app/views/shared/notes/_note.html.haml | 2 +- app/views/shared/notes/_notes_with_form.html.haml | 25 +- app/views/snippets/show.html.haml | 2 +- app/views/users/show.html.haml | 4 +- app/workers/build_success_worker.rb | 11 +- app/workers/expire_job_cache_worker.rb | 35 ++ app/workers/expire_pipeline_cache_worker.rb | 9 + app/workers/gitlab_usage_ping_worker.rb | 18 +- app/workers/pipeline_schedule_worker.rb | 2 +- app/workers/process_commit_worker.rb | 15 +- app/workers/project_web_hook_worker.rb | 11 - app/workers/remove_old_web_hook_logs_worker.rb | 10 + app/workers/system_hook_worker.rb | 10 - app/workers/web_hook_worker.rb | 13 + .../unreleased/10378-promote-blameless-culture.yml | 4 + .../unreleased/12614-fix-long-message-from-mr.yml | 4 + .../18927-reorder-issue-action-buttons.yml | 4 + ...-issue-for-project-that-has-issues-disabled.yml | 4 + ...-all-spinach-tests-with-rspec-feature-tests.yml | 4 + .../unreleased/24196-protected-variables.yml | 5 + changelogs/unreleased/25373-jira-links.yml | 4 + changelogs/unreleased/25680-CI_ENVIRONMENT_URL.yml | 4 + changelogs/unreleased/27439-memory-usage-info.yml | 4 + .../27614-improve-instant-comments-exp.yml | 4 + changelogs/unreleased/28080-system-checks.yml | 4 + .../unreleased/28694-hard-delete-user-from-api.yml | 4 + changelogs/unreleased/29852-latex-formatting.yml | 4 + .../unreleased/30410-revert-9347-and-10079.yml | 5 + changelogs/unreleased/30469-convdev-index.yml | 4 + ...0651-improve-container-registry-description.yml | 4 + ...30892-add-api-support-for-pipeline-schedule.yml | 4 + ...ki-is-not-searchable-with-guest-permissions.yml | 4 + ...-26135-ci-project-slug-enviroment-variables.yml | 4 + changelogs/unreleased/31448-jira-urls.yml | 4 + changelogs/unreleased/31511-jira-settings.yml | 4 + .../unreleased/31556-ci-coverage-paralel-rspec.yml | 4 + ...ed-runner-is-enabled-in-the-admin-dashboard.yml | 4 + ...add-uptime-of-gitlab-instance-in-admin-area.yml | 4 + .../31644-make-cookie-sessions-unique.yml | 4 + ...n-filter-in-search-bar-to-activate-dropdown.yml | 4 + .../unreleased/31849-pipeline-real-time-header.yml | 4 + .../31849-pipeline-show-view-realtime.yml | 5 + changelogs/unreleased/31943-document-go-183.yml | 3 + ...-file-size-limit-for-default-toggle-opening.yml | 4 + .../unreleased/32118-new-environment-btn-copy.yml | 4 + .../unreleased/32486-fix-note-emoji-placement.yml | 4 - changelogs/unreleased/32682-skipped-ci-icon.yml | 4 + changelogs/unreleased/32715-fix-note-padding.yml | 4 + ...pipeline_schedules-pages-throwing-error-500.yml | 4 + ...799-remove-no_turbolink-attribute-from-haml.yml | 4 + changelogs/unreleased/32807-company-icon.yml | 4 + .../32832-confidential-issue-overflow.yml | 5 + .../unreleased/32851-postgres-min-version.yml | 4 + ...tion-removed-the-newline-in-the-end-of-file.yml | 4 + ...tandard-gzip-compression-for-webpack-assets.yml | 4 + .../33000-tag-list-in-project-create-api.yml | 4 + ...ressed-yourself-todo-when-using-unsubscribe.yml | 5 + ...files-has-ceased-to-display-latex-equations.yml | 4 + ...issions-for-project-labels-and-group-labels.yml | 4 + ...3207-show-delete-option-in-admin-users-page.yml | 4 + .../unreleased/33215-fix-hard-delete-of-users.yml | 4 + ...project-for-user-api-ignores-path-parameter.yml | 4 + ...ents-for-count-badges-and-permission-badges.yml | 5 + changelogs/unreleased/aliyun-backup-provider.yml | 4 + .../unreleased/bugfix-v3-deploy_keys-can_push.yml | 4 + .../unreleased/ci-build-pipeline-header-vue.yml | 4 + .../dm-comment-on-mr-commit-discussion.yml | 4 + .../unreleased/dm-consistent-last-push-event.yml | 4 + .../dm-copy-as-gfm-without-empty-elements.yml | 4 + ...m-when-parts-of-other-elements-are-selected.yml | 4 + changelogs/unreleased/dm-discussions-n-plus-1.yml | 4 + .../dm-emails-are-not-user-references.yml | 4 + changelogs/unreleased/dm-fix-jump-button.yml | 4 + changelogs/unreleased/dm-gitmodules-parsing.yml | 4 + changelogs/unreleased/dm-gravatar-username.yml | 4 + .../unreleased/dm-more-dependency-linkers.yml | 4 + changelogs/unreleased/dm-oauth-config-for.yml | 4 + changelogs/unreleased/dm-outdated-system-note.yml | 4 + .../unreleased/dm-paste-code-inside-gfm-code.yml | 4 + changelogs/unreleased/feature-flags-flipper.yml | 4 + changelogs/unreleased/feature-rss-scoped-token.yml | 4 + .../fix-counter-cache-for-acts-as-taggable.yml | 4 + ...-merge-ability-for-protected-manual-actions.yml | 4 + .../unreleased/fix-migration-for-postgres.yml | 4 - .../fix-n-plus-one-queries-for-user-access.yml | 4 + ...ix-terminals-support-for-kubernetes-service.yml | 4 + changelogs/unreleased/fix_diff_line_comments.yml | 5 + changelogs/unreleased/gitaly-opt-out.yml | 4 + .../unreleased/introduce-source-to-pipelines.yml | 4 + .../issuable-form-create-label-sub-groups.yml | 4 + changelogs/unreleased/issue-edit-inline.yml | 4 + ...issue-template-reproduce-in-example-project.yml | 4 + changelogs/unreleased/issue_19262.yml | 4 + changelogs/unreleased/issue_32225_2.yml | 4 + .../unreleased/jouve-gitlab-ce-admin_keys.yml | 5 + .../mabes-gitlab-ce-bypass-auto-login.yml | 4 + .../unreleased/migrate-artifacts-to-a-new-path.yml | 4 + changelogs/unreleased/mrchrisw-catch-openssl.yml | 4 + .../unreleased/projects-api-import-status.yml | 4 + changelogs/unreleased/rename-builds-controller.yml | 4 + .../rework-authorizations-performance.yml | 6 + .../sh-fix-lfs-from-moving-across-filesystems.yml | 4 + .../sh-fix-submodules-trailing-spaces.yml | 4 + changelogs/unreleased/task-list-2.yml | 4 + .../unreleased/tc-improve-project-api-perf.yml | 4 + .../wait-for-ajax-handling-all-js-requests.yml | 4 + changelogs/unreleased/winh-current-user-filter.yml | 4 + .../unreleased/winh-styled-people-search-bar.yml | 4 + changelogs/unreleased/zj-drop-fk-if-exists.yml | 4 + changelogs/unreleased/zj-fix-pipeline-etag.yml | 4 + .../unreleased/zj-job-view-goes-real-time.yml | 4 + changelogs/unreleased/zj-realtime-env-list.yml | 9 + config/application.rb | 1 + config/gitlab.yml.example | 4 +- config/initializers/0_acts_as_taggable.rb | 9 + config/initializers/1_settings.rb | 7 +- config/initializers/active_record_locking.rb | 74 +++ config/initializers/active_record_preloader.rb | 15 + config/initializers/acts_as_taggable.rb | 5 - config/initializers/ar_monkey_patch.rb | 74 --- .../initializers/ar_speed_up_migration_checking.rb | 2 +- config/initializers/fast_gettext.rb | 1 + .../initializers/forbid_sidekiq_in_transactions.rb | 49 ++ config/initializers/postgresql_cte.rb | 132 ++++++ .../initializers/relative_naming_ci_namespace.rb | 2 +- config/initializers/server_uptime.rb | 1 + config/initializers/session_store.rb | 8 +- config/karma.config.js | 2 + config/locales/en.yml | 36 ++ config/locales/es.yml | 35 ++ config/routes.rb | 15 - config/routes/admin.rb | 18 +- config/routes/git_http.rb | 8 +- config/routes/group.rb | 18 +- config/routes/profile.rb | 1 + config/routes/project.rb | 86 ++-- config/routes/repository.rb | 6 +- config/routes/user.rb | 28 +- config/sidekiq_queues.yml | 3 +- config/webpack.config.js | 47 +- db/fixtures/development/11_keys.rb | 11 +- db/fixtures/development/14_pipelines.rb | 6 +- db/fixtures/development/17_cycle_analytics.rb | 11 +- .../21_conversational_development_index_metrics.rb | 40 ++ ...0160615191922_set_missing_stage_on_ci_builds.rb | 1 + ...drop_and_readd_has_external_wiki_in_projects.rb | 1 + ...3_set_confidential_issues_events_on_webhooks.rb | 1 + db/migrate/20160919144305_add_type_to_labels.rb | 1 + .../20161018124658_make_project_owners_masters.rb | 1 + ...e_slack_and_mattermost_notification_services.rb | 1 + .../20170317203554_index_routes_path_for_like.rb | 5 +- db/migrate/20170320173259_migrate_assignees.rb | 4 +- ...18_remove_index_for_users_current_sign_in_at.rb | 8 +- ...2628_remove_foreigh_key_ci_trigger_schedules.rb | 12 +- db/migrate/20170427103502_create_web_hook_logs.rb | 22 + ...0503140201_reschedule_project_authorizations.rb | 44 ++ ..._nested_groups_into_regular_groups_for_mysql.rb | 123 +++++ ...03185032_index_redirect_routes_path_for_like.rb | 5 +- ...82103_add_index_project_group_links_group_id.rb | 19 + .../20170521184006_add_change_position_to_notes.rb | 13 + .../20170523091700_add_rss_token_to_users.rb | 19 + ...ate_conversational_development_index_metrics.rb | 39 ++ .../20170524125940_add_source_to_ci_pipeline.rb | 9 + ...20170524161101_add_protected_to_ci_variables.rb | 15 + db/migrate/20170525174156_create_feature_tables.rb | 26 ++ ...21_reset_users_authorized_projects_populated.rb | 4 +- ...0309171644_reset_relative_position_for_issue.rb | 4 +- ...enable_auto_cancel_pending_pipelines_for_all.rb | 1 + ...0_remove_users_authorized_projects_populated.rb | 15 + .../20170523083112_migrate_old_artifacts.rb | 72 +++ db/schema.rb | 95 +++- doc/administration/gitaly/index.md | 4 +- doc/api/README.md | 1 + doc/api/build_variables.md | 28 +- doc/api/features.md | 83 ++++ doc/api/pipeline_schedules.md | 273 +++++++++++ doc/api/projects.md | 16 + doc/api/users.md | 3 + doc/ci/environments.md | 6 + doc/ci/variables/README.md | 30 +- doc/ci/yaml/README.md | 34 +- doc/customization/libravatar.md | 4 +- doc/development/README.md | 2 + doc/development/feature_flags.md | 7 + doc/development/i18n_guide.md | 15 +- doc/development/serializing_data.md | 84 ++++ doc/install/database_mysql.md | 2 +- doc/install/installation.md | 38 +- doc/install/kubernetes/gitlab_chart.md | 36 +- doc/install/requirements.md | 2 +- doc/integration/github.md | 21 +- doc/integration/saml.md | 3 + doc/raketasks/backup_restore.md | 2 +- doc/university/README.md | 1 + doc/update/9.0-to-9.1.md | 1 - doc/update/9.2-to-9.3.md | 305 ++++++++++++ doc/user/group/subgroups/index.md | 15 +- doc/user/profile/account/delete_account.md | 21 +- doc/user/profile/preferences.md | 8 +- doc/user/project/container_registry.md | 2 - doc/user/project/img/container_registry_panel.png | Bin 32310 -> 0 bytes doc/user/project/integrations/img/webhook_logs.png | Bin 0 -> 24066 bytes doc/user/project/integrations/jira.md | 3 +- doc/user/project/integrations/webhooks.md | 16 + doc/user/project/milestones/img/progress.png | Bin 0 -> 23491 bytes doc/user/project/milestones/index.md | 8 + doc/user/project/pipelines/schedules.md | 2 +- doc/workflow/gitlab_flow.md | 12 +- doc/workflow/lfs/lfs_administration.md | 2 +- features/dashboard/starred_projects.feature | 12 - features/project/hooks.feature | 37 -- features/project/merge_requests/accept.feature | 3 +- features/project/service.feature | 26 +- features/steps/dashboard/dashboard.rb | 2 +- features/steps/dashboard/event_filters.rb | 14 +- features/steps/dashboard/todos.rb | 4 +- features/steps/explore/projects.rb | 2 +- features/steps/group/members.rb | 4 +- features/steps/group/milestones.rb | 4 +- features/steps/project/builds/artifacts.rb | 4 +- features/steps/project/forked_merge_requests.rb | 7 +- features/steps/project/hooks.rb | 75 --- features/steps/project/merge_requests.rb | 55 ++- .../steps/project/merge_requests/acceptance.rb | 12 +- features/steps/project/merge_requests/revert.rb | 4 +- features/steps/project/pages.rb | 2 +- features/steps/project/project.rb | 4 +- features/steps/project/project_milestone.rb | 4 +- features/steps/project/services.rb | 72 ++- features/steps/project/snippets.rb | 6 +- features/steps/project/source/browse_files.rb | 7 +- features/steps/project/source/markdown_render.rb | 22 +- features/steps/shared/active_tab.rb | 4 +- features/steps/shared/builds.rb | 6 +- features/steps/shared/diff_note.rb | 4 +- features/steps/shared/markdown.rb | 6 +- features/steps/shared/note.rb | 10 +- features/steps/shared/paths.rb | 12 +- features/steps/snippets/snippets.rb | 4 +- features/support/env.rb | 4 +- lib/api/api.rb | 6 +- lib/api/commit_statuses.rb | 9 +- lib/api/commits.rb | 2 +- lib/api/entities.rb | 51 +- lib/api/features.rb | 36 ++ lib/api/groups.rb | 10 +- lib/api/helpers.rb | 46 +- lib/api/internal.rb | 17 +- lib/api/jobs.rb | 10 - lib/api/pipeline_schedules.rb | 131 ++++++ lib/api/pipelines.rb | 2 +- lib/api/projects.rb | 31 +- lib/api/repositories.rb | 2 +- lib/api/runner.rb | 13 +- lib/api/services.rb | 8 +- lib/api/time_tracking_endpoints.rb | 2 +- lib/api/users.rb | 3 +- lib/api/v3/builds.rb | 10 - lib/api/v3/commits.rb | 2 +- lib/api/v3/deploy_keys.rb | 1 + lib/api/v3/entities.rb | 7 +- lib/api/v3/groups.rb | 6 +- lib/api/v3/helpers.rb | 27 ++ lib/api/v3/projects.rb | 4 +- lib/api/v3/repositories.rb | 2 +- lib/api/v3/time_tracking_endpoints.rb | 2 +- lib/api/variables.rb | 4 +- lib/backup/artifacts.rb | 2 +- .../filter/ascii_doc_post_processing_filter.rb | 13 + lib/banzai/filter/sanitization_filter.rb | 4 + lib/banzai/pipeline/ascii_doc_pipeline.rb | 14 + lib/banzai/reference_parser/base_parser.rb | 5 +- .../representation/pull_request_comment.rb | 4 +- lib/ci/api/builds.rb | 10 +- lib/constraints/group_url_constrainer.rb | 6 +- lib/constraints/project_url_constrainer.rb | 4 +- lib/constraints/user_url_constrainer.rb | 6 +- lib/feature.rb | 53 +++ lib/gitlab/asciidoc.rb | 13 +- lib/gitlab/chat_commands/presenters/base.rb | 4 +- lib/gitlab/ci/status/build/cancelable.rb | 2 +- lib/gitlab/ci/status/build/common.rb | 2 +- lib/gitlab/ci/status/build/play.rb | 2 +- lib/gitlab/ci/status/build/retryable.rb | 2 +- lib/gitlab/ci/status/build/stop.rb | 2 +- lib/gitlab/ci/trace/stream.rb | 51 +- lib/gitlab/current_settings.rb | 5 +- lib/gitlab/database/migration_helpers.rb | 35 +- lib/gitlab/dependency_linker.rb | 11 +- lib/gitlab/dependency_linker/base_linker.rb | 75 ++- lib/gitlab/dependency_linker/cartfile_linker.rb | 14 + lib/gitlab/dependency_linker/cocoapods.rb | 10 + .../dependency_linker/composer_json_linker.rb | 18 + lib/gitlab/dependency_linker/gemfile_linker.rb | 23 +- lib/gitlab/dependency_linker/gemspec_linker.rb | 18 + lib/gitlab/dependency_linker/godeps_json_linker.rb | 26 ++ lib/gitlab/dependency_linker/json_linker.rb | 44 ++ lib/gitlab/dependency_linker/method_linker.rb | 39 ++ .../dependency_linker/package_json_linker.rb | 44 ++ lib/gitlab/dependency_linker/podfile_linker.rb | 15 + .../dependency_linker/podspec_json_linker.rb | 32 ++ lib/gitlab/dependency_linker/podspec_linker.rb | 24 + .../dependency_linker/requirements_txt_linker.rb | 17 + lib/gitlab/diff/file.rb | 79 ++-- lib/gitlab/diff/file_collection/base.rb | 25 +- .../diff/file_collection/merge_request_diff.rb | 3 +- lib/gitlab/diff/highlight.rb | 6 +- lib/gitlab/diff/line.rb | 4 + lib/gitlab/diff/position.rb | 30 +- lib/gitlab/diff/position_tracer.rb | 216 +++++---- lib/gitlab/email/message/repository_push.rb | 2 +- lib/gitlab/encoding_helper.rb | 62 +++ lib/gitlab/etag_caching/router.rb | 36 +- lib/gitlab/git/blame.rb | 2 +- lib/gitlab/git/blob.rb | 3 +- lib/gitlab/git/commit.rb | 2 +- lib/gitlab/git/diff.rb | 107 +++-- lib/gitlab/git/diff_collection.rb | 61 +-- lib/gitlab/git/encoding_helper.rb | 64 --- lib/gitlab/git/ref.rb | 2 +- lib/gitlab/git/repository.rb | 47 +- lib/gitlab/git/tree.rb | 2 +- lib/gitlab/gitaly_client.rb | 22 +- lib/gitlab/gitaly_client/commit.rb | 2 +- lib/gitlab/gitaly_client/diff.rb | 21 + lib/gitlab/gitaly_client/diff_stitcher.rb | 31 ++ lib/gitlab/gon_helper.rb | 4 + lib/gitlab/google_code_import/client.rb | 2 +- lib/gitlab/google_code_import/importer.rb | 18 +- lib/gitlab/group_hierarchy.rb | 104 +++++ lib/gitlab/health_checks/fs_shards_check.rb | 17 +- lib/gitlab/i18n.rb | 31 +- lib/gitlab/o_auth/provider.rb | 6 +- lib/gitlab/path_regex.rb | 265 +++++++++++ .../project_authorizations/with_nested_groups.rb | 125 +++++ .../without_nested_groups.rb | 35 ++ lib/gitlab/regex.rb | 80 +--- lib/gitlab/route_map.rb | 4 +- lib/gitlab/routes/legacy_builds.rb | 36 ++ lib/gitlab/sql/recursive_cte.rb | 62 +++ lib/gitlab/url_sanitizer.rb | 6 - lib/gitlab/utils.rb | 8 + lib/gitlab/visibility_level.rb | 2 +- lib/gitlab/workhorse.rb | 5 +- lib/support/init.d/gitlab | 2 +- lib/support/init.d/gitlab.default.example | 4 +- lib/system_check.rb | 21 + lib/system_check/app/active_users_check.rb | 17 + .../app/database_config_exists_check.rb | 25 + lib/system_check/app/git_config_check.rb | 42 ++ lib/system_check/app/git_version_check.rb | 29 ++ lib/system_check/app/gitlab_config_exists_check.rb | 24 + .../app/gitlab_config_up_to_date_check.rb | 30 ++ lib/system_check/app/init_script_exists_check.rb | 27 ++ .../app/init_script_up_to_date_check.rb | 43 ++ lib/system_check/app/log_writable_check.rb | 28 ++ lib/system_check/app/migrations_are_up_check.rb | 20 + .../app/orphaned_group_members_check.rb | 20 + .../app/projects_have_namespace_check.rb | 37 ++ lib/system_check/app/redis_version_check.rb | 25 + lib/system_check/app/ruby_version_check.rb | 27 ++ lib/system_check/app/tmp_writable_check.rb | 28 ++ .../app/uploads_directory_exists_check.rb | 21 + .../app/uploads_path_permission_check.rb | 36 ++ .../app/uploads_path_tmp_permission_check.rb | 40 ++ lib/system_check/base_check.rb | 129 +++++ lib/system_check/helpers.rb | 75 +++ lib/system_check/simple_executor.rb | 99 ++++ lib/tasks/gettext.rake | 8 + lib/tasks/gitlab/check.rake | 494 ++------------------ lib/tasks/gitlab/task_helpers.rb | 44 +- lib/tasks/tokens.rake | 10 + package.json | 6 +- qa/Dockerfile | 23 +- qa/Gemfile | 2 +- qa/Gemfile.lock | 10 + qa/qa/specs/config.rb | 35 +- qa/spec/spec_helper.rb | 1 - rubocop/cop/activerecord_serialize.rb | 24 + rubocop/cop/migration/update_column_in_batches.rb | 43 ++ rubocop/migration_helpers.rb | 5 +- rubocop/rubocop.rb | 2 + scripts/trigger-build | 3 +- spec/controllers/application_controller_spec.rb | 36 ++ spec/controllers/autocomplete_controller_spec.rb | 22 +- spec/controllers/groups_controller_spec.rb | 2 +- .../import/bitbucket_controller_spec.rb | 21 +- spec/controllers/import/gitlab_controller_spec.rb | 23 +- spec/controllers/profiles/keys_controller_spec.rb | 2 +- .../projects/artifacts_controller_spec.rb | 48 +- .../controllers/projects/builds_controller_spec.rb | 419 ----------------- .../projects/environments_controller_spec.rb | 29 +- .../controllers/projects/issues_controller_spec.rb | 2 +- spec/controllers/projects/jobs_controller_spec.rb | 446 ++++++++++++++++++ .../projects/merge_requests_controller_spec.rb | 16 +- .../projects/services_controller_spec.rb | 112 ++--- spec/controllers/sessions_controller_spec.rb | 31 ++ spec/db/production/settings.rb | 1 - spec/factories/ci/builds.rb | 3 +- spec/factories/ci/pipelines.rb | 31 +- spec/factories/ci/stages.rb | 2 + spec/factories/ci/trigger_requests.rb | 4 +- spec/factories/ci/variables.rb | 4 + spec/factories/commits.rb | 9 +- .../conversational_development_index_metrics.rb | 33 ++ spec/factories/file_uploader.rb | 20 - spec/factories/file_uploaders.rb | 22 + spec/factories/keys.rb | 19 +- spec/factories/project_statistics.rb | 8 +- spec/factories/project_wikis.rb | 2 + spec/factories/projects.rb | 14 + spec/factories/services.rb | 7 +- spec/factories/users.rb | 4 + spec/factories/web_hook_log.rb | 14 + spec/factories/wiki_directories.rb | 2 + spec/factories_spec.rb | 14 +- spec/features/admin/admin_builds_spec.rb | 16 +- .../admin_conversational_development_index_spec.rb | 40 ++ .../admin_disables_git_access_protocol_spec.rb | 2 +- spec/features/admin/admin_hook_logs_spec.rb | 40 ++ spec/features/admin/admin_hooks_spec.rb | 15 +- spec/features/admin/admin_labels_spec.rb | 4 +- spec/features/admin/admin_system_info_spec.rb | 3 + spec/features/admin/admin_users_spec.rb | 4 +- spec/features/atom/dashboard_issues_spec.rb | 15 +- spec/features/atom/dashboard_spec.rb | 9 +- spec/features/atom/issues_spec.rb | 23 +- spec/features/atom/users_spec.rb | 9 +- spec/features/auto_deploy_spec.rb | 2 +- spec/features/boards/add_issues_modal_spec.rb | 14 +- spec/features/boards/boards_spec.rb | 77 ++- spec/features/boards/issue_ordering_spec.rb | 27 +- spec/features/boards/keyboard_shortcut_spec.rb | 4 +- spec/features/boards/modal_filter_spec.rb | 32 +- spec/features/boards/new_issue_spec.rb | 10 +- spec/features/boards/sidebar_spec.rb | 46 +- spec/features/boards/sub_group_project_spec.rb | 6 +- spec/features/calendar_spec.rb | 10 +- spec/features/commits_spec.rb | 20 +- spec/features/container_registry_spec.rb | 2 +- spec/features/copy_as_gfm_spec.rb | 39 +- spec/features/cycle_analytics_spec.rb | 11 +- spec/features/dashboard/activity_spec.rb | 6 +- .../dashboard/datetime_on_tooltips_spec.rb | 4 +- spec/features/dashboard/groups_list_spec.rb | 6 +- spec/features/dashboard/issues_spec.rb | 89 ++-- spec/features/dashboard/merge_requests_spec.rb | 22 +- spec/features/dashboard/milestone_filter_spec.rb | 10 +- .../project_member_activity_index_spec.rb | 2 +- spec/features/dashboard/projects_spec.rb | 16 +- spec/features/dashboard_issues_spec.rb | 4 +- spec/features/expand_collapse_diffs_spec.rb | 33 +- spec/features/explore/groups_list_spec.rb | 6 +- spec/features/gitlab_flavored_markdown_spec.rb | 2 - spec/features/groups/activity_spec.rb | 8 +- spec/features/groups/group_name_toggle_spec.rb | 4 +- spec/features/groups/issues_spec.rb | 10 +- spec/features/groups/members/list_spec.rb | 4 +- spec/features/groups/show_spec.rb | 4 +- spec/features/groups_spec.rb | 4 +- spec/features/issues/award_emoji_spec.rb | 16 +- spec/features/issues/award_spec.rb | 8 +- .../features/issues/bulk_assignment_labels_spec.rb | 8 +- .../issues/create_branch_merge_request_spec.rb | 4 +- .../filtered_search/dropdown_assignee_spec.rb | 21 +- .../issues/filtered_search/dropdown_author_spec.rb | 23 +- .../issues/filtered_search/filter_issues_spec.rb | 20 +- .../issues/filtered_search/visual_tokens_spec.rb | 8 +- spec/features/issues/form_spec.rb | 98 +++- spec/features/issues/gfm_autocomplete_spec.rb | 10 +- spec/features/issues/issue_sidebar_spec.rb | 23 +- spec/features/issues/move_spec.rb | 8 +- spec/features/issues/notes_on_issues_spec.rb | 2 +- spec/features/issues/update_issues_spec.rb | 4 +- .../issues/user_uses_slash_commands_spec.rb | 2 +- spec/features/issues_spec.rb | 12 +- spec/features/merge_requests/closes_issues_spec.rb | 4 +- spec/features/merge_requests/conflicts_spec.rb | 24 +- spec/features/merge_requests/create_new_mr_spec.rb | 4 +- .../merge_requests/deleted_source_branch_spec.rb | 2 +- .../merge_requests/diff_notes_avatars_spec.rb | 12 +- spec/features/merge_requests/discussion_spec.rb | 43 +- spec/features/merge_requests/edit_mr_spec.rb | 13 + .../merge_requests/filter_merge_requests_spec.rb | 2 +- .../merge_immediately_with_pipeline_spec.rb | 2 +- .../merge_when_pipeline_succeeds_spec.rb | 14 +- .../merge_requests/mini_pipeline_graph_spec.rb | 4 +- .../only_allow_merge_if_build_succeeds_spec.rb | 24 +- spec/features/merge_requests/pipelines_spec.rb | 2 +- .../merge_requests/update_merge_requests_spec.rb | 4 +- .../merge_requests/user_posts_diff_notes_spec.rb | 6 +- .../merge_requests/user_posts_notes_spec.rb | 4 +- .../user_uses_slash_commands_spec.rb | 4 +- spec/features/merge_requests/versions_spec.rb | 8 +- .../merge_requests/widget_deployments_spec.rb | 4 +- spec/features/merge_requests/widget_spec.rb | 55 ++- spec/features/milestones/milestones_spec.rb | 6 +- spec/features/profile_spec.rb | 15 + spec/features/projects/activity/rss_spec.rb | 4 +- spec/features/projects/artifacts/browse_spec.rb | 25 + spec/features/projects/artifacts/download_spec.rb | 61 +++ spec/features/projects/artifacts/file_spec.rb | 24 +- spec/features/projects/artifacts/raw_spec.rb | 25 + .../blobs/blob_line_permalink_updater_spec.rb | 2 +- spec/features/projects/blobs/blob_show_spec.rb | 8 +- spec/features/projects/blobs/edit_spec.rb | 2 +- spec/features/projects/blobs/user_create_spec.rb | 2 +- spec/features/projects/builds_spec.rb | 90 ++-- spec/features/projects/commit/cherry_pick_spec.rb | 4 +- .../projects/commit/mini_pipeline_graph_spec.rb | 2 +- spec/features/projects/commit/rss_spec.rb | 8 +- spec/features/projects/compare_spec.rb | 9 +- ...eloper_views_empty_project_instructions_spec.rb | 12 +- .../projects/environments/environment_spec.rb | 57 ++- .../projects/environments/environments_spec.rb | 4 +- spec/features/projects/features_visibility_spec.rb | 12 +- spec/features/projects/files/browse_files_spec.rb | 6 +- .../projects/files/dockerfile_dropdown_spec.rb | 4 +- .../projects/files/find_file_keyboard_spec.rb | 2 +- .../projects/files/gitignore_dropdown_spec.rb | 4 +- .../projects/files/gitlab_ci_yml_dropdown_spec.rb | 4 +- .../project_owner_creates_license_file_spec.rb | 2 +- ...to_create_license_file_in_empty_project_spec.rb | 2 +- spec/features/projects/files/undo_template_spec.rb | 2 +- spec/features/projects/group_links_spec.rb | 2 +- spec/features/projects/issuable_templates_spec.rb | 14 +- spec/features/projects/issues/rss_spec.rb | 8 +- spec/features/projects/jobs_spec.rb | 520 +++++++++++++++++++++ .../projects/labels/update_prioritization_spec.rb | 10 +- spec/features/projects/main/rss_spec.rb | 4 +- spec/features/projects/members/group_links_spec.rb | 6 +- ...master_adds_member_with_expiration_date_spec.rb | 2 +- spec/features/projects/members/sorting_spec.rb | 11 +- .../projects/members/user_requests_access_spec.rb | 3 +- spec/features/projects/pipeline_schedules_spec.rb | 28 +- spec/features/projects/pipelines/pipeline_spec.rb | 2 - spec/features/projects/pipelines/pipelines_spec.rb | 18 +- spec/features/projects/ref_switcher_spec.rb | 6 +- .../projects/services/jira_service_spec.rb | 92 ++++ .../services/mattermost_slash_command_spec.rb | 18 +- .../projects/services/slack_slash_command_spec.rb | 14 +- .../projects/settings/integration_settings_spec.rb | 52 ++- spec/features/projects/snippets/show_spec.rb | 10 +- spec/features/projects/sub_group_issuables_spec.rb | 32 ++ spec/features/projects/tree/rss_spec.rb | 4 +- spec/features/projects/view_on_env_spec.rb | 12 +- .../projects/wiki/user_creates_wiki_page_spec.rb | 34 ++ .../wiki/user_git_access_wiki_page_spec.rb | 2 +- spec/features/search_spec.rb | 8 +- .../security/project/internal_access_spec.rb | 6 +- .../security/project/private_access_spec.rb | 6 +- .../security/project/public_access_spec.rb | 6 +- spec/features/snippets/create_snippet_spec.rb | 4 +- .../snippets/notes_on_personal_snippets_spec.rb | 2 +- spec/features/snippets/public_snippets_spec.rb | 2 +- spec/features/snippets/show_spec.rb | 10 +- spec/features/task_lists_spec.rb | 17 +- spec/features/todos/todos_filtering_spec.rb | 8 +- spec/features/todos/todos_spec.rb | 6 +- spec/features/u2f_spec.rb | 2 +- .../uploads/user_uploads_file_to_note_spec.rb | 4 +- spec/features/users/projects_spec.rb | 2 +- spec/features/users/rss_spec.rb | 4 +- spec/features/users/snippets_spec.rb | 6 +- spec/features/users_spec.rb | 8 +- spec/features/variables_spec.rb | 48 +- spec/finders/group_members_finder_spec.rb | 2 +- spec/finders/members_finder_spec.rb | 2 +- spec/finders/projects_finder_spec.rb | 15 +- .../api/schemas/entities/merge_request.json | 3 +- spec/fixtures/api/schemas/pipeline_schedule.json | 41 ++ spec/fixtures/api/schemas/pipeline_schedules.json | 4 + spec/helpers/avatars_helper_spec.rb | 101 ++++ spec/helpers/blob_helper_spec.rb | 28 +- spec/helpers/diff_helper_spec.rb | 37 +- spec/helpers/issuables_helper_spec.rb | 43 ++ spec/helpers/notes_helper_spec.rb | 8 +- spec/helpers/rss_helper_spec.rb | 8 +- spec/helpers/submodule_helper_spec.rb | 5 + spec/javascripts/build_spec.js | 308 ++++++------ spec/javascripts/copy_as_gfm_spec.js | 49 ++ .../droplab/plugins/ajax_filter_spec.js | 72 +++ spec/javascripts/environments/environment_spec.js | 2 +- .../environments/environments_store_spec.js | 9 + .../recent_searches_dropdown_content_spec.js | 4 + .../filtered_search/dropdown_user_spec.js | 2 +- .../filtered_search/dropdown_utils_spec.js | 50 +- .../filtered_search_manager_spec.js | 40 +- .../filtered_search_tokenizer_spec.js | 22 +- .../filtered_search_visual_tokens_spec.js | 406 +++++++++++----- spec/javascripts/fixtures/builds.rb | 33 -- spec/javascripts/fixtures/issues.rb | 11 + spec/javascripts/fixtures/jobs.rb | 33 ++ spec/javascripts/fixtures/raw.rb | 6 + spec/javascripts/fixtures/services.rb | 31 ++ .../helpers/filtered_search_spec_helper.js | 13 +- .../integrations/integration_settings_form_spec.js | 199 ++++++++ spec/javascripts/issue_show/components/app_spec.js | 365 ++++++++++++++- .../issue_show/components/edit_actions_spec.js | 147 ++++++ .../issue_show/components/edited_spec.js | 49 ++ .../components/fields/description_spec.js | 76 +++ .../components/fields/description_template_spec.js | 47 ++ .../components/fields/project_move_spec.js | 38 ++ .../issue_show/components/fields/title_spec.js | 66 +++ .../javascripts/issue_show/components/form_spec.js | 72 +++ .../issue_show/components/title_spec.js | 7 + spec/javascripts/issue_show/mock_data.js | 12 +- spec/javascripts/lib/utils/ajax_cache_spec.js | 31 ++ spec/javascripts/lib/utils/common_utils_spec.js | 2 +- spec/javascripts/lib/utils/number_utility_spec.js | 9 +- spec/javascripts/merge_request_spec.js | 4 +- spec/javascripts/merge_request_tabs_spec.js | 45 +- spec/javascripts/notebook/cells/markdown_spec.js | 57 +++ spec/javascripts/notes_spec.js | 138 ++++-- .../pipelines/graph/action_component_spec.js | 6 +- .../graph/dropdown_action_component_spec.js | 4 +- .../pipelines/graph/graph_component_spec.js | 59 ++- .../pipelines/graph/job_component_spec.js | 24 +- .../javascripts/pipelines/header_component_spec.js | 63 +++ .../pipelines/pipeline_details_mediator_spec.js | 41 ++ spec/javascripts/pipelines/pipeline_store_spec.js | 27 ++ spec/javascripts/pipelines/pipeline_url_spec.js | 3 +- spec/javascripts/raven/index_spec.js | 20 +- spec/javascripts/raven/raven_config_spec.js | 60 +-- spec/javascripts/sidebar/sidebar_assignees_spec.js | 12 + spec/javascripts/sidebar/sidebar_store_spec.js | 5 + .../components/mr_widget_memory_usage_spec.js | 51 +- .../states/mr_widget_ready_to_merge_spec.js | 37 +- .../vue_mr_widget/mr_widget_options_spec.js | 37 ++ .../vue_mr_widget/stores/get_state_key_spec.js | 12 +- .../vue_shared/components/commit_spec.js | 4 +- .../components/header_ci_component_spec.js | 93 ++++ .../vue_shared/components/markdown/field_spec.js | 121 +++++ .../vue_shared/components/markdown/header_spec.js | 67 +++ .../components/pipelines_table_row_spec.js | 4 +- .../vue_shared/components/time_ago_tooltip_spec.js | 68 +++ .../ascii_doc_post_processing_filter_spec.rb | 15 + spec/lib/banzai/filter/sanitization_filter_spec.rb | 16 + .../banzai/filter/user_reference_filter_spec.rb | 5 + .../banzai/reference_parser/user_parser_spec.rb | 25 +- spec/lib/feature_spec.rb | 26 ++ spec/lib/gitlab/asciidoc_spec.rb | 25 + .../cache/ci/project_pipeline_status_spec.rb | 4 +- spec/lib/gitlab/chat_commands/command_spec.rb | 7 +- spec/lib/gitlab/chat_commands/deploy_spec.rb | 7 +- spec/lib/gitlab/ci/status/build/common_spec.rb | 2 +- spec/lib/gitlab/ci/status/build/factory_spec.rb | 5 +- spec/lib/gitlab/ci/status/build/play_spec.rb | 10 +- spec/lib/gitlab/ci/trace/stream_spec.rb | 43 +- spec/lib/gitlab/cycle_analytics/events_spec.rb | 10 +- spec/lib/gitlab/database/migration_helpers_spec.rb | 11 +- .../v1/rename_namespaces_spec.rb | 52 +-- .../dependency_linker/cartfile_linker_spec.rb | 74 +++ .../dependency_linker/composer_json_linker_spec.rb | 82 ++++ .../dependency_linker/gemfile_linker_spec.rb | 2 +- .../dependency_linker/gemspec_linker_spec.rb | 66 +++ .../dependency_linker/godeps_json_linker_spec.rb | 84 ++++ .../dependency_linker/package_json_linker_spec.rb | 94 ++++ .../dependency_linker/podfile_linker_spec.rb | 53 +++ .../dependency_linker/podspec_json_linker_spec.rb | 96 ++++ .../dependency_linker/podspec_linker_spec.rb | 69 +++ .../requirements_txt_linker_spec.rb | 87 ++++ spec/lib/gitlab/dependency_linker_spec.rb | 72 +++ spec/lib/gitlab/diff/position_spec.rb | 6 +- spec/lib/gitlab/diff/position_tracer_spec.rb | 315 +++++++++---- spec/lib/gitlab/encoding_helper_spec.rb | 88 ++++ spec/lib/gitlab/etag_caching/router_spec.rb | 37 ++ spec/lib/gitlab/git/diff_collection_spec.rb | 67 ++- spec/lib/gitlab/git/diff_spec.rb | 66 ++- spec/lib/gitlab/git/encoding_helper_spec.rb | 88 ---- spec/lib/gitlab/git/repository_spec.rb | 15 +- spec/lib/gitlab/gitaly_client/diff_spec.rb | 30 ++ .../lib/gitlab/gitaly_client/diff_stitcher_spec.rb | 59 +++ spec/lib/gitlab/gitaly_client_spec.rb | 82 +++- spec/lib/gitlab/group_hierarchy_spec.rb | 53 +++ .../gitlab/health_checks/fs_shards_check_spec.rb | 83 +++- spec/lib/gitlab/highlight_spec.rb | 2 - spec/lib/gitlab/i18n_spec.rb | 32 +- spec/lib/gitlab/import_export/all_models.yml | 1 + .../gitlab/import_export/members_mapper_spec.rb | 8 +- .../gitlab/import_export/safe_model_attributes.yml | 2 + spec/lib/gitlab/o_auth/provider_spec.rb | 42 ++ spec/lib/gitlab/path_regex_spec.rb | 384 +++++++++++++++ spec/lib/gitlab/project_authorizations_spec.rb | 73 +++ spec/lib/gitlab/project_search_results_spec.rb | 4 +- spec/lib/gitlab/regex_spec.rb | 24 - spec/lib/gitlab/sql/recursive_cte_spec.rb | 49 ++ spec/lib/gitlab/url_sanitizer_spec.rb | 9 +- spec/lib/gitlab/utils_spec.rb | 11 +- spec/lib/gitlab/workhorse_spec.rb | 2 +- spec/lib/system_check/simple_executor_spec.rb | 223 +++++++++ spec/lib/system_check_spec.rb | 36 ++ spec/mailers/notify_spec.rb | 9 + spec/migrations/fill_authorized_projects_spec.rb | 18 - spec/migrations/migrate_old_artifacts_spec.rb | 117 +++++ ...ed_groups_into_regular_groups_for_mysql_spec.rb | 66 +++ .../migrations/update_retried_for_ci_build_spec.rb | 17 + .../update_retried_for_ci_builds_spec.rb | 17 - spec/models/blob_viewer/base_spec.rb | 70 +-- spec/models/ci/build_spec.rb | 165 ++++++- spec/models/ci/pipeline_schedule_spec.rb | 8 + spec/models/ci/pipeline_spec.rb | 28 +- spec/models/ci/variable_spec.rb | 33 +- spec/models/commit_status_spec.rb | 10 + spec/models/concerns/discussion_on_diff_spec.rb | 26 ++ spec/models/concerns/editable_spec.rb | 11 + spec/models/concerns/routable_spec.rb | 117 ----- spec/models/cycle_analytics/test_spec.rb | 3 +- spec/models/deployment_spec.rb | 13 + spec/models/diff_discussion_spec.rb | 7 +- spec/models/diff_note_spec.rb | 31 +- spec/models/environment_spec.rb | 27 +- spec/models/group_spec.rb | 2 +- spec/models/hooks/service_hook_spec.rb | 35 +- spec/models/hooks/system_hook_spec.rb | 22 + spec/models/hooks/web_hook_log_spec.rb | 30 ++ spec/models/hooks/web_hook_spec.rb | 93 ++-- spec/models/key_spec.rb | 10 +- spec/models/label_spec.rb | 17 + spec/models/members/project_member_spec.rb | 13 +- spec/models/merge_request_spec.rb | 64 ++- spec/models/milestone_spec.rb | 13 + spec/models/namespace_spec.rb | 22 +- spec/models/project_group_link_spec.rb | 2 +- spec/models/project_services/jira_service_spec.rb | 184 +++++--- .../project_services/kubernetes_service_spec.rb | 35 +- spec/models/project_spec.rb | 139 +++++- spec/models/project_team_spec.rb | 180 ++++--- spec/models/project_wiki_spec.rb | 18 +- spec/models/user_spec.rb | 168 ++++--- spec/policies/group_policy_spec.rb | 32 +- .../metric_presenter_spec.rb | 36 ++ spec/requests/api/commit_statuses_spec.rb | 4 +- spec/requests/api/commits_spec.rb | 5 +- spec/requests/api/features_spec.rb | 104 +++++ spec/requests/api/groups_spec.rb | 2 +- spec/requests/api/internal_spec.rb | 161 +++---- spec/requests/api/pipeline_schedules_spec.rb | 297 ++++++++++++ spec/requests/api/pipelines_spec.rb | 26 +- spec/requests/api/projects_spec.rb | 74 ++- spec/requests/api/users_spec.rb | 24 +- spec/requests/api/v3/commits_spec.rb | 5 +- spec/requests/api/v3/deploy_keys_spec.rb | 9 + spec/requests/api/v3/groups_spec.rb | 2 +- spec/requests/api/v3/projects_spec.rb | 15 +- spec/requests/api/variables_spec.rb | 7 +- .../projects/cycle_analytics_events_spec.rb | 3 +- spec/routing/admin_routing_spec.rb | 12 + spec/routing/project_routing_spec.rb | 16 + spec/routing/routing_spec.rb | 4 + spec/rubocop/cop/activerecord_serialize_spec.rb | 33 ++ .../cop/migration/update_column_in_batches_spec.rb | 94 ++++ spec/serializers/build_action_entity_spec.rb | 12 +- spec/serializers/build_artifact_entity_spec.rb | 22 +- spec/serializers/build_details_entity_spec.rb | 67 +++ spec/serializers/build_entity_spec.rb | 9 +- spec/serializers/merge_request_entity_spec.rb | 2 +- spec/serializers/pipeline_details_entity_spec.rb | 120 +++++ spec/serializers/pipeline_entity_spec.rb | 53 +-- spec/serializers/pipeline_serializer_spec.rb | 2 +- spec/serializers/runner_entity_spec.rb | 23 + spec/serializers/user_entity_spec.rb | 6 + spec/services/ci/create_pipeline_service_spec.rb | 36 +- .../ci/create_trigger_request_service_spec.rb | 2 + spec/services/ci/play_build_service_spec.rb | 17 +- spec/services/ci/process_pipeline_service_spec.rb | 7 +- spec/services/ci/retry_pipeline_service_spec.rb | 7 +- spec/services/create_deployment_service_spec.rb | 246 ++++------ .../update_diff_position_service_spec.rb | 193 ++++++++ spec/services/git_push_service_spec.rb | 14 + spec/services/git_tag_push_service_spec.rb | 14 + spec/services/gravatar_service_spec.rb | 20 + spec/services/issues/close_service_spec.rb | 6 + spec/services/issues/reopen_service_spec.rb | 7 + .../members/authorized_destroy_service_spec.rb | 2 +- spec/services/merge_requests/close_service_spec.rb | 2 + .../conflicts/resolve_service_spec.rb | 42 +- .../services/merge_requests/create_service_spec.rb | 31 ++ .../merge_when_pipeline_succeeds_service_spec.rb | 12 +- .../merge_requests/post_merge_service_spec.rb | 15 + .../services/merge_requests/reopen_service_spec.rb | 2 + .../services/merge_requests/update_service_spec.rb | 11 +- .../notes/diff_position_update_service_spec.rb | 175 ------- spec/services/projects/destroy_service_spec.rb | 2 +- spec/services/projects/import_service_spec.rb | 2 +- spec/services/search_service_spec.rb | 9 + spec/services/submit_usage_ping_service_spec.rb | 101 ++++ spec/services/system_note_service_spec.rb | 81 +++- spec/services/users/destroy_service_spec.rb | 8 +- .../refresh_authorized_projects_service_spec.rb | 66 +-- spec/services/web_hook_service_spec.rb | 137 ++++++ spec/services/wiki_pages/create_service_spec.rb | 40 +- spec/services/wiki_pages/destroy_service_spec.rb | 19 +- spec/services/wiki_pages/update_service_spec.rb | 42 +- spec/spec_helper.rb | 12 +- .../githubish_import_controller_shared_examples.rb | 16 +- spec/support/cycle_analytics_helpers.rb | 43 +- .../issuable_slash_commands_shared_examples.rb | 4 +- spec/support/features/rss_shared_examples.rb | 24 +- spec/support/gitaly.rb | 3 +- spec/support/helpers/key_generator_helper.rb | 41 ++ spec/support/import_spec_helper.rb | 2 +- spec/support/issuable_shared_examples.rb | 7 + spec/support/kubernetes_helpers.rb | 2 +- spec/support/matchers/execute_check.rb | 23 + .../access_control_ce_shared_examples.rb | 4 +- .../access_control_ce_shared_examples.rb | 2 +- spec/support/rake_helpers.rb | 5 + spec/support/snippets_shared_examples.rb | 2 +- spec/support/stub_configuration.rb | 4 + spec/support/target_branch_helpers.rb | 2 +- spec/support/test_env.rb | 15 +- spec/support/time_tracking_shared_examples.rb | 8 +- spec/support/wait_for_ajax.rb | 18 - spec/support/wait_for_requests.rb | 38 +- spec/support/wait_for_vue_resource.rb | 19 - spec/tasks/tokens_spec.rb | 6 + spec/uploaders/artifact_uploader_spec.rb | 38 ++ spec/uploaders/gitlab_uploader_spec.rb | 56 +++ spec/uploaders/lfs_object_uploader_spec.rb | 31 ++ spec/validators/dynamic_path_validator_spec.rb | 252 ++-------- spec/views/ci/status/_badge.html.haml_spec.rb | 2 +- spec/views/projects/blob/_viewer.html.haml_spec.rb | 4 +- .../views/projects/builds/_build.html.haml_spec.rb | 28 -- .../_generic_commit_status.html.haml_spec.rb | 28 -- spec/views/projects/builds/show.html.haml_spec.rb | 293 ------------ spec/views/projects/jobs/_build.html.haml_spec.rb | 28 ++ .../jobs/_generic_commit_status.html.haml_spec.rb | 28 ++ spec/views/projects/jobs/show.html.haml_spec.rb | 293 ++++++++++++ spec/workers/expire_job_cache_worker_spec.rb | 31 ++ spec/workers/expire_pipeline_cache_worker_spec.rb | 2 + spec/workers/gitlab_usage_ping_worker_spec.rb | 18 +- spec/workers/pipeline_schedule_worker_spec.rb | 3 +- spec/workers/process_commit_worker_spec.rb | 20 +- .../remove_old_web_hook_logs_worker_spec.rb | 18 + vendor/assets/javascripts/task_list.js | 258 ---------- yarn.lock | 222 +++++---- 1306 files changed, 25995 insertions(+), 9350 deletions(-) create mode 100644 .codeclimate.yml create mode 100644 app/assets/images/i2p-step.svg create mode 100644 app/assets/javascripts/environments/mixins/environments_mixin.js create mode 100644 app/assets/javascripts/integrations/index.js create mode 100644 app/assets/javascripts/integrations/integration_settings_form.js create mode 100644 app/assets/javascripts/issue_show/components/edit_actions.vue create mode 100644 app/assets/javascripts/issue_show/components/edited.vue create mode 100644 app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue create mode 100644 app/assets/javascripts/issue_show/components/fields/description.vue create mode 100644 app/assets/javascripts/issue_show/components/fields/description_template.vue create mode 100644 app/assets/javascripts/issue_show/components/fields/project_move.vue create mode 100644 app/assets/javascripts/issue_show/components/fields/title.vue create mode 100644 app/assets/javascripts/issue_show/components/form.vue create mode 100644 app/assets/javascripts/issue_show/components/locked_warning.vue create mode 100644 app/assets/javascripts/issue_show/event_hub.js create mode 100644 app/assets/javascripts/issue_show/mixins/update.js create mode 100644 app/assets/javascripts/pipelines/components/header_component.vue delete mode 100644 app/assets/javascripts/pipelines/components/pipeline_url.js create mode 100644 app/assets/javascripts/pipelines/components/pipeline_url.vue delete mode 100644 app/assets/javascripts/pipelines/graph_bundle.js create mode 100644 app/assets/javascripts/pipelines/pipeline_details_bundle.js create mode 100644 app/assets/javascripts/pipelines/pipeline_details_mediatior.js create mode 100644 app/assets/javascripts/vue_shared/components/header_ci_component.vue create mode 100644 app/assets/javascripts/vue_shared/components/markdown/field.vue create mode 100644 app/assets/javascripts/vue_shared/components/markdown/header.vue create mode 100644 app/assets/javascripts/vue_shared/components/markdown/toolbar.vue create mode 100644 app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue create mode 100644 app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue create mode 100644 app/assets/javascripts/vue_shared/mixins/timeago.js create mode 100644 app/assets/stylesheets/framework/notes.scss create mode 100644 app/assets/stylesheets/pages/convdev_index.scss delete mode 100644 app/controllers/admin/builds_controller.rb create mode 100644 app/controllers/admin/conversational_development_index_controller.rb create mode 100644 app/controllers/admin/hook_logs_controller.rb create mode 100644 app/controllers/admin/jobs_controller.rb create mode 100644 app/controllers/concerns/hooks_execution.rb create mode 100644 app/controllers/projects/build_artifacts_controller.rb create mode 100644 app/controllers/projects/hook_logs_controller.rb create mode 100644 app/controllers/projects/jobs_controller.rb create mode 100644 app/helpers/conversational_development_index_helper.rb create mode 100644 app/models/concerns/editable.rb create mode 100644 app/models/conversational_development_index/card.rb create mode 100644 app/models/conversational_development_index/idea_to_production_step.rb create mode 100644 app/models/conversational_development_index/metric.rb create mode 100644 app/models/hooks/web_hook_log.rb create mode 100644 app/presenters/conversational_development_index/metric_presenter.rb create mode 100644 app/serializers/build_details_entity.rb create mode 100644 app/serializers/pipeline_details_entity.rb create mode 100644 app/serializers/runner_entity.rb create mode 100644 app/services/discussions/update_diff_position_service.rb delete mode 100644 app/services/notes/diff_position_update_service.rb create mode 100644 app/services/submit_usage_ping_service.rb create mode 100644 app/services/web_hook_service.rb delete mode 100644 app/views/admin/background_jobs/_head.html.haml delete mode 100644 app/views/admin/builds/index.html.haml create mode 100644 app/views/admin/conversational_development_index/_callout.html.haml create mode 100644 app/views/admin/conversational_development_index/_card.html.haml create mode 100644 app/views/admin/conversational_development_index/_disabled.html.haml create mode 100644 app/views/admin/conversational_development_index/_no_data.html.haml create mode 100644 app/views/admin/conversational_development_index/show.html.haml create mode 100644 app/views/admin/hook_logs/_index.html.haml create mode 100644 app/views/admin/hook_logs/show.html.haml create mode 100644 app/views/admin/jobs/index.html.haml create mode 100644 app/views/admin/monitoring/_head.html.haml delete mode 100644 app/views/events/_event_last_push.html.haml create mode 100644 app/views/profiles/accounts/_reset_token.html.haml delete mode 100644 app/views/projects/builds/_header.html.haml delete mode 100644 app/views/projects/builds/_sidebar.html.haml delete mode 100644 app/views/projects/builds/_table.html.haml delete mode 100644 app/views/projects/builds/_user.html.haml delete mode 100644 app/views/projects/builds/index.html.haml delete mode 100644 app/views/projects/builds/show.html.haml delete mode 100644 app/views/projects/diffs/_image.html.haml create mode 100644 app/views/projects/diffs/viewers/_image.html.haml create mode 100644 app/views/projects/diffs/viewers/_text.html.haml create mode 100644 app/views/projects/hook_logs/_index.html.haml create mode 100644 app/views/projects/hook_logs/show.html.haml create mode 100644 app/views/projects/jobs/_header.html.haml create mode 100644 app/views/projects/jobs/_sidebar.html.haml create mode 100644 app/views/projects/jobs/_table.html.haml create mode 100644 app/views/projects/jobs/_user.html.haml create mode 100644 app/views/projects/jobs/index.html.haml create mode 100644 app/views/projects/jobs/show.html.haml create mode 100644 app/views/shared/hook_logs/_content.html.haml create mode 100644 app/views/shared/hook_logs/_status_label.html.haml create mode 100644 app/views/shared/icons/_convdev_no_data.svg create mode 100644 app/views/shared/icons/_convdev_no_index.svg create mode 100644 app/views/shared/icons/_convdev_overview.svg create mode 100644 app/views/shared/icons/_i2p_step_1.svg create mode 100644 app/views/shared/icons/_i2p_step_10.svg create mode 100644 app/views/shared/icons/_i2p_step_2.svg create mode 100644 app/views/shared/icons/_i2p_step_3.svg create mode 100644 app/views/shared/icons/_i2p_step_4.svg create mode 100644 app/views/shared/icons/_i2p_step_5.svg create mode 100644 app/views/shared/icons/_i2p_step_6.svg create mode 100644 app/views/shared/icons/_i2p_step_7.svg create mode 100644 app/views/shared/icons/_i2p_step_8.svg create mode 100644 app/views/shared/icons/_i2p_step_9.svg delete mode 100644 app/views/shared/icons/_scroll_down_hover_active.svg delete mode 100644 app/views/shared/icons/_scroll_up_hover_active.svg create mode 100644 app/views/shared/issuable/_user_dropdown_item.html.haml create mode 100644 app/workers/expire_job_cache_worker.rb delete mode 100644 app/workers/project_web_hook_worker.rb create mode 100644 app/workers/remove_old_web_hook_logs_worker.rb delete mode 100644 app/workers/system_hook_worker.rb create mode 100644 app/workers/web_hook_worker.rb create mode 100644 changelogs/unreleased/10378-promote-blameless-culture.yml create mode 100644 changelogs/unreleased/12614-fix-long-message-from-mr.yml create mode 100644 changelogs/unreleased/18927-reorder-issue-action-buttons.yml create mode 100644 changelogs/unreleased/19107-404-when-creating-new-milestone-or-issue-for-project-that-has-issues-disabled.yml create mode 100644 changelogs/unreleased/23036-replace-all-spinach-tests-with-rspec-feature-tests.yml create mode 100644 changelogs/unreleased/24196-protected-variables.yml create mode 100644 changelogs/unreleased/25373-jira-links.yml create mode 100644 changelogs/unreleased/25680-CI_ENVIRONMENT_URL.yml create mode 100644 changelogs/unreleased/27439-memory-usage-info.yml create mode 100644 changelogs/unreleased/27614-improve-instant-comments-exp.yml create mode 100644 changelogs/unreleased/28080-system-checks.yml create mode 100644 changelogs/unreleased/28694-hard-delete-user-from-api.yml create mode 100644 changelogs/unreleased/29852-latex-formatting.yml create mode 100644 changelogs/unreleased/30410-revert-9347-and-10079.yml create mode 100644 changelogs/unreleased/30469-convdev-index.yml create mode 100644 changelogs/unreleased/30651-improve-container-registry-description.yml create mode 100644 changelogs/unreleased/30892-add-api-support-for-pipeline-schedule.yml create mode 100644 changelogs/unreleased/30917-wiki-is-not-searchable-with-guest-permissions.yml create mode 100644 changelogs/unreleased/31061-26135-ci-project-slug-enviroment-variables.yml create mode 100644 changelogs/unreleased/31448-jira-urls.yml create mode 100644 changelogs/unreleased/31511-jira-settings.yml create mode 100644 changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml create mode 100644 changelogs/unreleased/31602-display-whether-shared-runner-is-enabled-in-the-admin-dashboard.yml create mode 100644 changelogs/unreleased/31616-add-uptime-of-gitlab-instance-in-admin-area.yml create mode 100644 changelogs/unreleased/31644-make-cookie-sessions-unique.yml create mode 100644 changelogs/unreleased/31757-single-click-on-filter-in-search-bar-to-activate-dropdown.yml create mode 100644 changelogs/unreleased/31849-pipeline-real-time-header.yml create mode 100644 changelogs/unreleased/31849-pipeline-show-view-realtime.yml create mode 100644 changelogs/unreleased/31943-document-go-183.yml create mode 100644 changelogs/unreleased/31983-increase-merge-request-diff-file-size-limit-for-default-toggle-opening.yml create mode 100644 changelogs/unreleased/32118-new-environment-btn-copy.yml delete mode 100644 changelogs/unreleased/32486-fix-note-emoji-placement.yml create mode 100644 changelogs/unreleased/32682-skipped-ci-icon.yml create mode 100644 changelogs/unreleased/32715-fix-note-padding.yml create mode 100644 changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml create mode 100644 changelogs/unreleased/32799-remove-no_turbolink-attribute-from-haml.yml create mode 100644 changelogs/unreleased/32807-company-icon.yml create mode 100644 changelogs/unreleased/32832-confidential-issue-overflow.yml create mode 100644 changelogs/unreleased/32851-postgres-min-version.yml create mode 100644 changelogs/unreleased/32983-merge-conflict-resolution-removed-the-newline-in-the-end-of-file.yml create mode 100644 changelogs/unreleased/32992-consider-using-zopfli-over-standard-gzip-compression-for-webpack-assets.yml create mode 100644 changelogs/unreleased/33000-tag-list-in-project-create-api.yml create mode 100644 changelogs/unreleased/33032-invalid-you-directly-addressed-yourself-todo-when-using-unsubscribe.yml create mode 100644 changelogs/unreleased/33048-markdown-rendering-of-md-files-has-ceased-to-display-latex-equations.yml create mode 100644 changelogs/unreleased/33154-permissions-for-project-labels-and-group-labels.yml create mode 100644 changelogs/unreleased/33207-show-delete-option-in-admin-users-page.yml create mode 100644 changelogs/unreleased/33215-fix-hard-delete-of-users.yml create mode 100644 changelogs/unreleased/33242-create-project-for-user-api-ignores-path-parameter.yml create mode 100644 changelogs/unreleased/UI-improvements-for-count-badges-and-permission-badges.yml create mode 100644 changelogs/unreleased/aliyun-backup-provider.yml create mode 100644 changelogs/unreleased/bugfix-v3-deploy_keys-can_push.yml create mode 100644 changelogs/unreleased/ci-build-pipeline-header-vue.yml create mode 100644 changelogs/unreleased/dm-comment-on-mr-commit-discussion.yml create mode 100644 changelogs/unreleased/dm-consistent-last-push-event.yml create mode 100644 changelogs/unreleased/dm-copy-as-gfm-without-empty-elements.yml create mode 100644 changelogs/unreleased/dm-copy-gfm-when-parts-of-other-elements-are-selected.yml create mode 100644 changelogs/unreleased/dm-discussions-n-plus-1.yml create mode 100644 changelogs/unreleased/dm-emails-are-not-user-references.yml create mode 100644 changelogs/unreleased/dm-fix-jump-button.yml create mode 100644 changelogs/unreleased/dm-gitmodules-parsing.yml create mode 100644 changelogs/unreleased/dm-gravatar-username.yml create mode 100644 changelogs/unreleased/dm-more-dependency-linkers.yml create mode 100644 changelogs/unreleased/dm-oauth-config-for.yml create mode 100644 changelogs/unreleased/dm-outdated-system-note.yml create mode 100644 changelogs/unreleased/dm-paste-code-inside-gfm-code.yml create mode 100644 changelogs/unreleased/feature-flags-flipper.yml create mode 100644 changelogs/unreleased/feature-rss-scoped-token.yml create mode 100644 changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml create mode 100644 changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml delete mode 100644 changelogs/unreleased/fix-migration-for-postgres.yml create mode 100644 changelogs/unreleased/fix-n-plus-one-queries-for-user-access.yml create mode 100644 changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml create mode 100644 changelogs/unreleased/fix_diff_line_comments.yml create mode 100644 changelogs/unreleased/gitaly-opt-out.yml create mode 100644 changelogs/unreleased/introduce-source-to-pipelines.yml create mode 100644 changelogs/unreleased/issuable-form-create-label-sub-groups.yml create mode 100644 changelogs/unreleased/issue-edit-inline.yml create mode 100644 changelogs/unreleased/issue-template-reproduce-in-example-project.yml create mode 100644 changelogs/unreleased/issue_19262.yml create mode 100644 changelogs/unreleased/issue_32225_2.yml create mode 100644 changelogs/unreleased/jouve-gitlab-ce-admin_keys.yml create mode 100644 changelogs/unreleased/mabes-gitlab-ce-bypass-auto-login.yml create mode 100644 changelogs/unreleased/migrate-artifacts-to-a-new-path.yml create mode 100644 changelogs/unreleased/mrchrisw-catch-openssl.yml create mode 100644 changelogs/unreleased/projects-api-import-status.yml create mode 100644 changelogs/unreleased/rename-builds-controller.yml create mode 100644 changelogs/unreleased/rework-authorizations-performance.yml create mode 100644 changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml create mode 100644 changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml create mode 100644 changelogs/unreleased/task-list-2.yml create mode 100644 changelogs/unreleased/tc-improve-project-api-perf.yml create mode 100644 changelogs/unreleased/wait-for-ajax-handling-all-js-requests.yml create mode 100644 changelogs/unreleased/winh-current-user-filter.yml create mode 100644 changelogs/unreleased/winh-styled-people-search-bar.yml create mode 100644 changelogs/unreleased/zj-drop-fk-if-exists.yml create mode 100644 changelogs/unreleased/zj-fix-pipeline-etag.yml create mode 100644 changelogs/unreleased/zj-job-view-goes-real-time.yml create mode 100644 changelogs/unreleased/zj-realtime-env-list.yml create mode 100644 config/initializers/0_acts_as_taggable.rb create mode 100644 config/initializers/active_record_locking.rb create mode 100644 config/initializers/active_record_preloader.rb delete mode 100644 config/initializers/acts_as_taggable.rb delete mode 100644 config/initializers/ar_monkey_patch.rb create mode 100644 config/initializers/forbid_sidekiq_in_transactions.rb create mode 100644 config/initializers/postgresql_cte.rb create mode 100644 config/initializers/server_uptime.rb create mode 100644 db/fixtures/development/21_conversational_development_index_metrics.rb create mode 100644 db/migrate/20170427103502_create_web_hook_logs.rb create mode 100644 db/migrate/20170503140201_reschedule_project_authorizations.rb create mode 100644 db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb create mode 100644 db/migrate/20170504182103_add_index_project_group_links_group_id.rb create mode 100644 db/migrate/20170521184006_add_change_position_to_notes.rb create mode 100644 db/migrate/20170523091700_add_rss_token_to_users.rb create mode 100644 db/migrate/20170523121229_create_conversational_development_index_metrics.rb create mode 100644 db/migrate/20170524125940_add_source_to_ci_pipeline.rb create mode 100644 db/migrate/20170524161101_add_protected_to_ci_variables.rb create mode 100644 db/migrate/20170525174156_create_feature_tables.rb create mode 100644 db/post_migrate/20170503120310_remove_users_authorized_projects_populated.rb create mode 100644 db/post_migrate/20170523083112_migrate_old_artifacts.rb create mode 100644 doc/api/features.md create mode 100644 doc/api/pipeline_schedules.md create mode 100644 doc/development/feature_flags.md create mode 100644 doc/development/serializing_data.md create mode 100644 doc/update/9.2-to-9.3.md delete mode 100644 doc/user/project/img/container_registry_panel.png create mode 100755 doc/user/project/integrations/img/webhook_logs.png create mode 100644 doc/user/project/milestones/img/progress.png delete mode 100644 features/dashboard/starred_projects.feature delete mode 100644 features/project/hooks.feature delete mode 100644 features/steps/project/hooks.rb create mode 100644 lib/api/features.rb create mode 100644 lib/api/pipeline_schedules.rb create mode 100644 lib/banzai/filter/ascii_doc_post_processing_filter.rb create mode 100644 lib/banzai/pipeline/ascii_doc_pipeline.rb create mode 100644 lib/feature.rb create mode 100644 lib/gitlab/dependency_linker/cartfile_linker.rb create mode 100644 lib/gitlab/dependency_linker/cocoapods.rb create mode 100644 lib/gitlab/dependency_linker/composer_json_linker.rb create mode 100644 lib/gitlab/dependency_linker/gemspec_linker.rb create mode 100644 lib/gitlab/dependency_linker/godeps_json_linker.rb create mode 100644 lib/gitlab/dependency_linker/json_linker.rb create mode 100644 lib/gitlab/dependency_linker/method_linker.rb create mode 100644 lib/gitlab/dependency_linker/package_json_linker.rb create mode 100644 lib/gitlab/dependency_linker/podfile_linker.rb create mode 100644 lib/gitlab/dependency_linker/podspec_json_linker.rb create mode 100644 lib/gitlab/dependency_linker/podspec_linker.rb create mode 100644 lib/gitlab/dependency_linker/requirements_txt_linker.rb create mode 100644 lib/gitlab/encoding_helper.rb delete mode 100644 lib/gitlab/git/encoding_helper.rb create mode 100644 lib/gitlab/gitaly_client/diff.rb create mode 100644 lib/gitlab/gitaly_client/diff_stitcher.rb create mode 100644 lib/gitlab/group_hierarchy.rb create mode 100644 lib/gitlab/path_regex.rb create mode 100644 lib/gitlab/project_authorizations/with_nested_groups.rb create mode 100644 lib/gitlab/project_authorizations/without_nested_groups.rb create mode 100644 lib/gitlab/routes/legacy_builds.rb create mode 100644 lib/gitlab/sql/recursive_cte.rb create mode 100644 lib/system_check.rb create mode 100644 lib/system_check/app/active_users_check.rb create mode 100644 lib/system_check/app/database_config_exists_check.rb create mode 100644 lib/system_check/app/git_config_check.rb create mode 100644 lib/system_check/app/git_version_check.rb create mode 100644 lib/system_check/app/gitlab_config_exists_check.rb create mode 100644 lib/system_check/app/gitlab_config_up_to_date_check.rb create mode 100644 lib/system_check/app/init_script_exists_check.rb create mode 100644 lib/system_check/app/init_script_up_to_date_check.rb create mode 100644 lib/system_check/app/log_writable_check.rb create mode 100644 lib/system_check/app/migrations_are_up_check.rb create mode 100644 lib/system_check/app/orphaned_group_members_check.rb create mode 100644 lib/system_check/app/projects_have_namespace_check.rb create mode 100644 lib/system_check/app/redis_version_check.rb create mode 100644 lib/system_check/app/ruby_version_check.rb create mode 100644 lib/system_check/app/tmp_writable_check.rb create mode 100644 lib/system_check/app/uploads_directory_exists_check.rb create mode 100644 lib/system_check/app/uploads_path_permission_check.rb create mode 100644 lib/system_check/app/uploads_path_tmp_permission_check.rb create mode 100644 lib/system_check/base_check.rb create mode 100644 lib/system_check/helpers.rb create mode 100644 lib/system_check/simple_executor.rb create mode 100644 rubocop/cop/activerecord_serialize.rb create mode 100644 rubocop/cop/migration/update_column_in_batches.rb delete mode 100644 spec/controllers/projects/builds_controller_spec.rb create mode 100644 spec/controllers/projects/jobs_controller_spec.rb create mode 100644 spec/factories/conversational_development_index_metrics.rb delete mode 100644 spec/factories/file_uploader.rb create mode 100644 spec/factories/file_uploaders.rb create mode 100644 spec/factories/web_hook_log.rb create mode 100644 spec/features/admin/admin_conversational_development_index_spec.rb create mode 100644 spec/features/admin/admin_hook_logs_spec.rb create mode 100644 spec/features/projects/artifacts/browse_spec.rb create mode 100644 spec/features/projects/artifacts/download_spec.rb create mode 100644 spec/features/projects/artifacts/raw_spec.rb create mode 100644 spec/features/projects/jobs_spec.rb create mode 100644 spec/features/projects/services/jira_service_spec.rb create mode 100644 spec/features/projects/sub_group_issuables_spec.rb create mode 100644 spec/fixtures/api/schemas/pipeline_schedule.json create mode 100644 spec/fixtures/api/schemas/pipeline_schedules.json create mode 100644 spec/javascripts/copy_as_gfm_spec.js create mode 100644 spec/javascripts/droplab/plugins/ajax_filter_spec.js delete mode 100644 spec/javascripts/fixtures/builds.rb create mode 100644 spec/javascripts/fixtures/jobs.rb create mode 100644 spec/javascripts/fixtures/services.rb create mode 100644 spec/javascripts/integrations/integration_settings_form_spec.js create mode 100644 spec/javascripts/issue_show/components/edit_actions_spec.js create mode 100644 spec/javascripts/issue_show/components/edited_spec.js create mode 100644 spec/javascripts/issue_show/components/fields/description_spec.js create mode 100644 spec/javascripts/issue_show/components/fields/description_template_spec.js create mode 100644 spec/javascripts/issue_show/components/fields/project_move_spec.js create mode 100644 spec/javascripts/issue_show/components/fields/title_spec.js create mode 100644 spec/javascripts/issue_show/components/form_spec.js create mode 100644 spec/javascripts/pipelines/header_component_spec.js create mode 100644 spec/javascripts/pipelines/pipeline_details_mediator_spec.js create mode 100644 spec/javascripts/pipelines/pipeline_store_spec.js create mode 100644 spec/javascripts/vue_shared/components/header_ci_component_spec.js create mode 100644 spec/javascripts/vue_shared/components/markdown/field_spec.js create mode 100644 spec/javascripts/vue_shared/components/markdown/header_spec.js create mode 100644 spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js create mode 100644 spec/lib/banzai/filter/ascii_doc_post_processing_filter_spec.rb create mode 100644 spec/lib/feature_spec.rb create mode 100644 spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb create mode 100644 spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb create mode 100644 spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb create mode 100644 spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb create mode 100644 spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb create mode 100644 spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb create mode 100644 spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb create mode 100644 spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb create mode 100644 spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb create mode 100644 spec/lib/gitlab/encoding_helper_spec.rb delete mode 100644 spec/lib/gitlab/git/encoding_helper_spec.rb create mode 100644 spec/lib/gitlab/gitaly_client/diff_spec.rb create mode 100644 spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb create mode 100644 spec/lib/gitlab/group_hierarchy_spec.rb create mode 100644 spec/lib/gitlab/o_auth/provider_spec.rb create mode 100644 spec/lib/gitlab/path_regex_spec.rb create mode 100644 spec/lib/gitlab/project_authorizations_spec.rb create mode 100644 spec/lib/gitlab/sql/recursive_cte_spec.rb create mode 100644 spec/lib/system_check/simple_executor_spec.rb create mode 100644 spec/lib/system_check_spec.rb delete mode 100644 spec/migrations/fill_authorized_projects_spec.rb create mode 100644 spec/migrations/migrate_old_artifacts_spec.rb create mode 100644 spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb create mode 100644 spec/migrations/update_retried_for_ci_build_spec.rb delete mode 100644 spec/migrations/update_retried_for_ci_builds_spec.rb create mode 100644 spec/models/concerns/editable_spec.rb create mode 100644 spec/models/hooks/web_hook_log_spec.rb create mode 100644 spec/presenters/conversational_development_index/metric_presenter_spec.rb create mode 100644 spec/requests/api/features_spec.rb create mode 100644 spec/requests/api/pipeline_schedules_spec.rb create mode 100644 spec/rubocop/cop/activerecord_serialize_spec.rb create mode 100644 spec/rubocop/cop/migration/update_column_in_batches_spec.rb create mode 100644 spec/serializers/build_details_entity_spec.rb create mode 100644 spec/serializers/pipeline_details_entity_spec.rb create mode 100644 spec/serializers/runner_entity_spec.rb create mode 100644 spec/services/discussions/update_diff_position_service_spec.rb create mode 100644 spec/services/gravatar_service_spec.rb create mode 100644 spec/services/merge_requests/post_merge_service_spec.rb delete mode 100644 spec/services/notes/diff_position_update_service_spec.rb create mode 100644 spec/services/submit_usage_ping_service_spec.rb create mode 100644 spec/services/web_hook_service_spec.rb create mode 100644 spec/support/helpers/key_generator_helper.rb create mode 100644 spec/support/issuable_shared_examples.rb create mode 100644 spec/support/matchers/execute_check.rb delete mode 100644 spec/support/wait_for_ajax.rb delete mode 100644 spec/support/wait_for_vue_resource.rb create mode 100644 spec/uploaders/artifact_uploader_spec.rb create mode 100644 spec/uploaders/gitlab_uploader_spec.rb create mode 100644 spec/uploaders/lfs_object_uploader_spec.rb delete mode 100644 spec/views/projects/builds/_build.html.haml_spec.rb delete mode 100644 spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb delete mode 100644 spec/views/projects/builds/show.html.haml_spec.rb create mode 100644 spec/views/projects/jobs/_build.html.haml_spec.rb create mode 100644 spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb create mode 100644 spec/views/projects/jobs/show.html.haml_spec.rb create mode 100644 spec/workers/expire_job_cache_worker_spec.rb create mode 100644 spec/workers/remove_old_web_hook_logs_worker_spec.rb delete mode 100644 vendor/assets/javascripts/task_list.js diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000000..e5636a13783 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,38 @@ +--- +engines: + brakeman: + enabled: true + bundler-audit: + enabled: true + duplication: + enabled: true + config: + languages: + - ruby + - javascript + eslint: + enabled: true + fixme: + enabled: true + rubocop: + enabled: true +ratings: + paths: + - Gemfile.lock + - "**.erb" + - "**.haml" + - "**.rb" + - "**.rhtml" + - "**.slim" + - "**.inc" + - "**.js" + - "**.jsx" + - "**.module" +exclude_paths: +- config/ +- db/ +- features/ +- node_modules/ +- spec/ +- vendor/ +- lib/api/v3/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 638553d7bf7..b442e48a3d0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -150,6 +150,7 @@ stages: # Trigger a package build on omnibus-gitlab repository build-package: + image: ruby:2.3-alpine before_script: [] services: [] variables: @@ -429,6 +430,7 @@ gitlab:assets:compile: USE_DB: "false" SKIP_STORAGE_VALIDATION: "true" WEBPACK_REPORT: "true" + NO_COMPRESSION: "true" script: - yarn install --pure-lockfile --production --cache-folder .yarn-cache - bundle exec rake gitlab:assets:compile @@ -486,25 +488,6 @@ lint:javascript:report: paths: - eslint-report.html -# Trigger docs build -# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process -trigger_docs: - stage: post-test - image: "alpine" - <<: *dedicated-runner - before_script: - - apk update && apk add curl - variables: - GIT_STRATEGY: "none" - cache: {} - artifacts: {} - script: - - "HTTP_STATUS=$(curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=${CI_PROJECT_NAME} --silent --output curl.log --write-out '%{http_code}' https://gitlab.com/api/v3/projects/1794617/trigger/builds)" - - if [ "${HTTP_STATUS}" -ne "201" ]; then echo "Error ${HTTP_STATUS}"; cat curl.log; echo; exit 1; fi - only: - - master@gitlab-org/gitlab-ce - - master@gitlab-org/gitlab-ee - pages: before_script: [] stage: pages diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index 58af062e75e..9d53a48409a 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -20,6 +20,12 @@ Please remove this notice if you're confident your issue isn't a duplicate. (How one can reproduce the issue - this is very important) +### Example Project + +(If possible, please create an example project here on GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report) + +(If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version) + ### What is the current *bug* behavior? (What actually happens) diff --git a/.rubocop.yml b/.rubocop.yml index 3cdafd96456..8f611a96702 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -390,6 +390,15 @@ Style/OpMethod: Style/ParenthesesAroundCondition: Enabled: true +# This cop (by default) checks for uses of methods Hash#has_key? and +# Hash#has_value? where it enforces Hash#key? and Hash#value? +# It is configurable to enforce the inverse, using `verbose` method +# names also. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: short, verbose +Style/PreferredHashMethods: + Enabled: true + # Checks for an obsolete RuntimeException argument in raise/fail. Style/RedundantException: Enabled: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index cf30f5728c0..e2d9c37479d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -236,13 +236,6 @@ Style/PerlBackrefs: Style/PredicateName: Enabled: false -# Offense count: 45 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: short, verbose -Style/PreferredHashMethods: - Enabled: false - # Offense count: 65 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. diff --git a/.scss-lint.yml b/.scss-lint.yml index 83c68309fa8..db234ad739c 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -11,11 +11,11 @@ linters: # !global, !important, and !optional flags. BangFormat: enabled: false - + # Whether or not to prefer `border: 0` over `border: none`. BorderZero: enabled: false - + # Reports when you define a rule set using a selector with chained classes # (a.k.a. adjoining classes). ChainedClasses: @@ -25,13 +25,13 @@ linters: # (e.g. `color: green` is a color keyword) ColorKeyword: enabled: false - + # Prefer color literals (keywords or hexadecimal codes) to be used only in # variable declarations. They should be referred to via variables everywhere # else. ColorVariable: enabled: true - + # Which form of comments to prefer in CSS. Comment: enabled: false @@ -39,7 +39,7 @@ linters: # Reports @debug statements (which you probably left behind accidentally). DebugStatement: enabled: false - + # Rule sets should be ordered as follows: # - @extend declarations # - @include declarations without inner @content @@ -54,19 +54,19 @@ linters: # more information. DisableLinterReason: enabled: true - + # Reports when you define the same property twice in a single rule set. DuplicateProperty: - enabled: false - + enabled: true + # Separate rule, function, and mixin declarations with empty lines. EmptyLineBetweenBlocks: enabled: true - + # Reports when you have an empty rule set. EmptyRule: enabled: true - + # Reports when you have an @extend directive. ExtendDirective: enabled: false @@ -75,49 +75,49 @@ linters: # when adding lines to the file, since SCM systems such as git won't # think that you touched the last line. FinalNewline: - enabled: false - + enabled: true + # HEX colors should use three-character values where possible. HexLength: enabled: false - + # HEX color values should use lower-case colors to differentiate between # letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`. HexNotation: enabled: true - + # Avoid using ID selectors. IdSelector: enabled: false - + # The basenames of @imported SCSS partials should not begin with an # underscore and should not include the filename extension. ImportPath: enabled: false - + # Avoid using !important in properties. It is usually indicative of a # misunderstanding of CSS specificity and can lead to brittle code. ImportantRule: enabled: false - + # Indentation should always be done in increments of 2 spaces. Indentation: enabled: true width: 2 - + # Don't write leading zeros for numeric values with a decimal point. LeadingZero: enabled: false - + # Reports when you define the same selector twice in a single sheet. MergeableSelector: enabled: false - + # Functions, mixins, variables, and placeholders should be declared # with all lowercase letters and hyphens instead of underscores. NameFormat: enabled: false - + # Avoid nesting selectors too deeply. NestingDepth: enabled: false @@ -129,12 +129,12 @@ linters: # Sort properties in a strict order. PropertySortOrder: enabled: false - + # Reports when you use an unknown or disabled CSS property # (ignoring vendor-prefixed properties). PropertySpelling: enabled: false - + # Configure which units are allowed for property values. PropertyUnits: enabled: false @@ -144,25 +144,25 @@ linters: # be declared with one colon. PseudoElement: enabled: true - + # Avoid qualifying elements in selectors (also known as "tag-qualifying"). QualifyingElement: enabled: false - + # Don't write selectors with a depth of applicability greater than 3. SelectorDepth: enabled: false - + # Selectors should always use hyphenated-lowercase, rather than camelCase or # snake_case. SelectorFormat: enabled: false convention: hyphenated_lowercase - + # Prefer the shortest shorthand form possible for properties that support it. Shorthand: enabled: true - + # Each property should have its own line, except in the special case of # single line rulesets. SingleLinePerProperty: @@ -173,11 +173,11 @@ linters: # individual selector occupy a single line. SingleLinePerSelector: enabled: true - + # Commas in lists should be followed by a space. SpaceAfterComma: enabled: false - + # Properties should be formatted with a single space separating the colon # from the property's value. SpaceAfterPropertyColon: @@ -197,12 +197,12 @@ linters: # colon. SpaceAfterVariableName: enabled: false - + # Operators should be formatted with a single space on both sides of an # infix operator. SpaceAroundOperator: enabled: true - + # Opening braces should be preceded by a single space. SpaceBeforeBrace: enabled: true @@ -210,7 +210,7 @@ linters: # Parentheses should not be padded with spaces. SpaceBetweenParens: enabled: false - + # Enforces that string literals should be written with a consistent form # of quotes (single or double). StringQuotes: @@ -241,7 +241,7 @@ linters: # be unnecessary. UnnecessaryParentReference: enabled: false - + # URLs should be valid and not contain protocols or domain names. UrlFormat: enabled: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 65d3a02d68f..4e6d8d398a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.2.1 (2017-05-23) + +- Fix placement of note emoji on hover. +- Fix migration for older PostgreSQL versions. + ## 9.2.0 (2017-05-22) - API: Filter merge requests by milestone and labels. (10924) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 50e2274e6d3..ab0fa336dd0 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -5.0.3 +5.0.5 diff --git a/Gemfile b/Gemfile index 9efb362e494..56f5a8f6a41 100644 --- a/Gemfile +++ b/Gemfile @@ -97,6 +97,7 @@ gem 'fog-google', '~> 0.5' gem 'fog-local', '~> 0.3' gem 'fog-openstack', '~> 0.1' gem 'fog-rackspace', '~> 0.1.1' +gem 'fog-aliyun', '~> 0.1.0' # for Google storage gem 'google-api-client', '~> 0.8.6' @@ -109,7 +110,7 @@ gem 'seed-fu', '~> 2.3.5' # Markdown and HTML processing gem 'html-pipeline', '~> 1.11.0' -gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' +gem 'deckar01-task_list', '2.0.0' gem 'gitlab-markup', '~> 1.5.1' gem 'redcarpet', '~> 3.4' gem 'RedCloth', '~> 4.3.2' @@ -367,6 +368,10 @@ gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' # Gitaly GRPC client -gem 'gitaly', '~> 0.7.0' +gem 'gitaly', '~> 0.8.0' gem 'toml-rb', '~> 0.3.15', require: false + +# Feature toggles +gem 'flipper', '~> 0.10.2' +gem 'flipper-active_record', '~> 0.10.2' diff --git a/Gemfile.lock b/Gemfile.lock index 873cd8781ef..be1f6555851 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -141,10 +141,8 @@ GEM database_cleaner (1.5.3) debug_inspector (0.0.2) debugger-ruby_core_source (1.3.8) - deckar01-task_list (1.0.6) - activesupport (~> 4.0) + deckar01-task_list (2.0.0) html-pipeline - rack (~> 1.0) default_value_for (3.0.2) activerecord (>= 3.2.0, < 5.1) descendants_tracker (0.0.4) @@ -208,9 +206,18 @@ GEM path_expander (~> 1.0) ruby_parser (~> 3.0) sexp_processor (~> 4.0) + flipper (0.10.2) + flipper-active_record (0.10.2) + activerecord (>= 3.2, < 6) + flipper (~> 0.10.2) flowdock (0.7.1) httparty (~> 0.7) multi_json + fog-aliyun (0.1.0) + fog-core (~> 1.27) + fog-json (~> 1.0) + ipaddress (~> 0.8) + xml-simple (~> 1.1) fog-aws (0.13.0) fog-core (~> 1.38) fog-json (~> 1.0) @@ -265,7 +272,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly (0.7.0) + gitaly (0.8.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -499,11 +506,10 @@ GEM omniauth (~> 1.0) omniauth-oauth2 (~> 1.0) omniauth-google-oauth2 (0.4.1) - addressable (~> 2.3) - jwt (~> 1.0) + jwt (~> 1.5.2) multi_json (~> 1.3) omniauth (>= 1.1.1) - omniauth-oauth2 (~> 1.3.1) + omniauth-oauth2 (>= 1.3.1) omniauth-kerberos (0.3.0) omniauth-multipassword timfel-krb5-auth (~> 0.8) @@ -896,7 +902,7 @@ DEPENDENCIES creole (~> 0.5.0) d3_rails (~> 3.5.0) database_cleaner (~> 1.5.0) - deckar01-task_list (= 1.0.6) + deckar01-task_list (= 2.0.0) default_value_for (~> 3.0.0) devise (~> 4.2) devise-two-factor (~> 3.0.0) @@ -910,6 +916,9 @@ DEPENDENCIES faraday (~> 0.11.0) ffaker (~> 2.4) flay (~> 2.8.0) + flipper (~> 0.10.2) + flipper-active_record (~> 0.10.2) + fog-aliyun (~> 0.1.0) fog-aws (~> 0.9) fog-core (~> 1.44) fog-google (~> 0.5) @@ -924,7 +933,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly (~> 0.7.0) + gitaly (~> 0.8.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) @@ -1060,4 +1069,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.14.6 + 1.15.0 diff --git a/app/assets/images/i2p-step.svg b/app/assets/images/i2p-step.svg new file mode 100644 index 00000000000..8886092ed82 --- /dev/null +++ b/app/assets/images/i2p-step.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index d7c62889dde..187fab084fd 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -111,7 +111,7 @@ export default class BlobViewer { BlobViewer.loadViewer(newViewer) .then((viewer) => { - $(viewer).syntaxHighlight(); + $(viewer).renderGFM(); this.$fileHolder.trigger('highlight:line'); gl.utils.handleLocationHash(); diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index e0a6f64dd42..0e4aa39226b 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -70,6 +70,7 @@ $(() => { gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); this.filterManager = new FilteredSearchBoards(Store.filter, true); + this.filterManager.setup(); // Listen for updateTokens event eventHub.$on('updateTokens', this.updateTokens); diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js index b214b5a7199..56a0fde5a91 100644 --- a/app/assets/javascripts/boards/components/modal/filters.js +++ b/app/assets/javascripts/boards/components/modal/filters.js @@ -13,6 +13,7 @@ export default { FilteredSearchContainer.container = this.$el; this.filteredSearch = new FilteredSearchBoards(this.store); + this.filteredSearch.setup(); this.filteredSearch.removeTokens(); this.filteredSearch.handleInputPlaceholder(); this.filteredSearch.toggleClearSearchButton(); diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 1264280284c..b37698fe9ca 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -2,7 +2,7 @@ import FilteredSearchContainer from '../filtered_search/container'; export default class FilteredSearchBoards extends gl.FilteredSearchManager { - constructor(store, updateUrl = false) { + constructor(store, updateUrl = false, cantEdit = []) { super('boards'); this.store = store; @@ -11,6 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { // Issue boards is slightly different, we handle all the requests async // instead or reloading the page, we just re-fire the list ajax requests this.isHandledAsync = true; + + this.cantEdit = cantEdit; } updateObject(path) { @@ -40,4 +42,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { // Get the placeholder back if search is empty this.filteredSearchInput.dispatchEvent(new Event('input')); } + + canEdit(tokenName) { + return this.cantEdit.indexOf(tokenName) === -1; + } } diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 97f279e4be4..072a899e9f2 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -2,15 +2,11 @@ consistent-return, prefer-rest-params */ /* global Breakpoints */ +import _ from 'underscore'; import { bytesToKiB } from './lib/utils/number_utils'; -const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; }; -const AUTO_SCROLL_OFFSET = 75; -const DOWN_BUILD_TRACE = '#down-build-trace'; - window.Build = (function () { Build.timeout = null; - Build.state = null; function Build(options) { @@ -23,21 +19,22 @@ window.Build = (function () { this.buildStage = this.options.buildStage; this.$document = $(document); this.logBytes = 0; + this.scrollOffsetPadding = 30; - this.updateDropdown = bind(this.updateDropdown, this); + this.updateDropdown = this.updateDropdown.bind(this); + this.getBuildTrace = this.getBuildTrace.bind(this); + this.scrollToBottom = this.scrollToBottom.bind(this); this.$body = $('body'); this.$buildTrace = $('#build-trace'); - this.$autoScrollContainer = $('.autoscroll-container'); - this.$autoScrollStatus = $('#autoscroll-status'); - this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text'); - this.$upBuildTrace = $('#up-build-trace'); - this.$downBuildTrace = $(DOWN_BUILD_TRACE); - this.$scrollTopBtn = $('#scroll-top'); - this.$scrollBottomBtn = $('#scroll-bottom'); this.$buildRefreshAnimation = $('.js-build-refresh'); - this.$buildScroll = $('#js-build-scroll'); this.$truncatedInfo = $('.js-truncated-info'); + this.$buildTraceOutput = $('.js-build-output'); + this.$scrollContainer = $('.js-scroll-container'); + + // Scroll controllers + this.$scrollTopBtn = $('.js-scroll-up'); + this.$scrollBottomBtn = $('.js-scroll-down'); clearTimeout(Build.timeout); // Init breakpoint checker @@ -56,54 +53,149 @@ window.Build = (function () { .off('click', '.stage-item') .on('click', '.stage-item', this.updateDropdown); - this.$document.on('scroll', this.initScrollMonitor.bind(this)); + // add event listeners to the scroll buttons + this.$scrollTopBtn + .off('click') + .on('click', this.scrollToTop.bind(this)); + + this.$scrollBottomBtn + .off('click') + .on('click', this.scrollToBottom.bind(this)); $(window) .off('resize.build') - .on('resize.build', this.sidebarOnResize.bind(this)); - - $('a', this.$buildScroll) - .off('click.stepTrace') - .on('click.stepTrace', this.stepTrace); + .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); this.updateArtifactRemoveDate(); - this.initScrollButtonAffix(); - this.invokeBuildTrace(); + + // eslint-disable-next-line + this.getBuildTrace() + .then(() => this.makeTraceScrollable()) + .then(() => this.scrollToBottom()); + + this.verifyTopPosition(); } + Build.prototype.makeTraceScrollable = function () { + this.$scrollContainer.niceScroll({ + cursorcolor: '#fff', + cursoropacitymin: 1, + cursorwidth: '3px', + railpadding: { top: 5, bottom: 5, right: 5 }, + }); + + this.$scrollContainer.on('scroll', _.throttle(this.toggleScroll.bind(this), 100)); + + this.toggleScroll(); + }; + + Build.prototype.canScroll = function () { + return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height(); + }; + + /** + * | | Up | Down | + * |--------------------------|----------|----------| + * | on scroll bottom | active | disabled | + * | on scroll top | disabled | active | + * | no scroll | disabled | disabled | + * | on.('scroll') is on top | disabled | active | + * | on('scroll) is on bottom | active | disabled | + * + */ + Build.prototype.toggleScroll = function () { + const bottomScroll = this.$scrollContainer.scrollTop() + + this.scrollOffsetPadding + + this.$scrollContainer.height(); + + if (this.canScroll()) { + if (this.$scrollContainer.scrollTop() === 0) { + this.toggleDisableButton(this.$scrollTopBtn, true); + this.toggleDisableButton(this.$scrollBottomBtn, false); + } else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) { + this.toggleDisableButton(this.$scrollTopBtn, false); + this.toggleDisableButton(this.$scrollBottomBtn, true); + } else { + this.toggleDisableButton(this.$scrollTopBtn, false); + this.toggleDisableButton(this.$scrollBottomBtn, false); + } + } + }; + + Build.prototype.scrollToTop = function () { + this.$scrollContainer.getNiceScroll(0).doScrollTop(0); + this.toggleScroll(); + }; + + Build.prototype.scrollToBottom = function () { + this.$scrollContainer.getNiceScroll(0).doScrollTo(this.$scrollContainer.prop('scrollHeight')); + this.toggleScroll(); + }; + + Build.prototype.toggleDisableButton = function ($button, disable) { + if (disable && $button.prop('disabled')) return; + $button.prop('disabled', disable); + }; + + Build.prototype.toggleScrollAnimation = function (toggle) { + this.$scrollBottomBtn.toggleClass('animate', toggle); + }; + + /** + * Build trace top position depends on the space ocupied by the elments rendered before + */ + Build.prototype.verifyTopPosition = function () { + const $buildPage = $('.build-page'); + + const $header = $('.build-header', $buildPage); + const $runnersStuck = $('.js-build-stuck', $buildPage); + const $startsEnvironment = $('.js-environment-container', $buildPage); + const $erased = $('.js-build-erased', $buildPage); + + let topPostion = 168; + + if ($header) { + topPostion += $header.outerHeight(); + } + + if ($runnersStuck) { + topPostion += $runnersStuck.outerHeight(); + } + + if ($startsEnvironment) { + topPostion += $startsEnvironment.outerHeight(); + } + + if ($erased) { + topPostion += $erased.outerHeight() + 10; + } + + this.$buildTrace.css({ + top: topPostion, + }); + }; + Build.prototype.initSidebar = function () { this.$sidebar = $('.js-build-sidebar'); this.$sidebar.niceScroll(); - this.$document - .off('click', '.js-sidebar-build-toggle') - .on('click', '.js-sidebar-build-toggle', this.toggleSidebar); - }; - - Build.prototype.invokeBuildTrace = function () { - return this.getBuildTrace(); }; Build.prototype.getBuildTrace = function () { return $.ajax({ url: `${this.pageUrl}/trace.json`, - dataType: 'json', - data: { - state: this.state, - }, - success: ((log) => { - const $buildContainer = $('.js-build-output'); - + data: this.state, + }) + .done((log) => { gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); - if (log.state) { this.state = log.state; } if (log.append) { - $buildContainer.append(log.html); + this.$buildTraceOutput.append(log.html); this.logBytes += log.size; } else { - $buildContainer.html(log.html); + this.$buildTraceOutput.html(log.html); this.logBytes = log.size; } @@ -114,141 +206,30 @@ window.Build = (function () { const size = bytesToKiB(this.logBytes); $('.js-truncated-info-size').html(`${size}`); this.$truncatedInfo.removeClass('hidden'); - this.initAffixTruncatedInfo(); } else { this.$truncatedInfo.addClass('hidden'); } - this.checkAutoscroll(); - if (!log.complete) { + this.toggleScrollAnimation(true); + Build.timeout = setTimeout(() => { - this.invokeBuildTrace(); + //eslint-disable-next-line + this.getBuildTrace() + .then(() => this.scrollToBottom()); }, 4000); } else { this.$buildRefreshAnimation.remove(); + this.toggleScrollAnimation(false); } if (log.status !== this.buildStatus) { - let pageUrl = this.pageUrl; - - if (this.$autoScrollStatus.data('state') === 'enabled') { - pageUrl += DOWN_BUILD_TRACE; - } - - gl.utils.visitUrl(pageUrl); + gl.utils.visitUrl(this.pageUrl); } - }), - error: () => { + }) + .fail(() => { this.$buildRefreshAnimation.remove(); - return this.initScrollMonitor(); - }, - }); - }; - - Build.prototype.checkAutoscroll = function () { - if (this.$autoScrollStatus.data('state') === 'enabled') { - return $('html,body').scrollTop(this.$buildTrace.height()); - } - - // Handle a situation where user started new build - // but never scrolled a page - if (!this.$scrollTopBtn.is(':visible') && - !this.$scrollBottomBtn.is(':visible') && - !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { - this.$scrollBottomBtn.show(); - } - }; - - Build.prototype.initScrollButtonAffix = function () { - // Hide everything initially - this.$scrollTopBtn.hide(); - this.$scrollBottomBtn.hide(); - this.$autoScrollContainer.hide(); - }; - - // Page scroll listener to detect if user has scrolling page - // and handle following cases - // 1) User is at Top of Build Log; - // - Hide Top Arrow button - // - Show Bottom Arrow button - // - Disable Autoscroll and hide indicator (when build is running) - // 2) User is at Bottom of Build Log; - // - Show Top Arrow button - // - Hide Bottom Arrow button - // - Enable Autoscroll and show indicator (when build is running) - // 3) User is somewhere in middle of Build Log; - // - Show Top Arrow button - // - Show Bottom Arrow button - // - Disable Autoscroll and hide indicator (when build is running) - Build.prototype.initScrollMonitor = function () { - if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && - !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { - // User is somewhere in middle of Build Log - - this.$scrollTopBtn.show(); - - if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed - this.$scrollBottomBtn.show(); - } else if (this.$buildRefreshAnimation.is(':visible') && - !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) { - this.$scrollBottomBtn.show(); - } else { - this.$scrollBottomBtn.hide(); - } - - // Hide Autoscroll Status Indicator - if (this.$scrollBottomBtn.is(':visible')) { - this.$autoScrollContainer.hide(); - this.$autoScrollStatusText.removeClass('animate'); - } else { - this.$autoScrollContainer.css({ - top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET, - }).show(); - this.$autoScrollStatusText.addClass('animate'); - } - } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && - !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { - // User is at Top of Build Log - - this.$scrollTopBtn.hide(); - this.$scrollBottomBtn.show(); - - this.$autoScrollContainer.hide(); - this.$autoScrollStatusText.removeClass('animate'); - } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && - gl.utils.isInViewport(this.$downBuildTrace.get(0))) || - (this.$buildRefreshAnimation.is(':visible') && - gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) { - // User is at Bottom of Build Log - - this.$scrollTopBtn.show(); - this.$scrollBottomBtn.hide(); - - // Show and Reposition Autoscroll Status Indicator - this.$autoScrollContainer.css({ - top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET, - }).show(); - this.$autoScrollStatusText.addClass('animate'); - } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && - gl.utils.isInViewport(this.$downBuildTrace.get(0))) { - // Build Log height is small - - this.$scrollTopBtn.hide(); - this.$scrollBottomBtn.hide(); - - // Hide Autoscroll Status Indicator - this.$autoScrollContainer.hide(); - this.$autoScrollStatusText.removeClass('animate'); - } - - if (this.buildStatus === 'running' || this.buildStatus === 'pending') { - // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. - this.$autoScrollStatus.data( - 'state', - gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled', - ); - } + }); }; Build.prototype.shouldHideSidebarForViewport = function () { @@ -257,18 +238,24 @@ window.Build = (function () { }; Build.prototype.toggleSidebar = function (shouldHide) { - const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; + const shouldShow = !shouldHide; - this.$buildScroll.toggleClass('sidebar-expanded', shouldShow) - .toggleClass('sidebar-collapsed', shouldHide); - this.$truncatedInfo.toggleClass('sidebar-expanded', shouldShow) + this.$buildTrace + .toggleClass('sidebar-expanded', shouldShow) .toggleClass('sidebar-collapsed', shouldHide); - this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow) + this.$sidebar + .toggleClass('right-sidebar-expanded', shouldShow) .toggleClass('right-sidebar-collapsed', shouldHide); }; Build.prototype.sidebarOnResize = function () { this.toggleSidebar(this.shouldHideSidebarForViewport()); + + this.verifyTopPosition(); + + if (this.$scrollContainer.getNiceScroll(0)) { + this.toggleScroll(); + } }; Build.prototype.sidebarOnClick = function () { @@ -301,24 +288,5 @@ window.Build = (function () { this.populateJobs(stage); }; - Build.prototype.stepTrace = function (e) { - e.preventDefault(); - - const $currentTarget = $(e.currentTarget); - $.scrollTo($currentTarget.attr('href'), { - offset: 0, - }); - }; - - Build.prototype.initAffixTruncatedInfo = function () { - const offsetTop = this.$buildTrace.offset().top; - - this.$truncatedInfo.affix({ - offset: { - top: offsetTop, - }, - }); - }; - return Build; })(); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index 98698143d22..082fbafb740 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -118,7 +118,7 @@ export default Vue.component('pipelines-table', { eventHub.$on('refreshPipelines', this.fetchPipelines); }, - beforeDestroyed() { + beforeDestroy() { eventHub.$off('refreshPipelines'); }, diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index 459cdd53f9b..ba9d9a3e1f7 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -18,12 +18,12 @@ const gfmRules = { }, }, TaskListFilter: { - 'input[type=checkbox].task-list-item-checkbox'(el, text) { + 'input[type=checkbox].task-list-item-checkbox'(el) { return `[${el.checked ? 'x' : ' '}]`; }, }, ReferenceFilter: { - '.tooltip'(el, text) { + '.tooltip'(el) { return ''; }, 'a.gfm:not([data-link=true])'(el, text) { @@ -39,15 +39,15 @@ const gfmRules = { }, }, TableOfContentsFilter: { - 'ul.section-nav'(el, text) { + 'ul.section-nav'(el) { return '[[_TOC_]]'; }, }, EmojiFilter: { - 'img.emoji'(el, text) { + 'img.emoji'(el) { return el.getAttribute('alt'); }, - 'gl-emoji'(el, text) { + 'gl-emoji'(el) { return `:${el.getAttribute('data-name')}:`; }, }, @@ -57,13 +57,13 @@ const gfmRules = { }, }, VideoLinkFilter: { - '.video-container'(el, text) { + '.video-container'(el) { const videoEl = el.querySelector('video'); if (!videoEl) return false; return CopyAsGFM.nodeToGFM(videoEl); }, - 'video'(el, text) { + 'video'(el) { return `![${el.dataset.title}](${el.getAttribute('src')})`; }, }, @@ -74,19 +74,19 @@ const gfmRules = { 'code.code.math[data-math-style=inline]'(el, text) { return `$\`${text}\`$`; }, - 'span.katex-display span.katex-mathml'(el, text) { + 'span.katex-display span.katex-mathml'(el) { const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); if (!mathAnnotation) return false; return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``; }, - 'span.katex-mathml'(el, text) { + 'span.katex-mathml'(el) { const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); if (!mathAnnotation) return false; return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`; }, - 'span.katex-html'(el, text) { + 'span.katex-html'(el) { // We don't want to include the content of this element in the copied text. return ''; }, @@ -95,7 +95,7 @@ const gfmRules = { }, }, SanitizationFilter: { - 'a[name]:not([href]):empty'(el, text) { + 'a[name]:not([href]):empty'(el) { return el.outerHTML; }, 'dl'(el, text) { @@ -143,7 +143,7 @@ const gfmRules = { }, }, MarkdownFilter: { - 'br'(el, text) { + 'br'(el) { // Two spaces at the end of a line are turned into a BR return ' '; }, @@ -162,7 +162,7 @@ const gfmRules = { 'blockquote'(el, text) { return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); }, - 'img'(el, text) { + 'img'(el) { return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; }, 'a.anchor'(el, text) { @@ -222,10 +222,10 @@ const gfmRules = { 'sup'(el, text) { return `^${text}`; }, - 'hr'(el, text) { + 'hr'(el) { return '-----'; }, - 'table'(el, text) { + 'table'(el) { const theadEl = el.querySelector('thead'); const tbodyEl = el.querySelector('tbody'); if (!theadEl || !tbodyEl) return false; @@ -233,11 +233,11 @@ const gfmRules = { const theadText = CopyAsGFM.nodeToGFM(theadEl); const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl); - return theadText + tbodyText; + return [theadText, tbodyText].join('\n'); }, 'thead'(el, text) { const cells = _.map(el.querySelectorAll('th'), (cell) => { - let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2; + let chars = CopyAsGFM.nodeToGFM(cell).length + 2; let before = ''; let after = ''; @@ -262,10 +262,15 @@ const gfmRules = { return before + middle + after; }); - return `${text}|${cells.join('|')}|`; + const separatorRow = `|${cells.join('|')}|`; + + return [text, separatorRow].join('\n'); }, - 'tr'(el, text) { - const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim()); + 'tr'(el) { + const cellEls = el.querySelectorAll('td, th'); + if (cellEls.length === 0) return false; + + const cells = _.map(cellEls, cell => CopyAsGFM.nodeToGFM(cell)); return `| ${cells.join(' | ')} |`; }, }, @@ -273,12 +278,12 @@ const gfmRules = { class CopyAsGFM { constructor() { - $(document).on('copy', '.md, .wiki', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); - $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { this.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); - $(document).on('paste', '.js-gfm-input', this.pasteGFM.bind(this)); + $(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); + $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); + $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM); } - copyAsGFM(e, transformer) { + static copyAsGFM(e, transformer) { const clipboardData = e.originalEvent.clipboardData; if (!clipboardData) return; @@ -292,26 +297,59 @@ class CopyAsGFM { e.stopPropagation(); clipboardData.setData('text/plain', el.textContent); - clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el)); + clipboardData.setData('text/x-gfm', this.nodeToGFM(el)); } - pasteGFM(e) { + static pasteGFM(e) { const clipboardData = e.originalEvent.clipboardData; if (!clipboardData) return; + const text = clipboardData.getData('text/plain'); const gfm = clipboardData.getData('text/x-gfm'); if (!gfm) return; e.preventDefault(); - window.gl.utils.insertText(e.target, gfm); + window.gl.utils.insertText(e.target, (textBefore, textAfter) => { + // If the text before the cursor contains an odd number of backticks, + // we are either inside an inline code span that starts with 1 backtick + // or a code block that starts with 3 backticks. + // This logic still holds when there are one or more _closed_ code spans + // or blocks that will have 2 or 6 backticks. + // This will break down when the actual code block contains an uneven + // number of backticks, but this is a rare edge case. + const backtickMatch = textBefore.match(/`/g); + const insideCodeBlock = backtickMatch && (backtickMatch.length % 2) === 1; + + if (insideCodeBlock) { + return text; + } + + return gfm; + }); } static transformGFMSelection(documentFragment) { - // If the documentFragment contains more than just Markdown, don't copy as GFM. - if (documentFragment.querySelector('.md, .wiki')) return null; + const gfmEls = documentFragment.querySelectorAll('.md, .wiki'); + switch (gfmEls.length) { + case 0: { + return documentFragment; + } + case 1: { + return gfmEls[0]; + } + default: { + const allGfmEl = document.createElement('div'); + + for (let i = 0; i < gfmEls.length; i += 1) { + const lineEl = gfmEls[i]; + allGfmEl.appendChild(lineEl); + allGfmEl.appendChild(document.createTextNode('\n\n')); + } - return documentFragment; + return allGfmEl; + } + } } static transformCodeSelection(documentFragment) { @@ -343,7 +381,7 @@ class CopyAsGFM { return codeEl; } - static nodeToGFM(node) { + static nodeToGFM(node, respectWhitespaceParam = false) { if (node.nodeType === Node.COMMENT_NODE) { return ''; } @@ -352,7 +390,9 @@ class CopyAsGFM { return node.textContent; } - const text = this.innerGFM(node); + const respectWhitespace = respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE'); + + const text = this.innerGFM(node, respectWhitespace); if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { return text; @@ -366,7 +406,17 @@ class CopyAsGFM { if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; - const result = func(node, text); + let result; + if (func.length === 2) { + // if `func` takes 2 arguments, it depends on text. + // if there is no text, we don't need to generate GFM for this node. + if (text.length === 0) continue; + + result = func(node, text); + } else { + result = func(node); + } + if (result === false) continue; return result; @@ -376,7 +426,7 @@ class CopyAsGFM { return text; } - static innerGFM(parentNode) { + static innerGFM(parentNode, respectWhitespace = false) { const nodes = parentNode.childNodes; const clonedParentNode = parentNode.cloneNode(true); @@ -386,13 +436,19 @@ class CopyAsGFM { const node = nodes[i]; const clonedNode = clonedNodes[i]; - const text = this.nodeToGFM(node); + const text = this.nodeToGFM(node, respectWhitespace); // `clonedNode.replaceWith(text)` is not yet widely supported clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode); } - return clonedParentNode.innerText || clonedParentNode.textContent; + let nodeText = clonedParentNode.innerText || clonedParentNode.textContent; + + if (!respectWhitespace) { + nodeText = nodeText.trim(); + } + + return nodeText; } } 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 8a0fd3bb4a7..37ddca29e71 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -16,6 +16,13 @@ const JumpToDiscussion = Vue.extend({ }; }, computed: { + buttonText: function () { + if (this.discussionId) { + return 'Jump to next unresolved discussion'; + } else { + return 'Jump to first unresolved discussion'; + } + }, allResolved: function () { return this.unresolvedDiscussionCount === 0; }, diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 2090a7e12d6..baa20d0c34a 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -118,13 +118,14 @@ import ShortcutsBlob from './shortcuts_blob'; shortcut_handler = new ShortcutsNavigation(); new UsersSelect(); break; - case 'projects:builds:show': + case 'projects:jobs:show': new Build(); break; case 'projects:merge_requests:index': case 'projects:issues:index': - if (gl.FilteredSearchManager) { - new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); + if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) { + const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); + filteredSearchManager.setup(); } Issuable.init(); new gl.IssuableBulkActions({ @@ -392,6 +393,9 @@ import ShortcutsBlob from './shortcuts_blob'; case 'users:show': new UserCallout(); break; + case 'admin:conversational_development_index:show': + new UserCallout(); + break; case 'snippets:show': new LineHighlighter(); new BlobViewer(); diff --git a/app/assets/javascripts/droplab/keyboard.js b/app/assets/javascripts/droplab/keyboard.js index 36740a430e1..02f1b805ce4 100644 --- a/app/assets/javascripts/droplab/keyboard.js +++ b/app/assets/javascripts/droplab/keyboard.js @@ -8,7 +8,7 @@ const Keyboard = function () { var isUpArrow = false; var isDownArrow = false; var removeHighlight = function removeHighlight(list) { - var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0); + var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider):not(.hidden)'), 0); var listItems = []; for(var i = 0; i < itemElements.length; i++) { var listItem = itemElements[i]; diff --git a/app/assets/javascripts/droplab/plugins/ajax_filter.js b/app/assets/javascripts/droplab/plugins/ajax_filter.js index cfd7e2ca189..1db20227a16 100644 --- a/app/assets/javascripts/droplab/plugins/ajax_filter.js +++ b/app/assets/javascripts/droplab/plugins/ajax_filter.js @@ -1,4 +1,5 @@ /* eslint-disable */ +import AjaxCache from '../../lib/utils/ajax_cache'; const AjaxFilter = { init: function(hook) { @@ -58,50 +59,27 @@ const AjaxFilter = { this.loading = true; var params = config.params || {}; params[config.searchKey] = searchValue; - var self = this; - self.cache = self.cache || {}; var url = config.endpoint + this.buildParams(params); - var urlCachedData = self.cache[url]; - if (urlCachedData) { - self._loadData(urlCachedData, config, self); - } else { - this._loadUrlData(url) - .then(function(data) { - self._loadData(data, config, self); - }, config.onError).catch(config.onError); - } - }, - - _loadUrlData: function _loadUrlData(url) { - var self = this; - return new Promise(function(resolve, reject) { - var xhr = new XMLHttpRequest; - xhr.open('GET', url, true); - xhr.onreadystatechange = function () { - if(xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === 200) { - var data = JSON.parse(xhr.responseText); - self.cache[url] = data; - return resolve(data); - } else { - return reject([xhr.responseText, xhr.status]); - } + return AjaxCache.retrieve(url) + .then((data) => { + this._loadData(data, config); + if (config.onLoadingFinished) { + config.onLoadingFinished(data); } - }; - xhr.send(); - }); + }) + .catch(config.onError); }, - _loadData: function _loadData(data, config, self) { - const list = self.hook.list; + _loadData(data, config) { + const list = this.hook.list; if (config.loadingTemplate && list.data === undefined || list.data.length === 0) { const dataLoadingTemplate = list.list.querySelector('[data-loading-template]'); if (dataLoadingTemplate) { - dataLoadingTemplate.outerHTML = self.listTemplate; + dataLoadingTemplate.outerHTML = this.listTemplate; } } - if (!self.destroyed) { + if (!this.destroyed) { var hookListChildren = list.list.children; var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic'); if (onlyDynamicList && data.length === 0) { @@ -109,7 +87,7 @@ const AjaxFilter = { } list.setData.call(list, data); } - self.notLoading(); + this.notLoading(); list.currentIndex = 0; }, diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 266cd3966c6..507e3c73189 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -194,7 +194,12 @@ window.DropzoneInput = (function() { $(child).val(beforeSelection + formattedText + afterSelection); textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); textarea.style.height = `${textarea.scrollHeight}px`; +<<<<<<< HEAD return formTextarea.trigger('input'); +======= + form_textarea.trigger("input"); + form_textarea.get(0).dispatchEvent(new Event('input')); +>>>>>>> b5b5b4a... Added description field to inline edit form }; getFilename = function(e) { diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue index d4e13f3c84a..14dcea5261a 100644 --- a/app/assets/javascripts/environments/components/environment.vue +++ b/app/assets/javascripts/environments/components/environment.vue @@ -1,5 +1,6 @@ diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js new file mode 100644 index 00000000000..25b24fbd6dc --- /dev/null +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -0,0 +1,17 @@ +export default { + methods: { + saveData(resp) { + const response = { + headers: resp.headers, + body: resp.json(), + }; + + this.isLoading = false; + + this.store.storeAvailableCount(response.body.available_count); + this.store.storeStoppedCount(response.body.stopped_count); + this.store.storeEnvironments(response.body.environments); + this.store.setPagination(response.headers); + }, + }, +}; diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js index 8adb53ea86d..03ab74b3338 100644 --- a/app/assets/javascripts/environments/services/environments_service.js +++ b/app/assets/javascripts/environments/services/environments_service.js @@ -10,7 +10,8 @@ export default class EnvironmentsService { this.folderResults = 3; } - get(scope, page) { + get(options = {}) { + const { scope, page } = options; return this.environments.get({ scope, page }); } diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 158e7922e3c..8a2f6a473de 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -153,4 +153,10 @@ export default class EnvironmentsStore { return updatedEnvironments; } + getOpenFolders() { + const environments = this.state.environments; + + return environments.filter(env => env.isFolder && env.isOpen); + } + } diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js index 15052dbd362..c51d4b056af 100644 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js @@ -13,13 +13,17 @@ export default { required: false, default: true, }, + allowedKeys: { + type: Array, + required: true, + }, }, computed: { processedItems() { return this.items.map((item) => { const { tokens, searchToken } - = gl.FilteredSearchTokenizer.processTokens(item); + = gl.FilteredSearchTokenizer.processTokens(item, this.allowedKeys); const resultantTokens = tokens.map(token => ({ prefix: `${token.key}:`, diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 5d92d29c399..2af242a69df 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -2,14 +2,18 @@ import Filter from '~/droplab/plugins/filter'; import './filtered_search_dropdown'; class DropdownHint extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { + constructor(droplab, dropdown, input, tokenKeys, filter) { super(droplab, dropdown, input, filter); this.config = { Filter: { template: 'hint', - filterFunction: gl.DropdownUtils.filterHint.bind(null, input), + filterFunction: gl.DropdownUtils.filterHint.bind(null, { + input, + allowedKeys: tokenKeys.getKeys(), + }), }, }; + this.tokenKeys = tokenKeys; } itemClicked(e) { @@ -52,20 +56,13 @@ class DropdownHint extends gl.FilteredSearchDropdown { } renderContent() { - const dropdownData = []; - - [].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => { - const { icon, hint, tag, type } = dropdownMenu.dataset; - if (icon && hint && tag) { - dropdownData.push( - Object.assign({ - icon: `fa-${icon}`, - hint, - tag: `<${tag}>`, - }, type && { type }), - ); - } - }); + const dropdownData = gl.FilteredSearchTokenKeys.get() + .map(tokenKey => ({ + icon: `fa-${tokenKey.icon}`, + hint: tokenKey.key, + tag: `<${tokenKey.symbol}${tokenKey.key}>`, + type: tokenKey.type, + })); this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config); this.droplab.setData(this.hookId, dropdownData); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index f20193eecba..34a9e34070c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -5,7 +5,7 @@ import Filter from '~/droplab/plugins/filter'; import './filtered_search_dropdown'; class DropdownNonUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter, endpoint, symbol) { + constructor(droplab, dropdown, input, tokenKeys, filter, endpoint, symbol) { super(droplab, dropdown, input, filter); this.symbol = symbol; this.config = { diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index 42538780e50..65c1b2050ac 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -4,7 +4,7 @@ import AjaxFilter from '~/droplab/plugins/ajax_filter'; import './filtered_search_dropdown'; class DropdownUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { + constructor(droplab, dropdown, input, tokenKeys, filter) { super(droplab, dropdown, input, filter); this.config = { AjaxFilter: { @@ -18,6 +18,9 @@ class DropdownUser extends gl.FilteredSearchDropdown { }, searchValueFunction: this.getSearchInput.bind(this), loadingTemplate: this.loadingTemplate, + onLoadingFinished: () => { + this.hideCurrentUser(); + }, onError() { /* eslint-disable no-new */ new Flash('An error occured fetching the dropdown data.'); @@ -25,6 +28,12 @@ class DropdownUser extends gl.FilteredSearchDropdown { }, }, }; + this.tokenKeys = tokenKeys; + } + + hideCurrentUser() { + const currenUserItem = this.dropdown.querySelector('.js-current-user'); + currenUserItem.classList.add('hidden'); } itemClicked(e) { @@ -43,7 +52,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { getSearchInput() { const query = gl.DropdownUtils.getSearchInput(this.input); - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query, this.tokenKeys.get()); let value = lastToken || ''; diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index bc7c1dffece..ef8fe071012 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -50,10 +50,12 @@ class DropdownUtils { return updatedItem; } - static filterHint(input, item) { + static filterHint(config, item) { + const { input, allowedKeys } = config; const updatedItem = item; const searchInput = gl.DropdownUtils.getSearchQuery(input); - const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput); + const { lastToken, tokens } = + gl.FilteredSearchTokenizer.processTokens(searchInput, allowedKeys); const lastKey = lastToken.key || lastToken || ''; const allowMultiple = item.type === 'array'; const itemInExistingTokens = tokens.some(t => t.key === item.hint); @@ -100,10 +102,13 @@ class DropdownUtils { if (token.classList.contains('js-visual-token')) { const name = token.querySelector('.name'); const value = token.querySelector('.value'); + const valueContainer = token.querySelector('.value-container'); const symbol = value && value.dataset.symbol ? value.dataset.symbol : ''; let valueText = ''; - if (value && value.innerText) { + if (valueContainer && valueContainer.dataset.originalValue) { + valueText = valueContainer.dataset.originalValue; + } else if (value && value.innerText) { valueText = value.innerText; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js index 5d48b8aacb2..132b6fe698a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -2,9 +2,9 @@ import './dropdown_hint'; import './dropdown_non_user'; import './dropdown_user'; import './dropdown_utils'; +import './filtered_search_token_keys'; import './filtered_search_dropdown_manager'; import './filtered_search_dropdown'; import './filtered_search_manager'; -import './filtered_search_token_keys'; import './filtered_search_tokenizer'; import './filtered_search_visual_tokens'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 49a6cd1ac77..6bc6bc43f51 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -2,10 +2,10 @@ import DropLab from '~/droplab/drop_lab'; import FilteredSearchContainer from './container'; class FilteredSearchDropdownManager { - constructor(baseEndpoint = '', page) { + constructor(baseEndpoint = '', tokenizer, page) { this.container = FilteredSearchContainer.container; this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); - this.tokenizer = gl.FilteredSearchTokenizer; + this.tokenizer = tokenizer; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.page = page; @@ -98,7 +98,8 @@ class FilteredSearchDropdownManager { if (!mappingKey.reference) { const dl = this.droplab; - const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; + const defaultArguments = + [null, dl, element, this.filteredSearchInput, this.filteredSearchTokenKeys, key]; const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); // Passing glArguments to `new gl[glClass]()` @@ -141,7 +142,8 @@ class FilteredSearchDropdownManager { setDropdown() { const query = gl.DropdownUtils.getSearchQuery(true); - const { lastToken, searchToken } = this.tokenizer.processTokens(query); + const { lastToken, searchToken } = + this.tokenizer.processTokens(query, this.filteredSearchTokenKeys.getKeys()); if (this.currentDropdown) { this.updateCurrentDropdownOffset(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 57d247e11a9..72214321be1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -6,6 +6,7 @@ import eventHub from './event_hub'; class FilteredSearchManager { constructor(page) { + this.page = page; this.container = FilteredSearchContainer.container; this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.filteredSearchInputForm = this.filteredSearchInput.form; @@ -15,17 +16,20 @@ class FilteredSearchManager { this.recentSearchesStore = new RecentSearchesStore({ isLocalStorageAvailable: RecentSearchesService.isAvailable(), + allowedKeys: this.filteredSearchTokenKeys.getKeys(), }); - const searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown'); - const projectPath = searchHistoryDropdownElement ? - searchHistoryDropdownElement.dataset.projectFullPath : 'project'; + this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown'); + const projectPath = this.searchHistoryDropdownElement ? + this.searchHistoryDropdownElement.dataset.projectFullPath : 'project'; let recentSearchesPagePrefix = 'issue-recent-searches'; - if (page === 'merge_requests') { + if (this.page === 'merge_requests') { recentSearchesPagePrefix = 'merge-request-recent-searches'; } const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`; this.recentSearchesService = new RecentSearchesService(recentSearchesKey); + } + setup() { // Fetch recent searches from localStorage this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch() .catch((error) => { @@ -46,12 +50,12 @@ class FilteredSearchManager { if (this.filteredSearchInput) { this.tokenizer = gl.FilteredSearchTokenizer; - this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page); + this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', this.tokenizer, this.page); this.recentSearchesRoot = new RecentSearchesRoot( this.recentSearchesStore, this.recentSearchesService, - searchHistoryDropdownElement, + this.searchHistoryDropdownElement, ); this.recentSearchesRoot.init(); @@ -101,11 +105,9 @@ class FilteredSearchManager { 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('click', this.removeTokenWrapper); - this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper); + this.tokensContainer.addEventListener('click', this.editTokenWrapper); this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper); - document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.addEventListener('click', this.unselectEditTokensWrapper); document.addEventListener('click', this.removeInputContainerFocusWrapper); document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper); @@ -123,11 +125,9 @@ class FilteredSearchManager { 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('click', this.removeTokenWrapper); - this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper); + this.tokensContainer.removeEventListener('click', this.editTokenWrapper); this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper); - document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.removeEventListener('click', this.unselectEditTokensWrapper); document.removeEventListener('click', this.removeInputContainerFocusWrapper); document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper); @@ -140,7 +140,9 @@ class FilteredSearchManager { if (e.keyCode === 8 || e.keyCode === 46) { const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); - if (this.filteredSearchInput.value === '' && lastVisualToken) { + const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim(); + const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName); + if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); gl.FilteredSearchVisualTokens.removeLastTokenPartial(); } @@ -201,23 +203,13 @@ class FilteredSearchManager { } } - static selectToken(e) { - const button = e.target.closest('.selectable'); - const removeButtonSelected = e.target.closest('.remove-token'); - - if (!removeButtonSelected && button) { - e.preventDefault(); - e.stopPropagation(); - gl.FilteredSearchVisualTokens.selectToken(button); - } - } - removeToken(e) { const removeButtonSelected = e.target.closest('.remove-token'); if (removeButtonSelected) { e.preventDefault(); - e.stopPropagation(); + // Prevent editToken from being triggered after token is removed + e.stopImmediatePropagation(); const button = e.target.closest('.selectable'); gl.FilteredSearchVisualTokens.selectToken(button, true); @@ -239,8 +231,12 @@ class FilteredSearchManager { editToken(e) { const token = e.target.closest('.js-visual-token'); + const sanitizedTokenName = token && token.querySelector('.name').textContent.trim(); + const canEdit = this.canEdit && this.canEdit(sanitizedTokenName); - if (token) { + if (token && canEdit) { + e.preventDefault(); + e.stopPropagation(); gl.FilteredSearchVisualTokens.editToken(token); this.tokenChange(); } @@ -318,7 +314,7 @@ class FilteredSearchManager { handleInputVisualToken() { const input = this.filteredSearchInput; const { tokens, searchToken } - = gl.FilteredSearchTokenizer.processTokens(input.value); + = this.tokenizer.processTokens(input.value, this.filteredSearchTokenKeys.getKeys()); const { isLastVisualTokenValid } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); @@ -390,7 +386,12 @@ class FilteredSearchManager { if (condition) { hasFilteredSearch = true; - gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value); + const canEdit = this.canEdit && this.canEdit(condition.tokenKey); + gl.FilteredSearchVisualTokens.addFilterVisualToken( + condition.tokenKey, + condition.value, + canEdit, + ); } else { // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + @@ -409,18 +410,27 @@ class FilteredSearchManager { } hasFilteredSearch = true; - gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); + const canEdit = this.canEdit && this.canEdit(sanitizedKey); + gl.FilteredSearchVisualTokens.addFilterVisualToken( + sanitizedKey, + `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, + canEdit, + ); } else if (!match && keyParam === 'assignee_id') { const id = parseInt(value, 10); if (usernameParams[id]) { hasFilteredSearch = true; - gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`); + const tokenName = 'assignee'; + const canEdit = this.canEdit && this.canEdit(tokenName); + gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit); } } else if (!match && keyParam === 'author_id') { const id = parseInt(value, 10); if (usernameParams[id]) { hasFilteredSearch = true; - gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`); + const tokenName = 'author'; + const canEdit = this.canEdit && this.canEdit(tokenName); + gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit); } } else if (!match && keyParam === 'search') { hasFilteredSearch = true; @@ -444,7 +454,7 @@ class FilteredSearchManager { this.saveCurrentSearchQuery(); const { tokens, searchToken } - = this.tokenizer.processTokens(searchQuery); + = this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys()); const currentState = gl.utils.getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); @@ -515,6 +525,11 @@ class FilteredSearchManager { this.filteredSearchInput.dispatchEvent(new CustomEvent('input')); this.search(); } + + // eslint-disable-next-line class-methods-use-this + canEdit() { + return true; + } } window.gl = window.gl || {}; diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index 1abad9d1b73..025d4d8795b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -3,21 +3,25 @@ const tokenKeys = [{ type: 'string', param: 'username', symbol: '@', + icon: 'pencil', }, { key: 'assignee', type: 'string', param: 'username', symbol: '@', + icon: 'user', }, { key: 'milestone', type: 'string', param: 'title', symbol: '%', + icon: 'clock-o', }, { key: 'label', type: 'array', param: 'name[]', symbol: '~', + icon: 'tag', }]; const alternativeTokenKeys = [{ @@ -56,6 +60,10 @@ class FilteredSearchTokenKeys { return tokenKeys; } + static getKeys() { + return tokenKeys.map(i => i.key); + } + static getAlternatives() { return alternativeTokenKeys; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js index aa513b3aeae..f2e66503e5e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js @@ -1,8 +1,7 @@ import './filtered_search_token_keys'; class FilteredSearchTokenizer { - static processTokens(input) { - const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key); + static processTokens(input, allowedKeys) { // Regex extracts `(token):(symbol)(value)` // 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'); diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index f3003b86493..e9278140af0 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,6 +1,7 @@ -import AjaxCache from '~/lib/utils/ajax_cache'; -import '~/flash'; /* global Flash */ +import AjaxCache from '../lib/utils/ajax_cache'; +import '../flash'; /* global Flash */ import FilteredSearchContainer from './container'; +import UsersCache from '../lib/utils/users_cache'; class FilteredSearchVisualTokens { static getLastVisualTokenBeforeInput() { @@ -36,15 +37,22 @@ class FilteredSearchVisualTokens { } } - static createVisualTokenElementHTML() { + static createVisualTokenElementHTML(canEdit = true) { + let removeTokenMarkup = ''; + if (canEdit) { + removeTokenMarkup = ` +
+ +
+ `; + } + return `
-
- -
+ ${removeTokenMarkup}
`; @@ -75,22 +83,52 @@ class FilteredSearchVisualTokens { .catch(() => new Flash('An error occurred while fetching label colors.')); } + static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) { + if (tokenValue === 'none') { + return Promise.resolve(); + } + + const username = tokenValue.replace(/^@/, ''); + return UsersCache.retrieve(username) + .then((user) => { + if (!user) { + return; + } + + /* eslint-disable no-param-reassign */ + tokenValueContainer.dataset.originalValue = tokenValue; + tokenValueElement.innerHTML = ` + ${user.name}'s avatar + ${user.name} + `; + /* eslint-enable no-param-reassign */ + }) + // ignore error and leave username in the search bar + .catch(() => { }); + } + static renderVisualTokenValue(parentElement, tokenName, tokenValue) { const tokenValueContainer = parentElement.querySelector('.value-container'); - tokenValueContainer.querySelector('.value').innerText = tokenValue; + const tokenValueElement = tokenValueContainer.querySelector('.value'); + tokenValueElement.innerText = tokenValue; - if (tokenName.toLowerCase() === 'label') { + const tokenType = tokenName.toLowerCase(); + if (tokenType === 'label') { FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue); + } else if ((tokenType === 'author') || (tokenType === 'assignee')) { + FilteredSearchVisualTokens.updateUserTokenAppearance( + tokenValueContainer, tokenValueElement, tokenValue, + ); } } - static addVisualTokenElement(name, value, isSearchTerm) { + static addVisualTokenElement(name, value, isSearchTerm, canEdit) { const li = document.createElement('li'); li.classList.add('js-visual-token'); li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token'); if (value) { - li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(); + li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(canEdit); FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value); } else { li.innerHTML = '
'; @@ -114,20 +152,20 @@ class FilteredSearchVisualTokens { } } - static addFilterVisualToken(tokenName, tokenValue) { + static addFilterVisualToken(tokenName, tokenValue, canEdit) { const { lastVisualToken, isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement; if (isLastVisualTokenValid) { - addVisualTokenElement(tokenName, tokenValue, false); + addVisualTokenElement(tokenName, tokenValue, false, canEdit); } else { const previousTokenName = lastVisualToken.querySelector('.name').innerText; const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container'); tokensContainer.removeChild(lastVisualToken); const value = tokenValue || tokenName; - addVisualTokenElement(previousTokenName, value, false); + addVisualTokenElement(previousTokenName, value, false, canEdit); } } @@ -146,6 +184,12 @@ class FilteredSearchVisualTokens { if (!lastVisualToken) return ''; + const valueContainer = lastVisualToken.querySelector('.value-container'); + const originalValue = valueContainer && valueContainer.dataset.originalValue; + if (originalValue) { + return originalValue; + } + const value = lastVisualToken.querySelector('.value'); const name = lastVisualToken.querySelector('.name'); @@ -198,17 +242,28 @@ class FilteredSearchVisualTokens { const inputLi = input.parentElement; tokenContainer.replaceChild(inputLi, token); - const name = token.querySelector('.name'); - const value = token.querySelector('.value'); + const nameElement = token.querySelector('.name'); + let value; - if (token.classList.contains('filtered-search-token') && value) { - FilteredSearchVisualTokens.addFilterVisualToken(name.innerText); - input.value = value.innerText; - } else { - // token is a search term - input.value = name.innerText; + if (token.classList.contains('filtered-search-token')) { + FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText); + + const valueContainerElement = token.querySelector('.value-container'); + value = valueContainerElement.dataset.originalValue; + + if (!value) { + const valueElement = valueContainerElement.querySelector('.value'); + value = valueElement.innerText; + } + } + + // token is a search term + if (!value) { + value = nameElement.innerText; } + input.value = value; + // Opens dropdown const inputEvent = new Event('input'); input.dispatchEvent(inputEvent); diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index b2e6f63aacf..27e49d4fb96 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -37,6 +37,7 @@ class RecentSearchesRoot { `, components: { diff --git a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js index 35fc15e4c87..aaa0c349d93 100644 --- a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js +++ b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js @@ -1,10 +1,11 @@ import _ from 'underscore'; class RecentSearchesStore { - constructor(initialState = {}) { + constructor(initialState = {}, allowedKeys) { this.state = Object.assign({ isLocalStorageAvailable: true, recentSearches: [], + allowedKeys, }, initialState); } diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index eec30624ff2..ccff8f0ace7 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -7,8 +7,21 @@ window.Flash = (function() { return $(this).fadeOut(); }; - function Flash(message, type, parent) { - var flash, textDiv; + /** + * Flash banner supports different types of Flash configurations + * along with ability to provide actionConfig which can be used to show + * additional action or link on banner next to message + * + * @param {String} message Flash message + * @param {String} type Type of Flash, it can be `notice` or `alert` (default) + * @param {Object} parent Reference to Parent element under which Flash needs to appear + * @param {Object} actionConfig Map of config to show action on banner + * @param {String} href URL to which action link should point (default '#') + * @param {String} title Title of action + * @param {Function} clickHandler Method to call when action is clicked on + */ + function Flash(message, type, parent, actionConfig) { + var flash, textDiv, actionLink; if (type == null) { type = 'alert'; } @@ -30,6 +43,23 @@ window.Flash = (function() { text: message }); textDiv.appendTo(flash); + + if (actionConfig) { + const actionLinkConfig = { + class: 'flash-action', + href: actionConfig.href || '#', + text: actionConfig.title + }; + + if (!actionConfig.href) { + actionLinkConfig.role = 'button'; + } + + actionLink = $('', actionLinkConfig); + + actionLink.appendTo(flash); + this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler); + } if (this.flashContainer.parent().hasClass('content-wrapper')) { textDiv.addClass('container-fluid container-limited'); } diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index b8a923cf619..401dec1a370 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -2,6 +2,7 @@ import emojiMap from 'emojis/digests.json'; import emojiAliases from 'emojis/aliases.json'; import { glEmojiTag } from '~/behaviors/gl_emoji'; import glRegexp from '~/lib/utils/regexp'; +import AjaxCache from '~/lib/utils/ajax_cache'; function sanitize(str) { return str.replace(/<(?:.|\n)*?>/gm, ''); @@ -35,6 +36,7 @@ class GfmAutoComplete { // This triggers at.js again // Needed for slash commands with suffixes (ex: /label ~) $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); + $input.on('clear-commands-cache.atwho', () => this.clearCache()); }); } @@ -375,11 +377,14 @@ class GfmAutoComplete { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); } else { - $.getJSON(this.dataSources[GfmAutoComplete.atTypeMap[at]], (data) => { - this.loadData($input, at, data); - }).fail(() => { this.isLoadingData[at] = false; }); + AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) + .then((data) => { + this.loadData($input, at, data); + }) + .catch(() => { this.isLoadingData[at] = false; }); } } + loadData($input, at, data) { this.isLoadingData[at] = false; this.cachedData[at] = data; @@ -389,6 +394,10 @@ class GfmAutoComplete { return $input.trigger('keyup'); } + clearCache() { + this.cachedData = {}; + } + static isLoading(data) { let dataToInspect = data; if (data && data.length > 0) { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 24c423dd01e..d34561e5512 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -468,8 +468,8 @@ GitLabDropdown = (function() { // Process the data to make sure rendered data // matches the correct layout - if (this.fullData && hasMultiSelect && this.options.processData) { - const inputValue = this.filterInput.val(); + const inputValue = this.filterInput.val(); + if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) { this.options.processData.call(this.options, inputValue, this.filteredFullData(), this.parseData.bind(this)); } @@ -740,6 +740,12 @@ GitLabDropdown = (function() { $input.attr('id', this.options.inputId); } + if (this.options.multiSelect) { + Object.keys(selectedObject).forEach((attribute) => { + $input.attr(`data-${attribute}`, selectedObject[attribute]); + }); + } + if (this.options.inputMeta) { $input.attr('data-meta', selectedObject[this.options.inputMeta]); } diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index 4f226ff96ea..4bef60264bb 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -31,9 +31,13 @@ class GlFieldErrors { * and prevents disabling of invalid submit button by application.js */ catchInvalidFormSubmit (event) { - if (!event.currentTarget.checkValidity()) { - event.preventDefault(); - event.stopPropagation(); + const $form = $(event.currentTarget); + + if (!$form.attr('novalidate')) { + if (!event.currentTarget.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } } } diff --git a/app/assets/javascripts/integrations/index.js b/app/assets/javascripts/integrations/index.js new file mode 100644 index 00000000000..10fe6bac0e8 --- /dev/null +++ b/app/assets/javascripts/integrations/index.js @@ -0,0 +1,7 @@ +/* eslint-disable no-new */ +import IntegrationSettingsForm from './integration_settings_form'; + +$(() => { + const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + integrationSettingsForm.init(); +}); diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js new file mode 100644 index 00000000000..ddd3a6aab99 --- /dev/null +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -0,0 +1,123 @@ +/* global Flash */ + +export default class IntegrationSettingsForm { + constructor(formSelector) { + this.$form = $(formSelector); + + // Form Metadata + this.canTestService = this.$form.data('can-test'); + this.testEndPoint = this.$form.data('test-url'); + + // Form Child Elements + this.$serviceToggle = this.$form.find('#service_active'); + this.$submitBtn = this.$form.find('button[type="submit"]'); + this.$submitBtnLoader = this.$submitBtn.find('.js-btn-spinner'); + this.$submitBtnLabel = this.$submitBtn.find('.js-btn-label'); + } + + init() { + // Initialize View + this.toggleServiceState(this.$serviceToggle.is(':checked')); + + // Bind Event Listeners + this.$serviceToggle.on('change', e => this.handleServiceToggle(e)); + this.$submitBtn.on('click', e => this.handleSettingsSave(e)); + } + + handleSettingsSave(e) { + // Check if Service is marked active, as if not marked active, + // We can skip testing it and directly go ahead to allow form to + // be submitted + if (!this.$serviceToggle.is(':checked')) { + return; + } + + // Service was marked active so now we check; + // 1) If form contents are valid + // 2) If this service can be tested + // If both conditions are true, we override form submission + // and test the service using provided configuration. + if (this.$form.get(0).checkValidity() && this.canTestService) { + e.preventDefault(); + this.testSettings(this.$form.serialize()); + } + } + + handleServiceToggle(e) { + this.toggleServiceState($(e.currentTarget).is(':checked')); + } + + /** + * Change Form's validation enforcement based on service status (active/inactive) + */ + toggleServiceState(serviceActive) { + this.toggleSubmitBtnLabel(serviceActive); + if (serviceActive) { + this.$form.removeAttr('novalidate'); + } else if (!this.$form.attr('novalidate')) { + this.$form.attr('novalidate', 'novalidate'); + } + } + + /** + * Toggle Submit button label based on Integration status and ability to test service + */ + toggleSubmitBtnLabel(serviceActive) { + let btnLabel = 'Save changes'; + + if (serviceActive && this.canTestService) { + btnLabel = 'Test settings and save changes'; + } + + this.$submitBtnLabel.text(btnLabel); + } + + /** + * Toggle Submit button state based on provided boolean value of `saveTestActive` + * When enabled, it does two things, and reverts back when disabled + * + * 1. It shows load spinner on submit button + * 2. Makes submit button disabled + */ + toggleSubmitBtnState(saveTestActive) { + if (saveTestActive) { + this.$submitBtn.disable(); + this.$submitBtnLoader.removeClass('hidden'); + } else { + this.$submitBtn.enable(); + this.$submitBtnLoader.addClass('hidden'); + } + } + + /* eslint-disable promise/catch-or-return, no-new */ + /** + * Test Integration config + */ + testSettings(formData) { + this.toggleSubmitBtnState(true); + $.ajax({ + type: 'PUT', + url: this.testEndPoint, + data: formData, + }) + .done((res) => { + if (res.error) { + new Flash(res.message, null, null, { + title: 'Save anyway', + clickHandler: (e) => { + e.preventDefault(); + this.$form.submit(); + }, + }); + } else { + this.$form.submit(); + } + }) + .fail(() => { + new Flash('Something went wrong on our end.'); + }) + .always(() => { + this.toggleSubmitBtnState(false); + }); + } +} diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 770a0dcd27e..a6c62ee0f7f 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -1,10 +1,18 @@ diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 4ad3eb7dfd7..1fe3df28872 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,5 +1,6 @@ diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue new file mode 100644 index 00000000000..e810fba6308 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -0,0 +1,87 @@ + + + diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue new file mode 100644 index 00000000000..d59e6d11032 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/edited.vue @@ -0,0 +1,56 @@ + + + + diff --git a/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue b/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue new file mode 100644 index 00000000000..a0ff08e9111 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue @@ -0,0 +1,23 @@ + + + diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue new file mode 100644 index 00000000000..0d4a39cc4ac --- /dev/null +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -0,0 +1,72 @@ + + + diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue new file mode 100644 index 00000000000..c679616cca6 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue @@ -0,0 +1,111 @@ + + + diff --git a/app/assets/javascripts/issue_show/components/fields/project_move.vue b/app/assets/javascripts/issue_show/components/fields/project_move.vue new file mode 100644 index 00000000000..f811fb0de24 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/fields/project_move.vue @@ -0,0 +1,83 @@ + + + diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue new file mode 100644 index 00000000000..b0d8a6628dd --- /dev/null +++ b/app/assets/javascripts/issue_show/components/fields/title.vue @@ -0,0 +1,94 @@ + + + diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue new file mode 100644 index 00000000000..d7e812b7f7f --- /dev/null +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -0,0 +1,135 @@ + + + diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue new file mode 100644 index 00000000000..1c2789f154a --- /dev/null +++ b/app/assets/javascripts/issue_show/components/locked_warning.vue @@ -0,0 +1,20 @@ + + + diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index a9dabd4cff1..a61ce414891 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -1,8 +1,12 @@ diff --git a/app/assets/javascripts/issue_show/event_hub.js b/app/assets/javascripts/issue_show/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/issue_show/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index f06e33dee60..6ea9f42e322 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -1,7 +1,79 @@ import Vue from 'vue'; +import eventHub from './event_hub'; import issuableApp from './components/app.vue'; import '../vue_shared/vue_resource_interceptor'; +<<<<<<< HEAD +document.addEventListener('DOMContentLoaded', () => { + const initialDataEl = document.getElementById('js-issuable-app-initial-data'); + const initialData = JSON.parse(initialDataEl.innerHTML.replace(/"/g, '"')); + + $('.issuable-edit').on('click', (e) => { + e.preventDefault(); + + eventHub.$emit('open.form'); + }); + + return new Vue({ + el: document.getElementById('js-issuable-app'), + components: { + issuableApp, + }, + data() { + return { +<<<<<<< HEAD +<<<<<<< HEAD + ...initialData, +======= + canUpdate: gl.utils.convertPermissionToBoolean(canUpdate), + canDestroy: gl.utils.convertPermissionToBoolean(canDestroy), + canMove: gl.utils.convertPermissionToBoolean(canMove), + endpoint, + issuableRef, + initialTitle: issuableTitleElement.innerHTML, + initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '', + initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '', + isConfidential: gl.utils.convertPermissionToBoolean(isConfidential), + markdownPreviewUrl, + markdownDocs, + projectPath: initialData.project_path, + projectNamespace: initialData.namespace_path, + projectsAutocompleteUrl, + issuableTemplates: initialData.templates, +>>>>>>> 17617a3... Moved value into computed property +======= + ...initialData, +>>>>>>> b2c2751... Changed all data to come through the JSON script element + }; + }, + render(createElement) { + return createElement('issuable-app', { + props: { + canUpdate: this.canUpdate, + canDestroy: this.canDestroy, + canMove: this.canMove, + endpoint: this.endpoint, + issuableRef: this.issuableRef, + initialTitleHtml: this.initialTitleHtml, + initialTitleText: this.initialTitleText, + initialDescriptionHtml: this.initialDescriptionHtml, + initialDescriptionText: this.initialDescriptionText, + issuableTemplates: this.issuableTemplates, + isConfidential: this.isConfidential, + markdownPreviewUrl: this.markdownPreviewUrl, + markdownDocs: this.markdownDocs, + projectPath: this.projectPath, + projectNamespace: this.projectNamespace, + projectsAutocompleteUrl: this.projectsAutocompleteUrl, + updatedAt: this.updatedAt, + updatedByName: this.updatedByName, + updatedByPath: this.updatedByPath, + }, + }); + }, + }); +}); +======= document.addEventListener('DOMContentLoaded', () => new Vue({ el: document.getElementById('js-issuable-app'), components: { @@ -12,10 +84,14 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ const issuableTitleElement = issuableElement.querySelector('.title'); const issuableDescriptionElement = issuableElement.querySelector('.wiki'); const issuableDescriptionTextarea = issuableElement.querySelector('.js-task-list-field'); + const { canUpdate, endpoint, issuableRef, + updatedAt, + updatedByName, + updatedByPath, } = issuableElement.dataset; return { @@ -25,6 +101,9 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ initialTitle: issuableTitleElement.innerHTML, initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '', initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '', + updatedAt, + updatedByName, + updatedByPath, }; }, render(createElement) { @@ -36,7 +115,11 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ initialTitle: this.initialTitle, initialDescriptionHtml: this.initialDescriptionHtml, initialDescriptionText: this.initialDescriptionText, + updatedAt: this.updatedAt, + updatedByName: this.updatedByName, + updatedByPath: this.updatedByPath, }, }); }, })); +>>>>>>> 07c984d... Port fix-realtime-edited-text-for-issues 9-2-stable fix to master. diff --git a/app/assets/javascripts/issue_show/mixins/animate.js b/app/assets/javascripts/issue_show/mixins/animate.js index eda6302aa8b..4816393da1f 100644 --- a/app/assets/javascripts/issue_show/mixins/animate.js +++ b/app/assets/javascripts/issue_show/mixins/animate.js @@ -4,7 +4,7 @@ export default { this.preAnimation = true; this.pulseAnimation = false; - this.$nextTick(() => { + setTimeout(() => { this.preAnimation = false; this.pulseAnimation = true; }); diff --git a/app/assets/javascripts/issue_show/mixins/update.js b/app/assets/javascripts/issue_show/mixins/update.js new file mode 100644 index 00000000000..72be65b426f --- /dev/null +++ b/app/assets/javascripts/issue_show/mixins/update.js @@ -0,0 +1,10 @@ +import eventHub from '../event_hub'; + +export default { + methods: { + updateIssuable() { + this.formState.updateLoading = true; + eventHub.$emit('update.issuable'); + }, + }, +}; diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js index 348ad8d6813..6f0fd0b1768 100644 --- a/app/assets/javascripts/issue_show/services/index.js +++ b/app/assets/javascripts/issue_show/services/index.js @@ -7,10 +7,23 @@ export default class Service { constructor(endpoint) { this.endpoint = endpoint; - this.resource = Vue.resource(this.endpoint); + this.resource = Vue.resource(`${this.endpoint}.json`, {}, { + realtimeChanges: { + method: 'GET', + url: `${this.endpoint}/realtime_changes`, + }, + }); } getData() { - return this.resource.get(); + return this.resource.realtimeChanges(); + } + + deleteIssuable() { + return this.resource.delete(); + } + + updateIssuable(data) { + return this.resource.update(data); } } diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index 8e89a2b7730..9ba3c285e70 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -1,16 +1,39 @@ export default class Store { constructor({ titleHtml, + titleText, descriptionHtml, descriptionText, + updatedAt, + updatedByName, + updatedByPath, }) { this.state = { titleHtml, - titleText: '', + titleText, descriptionHtml, descriptionText, taskStatus: '', - updatedAt: '', + updatedAt, + updatedByName, + updatedByPath, +<<<<<<< HEAD + }; + this.formState = { + title: '', + confidential: false, + description: '', + lockedWarningVisible: false, +<<<<<<< HEAD + move_to_project_id: 0, + updateLoading: false, +<<<<<<< HEAD +======= +>>>>>>> 6a14a51... Show warning if realtime data has changed since the form has opened +======= +>>>>>>> 6becf28... use formState to update loading of save button +======= +>>>>>>> 07c984d... Port fix-realtime-edited-text-for-issues 9-2-stable fix to master. }; } @@ -21,5 +44,32 @@ export default class Store { this.state.descriptionText = data.description_text; this.state.taskStatus = data.task_status; this.state.updatedAt = data.updated_at; + this.state.updatedByName = data.updated_by_name; + this.state.updatedByPath = data.updated_by_path; +<<<<<<< HEAD + } + + stateShouldUpdate(data) { + return { + title: this.state.titleText !== data.title_text, + description: this.state.descriptionText !== data.description_text, + }; + } + + setFormState(state) { + this.formState = Object.assign(this.formState, state); + } + + stateShouldUpdate(data) { + return { + title: this.state.titleText !== data.title_text, + description: this.state.descriptionText !== data.description_text, + }; + } + + setFormState(state) { + this.formState = Object.assign(this.formState, state); +======= +>>>>>>> 07c984d... Port fix-realtime-edited-text-for-issues 9-2-stable fix to master. } } diff --git a/app/assets/javascripts/lib/utils/ajax_cache.js b/app/assets/javascripts/lib/utils/ajax_cache.js index f1fe95e12e8..7477b5a5214 100644 --- a/app/assets/javascripts/lib/utils/ajax_cache.js +++ b/app/assets/javascripts/lib/utils/ajax_cache.js @@ -6,8 +6,8 @@ class AjaxCache extends Cache { this.pendingRequests = { }; } - retrieve(endpoint) { - if (this.hasData(endpoint)) { + retrieve(endpoint, forceRetrieve) { + if (this.hasData(endpoint) && !forceRetrieve) { return Promise.resolve(this.get(endpoint)); } diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 7e62773ae6c..a537267643e 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -198,10 +198,12 @@ const textBefore = value.substring(0, selectionStart); const textAfter = value.substring(selectionEnd, value.length); - const newText = textBefore + text + textAfter; + + const insertedText = text instanceof Function ? text(textBefore, textAfter) : text; + const newText = textBefore + insertedText + textAfter; target.value = newText; - target.selectionStart = target.selectionEnd = selectionStart + text.length; + target.selectionStart = target.selectionEnd = selectionStart + insertedText.length; // Trigger autosave $(target).trigger('input'); diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index 66f39122a66..973d6119158 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -1,47 +1,48 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, max-len */ -(function() { - (function(w) { - var notificationGranted, notifyMe, notifyPermissions; - notificationGranted = function(message, opts, onclick) { - var notification; - notification = new Notification(message, opts); - setTimeout(function() { - return notification.close(); - // Hide the notification after X amount of seconds - }, 8000); - if (onclick) { - return notification.onclick = onclick; - } - }; - notifyPermissions = function() { - if ('Notification' in window) { - return Notification.requestPermission(); - } - }; - notifyMe = function(message, body, icon, onclick) { - var opts; - opts = { - body: body, - icon: icon - }; - // Let's check if the browser supports notifications - if (!('Notification' in window)) { +function notificationGranted(message, opts, onclick) { + var notification; + notification = new Notification(message, opts); + setTimeout(function() { + // Hide the notification after X amount of seconds + return notification.close(); + }, 8000); + + return notification.onclick = onclick || notification.close; +} - // do nothing - } else if (Notification.permission === 'granted') { - // If it's okay let's create a notification +function notifyPermissions() { + if ('Notification' in window) { + return Notification.requestPermission(); + } +} + +function notifyMe(message, body, icon, onclick) { + var opts; + opts = { + body: body, + icon: icon + }; + // Let's check if the browser supports notifications + if (!('Notification' in window)) { + // do nothing + } else if (Notification.permission === 'granted') { + // If it's okay let's create a notification + return notificationGranted(message, opts, onclick); + } else if (Notification.permission !== 'denied') { + return Notification.requestPermission(function(permission) { + // If the user accepts, let's create a notification + if (permission === 'granted') { return notificationGranted(message, opts, onclick); - } else if (Notification.permission !== 'denied') { - return Notification.requestPermission(function(permission) { - // If the user accepts, let's create a notification - if (permission === 'granted') { - return notificationGranted(message, opts, onclick); - } - }); } - }; - w.notify = notifyMe; - return w.notifyPermissions = notifyPermissions; - })(window); -}).call(window); + }); + } +} + +const notify = { + notificationGranted, + notifyPermissions, + notifyMe, +}; + +export default notify; diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index f1b07408671..57394097944 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -42,3 +42,13 @@ export function formatRelevantDigits(number) { export function bytesToKiB(number) { return number / BYTES_IN_KIB; } + +/** + * Utility function that calculates MiB of the given bytes. + * + * @param {Number} number bytes + * @return {Number} MiB + */ +export function bytesToMiB(number) { + return number / (BYTES_IN_KIB * BYTES_IN_KIB); +} diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index b43c1c3aac6..601d01e1be1 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -170,7 +170,7 @@ gl.text.init = function(form) { }); }; gl.text.removeListeners = function(form) { - return $('.js-md', form).off(); + return $('.js-md', form).off('click'); }; gl.text.humanize = function(string) { return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index b9d2fc25c39..3328ff9cc23 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -66,7 +66,8 @@ w.gl.utils.removeParamQueryString = function(url, param) { })()).join('&'); }; w.gl.utils.removeParams = (params) => { - const url = new URL(window.location.href); + const url = document.createElement('a'); + url.href = window.location.href; params.forEach((param) => { url.search = w.gl.utils.removeParamQueryString(url.search, param); }); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index f0958972130..1ac82b7e291 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -56,7 +56,6 @@ import './lib/utils/animate'; import './lib/utils/bootstrap_linked_tabs'; import './lib/utils/common_utils'; import './lib/utils/datetime_utility'; -import './lib/utils/notify'; import './lib/utils/pretty_time'; import './lib/utils/text_utility'; import './lib/utils/url_utility'; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 22032d0f914..894ed81b044 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -285,7 +285,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // Similar to `toggler_behavior` in the discussion tab const hash = window.gl.utils.getLocationHash(); const anchor = hash && $container.find(`[id="${hash}"]`); - if (anchor) { + if (anchor && anchor.length > 0) { const notesContent = anchor.closest('.notes_content'); const lineType = notesContent.hasClass('new') ? 'new' : 'old'; notes.toggleDiffNote({ diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index 3e8240d10ec..814d2ea92b4 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -30,7 +30,7 @@ | \\s\\$(?!\\$) ) - (.+?) + ((.|\\n)+?) ( \\s\\\\end{[a-zA-Z]+}$ | @@ -45,15 +45,25 @@ let inline = false; if (typeof katex !== 'undefined') { - const katexString = text.replace(/\\/g, '\\'); - const matches = new RegExp(katexRegexString, 'gi').exec(katexString); + const katexString = text.replace(/&/g, '&') + .replace(/&=&/g, '\\space=\\space') + .replace(/<(\/?)em>/g, '_'); + const regex = new RegExp(katexRegexString, 'gi'); + const matchLocation = katexString.search(regex); + const numberOfMatches = katexString.match(regex); - if (matches && matches.length > 0) { - if (matches[1].trim() === '$' && matches[3].trim() === '$') { + if (numberOfMatches && numberOfMatches.length !== 0) { + if (matchLocation > 0) { + let matches = regex.exec(katexString); inline = true; - text = `${katexString.replace(matches[0], '')} ${katex.renderToString(matches[2])}`; + while (matches !== null) { + const renderedKatex = katex.renderToString(matches[0].replace(/\$/g, '')); + text = `${text.replace(matches[0], ` ${renderedKatex}`)}`; + matches = regex.exec(katexString); + } } else { + const matches = regex.exec(katexString); text = katex.renderToString(matches[2]); } } @@ -79,7 +89,7 @@ }, computed: { markdown() { - return marked(this.cell.source.join('')); + return marked(this.cell.source.join('').replace(/\\/g, '\\\\')); }, }, }; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index b0b1cfd6c8a..929965de5c1 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,4 +1,10 @@ -/* 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 */ +/* 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 ResolveService */ @@ -10,6 +16,7 @@ import autosize from 'vendor/autosize'; import Dropzone from 'dropzone'; import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.atwho'; +import AjaxCache from '~/lib/utils/ajax_cache'; import CommentTypeToggle from './comment_type_toggle'; import './autosave'; import './dropzone_input'; @@ -57,10 +64,9 @@ const normalizeNewlines = function(str) { this.updatedNotesTrackingMap = {}; this.last_fetched_at = last_fetched_at; this.noteable_url = document.URL; - this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge")); + this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge')); this.basePollingInterval = 15000; this.maxPollingSteps = 4; - this.flashErrors = []; this.cleanBinding(); this.addBinding(); @@ -87,61 +93,61 @@ const normalizeNewlines = function(str) { Notes.prototype.addBinding = function() { // Edit note link - $(document).on("click", ".js-note-edit", this.showEditForm.bind(this)); - $(document).on("click", ".note-edit-cancel", this.cancelEdit); + $(document).on('click', '.js-note-edit', this.showEditForm.bind(this)); + $(document).on('click', '.note-edit-cancel', this.cancelEdit); // Reopen and close actions for Issue/MR combined with note form submit - $(document).on("click", ".js-comment-submit-button", this.postComment); - $(document).on("click", ".js-comment-save-button", this.updateComment); - $(document).on("keyup input", ".js-note-text", this.updateTargetButtons); + $(document).on('click', '.js-comment-submit-button', this.postComment); + $(document).on('click', '.js-comment-save-button', this.updateComment); + $(document).on('keyup input', '.js-note-text', this.updateTargetButtons); // resolve a discussion $(document).on('click', '.js-comment-resolve-button', this.postComment); // remove a note (in general) - $(document).on("click", ".js-note-delete", this.removeNote); + $(document).on('click', '.js-note-delete', this.removeNote); // delete note attachment - $(document).on("click", ".js-note-attachment-delete", this.removeAttachment); + $(document).on('click', '.js-note-attachment-delete', this.removeAttachment); // reset main target form when clicking discard - $(document).on("click", ".js-note-discard", this.resetMainTargetForm); + $(document).on('click', '.js-note-discard', this.resetMainTargetForm); // update the file name when an attachment is selected - $(document).on("change", ".js-note-attachment-input", this.updateFormAttachment); + $(document).on('change', '.js-note-attachment-input', this.updateFormAttachment); // reply to diff/discussion notes - $(document).on("click", ".js-discussion-reply-button", this.onReplyToDiscussionNote); + $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); // add diff note - $(document).on("click", ".js-add-diff-note-button", this.onAddDiffNote); + $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote); // hide diff note form - $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm); + $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm); // toggle commit list - $(document).on("click", '.system-note-commit-list-toggler', this.toggleCommitList); + $(document).on('click', '.system-note-commit-list-toggler', this.toggleCommitList); // fetch notes when tab becomes visible - $(document).on("visibilitychange", this.visibilityChange); + $(document).on('visibilitychange', this.visibilityChange); // when issue status changes, we need to refresh data - $(document).on("issuable:change", this.refresh); + $(document).on('issuable:change', this.refresh); // ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs. - $(document).on("ajax:success", ".js-main-target-form", this.addNote); - $(document).on("ajax:success", ".js-discussion-note-form", this.addDiscussionNote); - $(document).on("ajax:success", ".js-main-target-form", this.resetMainTargetForm); - $(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton); + $(document).on('ajax:success', '.js-main-target-form', this.addNote); + $(document).on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote); + $(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm); + $(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton); // when a key is clicked on the notes - return $(document).on("keydown", ".js-note-text", this.keydownNoteText); + return $(document).on('keydown', '.js-note-text', this.keydownNoteText); }; Notes.prototype.cleanBinding = function() { - $(document).off("click", ".js-note-edit"); - $(document).off("click", ".note-edit-cancel"); - $(document).off("click", ".js-note-delete"); - $(document).off("click", ".js-note-attachment-delete"); - $(document).off("click", ".js-discussion-reply-button"); - $(document).off("click", ".js-add-diff-note-button"); - $(document).off("visibilitychange"); - $(document).off("keyup input", ".js-note-text"); - $(document).off("click", ".js-note-target-reopen"); - $(document).off("click", ".js-note-target-close"); - $(document).off("click", ".js-note-discard"); - $(document).off("keydown", ".js-note-text"); + $(document).off('click', '.js-note-edit'); + $(document).off('click', '.note-edit-cancel'); + $(document).off('click', '.js-note-delete'); + $(document).off('click', '.js-note-attachment-delete'); + $(document).off('click', '.js-discussion-reply-button'); + $(document).off('click', '.js-add-diff-note-button'); + $(document).off('visibilitychange'); + $(document).off('keyup input', '.js-note-text'); + $(document).off('click', '.js-note-target-reopen'); + $(document).off('click', '.js-note-target-close'); + $(document).off('click', '.js-note-discard'); + $(document).off('keydown', '.js-note-text'); $(document).off('click', '.js-comment-resolve-button'); - $(document).off("click", '.system-note-commit-list-toggler'); - $(document).off("ajax:success", ".js-main-target-form"); - $(document).off("ajax:success", ".js-discussion-note-form"); - $(document).off("ajax:complete", ".js-main-target-form"); + $(document).off('click', '.system-note-commit-list-toggler'); + $(document).off('ajax:success', '.js-main-target-form'); + $(document).off('ajax:success', '.js-discussion-note-form'); + $(document).off('ajax:complete', '.js-main-target-form'); }; Notes.initCommentTypeToggle = function (form) { @@ -231,8 +237,8 @@ const normalizeNewlines = function(str) { this.refreshing = true; return $.ajax({ url: this.notes_url, - headers: { "X-Last-Fetched-At": this.last_fetched_at }, - dataType: "json", + headers: { 'X-Last-Fetched-At': this.last_fetched_at }, + dataType: 'json', success: (function(_this) { return function(data) { var notes; @@ -303,7 +309,7 @@ const normalizeNewlines = function(str) { */ Notes.prototype.renderNote = function(noteEntity, $form, $notesList = $('.main-notes-list')) { - if (noteEntity.discussion_html != null) { + if (noteEntity.discussion_html) { return this.renderDiscussionNote(noteEntity, $form); } @@ -319,6 +325,9 @@ const normalizeNewlines = function(str) { if (Notes.isNewNote(noteEntity, this.note_ids)) { this.note_ids.push(noteEntity.id); + if ($notesList.length) { + $notesList.find('.system-note.being-posted').remove(); + } const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList); this.setupNewNote($newNote); @@ -368,8 +377,8 @@ const normalizeNewlines = function(str) { return; } this.note_ids.push(noteEntity.id); - form = $form || $(".js-discussion-note-form[data-discussion-id='" + noteEntity.discussion_id + "']"); - row = form.closest("tr"); + form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); + row = form.closest('tr'); lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); // is this the first note of discussion? @@ -386,7 +395,7 @@ const normalizeNewlines = function(str) { row.after($discussion); } else { // Merge new discussion HTML in - var $notes = $discussion.find('.notes[data-discussion-id="' + noteEntity.discussion_id + '"]'); + var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); var contentContainerClass = '.' + $notes.closest('.notes_content') .attr('class') .split(' ') @@ -397,7 +406,7 @@ const normalizeNewlines = function(str) { } // Init discussion on 'Discussion' page if it is merge request page const page = $('body').attr('data-page'); - if ((page && page.indexOf('projects:merge_request') === 0) || !noteEntity.diff_discussion_html) { + if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) { Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list')); } } else { @@ -450,13 +459,13 @@ const normalizeNewlines = function(str) { Notes.prototype.resetMainTargetForm = function(e) { var form; - form = $(".js-main-target-form"); + form = $('.js-main-target-form'); // remove validation errors - form.find(".js-errors").remove(); + form.find('.js-errors').remove(); // reset text and preview - form.find(".js-md-write-button").click(); - form.find(".js-note-text").val("").trigger("input"); - form.find(".js-note-text").data("autosave").reset(); + form.find('.js-md-write-button').click(); + form.find('.js-note-text').val('').trigger('input'); + form.find('.js-note-text').data('autosave').reset(); var event = document.createEvent('Event'); event.initEvent('autosize:update', true, false); @@ -467,8 +476,8 @@ const normalizeNewlines = function(str) { Notes.prototype.reenableTargetFormSubmitButton = function() { var form; - form = $(".js-main-target-form"); - return form.find(".js-note-text").trigger("input"); + form = $('.js-main-target-form'); + return form.find('.js-note-text').trigger('input'); }; /* @@ -480,18 +489,18 @@ const normalizeNewlines = function(str) { Notes.prototype.setupMainTargetNoteForm = function() { var form; // find the form - form = $(".js-new-note-form"); + form = $('.js-new-note-form'); // Set a global clone of the form for later cloning this.formClone = form.clone(); // show the form this.setupNoteForm(form); // fix classes - form.removeClass("js-new-note-form"); - form.addClass("js-main-target-form"); - form.find("#note_line_code").remove(); - form.find("#note_position").remove(); - form.find("#note_type").val(''); - form.find("#in_reply_to_discussion_id").remove(); + form.removeClass('js-new-note-form'); + form.addClass('js-main-target-form'); + form.find('#note_line_code').remove(); + form.find('#note_position').remove(); + form.find('#note_type').val(''); + form.find('#in_reply_to_discussion_id').remove(); form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove(); this.parentTimeline = form.parents('.timeline'); @@ -512,20 +521,20 @@ const normalizeNewlines = function(str) { Notes.prototype.setupNoteForm = function(form) { var textarea, key; new gl.GLForm(form, this.enableGFM); - textarea = form.find(".js-note-text"); + textarea = form.find('.js-note-text'); key = [ - "Note", - form.find("#note_noteable_type").val(), - form.find("#note_noteable_id").val(), - form.find("#note_commit_id").val(), - form.find("#note_type").val(), - form.find("#in_reply_to_discussion_id").val(), + 'Note', + form.find('#note_noteable_type').val(), + form.find('#note_noteable_id').val(), + form.find('#note_commit_id').val(), + form.find('#note_type').val(), + form.find('#in_reply_to_discussion_id').val(), // LegacyDiffNote - form.find("#note_line_code").val(), + form.find('#note_line_code').val(), // DiffNote - form.find("#note_position").val() + form.find('#note_position').val() ]; return new Autosave(textarea, key); }; @@ -670,7 +679,8 @@ const normalizeNewlines = function(str) { const $newNote = $(this.updatedNotesTrackingMap[noteId].html); $note.replaceWith($newNote); this.setupNewNote($newNote); - this.updatedNotesTrackingMap[noteId] = null; + // Now that we have taken care of the update, clear it out + delete this.updatedNotesTrackingMap[noteId]; } else { $note.find('.js-finish-edit-warning').hide(); @@ -722,14 +732,14 @@ const normalizeNewlines = function(str) { lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]') .closest('.notes_holder') .prev('.line_holder'); - $(".note[id='" + noteElId + "']").each((function(_this) { + $(`.note[id="${noteElId}"]`).each((function(_this) { // A same note appears in the "Discussion" and in the "Changes" tab, we have - // to remove all. Using $(".note[id='noteId']") ensure we get all the notes, - // where $("#noteId") would return only one. + // to remove all. Using $('.note[id='noteId']') ensure we get all the notes, + // where $('#noteId') would return only one. return function(i, el) { var $note, $notes; $note = $(el); - $notes = $note.closest(".discussion-notes"); + $notes = $note.closest('.discussion-notes'); if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (gl.diffNoteApps[noteElId]) { @@ -740,11 +750,11 @@ const normalizeNewlines = function(str) { $note.remove(); // check if this is the last note for this line - if ($notes.find(".note").length === 0) { - var notesTr = $notes.closest("tr"); + if ($notes.find('.note').length === 0) { + var notesTr = $notes.closest('tr'); // "Discussions" tab - $notes.closest(".timeline-entry").remove(); + $notes.closest('.timeline-entry').remove(); // The notes tr can contain multiple lists of notes, like on the parallel diff if (notesTr.find('.discussion-notes').length > 1) { @@ -768,11 +778,11 @@ const normalizeNewlines = function(str) { */ Notes.prototype.removeAttachment = function() { - const $note = $(this).closest(".note"); - $note.find(".note-attachment").remove(); - $note.find(".note-body > .note-text").show(); - $note.find(".note-header").show(); - return $note.find(".current-note-edit-form").remove(); + const $note = $(this).closest('.note'); + $note.find('.note-attachment').remove(); + $note.find('.note-body > .note-text').show(); + $note.find('.note-header').show(); + return $note.find('.current-note-edit-form').remove(); }; /* @@ -788,7 +798,7 @@ const normalizeNewlines = function(str) { Notes.prototype.replyToDiscussionNote = function(target) { var form, replyLink; form = this.cleanForm(this.formClone.clone()); - replyLink = $(target).closest(".js-discussion-reply-button"); + replyLink = $(target).closest('.js-discussion-reply-button'); // insert the form after the button replyLink .closest('.discussion-reply-holder') @@ -808,26 +818,26 @@ const normalizeNewlines = function(str) { Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) { // setup note target - var discussionID = dataHolder.data("discussionId"); + var discussionID = dataHolder.data('discussionId'); if (discussionID) { - form.attr("data-discussion-id", discussionID); - form.find("#in_reply_to_discussion_id").val(discussionID); + form.attr('data-discussion-id', discussionID); + form.find('#in_reply_to_discussion_id').val(discussionID); } - form.attr("data-line-code", dataHolder.data("lineCode")); - form.find("#line_type").val(dataHolder.data("lineType")); + form.attr('data-line-code', dataHolder.data('lineCode')); + form.find('#line_type').val(dataHolder.data('lineType')); - form.find("#note_noteable_type").val(dataHolder.data("noteableType")); - form.find("#note_noteable_id").val(dataHolder.data("noteableId")); - form.find("#note_commit_id").val(dataHolder.data("commitId")); - form.find("#note_type").val(dataHolder.data("noteType")); + form.find('#note_noteable_type').val(dataHolder.data('noteableType')); + form.find('#note_noteable_id').val(dataHolder.data('noteableId')); + form.find('#note_commit_id').val(dataHolder.data('commitId')); + form.find('#note_type').val(dataHolder.data('noteType')); // LegacyDiffNote - form.find("#note_line_code").val(dataHolder.data("lineCode")); + form.find('#note_line_code').val(dataHolder.data('lineCode')); // DiffNote - form.find("#note_position").val(dataHolder.attr("data-position")); + form.find('#note_position').val(dataHolder.attr('data-position')); form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text')); form.find('.js-note-target-close').remove(); @@ -836,7 +846,7 @@ const normalizeNewlines = function(str) { form .removeClass('js-main-target-form') - .addClass("discussion-form js-discussion-note-form"); + .addClass('discussion-form js-discussion-note-form'); if (typeof gl.diffNotesCompileComponents !== 'undefined') { var $commentBtn = form.find('comment-and-resolve-btn'); @@ -845,7 +855,7 @@ const normalizeNewlines = function(str) { gl.diffNotesCompileComponents(); } - form.find(".js-note-text").focus(); + form.find('.js-note-text').focus(); form .find('.js-comment-resolve-button') .attr('data-discussion-id', discussionID); @@ -878,21 +888,21 @@ const normalizeNewlines = function(str) { }) { var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; $link = $(target); - row = $link.closest("tr"); + row = $link.closest('tr'); const nextRow = row.next(); let targetRow = row; if (nextRow.is('.notes_holder')) { targetRow = nextRow; } - hasNotes = targetRow.is(".notes_holder"); + hasNotes = nextRow.is('.notes_holder'); addForm = false; let lineTypeSelector = ''; - rowCssToAdd = "
"; + rowCssToAdd = '
'; // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineTypeSelector = `.${lineType}`; - rowCssToAdd = "
"; + rowCssToAdd = '
'; } const notesContentSelector = `.notes_content${lineTypeSelector} .content`; let notesContent = targetRow.find(notesContentSelector); @@ -902,12 +912,12 @@ const normalizeNewlines = function(str) { notesContent = targetRow.find(notesContentSelector); if (notesContent.length) { notesContent.show(); - replyButton = notesContent.find(".js-discussion-reply-button:visible"); + replyButton = notesContent.find('.js-discussion-reply-button:visible'); if (replyButton.length) { this.replyToDiscussionNote(replyButton[0]); } else { // In parallel view, the form may not be present in one of the panes - noteForm = notesContent.find(".js-discussion-note-form"); + noteForm = notesContent.find('.js-discussion-note-form'); if (noteForm.length === 0) { addForm = true; } @@ -945,15 +955,15 @@ const normalizeNewlines = function(str) { Notes.prototype.removeDiscussionNoteForm = function(form) { var glForm, row; - row = form.closest("tr"); + row = form.closest('tr'); glForm = form.data('gl-form'); glForm.destroy(); - form.find(".js-note-text").data("autosave").reset(); + form.find('.js-note-text').data('autosave').reset(); // show the reply button (will only work for replies) form .prev('.discussion-reply-holder') .show(); - if (row.is(".js-temp-notes-holder")) { + if (row.is('.js-temp-notes-holder')) { // remove temporary row for diff lines return row.remove(); } else { @@ -965,7 +975,7 @@ const normalizeNewlines = function(str) { Notes.prototype.cancelDiscussionForm = function(e) { var form; e.preventDefault(); - form = $(e.target).closest(".js-discussion-note-form"); + form = $(e.target).closest('.js-discussion-note-form'); return this.removeDiscussionNoteForm(form); }; @@ -977,10 +987,10 @@ const normalizeNewlines = function(str) { Notes.prototype.updateFormAttachment = function() { var filename, form; - form = $(this).closest("form"); + form = $(this).closest('form'); // get only the basename - filename = $(this).val().replace(/^.*[\\\/]/, ""); - return form.find(".js-attachment-filename").text(filename); + filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find('.js-attachment-filename').text(filename); }; /* @@ -1111,12 +1121,14 @@ const normalizeNewlines = function(str) { }; Notes.prototype.addFlash = function(...flashParams) { - this.flashErrors.push(new Flash(...flashParams)); + this.flashInstance = new Flash(...flashParams); }; Notes.prototype.clearFlash = function() { - this.flashErrors.forEach(flash => flash.flashContainer.remove()); - this.flashErrors = []; + if (this.flashInstance && this.flashInstance.flashContainer) { + this.flashInstance.flashContainer.hide(); + this.flashInstance = null; + } }; Notes.prototype.cleanForm = function($form) { @@ -1180,7 +1192,7 @@ const normalizeNewlines = function(str) { Notes.prototype.getFormData = function($form) { return { formData: $form.serialize(), - formContent: $form.find('.js-note-text').val(), + formContent: _.escape($form.find('.js-note-text').val()), formAction: $form.attr('action'), }; }; @@ -1199,20 +1211,47 @@ const normalizeNewlines = function(str) { return formContent.replace(REGEX_SLASH_COMMANDS, '').trim(); }; + /** + * Gets appropriate description from slash commands found in provided `formContent` + */ + Notes.prototype.getSlashCommandDescription = function (formContent, availableSlashCommands = []) { + let tempFormContent; + + // Identify executed slash commands from `formContent` + const executedCommands = availableSlashCommands.filter((command, index) => { + const commandRegex = new RegExp(`/${command.name}`); + return commandRegex.test(formContent); + }); + + if (executedCommands && executedCommands.length) { + if (executedCommands.length > 1) { + tempFormContent = 'Applying multiple commands'; + } else { + const commandDescription = executedCommands[0].description.toLowerCase(); + tempFormContent = `Applying command to ${commandDescription}`; + } + } else { + tempFormContent = 'Applying command'; + } + + return tempFormContent; + }; + /** * Create placeholder note DOM element populated with comment body * that we will show while comment is being posted. * Once comment is _actually_ posted on server, we will have final element * in response that we will show in place of this temporary element. */ - Notes.prototype.createPlaceholderNote = function({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname }) { + Notes.prototype.createPlaceholderNote = function ({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) { const discussionClass = isDiscussionNote ? 'discussion' : ''; - const escapedFormContent = _.escape(formContent); const $tempNote = $( `
  • @@ -1225,7 +1264,7 @@ const normalizeNewlines = function(str) {
    -

    ${escapedFormContent}

    +

    ${formContent}

    @@ -1236,6 +1275,23 @@ const normalizeNewlines = function(str) { return $tempNote; }; + /** + * Create Placeholder System Note DOM element populated with slash command description + */ + Notes.prototype.createPlaceholderSystemNote = function ({ formContent, uniqueId }) { + const $tempNote = $( + `
  • +
    +
    + ${formContent} +
    +
    +
  • ` + ); + + return $tempNote; + }; + /** * This method does following tasks step-by-step whenever a new comment * is submitted by user (both main thread comments as well as discussion comments). @@ -1267,7 +1323,9 @@ const normalizeNewlines = function(str) { const isDiscussionForm = $form.hasClass('js-discussion-note-form'); const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); const { formData, formContent, formAction } = this.getFormData($form); - const uniqueId = _.uniqueId('tempNote_'); + let noteUniqueId; + let systemNoteUniqueId; + let hasSlashCommands = false; let $notesContainer; let tempFormContent; @@ -1288,16 +1346,28 @@ const normalizeNewlines = function(str) { tempFormContent = formContent; if (this.hasSlashCommands(formContent)) { tempFormContent = this.stripSlashCommands(formContent); + hasSlashCommands = true; } + // Show placeholder note if (tempFormContent) { - // Show placeholder note + noteUniqueId = _.uniqueId('tempNote_'); $notesContainer.append(this.createPlaceholderNote({ formContent: tempFormContent, - uniqueId, + uniqueId: noteUniqueId, isDiscussionNote, currentUsername: gon.current_username, currentUserFullname: gon.current_user_fullname, + currentUserAvatar: gon.current_user_avatar_url, + })); + } + + // Show placeholder system note + if (hasSlashCommands) { + systemNoteUniqueId = _.uniqueId('tempSystemNote_'); + $notesContainer.append(this.createPlaceholderSystemNote({ + formContent: this.getSlashCommandDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)), + uniqueId: systemNoteUniqueId, })); } @@ -1315,7 +1385,13 @@ const normalizeNewlines = function(str) { gl.utils.ajaxPost(formAction, formData) .then((note) => { // Submission successful! remove placeholder - $notesContainer.find(`#${uniqueId}`).remove(); + $notesContainer.find(`#${noteUniqueId}`).remove(); + + // Reset cached commands list when command is applied + if (hasSlashCommands) { + $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); + } + // Clear previous form errors this.clearFlashWrapper(); @@ -1352,7 +1428,11 @@ const normalizeNewlines = function(str) { $form.trigger('ajax:success', [note]); }).fail(() => { // Submission failed, remove placeholder note and show Flash error message - $notesContainer.find(`#${uniqueId}`).remove(); + $notesContainer.find(`#${noteUniqueId}`).remove(); + + if (hasSlashCommands) { + $notesContainer.find(`#${systemNoteUniqueId}`).remove(); + } // Show form again on UI on failure if (isDiscussionForm && $notesContainer.length) { diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 14c98847d93..77cbaeb43ef 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,68 +1,32 @@ + diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.js b/app/assets/javascripts/pipelines/components/pipeline_url.js deleted file mode 100644 index 7cd2e0f9366..00000000000 --- a/app/assets/javascripts/pipelines/components/pipeline_url.js +++ /dev/null @@ -1,56 +0,0 @@ -import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; - -export default { - props: [ - 'pipeline', - ], - computed: { - user() { - return !!this.pipeline.user; - }, - }, - components: { - userAvatarLink, - }, - template: ` - - - #{{pipeline.id}} - - by - - - API - - - latest - - - yaml invalid - - - stuck - - - `, -}; diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue new file mode 100644 index 00000000000..4781a8ff1da --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -0,0 +1,65 @@ + + diff --git a/app/assets/javascripts/pipelines/graph_bundle.js b/app/assets/javascripts/pipelines/graph_bundle.js deleted file mode 100644 index b7a6b5d8479..00000000000 --- a/app/assets/javascripts/pipelines/graph_bundle.js +++ /dev/null @@ -1,10 +0,0 @@ -import Vue from 'vue'; -import pipelineGraph from './components/graph/graph_component.vue'; - -document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#js-pipeline-graph-vue', - components: { - pipelineGraph, - }, - render: createElement => createElement('pipeline-graph'), -})); diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js new file mode 100644 index 00000000000..bfc416da50b --- /dev/null +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -0,0 +1,70 @@ +/* global Flash */ + +import Vue from 'vue'; +import PipelinesMediator from './pipeline_details_mediatior'; +import pipelineGraph from './components/graph/graph_component.vue'; +import pipelineHeader from './components/header_component.vue'; +import eventHub from './event_hub'; + +document.addEventListener('DOMContentLoaded', () => { + const dataset = document.querySelector('.js-pipeline-details-vue').dataset; + + const mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); + + mediator.fetchPipeline(); + + // eslint-disable-next-line + new Vue({ + el: '#js-pipeline-graph-vue', + data() { + return { + mediator, + }; + }, + components: { + pipelineGraph, + }, + render(createElement) { + return createElement('pipeline-graph', { + props: { + isLoading: this.mediator.state.isLoading, + pipeline: this.mediator.store.state.pipeline, + }, + }); + }, + }); + + // eslint-disable-next-line + new Vue({ + el: '#js-pipeline-header-vue', + data() { + return { + mediator, + }; + }, + components: { + pipelineHeader, + }, + created() { + eventHub.$on('headerPostAction', this.postAction); + }, + beforeDestroy() { + eventHub.$off('headerPostAction', this.postAction); + }, + methods: { + postAction(action) { + this.mediator.service.postAction(action.path) + .then(() => this.mediator.refreshPipeline()) + .catch(() => new Flash('An error occurred while making the request.')); + }, + }, + render(createElement) { + return createElement('pipeline-header', { + props: { + isLoading: this.mediator.state.isLoading, + pipeline: this.mediator.store.state.pipeline, + }, + }); + }, + }); +}); diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js new file mode 100644 index 00000000000..82537ea06f5 --- /dev/null +++ b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js @@ -0,0 +1,59 @@ +/* global Flash */ + +import Visibility from 'visibilityjs'; +import Poll from '../lib/utils/poll'; +import PipelineStore from './stores/pipeline_store'; +import PipelineService from './services/pipeline_service'; + +export default class pipelinesMediator { + constructor(options = {}) { + this.options = options; + this.store = new PipelineStore(); + this.service = new PipelineService(options.endpoint); + + this.state = {}; + this.state.isLoading = false; + } + + fetchPipeline() { + this.poll = new Poll({ + resource: this.service, + method: 'getPipeline', + successCallback: this.successCallback.bind(this), + errorCallback: this.errorCallback.bind(this), + }); + + if (!Visibility.hidden()) { + this.state.isLoading = true; + this.poll.makeRequest(); + } else { + this.refreshPipeline(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + } + + successCallback(response) { + const data = response.json(); + + this.state.isLoading = false; + this.store.storePipeline(data); + } + + errorCallback() { + this.state.isLoading = false; + return new Flash('An error occurred while fetching the pipeline.'); + } + + refreshPipeline() { + this.service.getPipeline() + .then(response => this.successCallback(response)) + .catch(() => this.errorCallback()); + } +} diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js index d6952d1ee5f..9f247af1dec 100644 --- a/app/assets/javascripts/pipelines/pipelines.js +++ b/app/assets/javascripts/pipelines/pipelines.js @@ -169,7 +169,7 @@ export default { eventHub.$on('refreshPipelines', this.fetchPipelines); }, - beforeDestroyed() { + beforeDestroy() { eventHub.$off('refreshPipelines'); }, diff --git a/app/assets/javascripts/pipelines/services/pipeline_service.js b/app/assets/javascripts/pipelines/services/pipeline_service.js index f1cc60c1ee0..3e0c52c7726 100644 --- a/app/assets/javascripts/pipelines/services/pipeline_service.js +++ b/app/assets/javascripts/pipelines/services/pipeline_service.js @@ -11,4 +11,9 @@ export default class PipelineService { getPipeline() { return this.pipeline.get(); } + + // eslint-disable-next-line + postAction(endpoint) { + return Vue.http.post(`${endpoint}.json`); + } } diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js index b21f84b4545..e2285494e62 100644 --- a/app/assets/javascripts/pipelines/services/pipelines_service.js +++ b/app/assets/javascripts/pipelines/services/pipelines_service.js @@ -33,8 +33,6 @@ export default class PipelinesService { /** * Post request for all pipelines actions. - * Endpoint content type needs to be: - * `Content-Type:application/x-www-form-urlencoded` * * @param {String} endpoint * @return {Promise} diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js index 86ab50d8f1e..052e34a8aef 100644 --- a/app/assets/javascripts/pipelines/stores/pipeline_store.js +++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js @@ -2,10 +2,10 @@ export default class PipelineStore { constructor() { this.state = {}; - this.state.graph = []; + this.state.pipeline = {}; } - storeGraph(graph = []) { - this.state.graph = graph; + storePipeline(pipeline = {}) { + this.state.pipeline = pipeline; } } diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 0ff0a3b6cc4..9896b88d487 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -51,6 +51,9 @@ import Api from './api'; this.groupId = $(select).data('group-id'); this.includeGroups = $(select).data('include-groups'); this.orderBy = $(select).data('order-by') || 'id'; + this.withIssuesEnabled = $(select).data('with-issues-enabled'); + this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled'); + placeholder = "Search for project"; if (this.includeGroups) { placeholder += " or group"; @@ -84,7 +87,11 @@ import Api from './api'; if (_this.groupId) { return Api.groupProjects(_this.groupId, query.term, projectsCallback); } else { - return Api.projects(query.term, { order_by: _this.orderBy }, projectsCallback); + return Api.projects(query.term, { + order_by: _this.orderBy, + with_issues_enabled: _this.withIssuesEnabled, + with_merge_requests_enabled: _this.withMergeRequestsEnabled + }, projectsCallback); } }; })(this), diff --git a/app/assets/javascripts/raven/index.js b/app/assets/javascripts/raven/index.js index 5325e495815..edc2293915f 100644 --- a/app/assets/javascripts/raven/index.js +++ b/app/assets/javascripts/raven/index.js @@ -6,6 +6,10 @@ const index = function index() { currentUserId: gon.current_user_id, whitelistUrls: [gon.gitlab_url], isProduction: process.env.NODE_ENV, + release: gon.revision, + tags: { + revision: gon.revision, + }, }); return RavenConfig; diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js index c7fe1cacf49..ae54fa5f1a9 100644 --- a/app/assets/javascripts/raven/raven_config.js +++ b/app/assets/javascripts/raven/raven_config.js @@ -1,4 +1,5 @@ import Raven from 'raven-js'; +import $ from 'jquery'; const IGNORE_ERRORS = [ // Random plugins/extensions @@ -57,6 +58,8 @@ const RavenConfig = { configure() { Raven.config(this.options.sentryDsn, { + release: this.options.release, + tags: this.options.tags, whitelistUrls: this.options.whitelistUrls, environment: this.options.isProduction ? 'production' : 'development', ignoreErrors: this.IGNORE_ERRORS, @@ -72,7 +75,7 @@ const RavenConfig = { }, bindRavenErrors() { - window.$(document).on('ajaxError.raven', this.handleRavenErrors); + $(document).on('ajaxError.raven', this.handleRavenErrors); }, handleRavenErrors(event, req, config, err) { diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index b07b3a4d3a5..51448252c0f 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -38,7 +38,7 @@ import './shortcuts_navigation'; } ShortcutsIssuable.prototype.replyWithSelectedText = function() { - var quote, documentFragment, selected, separator; + var quote, documentFragment, el, selected, separator; var replyField = $('.js-main-target-form #note_note'); documentFragment = window.gl.utils.getSelectedFragment(); @@ -47,10 +47,8 @@ import './shortcuts_navigation'; return; } - // If the documentFragment contains more than just Markdown, don't copy as GFM. - if (documentFragment.querySelector('.md, .wiki')) return; - - selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment); + el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); + selected = window.gl.CopyAsGFM.nodeToGFM(el); if (selected.trim() === "") { return; @@ -79,7 +77,9 @@ import './shortcuts_navigation'; ShortcutsIssuable.prototype.editIssue = function() { var $editBtn; $editBtn = $('.issuable-edit'); - return gl.utils.visitUrl($editBtn.attr('href')); + // Need to click the element as on issues, editing is inline + // on merge request, editing is on a different page + $editBtn.get(0).click(); }; ShortcutsIssuable.prototype.openSidebarDropdown = function(name) { diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js index 1488a66c695..da4abf0b68f 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js @@ -69,10 +69,11 @@ export default {
    a.value) + .filter((input) => { + const userId = parseInt(input.value, 10); + const inUsersArray = users.find(u => u.id === userId); + + return !inUsersArray && userId !== 0; + }) + .map((input) => { + const userId = parseInt(input.value, 10); + const { avatarUrl, avatar_url, name, username } = input.dataset; + return { + avatar_url: avatarUrl || avatar_url, + id: userId, + name, + username, + }; + }); + + users = data.concat(selectedUsers); + } + let anyUser; let index; let j; @@ -645,7 +675,7 @@ UsersSelect.prototype.users = function(query, options, callback) { url: url, data: { search: query, - per_page: 20, + per_page: options.perPage || 20, active: true, project_id: options.projectId || null, group_id: options.groupId || null, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index 486b13e60af..8155218681c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -1,4 +1,6 @@ import statusCodes from '~/lib/utils/http_status'; +import { bytesToMiB } from '~/lib/utils/number_utils'; + import MemoryGraph from '../../vue_shared/components/memory_graph'; import MRWidgetService from '../services/mr_widget_service'; @@ -9,8 +11,8 @@ export default { }, data() { return { - // memoryFrom: 0, - // memoryTo: 0, + memoryFrom: 0, + memoryTo: 0, memoryMetrics: [], deploymentTime: 0, hasMetrics: false, @@ -35,18 +37,38 @@ export default { shouldShowMetricsUnavailable() { return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed; }, + memoryChangeType() { + const memoryTo = Number(this.memoryTo); + const memoryFrom = Number(this.memoryFrom); + + if (memoryTo > memoryFrom) { + return 'increased'; + } else if (memoryTo < memoryFrom) { + return 'decreased'; + } + + return 'unchanged'; + }, }, methods: { + getMegabytes(bytesString) { + const valueInBytes = Number(bytesString).toFixed(2); + return (bytesToMiB(valueInBytes)).toFixed(2); + }, computeGraphData(metrics, deploymentTime) { this.loadingMetrics = false; - const { memory_values } = metrics; - // if (memory_previous.length > 0) { - // this.memoryFrom = Number(memory_previous[0].value[1]).toFixed(2); - // } - // - // if (memory_current.length > 0) { - // this.memoryTo = Number(memory_current[0].value[1]).toFixed(2); - // } + const { memory_before, memory_after, memory_values } = metrics; + + // Both `memory_before` and `memory_after` objects + // have peculiar structure where accessing only a specific + // index yeilds correct value that we can use to show memory delta. + if (memory_before.length > 0) { + this.memoryFrom = this.getMegabytes(memory_before[0].value[1]); + } + + if (memory_after.length > 0) { + this.memoryTo = this.getMegabytes(memory_after[0].value[1]); + } if (memory_values.length > 0) { this.hasMetrics = true; @@ -102,7 +124,7 @@ export default {

    - Deployment memory usage: + Memory usage {{memoryChangeType}} from {{memoryFrom}}MB to {{memoryTo}}MB

    1; @@ -252,8 +255,9 @@ export default {