summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorConstance Okoghenun <cokoghenun@gitlab.com>2018-06-21 21:43:23 +0100
committerConstance Okoghenun <cokoghenun@gitlab.com>2018-06-21 21:43:23 +0100
commit0ca36536e7dfca2f13130cd3d4f15f8f11b649dc (patch)
tree751dac85bbabc7ca363509cd2b93ee5389602f99 /app
parent95600c34c02986211a4784c3cd30adf94fc25127 (diff)
parent89868116c67b4b57d8aec2024d5838d49460588d (diff)
downloadgitlab-ce-0ca36536e7dfca2f13130cd3d4f15f8f11b649dc.tar.gz
Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into 39543-milestone-page-list-redesign
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/activities.js2
-rw-r--r--app/assets/javascripts/autosave.js6
-rw-r--r--app/assets/javascripts/awards_handler.js191
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js2
-rw-r--r--app/assets/javascripts/blob/pdf/index.js1
-rw-r--r--app/assets/javascripts/blob/viewer/index.js2
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js3
-rw-r--r--app/assets/javascripts/boards/components/board.js37
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js4
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue (renamed from app/assets/javascripts/boards/components/modal/empty_state.js)62
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js4
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js2
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js1
-rw-r--r--app/assets/javascripts/boards/index.js2
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js1
-rw-r--r--app/assets/javascripts/boards/models/issue.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js2
-rw-r--r--app/assets/javascripts/boards/models/milestone.js2
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js4
-rw-r--r--app/assets/javascripts/build_artifacts.js2
-rw-r--r--app/assets/javascripts/commit/image_file.js4
-rw-r--r--app/assets/javascripts/commits.js2
-rw-r--r--app/assets/javascripts/compare_autocomplete.js2
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js8
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue2
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js5
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js61
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js5
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js2
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js30
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js6
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js2
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js2
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js40
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue197
-rw-r--r--app/assets/javascripts/diffs/components/changed_files.vue184
-rw-r--r--app/assets/javascripts/diffs/components/changed_files_dropdown.vue124
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue55
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions_dropdown.vue165
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue38
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue39
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue191
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue254
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue105
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue203
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue93
-rw-r--r--app/assets/javascripts/diffs/components/edit_button.vue42
-rw-r--r--app/assets/javascripts/diffs/components/hidden_files_warning.vue51
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue117
-rw-r--r--app/assets/javascripts/diffs/components/no_changes.vue49
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue224
-rw-r--r--app/assets/javascripts/diffs/constants.js24
-rw-r--r--app/assets/javascripts/diffs/index.js39
-rw-r--r--app/assets/javascripts/diffs/mixins/changed_files.js38
-rw-r--r--app/assets/javascripts/diffs/mixins/diff_content.js89
-rw-r--r--app/assets/javascripts/diffs/store/actions.js99
-rw-r--r--app/assets/javascripts/diffs/store/getters.js16
-rw-r--r--app/assets/javascripts/diffs/store/index.js11
-rw-r--r--app/assets/javascripts/diffs/store/modules/index.js25
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js11
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js85
-rw-r--r--app/assets/javascripts/diffs/store/utils.js172
-rw-r--r--app/assets/javascripts/dispatcher.js2
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue2
-rw-r--r--app/assets/javascripts/gl_dropdown.js2
-rw-r--r--app/assets/javascripts/groups/components/app.vue1
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue63
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue9
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue26
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue30
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue4
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue12
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff_worker.js2
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js1
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js2
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js1
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js2
-rw-r--r--app/assets/javascripts/issuable_form.js2
-rw-r--r--app/assets/javascripts/issue.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/locked_warning.vue2
-rw-r--r--app/assets/javascripts/label_manager.js2
-rw-r--r--app/assets/javascripts/labels_select.js2
-rw-r--r--app/assets/javascripts/lazy_loader.js4
-rw-r--r--app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js144
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js7
-rw-r--r--app/assets/javascripts/lib/utils/notify.js2
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js2
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js21
-rw-r--r--app/assets/javascripts/line_highlighter.js14
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js2
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js2
-rw-r--r--app/assets/javascripts/merge_request.js3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js11
-rw-r--r--app/assets/javascripts/milestone.js4
-rw-r--r--app/assets/javascripts/milestone_select.js7
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue2
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js2
-rw-r--r--app/assets/javascripts/mr_notes/index.js51
-rw-r--r--app/assets/javascripts/mr_notes/stores/actions.js7
-rw-r--r--app/assets/javascripts/mr_notes/stores/getters.js5
-rw-r--r--app/assets/javascripts/mr_notes/stores/index.js15
-rw-r--r--app/assets/javascripts/mr_notes/stores/modules/index.js12
-rw-r--r--app/assets/javascripts/mr_notes/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/mr_notes/stores/mutations.js7
-rw-r--r--app/assets/javascripts/namespace_select.js2
-rw-r--r--app/assets/javascripts/network/branch_graph.js8
-rw-r--r--app/assets/javascripts/new_branch_form.js2
-rw-r--r--app/assets/javascripts/new_commit_form.js2
-rw-r--r--app/assets/javascripts/notes.js274
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue51
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue125
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue19
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue20
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue9
-rw-r--r--app/assets/javascripts/notes/components/note_edited_text.vue13
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue53
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue11
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue207
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue54
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue82
-rw-r--r--app/assets/javascripts/notes/constants.js2
-rw-r--r--app/assets/javascripts/notes/index.js81
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js6
-rw-r--r--app/assets/javascripts/notes/mixins/noteable.js9
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js36
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js6
-rw-r--r--app/assets/javascripts/notes/stores/actions.js41
-rw-r--r--app/assets/javascripts/notes/stores/getters.js71
-rw-r--r--app/assets/javascripts/notes/stores/index.js26
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js26
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js4
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js96
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js6
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js9
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js8
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js17
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/network/network.js2
-rw-r--r--app/assets/javascripts/pages/projects/project.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.js4
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js3
-rw-r--r--app/assets/javascripts/pages/sessions/new/oauth_remember_me.js1
-rw-r--r--app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js5
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js2
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue1
-rw-r--r--app/assets/javascripts/preview_markdown.js2
-rw-r--r--app/assets/javascripts/profile/gl_crop.js9
-rw-r--r--app/assets/javascripts/profile/profile.js3
-rw-r--r--app/assets/javascripts/project_find_file.js7
-rw-r--r--app/assets/javascripts/project_import.js2
-rw-r--r--app/assets/javascripts/project_select.js2
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js2
-rw-r--r--app/assets/javascripts/right_sidebar.js2
-rw-r--r--app/assets/javascripts/search_autocomplete.js4
-rw-r--r--app/assets/javascripts/settings_panels.js4
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js4
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js24
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue2
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js1
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js2
-rw-r--r--app/assets/javascripts/single_file_diff.js2
-rw-r--r--app/assets/javascripts/snippet/snippet_embed.js2
-rw-r--r--app/assets/javascripts/syntax_highlight.js2
-rw-r--r--app/assets/javascripts/tree.js2
-rw-r--r--app/assets/javascripts/users_select.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue141
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js2
-rw-r--r--app/assets/javascripts/zen_mode.js2
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss42
-rw-r--r--app/assets/stylesheets/framework/animations.scss9
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/flash.scss14
-rw-r--r--app/assets/stylesheets/framework/forms.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss4
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss9
-rw-r--r--app/assets/stylesheets/highlight/dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss4
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss4
-rw-r--r--app/assets/stylesheets/pages/commits.scss14
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss3
-rw-r--r--app/assets/stylesheets/pages/diff.scss21
-rw-r--r--app/assets/stylesheets/pages/issuable.scss2
-rw-r--r--app/assets/stylesheets/pages/labels.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss5
-rw-r--r--app/assets/stylesheets/pages/note_form.scss16
-rw-r--r--app/assets/stylesheets/pages/notes.scss70
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss2
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss8
-rw-r--r--app/assets/stylesheets/pages/projects.scss1
-rw-r--r--app/assets/stylesheets/pages/repo.scss60
-rw-r--r--app/assets/stylesheets/pages/settings.scss10
-rw-r--r--app/controllers/admin/hooks_controller.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/issues_action.rb12
-rw-r--r--app/controllers/concerns/issues_calendar.rb24
-rw-r--r--app/controllers/concerns/notes_actions.rb6
-rw-r--r--app/controllers/concerns/uploads_actions.rb13
-rw-r--r--app/controllers/dashboard_controller.rb2
-rw-r--r--app/controllers/projects/artifacts_controller.rb9
-rw-r--r--app/controllers/projects/blob_controller.rb57
-rw-r--r--app/controllers/projects/branches_controller.rb5
-rw-r--r--app/controllers/projects/commits_controller.rb14
-rw-r--r--app/controllers/projects/discussions_controller.rb9
-rw-r--r--app/controllers/projects/issues_controller.rb10
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb25
-rw-r--r--app/controllers/projects/merge_requests_controller.rb5
-rw-r--r--app/controllers/projects_controller.rb4
-rw-r--r--app/finders/notes_finder.rb2
-rw-r--r--app/helpers/issuables_helper.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/notes_helper.rb6
-rw-r--r--app/helpers/projects_helper.rb48
-rw-r--r--app/mailers/emails/merge_requests.rb2
-rw-r--r--app/models/clusters/applications/prometheus.rb3
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/concerns/redis_cacheable.rb2
-rw-r--r--app/models/discussion.rb4
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/label.rb15
-rw-r--r--app/models/merge_request.rb17
-rw-r--r--app/models/note.rb1
-rw-r--r--app/models/project.rb6
-rw-r--r--app/models/project_services/chat_notification_service.rb1
-rw-r--r--app/models/repository.rb7
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/presenters/merge_request_presenter.rb11
-rw-r--r--app/serializers/blob_entity.rb4
-rw-r--r--app/serializers/diff_file_entity.rb123
-rw-r--r--app/serializers/diffs_entity.rb65
-rw-r--r--app/serializers/diffs_serializer.rb3
-rw-r--r--app/serializers/discussion_entity.rb49
-rw-r--r--app/serializers/merge_request_basic_entity.rb4
-rw-r--r--app/serializers/merge_request_diff_entity.rb46
-rw-r--r--app/serializers/merge_request_user_entity.rb24
-rw-r--r--app/serializers/merge_request_widget_entity.rb12
-rw-r--r--app/serializers/note_entity.rb28
-rw-r--r--app/services/ci/register_job_service.rb12
-rw-r--r--app/services/concerns/issues/resolve_discussions.rb1
-rw-r--r--app/services/projects/autocomplete_service.rb2
-rw-r--r--app/uploaders/favicon_uploader.rb11
-rw-r--r--app/views/admin/appearances/_form.html.haml6
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml4
-rw-r--r--app/views/email_rejection_mailer/rejection.html.haml1
-rw-r--r--app/views/email_rejection_mailer/rejection.text.haml1
-rw-r--r--app/views/explore/projects/_filter.html.haml2
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml2
-rw-r--r--app/views/layouts/header/_new_dropdown.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml18
-rw-r--r--app/views/notify/merge_request_unmergeable_email.html.haml6
-rw-r--r--app/views/notify/merge_request_unmergeable_email.text.haml5
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/new_merge_request_email.text.erb2
-rw-r--r--app/views/projects/_new_project_fields.html.haml2
-rw-r--r--app/views/projects/blob/_header.html.haml2
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml2
-rw-r--r--app/views/projects/clusters/_sidebar.html.haml6
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml23
-rw-r--r--app/views/projects/clusters/gcp/login.html.haml5
-rw-r--r--app/views/projects/commits/_commit.html.haml3
-rw-r--r--app/views/projects/deploy_tokens/_index.html.haml9
-rw-r--r--app/views/projects/deploy_tokens/_new_deploy_token.html.haml28
-rw-r--r--app/views/projects/empty.html.haml5
-rw-r--r--app/views/projects/issues/_discussion.html.haml3
-rw-r--r--app/views/projects/issues/_new_branch.html.haml4
-rw-r--r--app/views/projects/merge_requests/diffs/_version_controls.html.haml4
-rw-r--r--app/views/projects/merge_requests/show.html.haml35
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/refs/logs_tree.js.haml2
-rw-r--r--app/views/projects/services/_index.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml2
-rw-r--r--app/views/projects/wikis/git_access.html.haml2
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml5
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml4
-rw-r--r--app/views/shared/notes/_note.html.haml4
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml7
-rw-r--r--app/views/shared/notifications/_button.html.haml4
-rw-r--r--app/views/shared/runners/show.html.haml14
-rw-r--r--app/workers/repository_fork_worker.rb26
314 files changed, 5228 insertions, 1536 deletions
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index c117d080bda..de4566bb119 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign, class-methods-use-this */
+/* eslint-disable class-methods-use-this */
import $ from 'jquery';
import Cookies from 'js-cookie';
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 0da872db7e5..fa00a3cf386 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */
+/* eslint-disable no-param-reassign, prefer-template, no-void, consistent-return */
import AccessorUtilities from './lib/utils/accessor';
@@ -31,7 +31,9 @@ export default class Autosave {
// https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
const event = new Event('change', { bubbles: true, cancelable: false });
const field = this.field.get(0);
- field.dispatchEvent(event);
+ if (field) {
+ field.dispatchEvent(event);
+ }
}
save() {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index eb0f06efab4..70f20c5c7cf 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -11,7 +11,8 @@ import axios from './lib/utils/axios_utils';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
-const requestAnimationFrame = window.requestAnimationFrame ||
+const requestAnimationFrame =
+ window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.setTimeout;
@@ -37,21 +38,28 @@ class AwardsHandler {
this.emoji = emoji;
this.eventListeners = [];
// If the user shows intent let's pre-build the menu
- this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
- const $menu = $('.emoji-menu');
- if ($menu.length === 0) {
- requestAnimationFrame(() => {
- this.createEmojiMenu();
- });
- }
- });
- this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
+ this.registerEventListener(
+ 'one',
+ $(document),
+ 'mouseenter focus',
+ '.js-add-award',
+ 'mouseenter focus',
+ () => {
+ const $menu = $('.emoji-menu');
+ if ($menu.length === 0) {
+ requestAnimationFrame(() => {
+ this.createEmojiMenu();
+ });
+ }
+ },
+ );
+ this.registerEventListener('on', $(document), 'click', '.js-add-award', e => {
e.stopPropagation();
e.preventDefault();
this.showEmojiMenu($(e.currentTarget));
});
- this.registerEventListener('on', $('html'), 'click', (e) => {
+ this.registerEventListener('on', $('html'), 'click', e => {
const $target = $(e.target);
if (!$target.closest('.emoji-menu').length) {
$('.js-awards-block.current').removeClass('current');
@@ -61,12 +69,14 @@ class AwardsHandler {
}
}
});
- this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => {
+ this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', e => {
e.preventDefault();
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
const $spriteIconElement = $target.find('.icon');
- const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
+ const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data(
+ 'name',
+ );
$target.closest('.js-awards-block').addClass('current');
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName);
@@ -83,7 +93,10 @@ class AwardsHandler {
showEmojiMenu($addBtn) {
if ($addBtn.hasClass('js-note-emoji')) {
- $addBtn.closest('.note').find('.js-awards-block').addClass('current');
+ $addBtn
+ .closest('.note')
+ .find('.js-awards-block')
+ .addClass('current');
} else {
$addBtn.closest('.js-awards-block').addClass('current');
}
@@ -177,32 +190,38 @@ class AwardsHandler {
const remainingCategories = Object.keys(categoryMap).slice(1);
const allCategoriesAddedPromise = remainingCategories.reduce(
(promiseChain, categoryNameKey) =>
- promiseChain.then(() =>
- new Promise((resolve) => {
- const emojisInCategory = categoryMap[categoryNameKey];
- const categoryMarkup = this.renderCategory(
- categoryLabelMap[categoryNameKey],
- emojisInCategory,
- );
- requestAnimationFrame(() => {
- emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
- resolve();
- });
- }),
- ),
+ promiseChain.then(
+ () =>
+ new Promise(resolve => {
+ const emojisInCategory = categoryMap[categoryNameKey];
+ const categoryMarkup = this.renderCategory(
+ categoryLabelMap[categoryNameKey],
+ emojisInCategory,
+ );
+ requestAnimationFrame(() => {
+ emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
+ resolve();
+ });
+ }),
+ ),
Promise.resolve(),
);
- allCategoriesAddedPromise.then(() => {
- // Used for tests
- // We check for the menu in case it was destroyed in the meantime
- if (menu) {
- menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
- }
- }).catch((err) => {
- emojiContentElement.insertAdjacentHTML('beforeend', '<p>We encountered an error while adding the remaining categories</p>');
- throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
- });
+ allCategoriesAddedPromise
+ .then(() => {
+ // Used for tests
+ // We check for the menu in case it was destroyed in the meantime
+ if (menu) {
+ menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
+ }
+ })
+ .catch(err => {
+ emojiContentElement.insertAdjacentHTML(
+ 'beforeend',
+ '<p>We encountered an error while adding the remaining categories</p>',
+ );
+ throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
+ });
}
renderCategory(name, emojiList, opts = {}) {
@@ -211,7 +230,9 @@ class AwardsHandler {
${name}
</h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
- ${emojiList.map(emojiName => `
+ ${emojiList
+ .map(
+ emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${this.emoji.glEmojiTag(emojiName, {
@@ -219,7 +240,9 @@ class AwardsHandler {
})}
</button>
</li>
- `).join('\n')}
+ `,
+ )
+ .join('\n')}
</ul>
`;
}
@@ -232,7 +255,7 @@ class AwardsHandler {
top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`,
};
if (position === 'right') {
- css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`;
+ css.left = `${$addBtn.offset().left - $menu.outerWidth() + 20}px`;
$menu.addClass('is-aligned-right');
} else {
css.left = `${$addBtn.offset().left}px`;
@@ -416,7 +439,10 @@ class AwardsHandler {
</button>
`;
const $emojiButton = $(buttonHtml);
- $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('name', emojiName);
+ $emojiButton
+ .insertBefore(votesBlock.find('.js-award-holder'))
+ .find('.emoji-icon')
+ .data('name', emojiName);
this.animateEmoji($emojiButton);
$('.award-control').tooltip();
votesBlock.removeClass('current');
@@ -426,7 +452,7 @@ class AwardsHandler {
const className = 'pulse animated once short';
$emoji.addClass(className);
- this.registerEventListener('on', $emoji, animationEndEventString, (e) => {
+ this.registerEventListener('on', $emoji, animationEndEventString, e => {
$(e.currentTarget).removeClass(className);
});
}
@@ -444,15 +470,16 @@ class AwardsHandler {
if (this.isUserAuthored($emojiButton)) {
this.userAuthored($emojiButton);
} else {
- axios.post(awardUrl, {
- name: emoji,
- })
- .then(({ data }) => {
- if (data.ok) {
- callback();
- }
- })
- .catch(() => flash(__('Something went wrong on our end.')));
+ axios
+ .post(awardUrl, {
+ name: emoji,
+ })
+ .then(({ data }) => {
+ if (data.ok) {
+ callback();
+ }
+ })
+ .catch(() => flash(__('Something went wrong on our end.')));
}
}
@@ -486,26 +513,33 @@ class AwardsHandler {
}
getFrequentlyUsedEmojis() {
- return this.frequentlyUsedEmojis || (() => {
- const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
- this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
- inputName => this.emoji.isEmojiNameValid(inputName),
- );
-
- return this.frequentlyUsedEmojis;
- })();
+ return (
+ this.frequentlyUsedEmojis ||
+ (() => {
+ const frequentlyUsedEmojis = _.uniq(
+ (Cookies.get('frequently_used_emojis') || '').split(','),
+ );
+ this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(inputName =>
+ this.emoji.isEmojiNameValid(inputName),
+ );
+
+ return this.frequentlyUsedEmojis;
+ })()
+ );
}
setupSearch() {
const $search = $('.js-emoji-menu-search');
- this.registerEventListener('on', $search, 'input', (e) => {
- const term = $(e.target).val().trim();
+ this.registerEventListener('on', $search, 'input', e => {
+ const term = $(e.target)
+ .val()
+ .trim();
this.searchEmojis(term);
});
const $menu = $('.emoji-menu');
- this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
+ this.registerEventListener('on', $menu, transitionEndEventString, e => {
if (e.target === e.currentTarget) {
// Clear the search
this.searchEmojis('');
@@ -523,19 +557,26 @@ class AwardsHandler {
// Generate a search result block
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
const foundEmojis = this.findMatchingEmojiElements(term).show();
- const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
+ const ul = $('<ul>')
+ .addClass('emoji-menu-list emoji-menu-search')
+ .append(foundEmojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
- $('.emoji-menu-content').append(h5).append(ul);
+ $('.emoji-menu-content')
+ .append(h5)
+ .append(ul);
} else {
- $('.emoji-menu-content').children().show();
+ $('.emoji-menu-content')
+ .children()
+ .show();
}
}
findMatchingEmojiElements(query) {
const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
- const $matchingElements = $emojiElements
- .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
+ const $matchingElements = $emojiElements.filter(
+ (i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
+ );
return $matchingElements.closest('li').clone();
}
@@ -550,16 +591,13 @@ class AwardsHandler {
$emojiMenu.addClass(IS_RENDERED);
// enqueues animation as a microtask, so it begins ASAP once IS_RENDERED added
- return Promise.resolve()
- .then(() => $emojiMenu.addClass(IS_VISIBLE));
+ return Promise.resolve().then(() => $emojiMenu.addClass(IS_VISIBLE));
}
hideMenuElement($emojiMenu) {
- $emojiMenu.on(transitionEndEventString, (e) => {
+ $emojiMenu.on(transitionEndEventString, e => {
if (e.currentTarget === e.target) {
- $emojiMenu
- .removeClass(IS_RENDERED)
- .off(transitionEndEventString);
+ $emojiMenu.removeClass(IS_RENDERED).off(transitionEndEventString);
}
});
@@ -567,7 +605,7 @@ class AwardsHandler {
}
destroy() {
- this.eventListeners.forEach((entry) => {
+ this.eventListeners.forEach(entry => {
entry.element.off.call(entry.element, ...entry.args);
});
$('.emoji-menu').remove();
@@ -577,8 +615,9 @@ class AwardsHandler {
let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
- awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji')
- .then(Emoji => new AwardsHandler(Emoji));
+ awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(
+ Emoji => new AwardsHandler(Emoji),
+ );
}
return awardsHandlerPromise;
}
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index 1ea6dd909e9..9745e37acce 100644
--- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -1,4 +1,4 @@
-/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
+/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, max-len, no-restricted-syntax, guard-for-in, no-continue */
import $ from 'jquery';
import _ from 'underscore';
diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js
index 70136cc4087..7d5f487c4ba 100644
--- a/app/assets/javascripts/blob/pdf/index.js
+++ b/app/assets/javascripts/blob/pdf/index.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-new */
import Vue from 'vue';
import pdfLab from '../../pdf/index.vue';
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index f61c0be9230..5485248cfaf 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -70,7 +70,7 @@ export default class BlobViewer {
const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)');
let initialViewerName = initialViewer.getAttribute('data-type');
- if (this.switcher && location.hash.indexOf('#L') === 0) {
+ if (this.switcher && window.location.hash.indexOf('#L') === 0) {
initialViewerName = 'simple';
}
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 4424232f642..a603d89b84a 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -1,5 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
-/* global EditBlob */
+/* eslint-disable no-new */
import $ from 'jquery';
import NewCommitForm from '../new_commit_form';
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 86b888c66c8..a2355d7fd5c 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -1,6 +1,5 @@
-/* eslint-disable comma-dangle, space-before-function-paren, one-var */
+/* eslint-disable comma-dangle */
-import $ from 'jquery';
import Sortable from 'sortablejs';
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
@@ -57,40 +56,6 @@ gl.issueBoards.Board = Vue.extend({
});
},
deep: true,
- },
- detailIssue: {
- handler () {
- if (!Object.keys(this.detailIssue.issue).length) return;
-
- const issue = this.list.findIssue(this.detailIssue.issue.id);
-
- if (issue) {
- const offsetLeft = this.$el.offsetLeft;
- const boardsList = document.querySelectorAll('.boards-list')[0];
- const left = boardsList.scrollLeft - offsetLeft;
- let right = (offsetLeft + this.$el.offsetWidth);
-
- if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
- // -290 here because width of boardsList is animating so therefore
- // getting the width here is incorrect
- // 290 is the width of the sidebar
- right -= (boardsList.offsetWidth - 290);
- } else {
- right -= boardsList.offsetWidth;
- }
-
- if (right - boardsList.scrollLeft > 0) {
- $(boardsList).animate({
- scrollLeft: right
- }, this.sortableOptions.animation);
- } else if (left > 0) {
- $(boardsList).animate({
- scrollLeft: offsetLeft
- }, this.sortableOptions.animation);
- }
- }
- },
- deep: true
}
},
mounted () {
diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js
index 4482b3b3e70..c5945e8098d 100644
--- a/app/assets/javascripts/boards/components/board_delete.js
+++ b/app/assets/javascripts/boards/components/board_delete.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, space-before-function-paren, no-alert */
+/* eslint-disable comma-dangle, no-alert */
import $ from 'jquery';
import Vue from 'vue';
@@ -17,7 +17,7 @@ gl.issueBoards.BoardDelete = Vue.extend({
deleteBoard () {
$(this.$el).tooltip('hide');
- if (confirm('Are you sure you want to delete this list?')) {
+ if (window.confirm('Are you sure you want to delete this list?')) {
this.list.destroy();
}
}
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 82fe6b0c5fb..b717c4b0fd4 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, space-before-function-paren, no-new */
+/* eslint-disable comma-dangle, no-new */
import $ from 'jquery';
import Vue from 'vue';
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.vue
index 888bc9d7ef2..dbd69f84526 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -1,8 +1,8 @@
-import Vue from 'vue';
+<script>
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
-gl.issueBoards.ModalEmptyState = Vue.extend({
+export default {
mixins: [modalMixin],
props: {
newIssuePath: {
@@ -38,32 +38,36 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
return obj;
},
},
- template: `
- <section class="empty-state">
- <div class="row">
- <div class="col-12 col-md-6 order-md-last">
- <aside class="svg-content"><img :src="emptyStateSvg"/></aside>
- </div>
- <div class="col-12 col-md-6 order-md-first">
- <div class="text-content">
- <h4>{{ contents.title }}</h4>
- <p v-html="contents.content"></p>
- <a
- :href="newIssuePath"
- class="btn btn-success btn-inverted"
- v-if="activeTab === 'all'">
- New issue
- </a>
- <button
- type="button"
- class="btn btn-default"
- @click="changeTab('all')"
- v-if="activeTab === 'selected'">
- Open issues
- </button>
- </div>
+};
+</script>
+
+<template>
+ <section class="empty-state">
+ <div class="row">
+ <div class="col-12 col-md-6 order-md-last">
+ <aside class="svg-content"><img :src="emptyStateSvg"/></aside>
+ </div>
+ <div class="col-12 col-md-6 order-md-first">
+ <div class="text-content">
+ <h4>{{ contents.title }}</h4>
+ <p v-html="contents.content"></p>
+ <a
+ v-if="activeTab === 'all'"
+ :href="newIssuePath"
+ class="btn btn-success btn-inverted"
+ >
+ New issue
+ </a>
+ <button
+ v-if="activeTab === 'selected'"
+ class="btn btn-default"
+ type="button"
+ @click="changeTab('all')"
+ >
+ Open issues
+ </button>
</div>
</div>
- </section>
- `,
-});
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index c8b2f45f177..c10397eaaba 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -6,15 +6,15 @@ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import './header';
import './list';
import './footer';
-import './empty_state';
+import EmptyState from './empty_state.vue';
import ModalStore from '../../stores/modal_store';
gl.issueBoards.IssuesModal = Vue.extend({
components: {
+ EmptyState,
'modal-header': gl.issueBoards.ModalHeader,
'modal-list': gl.issueBoards.ModalList,
'modal-footer': gl.issueBoards.ModalFooter,
- 'empty-state': gl.issueBoards.ModalEmptyState,
loadingIcon,
},
props: {
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 6dcd4aaec43..448ab9ed135 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-new, space-before-function-paren, one-var, promise/catch-or-return, max-len */
+/* eslint-disable func-names, no-new, promise/catch-or-return */
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 70367c4f711..46d61ebbf24 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -1,4 +1,3 @@
-/* eslint-disable class-methods-use-this */
import FilteredSearchContainer from '../filtered_search/container';
import FilteredSearchManager from '../filtered_search/filtered_search_manager';
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index cdad8d238e3..2d9141bf71c 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,4 +1,4 @@
-/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
+/* eslint-disable quote-props, comma-dangle */
import $ from 'jquery';
import _ from 'underscore';
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index ac316c31deb..a8df45fc473 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
/* global DocumentTouch */
import $ from 'jquery';
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index b381d48d625..b85266b6bc3 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
+/* eslint-disable no-unused-vars, comma-dangle */
/* global ListLabel */
/* global ListMilestone */
/* global ListAssignee */
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 1f0fe7f9e85..e35f277a865 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
+/* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len */
/* global ListIssue */
import ListLabel from '~/vue_shared/models/label';
diff --git a/app/assets/javascripts/boards/models/milestone.js b/app/assets/javascripts/boards/models/milestone.js
index c867b06d320..17d15278a74 100644
--- a/app/assets/javascripts/boards/models/milestone.js
+++ b/app/assets/javascripts/boards/models/milestone.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-unused-vars */
-
class ListMilestone {
constructor(obj) {
this.id = obj.id;
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 7dc83843e9b..333338489bc 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */
+/* eslint-disable comma-dangle, no-shadow */
/* global List */
import $ from 'jquery';
@@ -145,6 +145,6 @@ gl.issueBoards.BoardsStore = {
return filteredList[0];
},
updateFiltersUrl () {
- history.pushState(null, null, `?${this.filter.path}`);
+ window.history.pushState(null, null, `?${this.filter.path}`);
}
};
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index 3fa16517388..e338376fcaa 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-return-assign */
+/* eslint-disable func-names, prefer-arrow-callback */
import $ from 'jquery';
import { visitUrl } from './lib/utils/url_utility';
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 7f3d04655a7..2d180e9903a 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
+/* eslint-disable func-names, wrap-iife, no-var, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, max-len */
import $ from 'jquery';
@@ -95,7 +95,7 @@ export default class ImageFile {
});
return [maxWidth, maxHeight];
}
- // eslint-disable-next-line
+
views = {
'two-up': function() {
return $('.two-up.view .wrap', this.file).each((function(_this) {
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 7e2a3573f81..9a3ea7a55b6 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -45,7 +45,7 @@ export default class CommitsList {
this.content.fadeTo('fast', 1.0);
// Change url so if user reload a page - search results are saved
- history.replaceState({
+ window.history.replaceState({
page: commitsUrl,
}, document.title, commitsUrl);
})
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index ffe15f02f2e..a252036d657 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
+/* eslint-disable func-names, one-var, no-var, one-var-declaration-per-line, object-shorthand, no-else-return, max-len */
import $ from 'jquery';
import { __ } from './locale';
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index 108082799ef..f77a5730b77 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -366,7 +366,7 @@ export default class CreateMergeRequestDropdown {
removeMessage(target) {
const { input, message } = this.getTargetData(target);
const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline'];
- const messageClasses = ['gl-field-hint', 'gl-field-error-message', 'gl-field-success-message'];
+ const messageClasses = ['text-muted', 'text-danger', 'text-success'];
inputClasses.forEach(cssClass => input.classList.remove(cssClass));
messageClasses.forEach(cssClass => message.classList.remove(cssClass));
@@ -393,7 +393,7 @@ export default class CreateMergeRequestDropdown {
this.removeMessage(target);
input.classList.add('gl-field-success-outline');
- message.classList.add('gl-field-success-message');
+ message.classList.add('text-success');
message.textContent = sprintf(__('%{text} is available'), { text });
message.style.display = 'inline-block';
}
@@ -403,7 +403,7 @@ export default class CreateMergeRequestDropdown {
const text = target === 'branch' ? __('branch name') : __('source');
this.removeMessage(target);
- message.classList.add('gl-field-hint');
+ message.classList.add('text-muted');
message.textContent = sprintf(__('Checking %{text} availability…'), { text });
message.style.display = 'inline-block';
}
@@ -415,7 +415,7 @@ export default class CreateMergeRequestDropdown {
this.removeMessage(target);
input.classList.add('gl-field-error-outline');
- message.classList.add('gl-field-error-message');
+ message.classList.add('text-danger');
message.textContent = text;
message.style.display = 'inline-block';
}
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 2cfa13fdc75..d91e4809126 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -98,7 +98,7 @@ export default {
},
disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert
- if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
+ if (window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
this.service
.disableKey(deployKey.id)
.then(this.fetchKeys)
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 2ce4b050763..66b20cc8739 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -1,11 +1,10 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */
-/* global DiscussionMixins */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, no-lonely-if, no-continue, brace-style, max-len, quotes */
/* global CommentsStore */
import $ from 'jquery';
import Vue from 'vue';
-import '../mixins/discussion';
+import DiscussionMixins from '../mixins/discussion';
const JumpToDiscussion = Vue.extend({
mixins: [DiscussionMixins],
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index 07f3be29090..a69b34b0db8 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -1,4 +1,3 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, max-len */
/* global CommentsStore */
/* global ResolveService */
@@ -41,54 +40,54 @@ const ResolveBtn = Vue.extend({
required: true,
},
},
- data: function () {
+ data() {
return {
discussions: CommentsStore.state,
- loading: false
+ loading: false,
};
},
computed: {
- discussion: function () {
+ discussion() {
return this.discussions[this.discussionId];
},
- note: function () {
+ note() {
return this.discussion ? this.discussion.getNote(this.noteId) : {};
},
- buttonText: function () {
+ buttonText() {
if (this.isResolved) {
return `Resolved by ${this.resolvedByName}`;
} else if (this.canResolve) {
return 'Mark as resolved';
- } else {
- return 'Unable to resolve';
}
+
+ return 'Unable to resolve';
},
- isResolved: function () {
+ isResolved() {
if (this.note) {
return this.note.resolved;
- } else {
- return false;
}
+
+ return false;
},
- resolvedByName: function () {
+ resolvedByName() {
return this.note.resolved_by;
},
},
watch: {
- 'discussions': {
+ discussions: {
handler: 'updateTooltip',
- deep: true
- }
+ deep: true,
+ },
},
- mounted: function () {
+ mounted() {
$(this.$refs.button).tooltip({
- container: 'body'
+ container: 'body',
});
},
- beforeDestroy: function () {
+ beforeDestroy() {
CommentsStore.delete(this.discussionId, this.noteId);
},
- created: function () {
+ created() {
CommentsStore.create({
discussionId: this.discussionId,
noteId: this.noteId,
@@ -101,43 +100,41 @@ const ResolveBtn = Vue.extend({
});
},
methods: {
- updateTooltip: function () {
+ updateTooltip() {
this.$nextTick(() => {
$(this.$refs.button)
.tooltip('hide')
.tooltip('_fixTitle');
});
},
- resolve: function () {
+ resolve() {
if (!this.canResolve) return;
let promise;
this.loading = true;
if (this.isResolved) {
- promise = ResolveService
- .unresolve(this.noteId);
+ promise = ResolveService.unresolve(this.noteId);
} else {
- promise = ResolveService
- .resolve(this.noteId);
+ promise = ResolveService.resolve(this.noteId);
}
promise
.then(resp => resp.json())
- .then((data) => {
+ .then(data => {
this.loading = false;
- const resolved_by = data ? data.resolved_by : null;
+ const resolvedBy = data ? data.resolved_by : null;
- CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
+ CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolvedBy);
this.discussion.updateHeadline(data);
gl.mrWidget.checkStatus();
- document.dispatchEvent(new CustomEvent('refreshVueNotes'));
-
this.updateTooltip();
})
- .catch(() => new Flash('An error occurred when trying to resolve a comment. Please try again.'));
- }
+ .catch(
+ () => new Flash('An error occurred when trying to resolve a comment. Please try again.'),
+ );
+ },
},
});
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js
index 9f613410e81..e2683e09f40 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js
@@ -1,10 +1,9 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
-/* global DiscussionMixins */
+/* eslint-disable comma-dangle, object-shorthand, func-names */
/* global CommentsStore */
import Vue from 'vue';
-import '../mixins/discussion';
+import DiscussionMixins from '../mixins/discussion';
window.ResolveCount = Vue.extend({
mixins: [DiscussionMixins],
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
index 210a00c5fc2..5ed13488788 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
@@ -1,4 +1,4 @@
-/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */
+/* eslint-disable object-shorthand, func-names, comma-dangle, no-else-return, quotes */
/* global CommentsStore */
/* global ResolveService */
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index d5161ab7df9..a9800a11644 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -1,5 +1,4 @@
-/* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */
-/* global ResolveCount */
+/* eslint-disable func-names, new-cap */
import $ from 'jquery';
import Vue from 'vue';
@@ -15,12 +14,13 @@ import './components/resolve_count';
import './components/resolve_discussion_btn';
import './components/diff_note_avatars';
import './components/new_issue_for_discussion';
-import { hasVueMRDiscussionsCookie } from '../lib/utils/common_utils';
export default () => {
- const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box');
+ const projectPathHolder =
+ document.querySelector('.merge-request') || document.querySelector('.commit-box');
const projectPath = projectPathHolder.dataset.projectPath;
- const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
+ const COMPONENT_SELECTOR =
+ 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
window.gl = window.gl || {};
window.gl.diffNoteApps = {};
@@ -28,9 +28,9 @@ export default () => {
window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
gl.diffNotesCompileComponents = () => {
- $('diff-note-avatars').each(function () {
+ $('diff-note-avatars').each(function() {
const tmp = Vue.extend({
- template: $(this).get(0).outerHTML
+ template: $(this).get(0).outerHTML,
});
const tmpApp = new tmp().$mount();
@@ -41,12 +41,12 @@ export default () => {
});
});
- const $components = $(COMPONENT_SELECTOR).filter(function () {
+ const $components = $(COMPONENT_SELECTOR).filter(function() {
return $(this).closest('resolve-count').length !== 1;
});
if ($components) {
- $components.each(function () {
+ $components.each(function() {
const $this = $(this);
const noteId = $this.attr(':note-id');
const discussionId = $this.attr(':discussion-id');
@@ -54,7 +54,7 @@ export default () => {
if ($this.is('comment-and-resolve-btn') && !discussionId) return;
const tmp = Vue.extend({
- template: $this.get(0).outerHTML
+ template: $this.get(0).outerHTML,
});
const tmpApp = new tmp().$mount();
@@ -69,15 +69,5 @@ export default () => {
gl.diffNotesCompileComponents();
- const resolveCountAppEl = document.querySelector('#resolve-count-app');
- if (!hasVueMRDiscussionsCookie() && resolveCountAppEl) {
- new Vue({
- el: resolveCountAppEl,
- components: {
- 'resolve-count': ResolveCount
- },
- });
- }
-
$(window).trigger('resize.nav');
};
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js b/app/assets/javascripts/diff_notes/mixins/discussion.js
index 36c4abf02cf..ef35b589e58 100644
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js
@@ -1,6 +1,6 @@
-/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */
+/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, */
-window.DiscussionMixins = {
+const DiscussionMixins = {
computed: {
discussionCount: function () {
return Object.keys(this.discussions).length;
@@ -33,3 +33,5 @@ window.DiscussionMixins = {
}
}
};
+
+export default DiscussionMixins;
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js
index c97c559dd14..787e6d8855f 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js
+++ b/app/assets/javascripts/diff_notes/models/discussion.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */
+/* eslint-disable camelcase, guard-for-in, no-restricted-syntax */
/* global NoteModel */
import $ from 'jquery';
diff --git a/app/assets/javascripts/diff_notes/models/note.js b/app/assets/javascripts/diff_notes/models/note.js
index 04465aa507e..825a69deeec 100644
--- a/app/assets/javascripts/diff_notes/models/note.js
+++ b/app/assets/javascripts/diff_notes/models/note.js
@@ -1,5 +1,3 @@
-/* eslint-disable camelcase, no-unused-vars */
-
class NoteModel {
constructor(discussionId, noteObj) {
this.discussionId = discussionId;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index d16f9297de1..0b3568e432d 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -8,8 +8,12 @@ window.gl = window.gl || {};
class ResolveServiceClass {
constructor(root) {
- this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
- this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`);
+ this.noteResource = Vue.resource(
+ `${root}/notes{/noteId}/resolve?html=true`,
+ );
+ this.discussionResource = Vue.resource(
+ `${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
+ );
}
resolve(noteId) {
@@ -33,7 +37,7 @@ class ResolveServiceClass {
promise
.then(resp => resp.json())
- .then((data) => {
+ .then(data => {
discussion.loading = false;
const resolvedBy = data ? data.resolved_by : null;
@@ -45,9 +49,13 @@ class ResolveServiceClass {
if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data);
- document.dispatchEvent(new CustomEvent('refreshVueNotes'));
})
- .catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'));
+ .catch(
+ () =>
+ new Flash(
+ 'An error occurred when trying to resolve a discussion. Please try again.',
+ ),
+ );
}
resolveAll(mergeRequestId, discussionId) {
@@ -55,10 +63,13 @@ class ResolveServiceClass {
discussion.loading = true;
- return this.discussionResource.save({
- mergeRequestId,
- discussionId,
- }, {});
+ return this.discussionResource.save(
+ {
+ mergeRequestId,
+ discussionId,
+ },
+ {},
+ );
}
unResolveAll(mergeRequestId, discussionId) {
@@ -66,10 +77,13 @@ class ResolveServiceClass {
discussion.loading = true;
- return this.discussionResource.delete({
- mergeRequestId,
- discussionId,
- }, {});
+ return this.discussionResource.delete(
+ {
+ mergeRequestId,
+ discussionId,
+ },
+ {},
+ );
}
}
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js
index d802db7d3af..d7da7d974f3 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js
+++ b/app/assets/javascripts/diff_notes/stores/comments.js
@@ -1,4 +1,4 @@
-/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
+/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len */
/* global DiscussionModel */
import Vue from 'vue';
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
new file mode 100644
index 00000000000..82ca10f4163
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -0,0 +1,197 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
+import CompareVersions from './compare_versions.vue';
+import ChangedFiles from './changed_files.vue';
+import DiffFile from './diff_file.vue';
+import NoChanges from './no_changes.vue';
+import HiddenFilesWarning from './hidden_files_warning.vue';
+
+export default {
+ name: 'DiffsApp',
+ components: {
+ Icon,
+ LoadingIcon,
+ CompareVersions,
+ ChangedFiles,
+ DiffFile,
+ NoChanges,
+ HiddenFilesWarning,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ shouldShow: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ activeFile: '',
+ };
+ },
+ computed: {
+ ...mapState({
+ isLoading: state => state.diffs.isLoading,
+ diffFiles: state => state.diffs.diffFiles,
+ diffViewType: state => state.diffs.diffViewType,
+ mergeRequestDiffs: state => state.diffs.mergeRequestDiffs,
+ mergeRequestDiff: state => state.diffs.mergeRequestDiff,
+ latestVersionPath: state => state.diffs.latestVersionPath,
+ startVersion: state => state.diffs.startVersion,
+ commit: state => state.diffs.commit,
+ targetBranchName: state => state.diffs.targetBranchName,
+ renderOverflowWarning: state => state.diffs.renderOverflowWarning,
+ numTotalFiles: state => state.diffs.realSize,
+ numVisibleFiles: state => state.diffs.size,
+ plainDiffPath: state => state.diffs.plainDiffPath,
+ emailPatchPath: state => state.diffs.emailPatchPath,
+ }),
+ ...mapGetters(['isParallelView']),
+ targetBranch() {
+ return {
+ branchName: this.targetBranchName,
+ versionIndex: -1,
+ path: '',
+ };
+ },
+ notAllCommentsDisplayed() {
+ if (this.commit) {
+ return __('Only comments from the following commit are shown below');
+ } else if (this.startVersion) {
+ return __(
+ "Not all comments are displayed because you're comparing two versions of the diff.",
+ );
+ }
+ return __(
+ "Not all comments are displayed because you're viewing an old version of the diff.",
+ );
+ },
+ showLatestVersion() {
+ if (this.commit) {
+ return __('Show latest version of the diff');
+ }
+ return __('Show latest version');
+ },
+ },
+ watch: {
+ diffViewType() {
+ this.adjustView();
+ },
+ shouldShow() {
+ this.adjustView();
+ },
+ },
+ mounted() {
+ this.setEndpoint(this.endpoint);
+ this
+ .fetchDiffFiles()
+ .catch(() => {
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
+ },
+ created() {
+ this.adjustView();
+ },
+ methods: {
+ ...mapActions(['setEndpoint', 'fetchDiffFiles']),
+ setActive(filePath) {
+ this.activeFile = filePath;
+ },
+ unsetActive(filePath) {
+ if (this.activeFile === filePath) {
+ this.activeFile = '';
+ }
+ },
+ adjustView() {
+ if (this.shouldShow && this.isParallelView) {
+ window.mrTabs.expandViewContainer();
+ } else {
+ window.mrTabs.resetViewContainer();
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="shouldShow">
+ <div
+ v-if="isLoading"
+ class="loading"
+ >
+ <loading-icon />
+ </div>
+ <div
+ v-else
+ id="diffs"
+ :class="{ active: shouldShow }"
+ class="diffs tab-pane"
+ >
+ <compare-versions
+ v-if="!commit && mergeRequestDiffs.length > 1"
+ :merge-request-diffs="mergeRequestDiffs"
+ :merge-request-diff="mergeRequestDiff"
+ :start-version="startVersion"
+ :target-branch="targetBranch"
+ />
+
+ <hidden-files-warning
+ v-if="renderOverflowWarning"
+ :visible="numVisibleFiles"
+ :total="numTotalFiles"
+ :plain-diff-path="plainDiffPath"
+ :email-patch-path="emailPatchPath"
+ />
+
+ <div
+ v-if="commit || startVersion || (mergeRequestDiff && !mergeRequestDiff.latest)"
+ class="mr-version-controls"
+ >
+ <div class="content-block comments-disabled-notif clearfix">
+ <i class="fa fa-info-circle"></i>
+ {{ notAllCommentsDisplayed }}
+ <div class="pull-right">
+ <a
+ :href="latestVersionPath"
+ class="btn btn-sm"
+ >
+ {{ showLatestVersion }}
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <changed-files
+ :diff-files="diffFiles"
+ :active-file="activeFile"
+ />
+
+ <div
+ v-if="diffFiles.length > 0"
+ class="files"
+ >
+ <diff-file
+ v-for="file in diffFiles"
+ :key="file.newPath"
+ :file="file"
+ :current-user="currentUser"
+ @setActive="setActive(file.filePath)"
+ @unsetActive="unsetActive(file.filePath)"
+ />
+ </div>
+ <no-changes v-else />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/changed_files.vue b/app/assets/javascripts/diffs/components/changed_files.vue
new file mode 100644
index 00000000000..c5ef9fefc2f
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/changed_files.vue
@@ -0,0 +1,184 @@
+<script>
+import { mapGetters, mapActions } from 'vuex';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { pluralize } from '~/lib/utils/text_utility';
+import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
+import { contentTop } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import ChangedFilesDropdown from './changed_files_dropdown.vue';
+import changedFilesMixin from '../mixins/changed_files';
+
+export default {
+ components: {
+ Icon,
+ ChangedFilesDropdown,
+ ClipboardButton,
+ },
+ mixins: [changedFilesMixin],
+ props: {
+ activeFile: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isStuck: false,
+ maxWidth: 'auto',
+ offsetTop: 0,
+ };
+ },
+ computed: {
+ ...mapGetters(['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
+ sumAddedLines() {
+ return this.sumValues('addedLines');
+ },
+ sumRemovedLines() {
+ return this.sumValues('removedLines');
+ },
+ whitespaceVisible() {
+ return !getParameterValues('w')[0];
+ },
+ toggleWhitespaceText() {
+ if (this.whitespaceVisible) {
+ return __('Hide whitespace changes');
+ }
+ return __('Show whitespace changes');
+ },
+ toggleWhitespacePath() {
+ if (this.whitespaceVisible) {
+ return mergeUrlParams({ w: 1 }, window.location.href);
+ }
+
+ return mergeUrlParams({ w: 0 }, window.location.href);
+ },
+ top() {
+ return `${this.offsetTop}px`;
+ },
+ },
+ created() {
+ document.addEventListener('scroll', this.handleScroll);
+ this.offsetTop = contentTop();
+ },
+ beforeDestroy() {
+ document.removeEventListener('scroll', this.handleScroll);
+ },
+ methods: {
+ ...mapActions(['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']),
+ pluralize,
+ handleScroll() {
+ if (!this.updating) {
+ requestAnimationFrame(this.updateIsStuck);
+ this.updating = true;
+ }
+ },
+ updateIsStuck() {
+ if (!this.$refs.wrapper) {
+ return;
+ }
+
+ const scrollPosition = window.scrollY;
+
+ this.isStuck = scrollPosition + this.offsetTop >= this.$refs.placeholder.offsetTop;
+ this.updating = false;
+ },
+ sumValues(key) {
+ return this.diffFiles.reduce((total, file) => total + file[key], 0);
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <div ref="placeholder"></div>
+ <div
+ ref="wrapper"
+ :style="{ top }"
+ :class="{'is-stuck': isStuck}"
+ class="content-block oneline-block diff-files-changed diff-files-changed-merge-request
+ files-changed js-diff-files-changed"
+ >
+ <div class="files-changed-inner">
+ <div
+ class="inline-parallel-buttons d-none d-md-block"
+ >
+ <a
+ v-if="areAllFilesCollapsed"
+ class="btn btn-default"
+ @click="expandAllFiles"
+ >
+ {{ __('Expand all') }}
+ </a>
+ <a
+ :href="toggleWhitespacePath"
+ class="btn btn-default"
+ >
+ {{ toggleWhitespaceText }}
+ </a>
+ <div class="btn-group">
+ <button
+ id="inline-diff-btn"
+ :class="{ active: isInlineView }"
+ type="button"
+ class="btn js-inline-diff-button"
+ data-view-type="inline"
+ @click="setInlineDiffViewType"
+ >
+ {{ __('Inline') }}
+ </button>
+ <button
+ id="parallel-diff-btn"
+ :class="{ active: isParallelView }"
+ type="button"
+ class="btn js-parallel-diff-button"
+ data-view-type="parallel"
+ @click="setParallelDiffViewType"
+ >
+ {{ __('Side-by-side') }}
+ </button>
+ </div>
+ </div>
+
+ <div class="commit-stat-summary dropdown">
+ <changed-files-dropdown
+ :diff-files="diffFiles"
+ />
+
+ <span
+ v-show="activeFile"
+ class="prepend-left-5"
+ >
+ <strong class="prepend-right-5">
+ {{ truncatedDiffPath(activeFile) }}
+ </strong>
+ <clipboard-button
+ :text="activeFile"
+ :title="s__('Copy file name to clipboard')"
+ tooltip-placement="bottom"
+ tooltip-container="body"
+ class="btn btn-default btn-transparent btn-clipboard"
+ />
+ </span>
+
+ <span
+ v-show="!isStuck"
+ id="diff-stats"
+ class="diff-stats-additions-deletions-expanded"
+ >
+ with
+ <strong class="cgreen">
+ {{ pluralize(`${sumAddedLines} addition`, sumAddedLines) }}
+ </strong>
+ and
+ <strong class="cred">
+ {{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }}
+ </strong>
+ </span>
+ </div>
+ </div>
+ </div>
+ </span>
+</template>
diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
new file mode 100644
index 00000000000..f224b9dd246
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
@@ -0,0 +1,124 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import changedFilesMixin from '../mixins/changed_files';
+
+export default {
+ components: {
+ Icon,
+ },
+ mixins: [changedFilesMixin],
+ data() {
+ return {
+ searchText: '',
+ };
+ },
+ computed: {
+ filteredDiffFiles() {
+ return this.diffFiles.filter(file =>
+ file.filePath.toLowerCase().includes(this.searchText.toLowerCase()),
+ );
+ },
+ },
+ methods: {
+ clearSearch() {
+ this.searchText = '';
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ Showing
+ <button
+ class="diff-stats-summary-toggler"
+ data-toggle="dropdown"
+ type="button"
+ aria-expanded="false"
+ >
+ <span>
+ {{ n__('%d changed file', '%d changed files', diffFiles.length) }}
+ </span>
+ <icon
+ :size="8"
+ name="chevron-down"
+ />
+ </button>
+ <div class="dropdown-menu diff-file-changes">
+ <div class="dropdown-input">
+ <input
+ v-model="searchText"
+ type="search"
+ class="dropdown-input-field"
+ placeholder="Search files"
+ autocomplete="off"
+ />
+ <i
+ v-if="searchText.length === 0"
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-search dropdown-input-search">
+ </i>
+ <i
+ v-else
+ role="button"
+ class="fa fa-times dropdown-input-search"
+ @click="clearSearch"
+ ></i>
+ </div>
+ <ul>
+ <li
+ v-for="diffFile in filteredDiffFiles"
+ :key="diffFile.name"
+ >
+ <a
+ :href="`#${diffFile.fileHash}`"
+ :title="diffFile.newPath"
+ class="diff-changed-file"
+ >
+ <icon
+ :name="fileChangedIcon(diffFile)"
+ :size="16"
+ :class="fileChangedClass(diffFile)"
+ class="diff-file-changed-icon append-right-8"
+ />
+ <span class="diff-changed-file-content append-right-8">
+ <strong
+ v-if="diffFile.blob && diffFile.blob.name"
+ class="diff-changed-file-name"
+ >
+ {{ diffFile.blob.name }}
+ </strong>
+ <strong
+ v-else
+ class="diff-changed-blank-file-name"
+ >
+ {{ s__('Diffs|No file name available') }}
+ </strong>
+ <span class="diff-changed-file-path prepend-top-5">
+ {{ truncatedDiffPath(diffFile.blob.path) }}
+ </span>
+ </span>
+ <span class="diff-changed-stats">
+ <span class="cgreen">
+ +{{ diffFile.addedLines }}
+ </span>
+ <span class="cred">
+ -{{ diffFile.removedLines }}
+ </span>
+ </span>
+ </a>
+ </li>
+
+ <li
+ v-show="filteredDiffFiles.length === 0"
+ class="dropdown-menu-empty-item"
+ >
+ <a>
+ {{ __('No files found') }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </span>
+</template>
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
new file mode 100644
index 00000000000..1c9ad8e77f1
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -0,0 +1,55 @@
+<script>
+import CompareVersionsDropdown from './compare_versions_dropdown.vue';
+
+export default {
+ components: {
+ CompareVersionsDropdown,
+ },
+ props: {
+ mergeRequestDiffs: {
+ type: Array,
+ required: true,
+ },
+ mergeRequestDiff: {
+ type: Object,
+ required: true,
+ },
+ startVersion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ targetBranch: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ comparableDiffs() {
+ return this.mergeRequestDiffs.slice(1);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="mr-version-controls">
+ <div class="mr-version-menus-container content-block">
+ Changes between
+ <compare-versions-dropdown
+ :other-versions="mergeRequestDiffs"
+ :merge-request-version="mergeRequestDiff"
+ :show-commit-count="true"
+ class="mr-version-dropdown"
+ />
+ and
+ <compare-versions-dropdown
+ :other-versions="comparableDiffs"
+ :start-version="startVersion"
+ :target-branch="targetBranch"
+ class="mr-version-compare-dropdown"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
new file mode 100644
index 00000000000..96cccb49378
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
@@ -0,0 +1,165 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import { n__, __ } from '~/locale';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ components: {
+ Icon,
+ TimeAgo,
+ },
+ props: {
+ otherVersions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ mergeRequestVersion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ startVersion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ targetBranch: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ showCommitCount: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ baseVersion() {
+ return {
+ name: 'hii',
+ versionIndex: -1,
+ };
+ },
+ targetVersions() {
+ if (this.mergeRequestVersion) {
+ return this.otherVersions;
+ }
+ return [...this.otherVersions, this.targetBranch];
+ },
+ selectedVersionName() {
+ const selectedVersion = this.startVersion || this.targetBranch || this.mergeRequestVersion;
+ return this.versionName(selectedVersion);
+ },
+ },
+ methods: {
+ commitsText(version) {
+ return n__(
+ `${version.commitsCount} commit,`,
+ `${version.commitsCount} commits,`,
+ version.commitsCount,
+ );
+ },
+ href(version) {
+ if (this.showCommitCount) {
+ return version.versionPath;
+ }
+ return version.comparePath;
+ },
+ versionName(version) {
+ if (this.isLatest(version)) {
+ return __('latest version');
+ }
+ if (this.targetBranch && (this.isBase(version) || !version)) {
+ return this.targetBranch.branchName;
+ }
+ return `version ${version.versionIndex}`;
+ },
+ isActive(version) {
+ if (!version) {
+ return false;
+ }
+
+ if (this.targetBranch) {
+ return (
+ (this.isBase(version) && !this.startVersion) ||
+ (this.startVersion && this.startVersion.versionIndex === version.versionIndex)
+ );
+ }
+
+ return version.versionIndex === this.mergeRequestVersion.versionIndex;
+ },
+ isBase(version) {
+ if (!version || !this.targetBranch) {
+ return false;
+ }
+ return version.versionIndex === -1;
+ },
+ isLatest(version) {
+ return (
+ this.mergeRequestVersion && version.versionIndex === this.targetVersions[0].versionIndex
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="dropdown inline">
+ <a
+ class="dropdown-toggle btn btn-default"
+ data-toggle="dropdown"
+ aria-expanded="false"
+ >
+ <span>
+ {{ selectedVersionName }}
+ </span>
+ <Icon
+ :size="12"
+ name="angle-down"
+ />
+ </a>
+ <div class="dropdown-menu dropdown-select dropdown-menu-selectable">
+ <div class="dropdown-content">
+ <ul>
+ <li
+ v-for="version in targetVersions"
+ :key="version.id"
+ >
+ <a
+ :class="{ 'is-active': isActive(version) }"
+ :href="href(version)"
+ >
+ <div>
+ <strong>
+ {{ versionName(version) }}
+ <template v-if="isBase(version)">
+ (base)
+ </template>
+ </strong>
+ </div>
+ <div>
+ <small class="commit-sha">
+ {{ version.truncatedCommitSha }}
+ </small>
+ </div>
+ <div>
+ <small>
+ <template v-if="showCommitCount">
+ {{ commitsText(version) }}
+ </template>
+ <time-ago
+ v-if="version.createdAt"
+ :time="version.createdAt"
+ class="js-timeago js-timeago-render"
+ />
+ </small>
+ </div>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </span>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
new file mode 100644
index 00000000000..adcd22f7876
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -0,0 +1,38 @@
+<script>
+import { mapGetters } from 'vuex';
+import InlineDiffView from './inline_diff_view.vue';
+import ParallelDiffView from './parallel_diff_view.vue';
+
+export default {
+ components: {
+ InlineDiffView,
+ ParallelDiffView,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters(['isInlineView', 'isParallelView']),
+ },
+};
+</script>
+
+<template>
+ <div class="diff-content">
+ <div class="diff-viewer">
+ <inline-diff-view
+ v-if="isInlineView"
+ :diff-file="diffFile"
+ :diff-lines="diffFile.highlightedDiffLines || []"
+ />
+ <parallel-diff-view
+ v-if="isParallelView"
+ :diff-file="diffFile"
+ :diff-lines="diffFile.parallelDiffLines || []"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
new file mode 100644
index 00000000000..39d535036f6
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -0,0 +1,39 @@
+<script>
+import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
+
+export default {
+ components: {
+ noteableDiscussion,
+ },
+ props: {
+ discussions: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="discussions.length"
+ >
+ <div
+ v-for="discussion in discussions"
+ :key="discussion.id"
+ class="discussion-notes diff-discussions"
+ >
+ <ul
+ :data-discussion-id="discussion.id"
+ class="notes"
+ >
+ <noteable-discussion
+ :discussion="discussion"
+ :render-header="false"
+ :render-diff-file="false"
+ :always-expanded="true"
+ />
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
new file mode 100644
index 00000000000..108eefdac5f
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -0,0 +1,191 @@
+<script>
+import { mapActions } from 'vuex';
+import _ from 'underscore';
+import { __, sprintf } from '~/locale';
+import createFlash from '~/flash';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import DiffFileHeader from './diff_file_header.vue';
+import DiffContent from './diff_content.vue';
+
+export default {
+ components: {
+ DiffFileHeader,
+ DiffContent,
+ LoadingIcon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isActive: false,
+ isLoadingCollapsedDiff: false,
+ forkMessageVisible: false,
+ };
+ },
+ computed: {
+ isDiscussionsExpanded() {
+ return true; // TODO: @fatihacet - Fix this.
+ },
+ isCollapsed() {
+ return this.file.collapsed || false;
+ },
+ viewBlobLink() {
+ return sprintf(
+ __('You can %{linkStart}view the blob%{linkEnd} instead.'),
+ {
+ linkStart: `<a href="${_.escape(this.file.viewPath)}">`,
+ linkEnd: '</a>',
+ },
+ false,
+ );
+ },
+ },
+ mounted() {
+ document.addEventListener('scroll', this.handleScroll);
+ },
+ beforeDestroy() {
+ document.removeEventListener('scroll', this.handleScroll);
+ },
+ methods: {
+ ...mapActions(['loadCollapsedDiff']),
+ handleToggle() {
+ const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file;
+
+ if (collapsed && !highlightedDiffLines && !parallelDiffLines.length) {
+ this.handleLoadCollapsedDiff();
+ } else {
+ this.file.collapsed = !this.file.collapsed;
+ }
+ },
+ handleScroll() {
+ if (!this.updating) {
+ requestAnimationFrame(this.scrollUpdate.bind(this));
+ this.updating = true;
+ }
+ },
+ scrollUpdate() {
+ const header = document.querySelector('.js-diff-files-changed');
+ if (!header) {
+ this.updating = false;
+ return;
+ }
+
+ const { top, bottom } = this.$el.getBoundingClientRect();
+ const { top: topOfFixedHeader, bottom: bottomOfFixedHeader } = header.getBoundingClientRect();
+
+ const headerOverlapsContent = top < topOfFixedHeader && bottom > bottomOfFixedHeader;
+ const fullyAboveHeader = bottom < bottomOfFixedHeader;
+ const fullyBelowHeader = top > topOfFixedHeader;
+
+ if (headerOverlapsContent && !this.isActive) {
+ this.$emit('setActive');
+ this.isActive = true;
+ } else if (this.isActive && (fullyAboveHeader || fullyBelowHeader)) {
+ this.$emit('unsetActive');
+ this.isActive = false;
+ }
+
+ this.updating = false;
+ },
+ handleLoadCollapsedDiff() {
+ this.isLoadingCollapsedDiff = true;
+
+ this.loadCollapsedDiff(this.file)
+ .then(() => {
+ this.isLoadingCollapsedDiff = false;
+ this.file.collapsed = false;
+ })
+ .catch(() => {
+ this.isLoadingCollapsedDiff = false;
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
+ },
+ showForkMessage() {
+ this.forkMessageVisible = true;
+ },
+ hideForkMessage() {
+ this.forkMessageVisible = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ :id="file.fileHash"
+ class="diff-file file-holder"
+ >
+ <diff-file-header
+ :current-user="currentUser"
+ :diff-file="file"
+ :collapsible="true"
+ :expanded="!isCollapsed"
+ :discussions-expanded="isDiscussionsExpanded"
+ :add-merge-request-buttons="true"
+ class="js-file-title file-title"
+ @toggleFile="handleToggle"
+ @showForkMessage="showForkMessage"
+ />
+
+ <div
+ v-if="forkMessageVisible"
+ class="js-file-fork-suggestion-section file-fork-suggestion">
+ <span class="file-fork-suggestion-note">
+ You're not allowed to <span class="js-file-fork-suggestion-section-action">edit</span>
+ files in this project directly. Please fork this project,
+ make your changes there, and submit a merge request.
+ </span>
+ <a
+ :href="file.forkPath"
+ class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
+ >
+ Fork
+ </a>
+ <button
+ class="js-cancel-fork-suggestion-button btn btn-grouped"
+ type="button"
+ @click="hideForkMessage"
+ >
+ Cancel
+ </button>
+ </div>
+
+ <diff-content
+ v-show="!isCollapsed"
+ :class="{ hidden: isCollapsed || file.tooLarge }"
+ :diff-file="file"
+ />
+ <loading-icon
+ v-if="isLoadingCollapsedDiff"
+ class="diff-content loading"
+ />
+ <div
+ v-show="isCollapsed && !isLoadingCollapsedDiff && !file.tooLarge"
+ class="nothing-here-block diff-collapsed"
+ >
+ {{ __('This diff is collapsed.') }}
+ <a
+ class="click-to-expand js-click-to-expand"
+ href="#"
+ @click.prevent="handleToggle"
+ >
+ {{ __('Click to expand it.') }}
+ </a>
+ </div>
+ <div
+ v-if="file.tooLarge"
+ class="nothing-here-block diff-collapsed js-too-large-diff"
+ >
+ {{ __('This source diff could not be displayed because it is too large.') }}
+ <span v-html="viewBlobLink"></span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
new file mode 100644
index 00000000000..6bad389f778
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -0,0 +1,254 @@
+<script>
+import _ from 'underscore';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import Tooltip from '~/vue_shared/directives/tooltip';
+import { truncateSha } from '~/lib/utils/text_utility';
+import { __, s__, sprintf } from '~/locale';
+import EditButton from './edit_button.vue';
+
+export default {
+ components: {
+ ClipboardButton,
+ EditButton,
+ Icon,
+ },
+ directives: {
+ Tooltip,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ collapsible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ addMergeRequestButtons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ expanded: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ discussionsExpanded: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ blobForkSuggestion: null,
+ };
+ },
+ computed: {
+ icon() {
+ if (this.diffFile.submodule) {
+ return 'archive';
+ }
+
+ return this.diffFile.blob.icon;
+ },
+ titleLink() {
+ if (this.diffFile.submodule) {
+ return this.diffFile.submoduleTreeUrl || this.diffFile.submoduleLink;
+ }
+
+ return `#${this.diffFile.fileHash}`;
+ },
+ filePath() {
+ if (this.diffFile.submodule) {
+ return `${this.diffFile.filePath} @ ${truncateSha(this.diffFile.blob.id)}`;
+ }
+
+ if (this.diffFile.deletedFile) {
+ return sprintf(__('%{filePath} deleted'), { filePath: this.diffFile.filePath }, false);
+ }
+
+ return this.diffFile.filePath;
+ },
+ titleTag() {
+ return this.diffFile.fileHash ? 'a' : 'span';
+ },
+ isUsingLfs() {
+ return this.diffFile.storedExternally && this.diffFile.externalStorage === 'lfs';
+ },
+ collapseIcon() {
+ return this.expanded ? 'chevron-down' : 'chevron-right';
+ },
+ isDiscussionsExpanded() {
+ return this.discussionsExpanded && this.expanded;
+ },
+ viewFileButtonText() {
+ const truncatedContentSha = _.escape(truncateSha(this.diffFile.contentSha));
+ return sprintf(
+ s__('MergeRequests|View file @ %{commitId}'),
+ {
+ commitId: `<span class="commit-sha">${truncatedContentSha}</span>`,
+ },
+ false,
+ );
+ },
+ viewReplacedFileButtonText() {
+ const truncatedBaseSha = _.escape(truncateSha(this.diffFile.diffRefs.baseSha));
+ return sprintf(
+ s__('MergeRequests|View replaced file @ %{commitId}'),
+ {
+ commitId: `<span class="commit-sha">${truncatedBaseSha}</span>`,
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ handleToggle(e, checkTarget) {
+ if (!checkTarget || e.target === this.$refs.header) {
+ this.$emit('toggleFile');
+ }
+ },
+ showForkMessage() {
+ this.$emit('showForkMessage');
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ ref="header"
+ class="js-file-title file-title file-title-flex-parent"
+ @click="handleToggle($event, true)"
+ >
+ <div class="file-header-content">
+ <icon
+ v-if="collapsible"
+ :name="collapseIcon"
+ :size="16"
+ aria-hidden="true"
+ class="diff-toggle-caret"
+ @click.stop="handleToggle"
+ />
+ <a
+ ref="titleWrapper"
+ :href="titleLink"
+ >
+ <i
+ :class="`fa-${icon}`"
+ class="fa fa-fw"
+ aria-hidden="true"
+ ></i>
+ <span v-if="diffFile.renamedFile">
+ <strong
+ v-tooltip
+ :title="diffFile.oldPath"
+ class="file-title-name"
+ data-container="body"
+ >
+ {{ diffFile.oldPath }}
+ </strong>
+ →
+ <strong
+ v-tooltip
+ :title="diffFile.newPath"
+ class="file-title-name"
+ data-container="body"
+ >
+ {{ diffFile.newPath }}
+ </strong>
+ </span>
+
+ <strong
+ v-tooltip
+ v-else
+ :title="filePath"
+ class="file-title-name"
+ data-container="body"
+ >
+ {{ filePath }}
+ </strong>
+ </a>
+
+ <clipboard-button
+ :title="__('Copy file path to clipboard')"
+ :text="diffFile.filePath"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+
+ <small
+ v-if="diffFile.modeChanged"
+ ref="fileMode"
+ >
+ {{ diffFile.aMode }} → {{ diffFile.bMode }}
+ </small>
+
+ <span
+ v-if="isUsingLfs"
+ class="label label-lfs append-right-5"
+ >
+ {{ __('LFS') }}
+ </span>
+ </div>
+
+ <div
+ v-if="!diffFile.submodule && addMergeRequestButtons"
+ class="file-actions d-none d-md-block"
+ >
+ <template
+ v-if="diffFile.blob && diffFile.blob.readableText"
+ >
+ <button
+ :class="{ active: isDiscussionsExpanded }"
+ :title="s__('MergeRequests|Toggle comments for this file')"
+ class="btn js-toggle-diff-comments"
+ type="button"
+ >
+ <icon name="comment" />
+ </button>
+
+ <edit-button
+ v-if="!diffFile.deletedFile"
+ :current-user="currentUser"
+ :edit-path="diffFile.editPath"
+ :can-modify-blob="diffFile.canModifyBlob"
+ @showForkMessage="showForkMessage"
+ />
+ </template>
+
+ <a
+ v-if="diffFile.replacedViewPath"
+ :href="diffFile.replacedViewPath"
+ class="btn view-file js-view-file"
+ v-html="viewReplacedFileButtonText"
+ >
+ </a>
+ <a
+ :href="diffFile.viewPath"
+ class="btn view-file js-view-file"
+ v-html="viewFileButtonText"
+ >
+ </a>
+
+ <a
+ v-tooltip
+ v-if="diffFile.externalUrl"
+ :href="diffFile.externalUrl"
+ :title="`View on ${diffFile.formattedExternalUrl}`"
+ target="_blank"
+ rel="noopener noreferrer"
+ class="btn btn-file-option"
+ >
+ <icon name="external-link" />
+ </a>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
new file mode 100644
index 00000000000..3193b18becb
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -0,0 +1,105 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { pluralize, truncate } from '~/lib/utils/text_utility';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ UserAvatarImage,
+ },
+ props: {
+ discussions: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ discussionsExpanded() {
+ return this.discussions.every(discussion => discussion.expanded);
+ },
+ allDiscussions() {
+ return this.discussions.reduce((acc, note) => acc.concat(note.notes), []);
+ },
+ notesInGutter() {
+ return this.allDiscussions.slice(0, COUNT_OF_AVATARS_IN_GUTTER).map(n => ({
+ note: n.note,
+ author: n.author,
+ }));
+ },
+ moreCount() {
+ return this.allDiscussions.length - this.notesInGutter.length;
+ },
+ moreText() {
+ if (this.moreCount === 0) {
+ return '';
+ }
+
+ return pluralize(`${this.moreCount} more comment`, this.moreCount);
+ },
+ },
+ methods: {
+ ...mapActions(['toggleDiscussion']),
+ getTooltipText(noteData) {
+ let note = noteData.note;
+
+ if (note.length > LENGTH_OF_AVATAR_TOOLTIP) {
+ note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP);
+ }
+
+ return `${noteData.author.name}: ${note}`;
+ },
+ toggleDiscussions() {
+ this.discussions.forEach(discussion => {
+ this.toggleDiscussion({
+ discussionId: discussion.id,
+ });
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="diff-comment-avatar-holders">
+ <button
+ v-if="discussionsExpanded"
+ type="button"
+ aria-label="Show comments"
+ class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
+ @click="toggleDiscussions"
+ >
+ <icon
+ :size="12"
+ name="collapse"
+ />
+ </button>
+ <template v-else>
+ <user-avatar-image
+ v-for="note in notesInGutter"
+ :key="note.id"
+ :img-src="note.author.avatar_url"
+ :tooltip-text="getTooltipText(note)"
+ :size="19"
+ class="diff-comment-avatar js-diff-comment-avatar"
+ @click.native="toggleDiscussions"
+ />
+ <span
+ v-tooltip
+ v-if="moreText"
+ :title="moreText"
+ class="diff-comments-more-count has-tooltip js-diff-comment-avatar js-diff-comment-plus"
+ data-container="body"
+ data-placement="top"
+ role="button"
+ @click="toggleDiscussions"
+ >+{{ moreCount }}</span>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
new file mode 100644
index 00000000000..05dca0cdd9a
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -0,0 +1,203 @@
+<script>
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import { mapState, mapGetters, mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import DiffGutterAvatars from './diff_gutter_avatars.vue';
+import {
+ MATCH_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ OLD_NO_NEW_LINE_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+ LINE_POSITION_RIGHT,
+ UNFOLD_COUNT,
+} from '../constants';
+import * as utils from '../store/utils';
+
+export default {
+ components: {
+ DiffGutterAvatars,
+ Icon,
+ },
+ props: {
+ fileHash: {
+ type: String,
+ required: true,
+ },
+ contextLinesPath: {
+ type: String,
+ required: true,
+ },
+ lineType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lineNumber: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ lineCode: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ linePosition: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ metaData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ showCommentButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isBottom: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapState({
+ diffViewType: state => state.diffs.diffViewType,
+ diffFiles: state => state.diffs.diffFiles,
+ }),
+ ...mapGetters(['isLoggedIn', 'discussionsByLineCode']),
+ isMatchLine() {
+ return this.lineType === MATCH_LINE_TYPE;
+ },
+ isContextLine() {
+ return this.lineType === CONTEXT_LINE_TYPE;
+ },
+ isMetaLine() {
+ return this.lineType === OLD_NO_NEW_LINE_TYPE || this.lineType === NEW_NO_NEW_LINE_TYPE;
+ },
+ lineHref() {
+ return this.lineCode ? `#${this.lineCode}` : '#';
+ },
+ shouldShowCommentButton() {
+ return (
+ this.isLoggedIn &&
+ this.showCommentButton &&
+ !this.isMatchLine &&
+ !this.isContextLine &&
+ !this.hasDiscussions &&
+ !this.isMetaLine
+ );
+ },
+ discussions() {
+ return this.discussionsByLineCode[this.lineCode] || [];
+ },
+ hasDiscussions() {
+ return this.discussions.length > 0;
+ },
+ shouldShowAvatarsOnGutter() {
+ let render = this.hasDiscussions && this.showCommentButton;
+
+ if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) {
+ render = false;
+ }
+
+ return render;
+ },
+ },
+ methods: {
+ ...mapActions(['loadMoreLines']),
+ handleCommentButton() {
+ this.$emit('showCommentForm', { lineCode: this.lineCode });
+ },
+ handleLoadMoreLines() {
+ if (this.isRequesting) {
+ return;
+ }
+
+ this.isRequesting = true;
+ const endpoint = this.contextLinesPath;
+ const oldLineNumber = this.metaData.oldPos || 0;
+ const newLineNumber = this.metaData.newPos || 0;
+ const offset = newLineNumber - oldLineNumber;
+ const bottom = this.isBottom;
+ const fileHash = this.fileHash;
+ const view = this.diffViewType;
+ let unfold = true;
+ let lineNumber = newLineNumber - 1;
+ let since = lineNumber - UNFOLD_COUNT;
+ let to = lineNumber;
+
+ if (bottom) {
+ lineNumber = newLineNumber + 1;
+ since = lineNumber;
+ to = lineNumber + UNFOLD_COUNT;
+ } else {
+ const diffFile = utils.findDiffFile(this.diffFiles, this.fileHash);
+ const indexForInline = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, {
+ oldLineNumber,
+ newLineNumber,
+ });
+ const prevLine = diffFile.highlightedDiffLines[indexForInline - 2];
+ const prevLineNumber = (prevLine && prevLine.newLine) || 0;
+
+ if (since <= prevLineNumber + 1) {
+ since = prevLineNumber + 1;
+ unfold = false;
+ }
+ }
+
+ const params = { since, to, bottom, offset, unfold, view };
+ const lineNumbers = { oldLineNumber, newLineNumber };
+ this.loadMoreLines({ endpoint, params, lineNumbers, fileHash })
+ .then(() => {
+ this.isRequesting = false;
+ })
+ .catch(() => {
+ createFlash(s__('Diffs|Something went wrong while fetching diff lines.'));
+ this.isRequesting = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <span
+ v-if="isMatchLine"
+ class="context-cell"
+ role="button"
+ @click="handleLoadMoreLines"
+ >...</span>
+ <template
+ v-else
+ >
+ <button
+ v-show="shouldShowCommentButton"
+ type="button"
+ class="add-diff-note js-add-diff-note-button"
+ title="Add a comment to this line"
+ @click="handleCommentButton"
+ >
+ <icon
+ :size="12"
+ name="comment"
+ />
+ </button>
+ <a
+ v-if="lineNumber"
+ :data-linenumber="lineNumber"
+ :href="lineHref"
+ >
+ </a>
+ <diff-gutter-avatars
+ v-if="shouldShowAvatarsOnGutter"
+ :discussions="discussions"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
new file mode 100644
index 00000000000..86f5e98194d
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -0,0 +1,93 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import noteForm from '../../notes/components/note_form.vue';
+import { getNoteFormData } from '../store/utils';
+
+export default {
+ components: {
+ noteForm,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ line: {
+ type: Object,
+ required: true,
+ },
+ position: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ noteTargetLine: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState({
+ noteableData: state => state.notes.noteableData,
+ diffViewType: state => state.diffs.diffViewType,
+ }),
+ ...mapGetters(['noteableType', 'getNotesDataByProp']),
+ },
+ methods: {
+ ...mapActions(['cancelCommentForm', 'saveNote', 'fetchDiscussions']),
+ handleCancelCommentForm() {
+ this.cancelCommentForm({
+ lineCode: this.line.lineCode,
+ });
+ },
+ handleSaveNote(note) {
+ const postData = getNoteFormData({
+ note,
+ noteableData: this.noteableData,
+ noteableType: this.noteableType,
+ noteTargetLine: this.noteTargetLine,
+ diffViewType: this.diffViewType,
+ diffFile: this.diffFile,
+ linePosition: this.position,
+ });
+
+ this.saveNote(postData)
+ .then(() => {
+ const endpoint = this.getNotesDataByProp('discussionsPath');
+
+ this.fetchDiscussions(endpoint)
+ .then(() => {
+ this.handleCancelCommentForm();
+ })
+ .catch(() => {
+ createFlash(s__('MergeRequests|Updating discussions failed'));
+ });
+ })
+ .catch(() => {
+ createFlash(s__('MergeRequests|Saving the comment failed'));
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="content discussion-form discussion-form-container discussion-notes"
+ >
+ <note-form
+ :is-editing="true"
+ :line-code="line.lineCode"
+ save-button-title="Comment"
+ class="diff-comment-form"
+ @cancelForm="handleCancelCommentForm"
+ @handleFormUpdate="handleSaveNote"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue
new file mode 100644
index 00000000000..ebf90631d76
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/edit_button.vue
@@ -0,0 +1,42 @@
+<script>
+export default {
+ props: {
+ editPath: {
+ type: String,
+ required: true,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ canModifyBlob: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ handleEditClick(evt) {
+ if (!this.currentUser || this.canModifyBlob) {
+ // if we can Edit, do default Edit button behavior
+ return;
+ }
+
+ if (this.currentUser.canFork && this.currentUser.canCreateMergeRequest) {
+ evt.preventDefault();
+ this.$emit('showForkMessage');
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <a
+ :href="editPath"
+ class="btn btn-default js-edit-blob"
+ @click="handleEditClick"
+ >
+ Edit
+ </a>
+</template>
diff --git a/app/assets/javascripts/diffs/components/hidden_files_warning.vue b/app/assets/javascripts/diffs/components/hidden_files_warning.vue
new file mode 100644
index 00000000000..017dcfcc357
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/hidden_files_warning.vue
@@ -0,0 +1,51 @@
+<script>
+export default {
+ props: {
+ total: {
+ type: String,
+ required: true,
+ },
+ visible: {
+ type: Number,
+ required: true,
+ },
+ plainDiffPath: {
+ type: String,
+ required: true,
+ },
+ emailPatchPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="alert alert-warning">
+ <h4>
+ {{ __('Too many changes to show.') }}
+ <div class="pull-right">
+ <a
+ :href="plainDiffPath"
+ class="btn btn-sm"
+ >
+ {{ __('Plain diff') }}
+ </a>
+ <a
+ :href="emailPatchPath"
+ class="btn btn-sm"
+ >
+ {{ __('Email patch') }}
+ </a>
+ </div>
+ </h4>
+ <p>
+ To preserve performance only
+ <strong>
+ {{ visible }} of {{ total }}
+ </strong>
+ files are displayed.
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
new file mode 100644
index 00000000000..0ed3dc7f3ad
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -0,0 +1,117 @@
+<script>
+import diffContentMixin from '../mixins/diff_content';
+import {
+ MATCH_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ OLD_NO_NEW_LINE_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+ LINE_HOVER_CLASS_NAME,
+ LINE_UNFOLD_CLASS_NAME,
+} from '../constants';
+
+export default {
+ mixins: [diffContentMixin],
+ methods: {
+ handleMouse(lineCode, isOver) {
+ this.hoveredLineCode = isOver ? lineCode : null;
+ },
+ getLineClass(line) {
+ const isSameLine = this.hoveredLineCode && this.hoveredLineCode === line.lineCode;
+ const isMatchLine = line.type === MATCH_LINE_TYPE;
+ const isContextLine = line.type === CONTEXT_LINE_TYPE;
+ const isMetaLine = line.type === OLD_NO_NEW_LINE_TYPE || line.type === NEW_NO_NEW_LINE_TYPE;
+
+ return {
+ [line.type]: line.type,
+ [LINE_UNFOLD_CLASS_NAME]: isMatchLine,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn && isSameLine && !isMatchLine && !isContextLine && !isMetaLine,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <table
+ :class="userColorScheme"
+ :data-commit-id="commitId"
+ class="code diff-wrap-lines js-syntax-highlight text-file">
+ <tbody>
+ <template
+ v-for="(line, index) in normalizedDiffLines"
+ >
+ <tr
+ :id="line.lineCode || `${fileHash}_${line.oldLine}_${line.newLine}`"
+ :key="line.lineCode"
+ :class="getRowClass(line)"
+ class="line_holder"
+ @mouseover="handleMouse(line.lineCode, true)"
+ @mouseout="handleMouse(line.lineCode, false)"
+ >
+ <td
+ :class="getLineClass(line)"
+ class="diff-line-num old_line"
+ >
+ <diff-line-gutter-content
+ :file-hash="fileHash"
+ :line-type="line.type"
+ :line-code="line.lineCode"
+ :line-number="line.oldLine"
+ :meta-data="line.metaData"
+ :show-comment-button="true"
+ :context-lines-path="diffFile.contextLinesPath"
+ :is-bottom="index + 1 === diffLinesLength"
+ @showCommentForm="handleShowCommentForm"
+ />
+ </td>
+ <td
+ :class="getLineClass(line)"
+ class="diff-line-num new_line"
+ >
+ <diff-line-gutter-content
+ :file-hash="fileHash"
+ :line-type="line.type"
+ :line-code="line.lineCode"
+ :line-number="line.newLine"
+ :meta-data="line.metaData"
+ :is-bottom="index + 1 === diffLinesLength"
+ :context-lines-path="diffFile.contextLinesPath"
+ />
+ </td>
+ <td
+ :class="line.type"
+ class="line_content"
+ v-html="line.richText"
+ >
+ </td>
+ </tr>
+ <tr
+ v-if="isDiscussionExpanded(line.lineCode) || diffLineCommentForms[line.lineCode]"
+ :key="index"
+ :class="discussionsByLineCode[line.lineCode] ? '' : 'js-temp-notes-holder'"
+ class="notes_holder"
+ >
+ <td
+ class="notes_line"
+ colspan="2"
+ ></td>
+ <td class="notes_content">
+ <div class="content">
+ <diff-discussions
+ :discussions="discussionsByLineCode[line.lineCode] || []"
+ />
+ <diff-line-note-form
+ v-if="diffLineCommentForms[line.lineCode]"
+ :diff-file="diffFile"
+ :diff-lines="diffLines"
+ :line="line"
+ :note-target-line="diffLines[index]"
+ />
+ </div>
+ </td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+</template>
diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue
new file mode 100644
index 00000000000..d817157fbcd
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/no_changes.vue
@@ -0,0 +1,49 @@
+<script>
+import { mapState } from 'vuex';
+import emptyImage from '~/../../views/shared/icons/_mr_widget_empty_state.svg';
+
+export default {
+ data() {
+ return {
+ emptyImage,
+ };
+ },
+ computed: {
+ ...mapState({
+ sourceBranch: state => state.notes.noteableData.source_branch,
+ targetBranch: state => state.notes.noteableData.target_branch,
+ newBlobPath: state => state.notes.noteableData.new_blob_path,
+ }),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="row empty-state nothing-here-block"
+ >
+ <div class="col-xs-12">
+ <div class="svg-content">
+ <span
+ v-html="emptyImage"
+ ></span>
+ </div>
+ </div>
+ <div class="col-xs-12">
+ <div class="text-content text-center">
+ No changes between
+ <span class="ref-name">{{ sourceBranch }}</span>
+ and
+ <span class="ref-name">{{ targetBranch }}</span>
+ <div class="text-center">
+ <a
+ :href="newBlobPath"
+ class="btn btn-save"
+ >
+ {{ __('Create commit') }}
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
new file mode 100644
index 00000000000..2ddf8e6c6ed
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -0,0 +1,224 @@
+<script>
+import diffContentMixin from '../mixins/diff_content';
+import {
+ EMPTY_CELL_TYPE,
+ MATCH_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ OLD_NO_NEW_LINE_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+ LINE_HOVER_CLASS_NAME,
+ LINE_UNFOLD_CLASS_NAME,
+ LINE_POSITION_RIGHT,
+} from '../constants';
+
+export default {
+ mixins: [diffContentMixin],
+ computed: {
+ parallelDiffLines() {
+ return this.normalizedDiffLines.map(line => {
+ if (!line.left) {
+ Object.assign(line, { left: { type: EMPTY_CELL_TYPE } });
+ } else if (!line.right) {
+ Object.assign(line, { right: { type: EMPTY_CELL_TYPE } });
+ }
+
+ return line;
+ });
+ },
+ },
+ methods: {
+ hasDiscussion(line) {
+ const discussions = this.discussionsByLineCode;
+ const hasDiscussion = discussions[line.left.lineCode] || discussions[line.right.lineCode];
+
+ return hasDiscussion;
+ },
+ getClassName(line, position) {
+ const { type, lineCode } = line[position];
+ const isMatchLine = type === MATCH_LINE_TYPE;
+ const isContextLine = !isMatchLine && type !== EMPTY_CELL_TYPE && type !== CONTEXT_LINE_TYPE;
+ const isMetaLine = type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE;
+ const isSameLine = this.hoveredLineCode && this.hoveredLineCode === lineCode;
+ const isSameSection = position === this.hoveredSection;
+
+ return {
+ [type]: type,
+ [LINE_UNFOLD_CLASS_NAME]: isMatchLine,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn && isContextLine && isSameLine && isSameSection && !isMetaLine,
+ };
+ },
+ handleMouse(e, line, isHover) {
+ if (isHover) {
+ const cell = e.target.closest('td');
+
+ if (this.$refs.leftLines.indexOf(cell) > -1) {
+ this.hoveredLineCode = line.left.lineCode;
+ this.hoveredSection = 'left';
+ } else if (this.$refs.rightLines.indexOf(cell) > -1) {
+ this.hoveredLineCode = line.right.lineCode;
+ this.hoveredSection = 'right';
+ }
+ } else {
+ this.hoveredLineCode = null;
+ this.hoveredSection = null;
+ }
+ },
+ shouldRenderDiscussionsRow(line) {
+ const hasDiscussion = this.hasDiscussion(line) && this.hasAnyExpandedDiscussion(line);
+ const hasCommentFormOnLeft = this.diffLineCommentForms[line.left.lineCode];
+ const hasCommentFormOnRight = this.diffLineCommentForms[line.right.lineCode];
+
+ return hasDiscussion || hasCommentFormOnLeft || hasCommentFormOnRight;
+ },
+ shouldRenderDiscussions(line, position) {
+ const { lineCode } = line[position];
+ let render = this.discussionsByLineCode[lineCode] && this.isDiscussionExpanded(lineCode);
+
+ // Avoid rendering context line discussions on the right side in parallel view
+ if (position === LINE_POSITION_RIGHT) {
+ render = render && line.right.type;
+ }
+
+ return render;
+ },
+ hasAnyExpandedDiscussion(line) {
+ const isLeftExpanded = this.isDiscussionExpanded(line.left.lineCode);
+ const isRightExpanded = this.isDiscussionExpanded(line.right.lineCode);
+
+ return isLeftExpanded || isRightExpanded;
+ },
+ getLineCode(line, side) {
+ const lineCode = side.lineCode;
+ if (lineCode) {
+ return lineCode;
+ }
+
+ return `${this.fileHash}_${line.left.oldLine}_${line.right.newLine}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ :class="userColorScheme"
+ :data-commit-id="commitId"
+ class="code diff-wrap-lines js-syntax-highlight text-file">
+ <table>
+ <tbody>
+ <template
+ v-for="(line, index) in parallelDiffLines"
+ >
+ <tr
+ :key="index"
+ :class="getRowClass(line)"
+ class="line_holder parallel"
+ @mouseover="handleMouse($event, line, true)"
+ @mouseout="handleMouse($event, line, false)"
+ >
+ <td
+ ref="leftLines"
+ :class="getClassName(line, 'left')"
+ class="diff-line-num old_line"
+ >
+ <diff-line-gutter-content
+ :file-hash="fileHash"
+ :line-type="line.left.type"
+ :line-code="line.left.lineCode"
+ :line-number="line.left.oldLine"
+ :meta-data="line.left.metaData"
+ :show-comment-button="true"
+ :context-lines-path="diffFile.contextLinesPath"
+ :is-bottom="index + 1 === diffLinesLength"
+ line-position="left"
+ @showCommentForm="handleShowCommentForm"
+ />
+ </td>
+ <td
+ ref="leftLines"
+ :class="getClassName(line, 'left')"
+ :id="getLineCode(line, line.left)"
+ class="line_content parallel left-side"
+ v-html="line.left.richText"
+ >
+ </td>
+ <td
+ ref="rightLines"
+ :class="getClassName(line, 'right')"
+ class="diff-line-num new_line"
+ >
+ <diff-line-gutter-content
+ :file-hash="fileHash"
+ :line-type="line.right.type"
+ :line-code="line.right.lineCode"
+ :line-number="line.right.newLine"
+ :meta-data="line.right.metaData"
+ :show-comment-button="true"
+ :context-lines-path="diffFile.contextLinesPath"
+ :is-bottom="index + 1 === diffLinesLength"
+ line-position="right"
+ @showCommentForm="handleShowCommentForm"
+ />
+ </td>
+ <td
+ ref="rightLines"
+ :class="getClassName(line, 'right')"
+ :id="getLineCode(line, line.right)"
+ class="line_content parallel right-side"
+ v-html="line.right.richText"
+ >
+ </td>
+ </tr>
+ <tr
+ v-if="shouldRenderDiscussionsRow(line)"
+ :key="line.left.lineCode || line.right.lineCode"
+ :class="hasDiscussion(line) ? '' : 'js-temp-notes-holder'"
+ class="notes_holder"
+ >
+ <td class="notes_line old"></td>
+ <td class="notes_content parallel old">
+ <div
+ v-if="shouldRenderDiscussions(line, 'left')"
+ class="content"
+ >
+ <diff-discussions
+ :discussions="discussionsByLineCode[line.left.lineCode]"
+ />
+ </div>
+ <diff-line-note-form
+ v-if="diffLineCommentForms[line.left.lineCode] &&
+ diffLineCommentForms[line.left.lineCode]"
+ :diff-file="diffFile"
+ :diff-lines="diffLines"
+ :line="line.left"
+ :note-target-line="diffLines[index].left"
+ position="left"
+ />
+ </td>
+ <td class="notes_line new"></td>
+ <td class="notes_content parallel new">
+ <div
+ v-if="shouldRenderDiscussions(line, 'right')"
+ class="content"
+ >
+ <diff-discussions
+ :discussions="discussionsByLineCode[line.right.lineCode]"
+ />
+ </div>
+ <diff-line-note-form
+ v-if="diffLineCommentForms[line.right.lineCode] &&
+ diffLineCommentForms[line.right.lineCode] && line.right.type"
+ :diff-file="diffFile"
+ :diff-lines="diffLines"
+ :line="line.right"
+ :note-target-line="diffLines[index].right"
+ position="right"
+ />
+ </td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
new file mode 100644
index 00000000000..1a7478b307e
--- /dev/null
+++ b/app/assets/javascripts/diffs/constants.js
@@ -0,0 +1,24 @@
+export const INLINE_DIFF_VIEW_TYPE = 'inline';
+export const PARALLEL_DIFF_VIEW_TYPE = 'parallel';
+export const MATCH_LINE_TYPE = 'match';
+export const OLD_NO_NEW_LINE_TYPE = 'old-nonewline';
+export const NEW_NO_NEW_LINE_TYPE = 'new-nonewline';
+export const CONTEXT_LINE_TYPE = 'context';
+export const EMPTY_CELL_TYPE = 'empty-cell';
+export const COMMENT_FORM_TYPE = 'commentForm';
+export const DIFF_NOTE_TYPE = 'DiffNote';
+export const NEW_LINE_TYPE = 'new';
+export const OLD_LINE_TYPE = 'old';
+export const TEXT_DIFF_POSITION_TYPE = 'text';
+
+export const LINE_POSITION_LEFT = 'left';
+export const LINE_POSITION_RIGHT = 'right';
+
+export const DIFF_VIEW_COOKIE_NAME = 'diff_view';
+export const LINE_HOVER_CLASS_NAME = 'is-over';
+export const LINE_UNFOLD_CLASS_NAME = 'unfold js-unfold';
+export const CONTEXT_LINE_CLASS_NAME = 'diff-expanded';
+
+export const UNFOLD_COUNT = 20;
+export const COUNT_OF_AVATARS_IN_GUTTER = 3;
+export const LENGTH_OF_AVATAR_TOOLTIP = 17;
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
new file mode 100644
index 00000000000..f6840f87034
--- /dev/null
+++ b/app/assets/javascripts/diffs/index.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import { mapState } from 'vuex';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import diffsApp from './components/app.vue';
+
+export default function initDiffsApp(store) {
+ return new Vue({
+ el: '#js-diffs-app',
+ name: 'MergeRequestDiffs',
+ components: {
+ diffsApp,
+ },
+ store,
+ data() {
+ const { dataset } = document.querySelector(this.$options.el);
+
+ return {
+ endpoint: dataset.endpoint,
+ currentUser: convertObjectPropsToCamelCase(JSON.parse(dataset.currentUserData), {
+ deep: true,
+ }),
+ };
+ },
+ computed: {
+ ...mapState({
+ activeTab: state => state.page.activeTab,
+ }),
+ },
+ render(createElement) {
+ return createElement('diffs-app', {
+ props: {
+ endpoint: this.endpoint,
+ currentUser: this.currentUser,
+ shouldShow: this.activeTab === 'diffs',
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/diffs/mixins/changed_files.js b/app/assets/javascripts/diffs/mixins/changed_files.js
new file mode 100644
index 00000000000..da1339f0ffa
--- /dev/null
+++ b/app/assets/javascripts/diffs/mixins/changed_files.js
@@ -0,0 +1,38 @@
+export default {
+ props: {
+ diffFiles: {
+ type: Array,
+ required: true,
+ },
+ },
+ methods: {
+ fileChangedIcon(diffFile) {
+ if (diffFile.deletedFile) {
+ return 'file-deletion';
+ } else if (diffFile.newFile) {
+ return 'file-addition';
+ }
+ return 'file-modified';
+ },
+ fileChangedClass(diffFile) {
+ if (diffFile.deletedFile) {
+ return 'cred';
+ } else if (diffFile.newFile) {
+ return 'cgreen';
+ }
+
+ return '';
+ },
+ truncatedDiffPath(path) {
+ const maxLength = 60;
+
+ if (path.length > maxLength) {
+ const start = path.length - maxLength;
+ const end = start + maxLength;
+ return `...${path.slice(start, end)}`;
+ }
+
+ return path;
+ },
+ },
+};
diff --git a/app/assets/javascripts/diffs/mixins/diff_content.js b/app/assets/javascripts/diffs/mixins/diff_content.js
new file mode 100644
index 00000000000..bef06ad2b52
--- /dev/null
+++ b/app/assets/javascripts/diffs/mixins/diff_content.js
@@ -0,0 +1,89 @@
+import { mapState, mapGetters, mapActions } from 'vuex';
+import diffDiscussions from '../components/diff_discussions.vue';
+import diffLineGutterContent from '../components/diff_line_gutter_content.vue';
+import diffLineNoteForm from '../components/diff_line_note_form.vue';
+import { trimFirstCharOfLineContent } from '../store/utils';
+import { CONTEXT_LINE_TYPE, CONTEXT_LINE_CLASS_NAME } from '../constants';
+
+export default {
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ hoveredLineCode: null,
+ hoveredSection: null,
+ };
+ },
+ components: {
+ diffDiscussions,
+ diffLineNoteForm,
+ diffLineGutterContent,
+ },
+ computed: {
+ ...mapState({
+ diffLineCommentForms: state => state.diffs.diffLineCommentForms,
+ }),
+ ...mapGetters(['discussionsByLineCode', 'isLoggedIn', 'commit']),
+ commitId() {
+ return this.commit && this.commit.id;
+ },
+ userColorScheme() {
+ return window.gon.user_color_scheme;
+ },
+ normalizedDiffLines() {
+ return this.diffLines.map(line => {
+ if (line.richText) {
+ return this.trimFirstChar(line);
+ }
+
+ if (line.left) {
+ Object.assign(line, { left: this.trimFirstChar(line.left) });
+ }
+
+ if (line.right) {
+ Object.assign(line, { right: this.trimFirstChar(line.right) });
+ }
+
+ return line;
+ });
+ },
+ diffLinesLength() {
+ return this.normalizedDiffLines.length;
+ },
+ fileHash() {
+ return this.diffFile.fileHash;
+ },
+ },
+ methods: {
+ ...mapActions(['showCommentForm', 'cancelCommentForm']),
+ getRowClass(line) {
+ const isContextLine = line.left
+ ? line.left.type === CONTEXT_LINE_TYPE
+ : line.type === CONTEXT_LINE_TYPE;
+
+ return {
+ [line.type]: line.type,
+ [CONTEXT_LINE_CLASS_NAME]: isContextLine,
+ };
+ },
+ trimFirstChar(line) {
+ return trimFirstCharOfLineContent(line);
+ },
+ handleShowCommentForm(params) {
+ this.showCommentForm({ lineCode: params.lineCode });
+ },
+ isDiscussionExpanded(lineCode) {
+ const discussions = this.discussionsByLineCode[lineCode];
+
+ return discussions ? discussions.every(discussion => discussion.expanded) : false;
+ },
+ },
+};
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
new file mode 100644
index 00000000000..f8089b314d3
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -0,0 +1,99 @@
+import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
+import Cookies from 'js-cookie';
+import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import * as types from './mutation_types';
+import {
+ PARALLEL_DIFF_VIEW_TYPE,
+ INLINE_DIFF_VIEW_TYPE,
+ DIFF_VIEW_COOKIE_NAME,
+} from '../constants';
+
+export const setEndpoint = ({ commit }, endpoint) => {
+ commit(types.SET_ENDPOINT, endpoint);
+};
+
+export const setLoadingState = ({ commit }, state) => {
+ commit(types.SET_LOADING, state);
+};
+
+export const fetchDiffFiles = ({ state, commit }) => {
+ commit(types.SET_LOADING, true);
+
+ return axios
+ .get(state.endpoint)
+ .then(res => {
+ commit(types.SET_LOADING, false);
+ commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
+ commit(types.SET_DIFF_DATA, res.data);
+ return Vue.nextTick();
+ })
+ .then(handleLocationHash);
+};
+
+export const setInlineDiffViewType = ({ commit }) => {
+ commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE);
+
+ Cookies.set(DIFF_VIEW_COOKIE_NAME, INLINE_DIFF_VIEW_TYPE);
+ const url = mergeUrlParams({ view: INLINE_DIFF_VIEW_TYPE }, window.location.href);
+ historyPushState(url);
+};
+
+export const setParallelDiffViewType = ({ commit }) => {
+ commit(types.SET_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE);
+
+ Cookies.set(DIFF_VIEW_COOKIE_NAME, PARALLEL_DIFF_VIEW_TYPE);
+ const url = mergeUrlParams({ view: PARALLEL_DIFF_VIEW_TYPE }, window.location.href);
+ historyPushState(url);
+};
+
+export const showCommentForm = ({ commit }, params) => {
+ commit(types.ADD_COMMENT_FORM_LINE, params);
+};
+
+export const cancelCommentForm = ({ commit }, params) => {
+ commit(types.REMOVE_COMMENT_FORM_LINE, params);
+};
+
+export const loadMoreLines = ({ commit }, options) => {
+ const { endpoint, params, lineNumbers, fileHash } = options;
+
+ params.from_merge_request = true;
+
+ return axios.get(endpoint, { params }).then(res => {
+ const contextLines = res.data || [];
+
+ commit(types.ADD_CONTEXT_LINES, {
+ lineNumbers,
+ contextLines,
+ params,
+ fileHash,
+ });
+ });
+};
+
+export const loadCollapsedDiff = ({ commit }, file) =>
+ axios.get(file.loadCollapsedDiffUrl).then(res => {
+ commit(types.ADD_COLLAPSED_DIFFS, {
+ file,
+ data: res.data,
+ });
+ });
+
+export const expandAllFiles = ({ commit }) => {
+ commit(types.EXPAND_ALL_FILES);
+};
+
+export default {
+ setEndpoint,
+ setLoadingState,
+ fetchDiffFiles,
+ setInlineDiffViewType,
+ setParallelDiffViewType,
+ showCommentForm,
+ cancelCommentForm,
+ loadMoreLines,
+ loadCollapsedDiff,
+ expandAllFiles,
+};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
new file mode 100644
index 00000000000..66d0f47d102
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -0,0 +1,16 @@
+import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
+
+export default {
+ isParallelView(state) {
+ return state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
+ },
+ isInlineView(state) {
+ return state.diffViewType === INLINE_DIFF_VIEW_TYPE;
+ },
+ areAllFilesCollapsed(state) {
+ return state.diffFiles.every(file => file.collapsed);
+ },
+ commit(state) {
+ return state.commit;
+ },
+};
diff --git a/app/assets/javascripts/diffs/store/index.js b/app/assets/javascripts/diffs/store/index.js
new file mode 100644
index 00000000000..e6aa8f5b12a
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/index.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import diffsModule from './modules';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ modules: {
+ diffs: diffsModule,
+ },
+});
diff --git a/app/assets/javascripts/diffs/store/modules/index.js b/app/assets/javascripts/diffs/store/modules/index.js
new file mode 100644
index 00000000000..882a098c977
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/modules/index.js
@@ -0,0 +1,25 @@
+import Cookies from 'js-cookie';
+import { getParameterValues } from '~/lib/utils/url_utility';
+import actions from '../actions';
+import getters from '../getters';
+import mutations from '../mutations';
+import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
+
+const viewTypeFromQueryString = getParameterValues('view')[0];
+const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
+const defaultViewType = INLINE_DIFF_VIEW_TYPE;
+
+export default {
+ state: {
+ isLoading: true,
+ endpoint: '',
+ commit: null,
+ diffFiles: [],
+ mergeRequestDiffs: [],
+ diffLineCommentForms: {},
+ diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
+ },
+ getters,
+ actions,
+ mutations,
+};
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
new file mode 100644
index 00000000000..a65b205b8e7
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -0,0 +1,11 @@
+export const SET_ENDPOINT = 'SET_ENDPOINT';
+export const SET_LOADING = 'SET_LOADING';
+export const SET_DIFF_DATA = 'SET_DIFF_DATA';
+export const SET_DIFF_FILES = 'SET_DIFF_FILES';
+export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE';
+export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS';
+export const ADD_COMMENT_FORM_LINE = 'ADD_COMMENT_FORM_LINE';
+export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE';
+export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
+export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
+export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
new file mode 100644
index 00000000000..fd9ea73e33d
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import _ from 'underscore';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils';
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_ENDPOINT](state, endpoint) {
+ Object.assign(state, { endpoint });
+ },
+
+ [types.SET_LOADING](state, isLoading) {
+ Object.assign(state, { isLoading });
+ },
+
+ [types.SET_DIFF_DATA](state, data) {
+ Object.assign(state, {
+ ...convertObjectPropsToCamelCase(data, { deep: true }),
+ });
+ },
+
+ [types.SET_DIFF_FILES](state, diffFiles) {
+ Object.assign(state, {
+ diffFiles: convertObjectPropsToCamelCase(diffFiles, { deep: true }),
+ });
+ },
+
+ [types.SET_MERGE_REQUEST_DIFFS](state, mergeRequestDiffs) {
+ Object.assign(state, {
+ mergeRequestDiffs: convertObjectPropsToCamelCase(mergeRequestDiffs, { deep: true }),
+ });
+ },
+
+ [types.SET_DIFF_VIEW_TYPE](state, diffViewType) {
+ Object.assign(state, { diffViewType });
+ },
+
+ [types.ADD_COMMENT_FORM_LINE](state, { lineCode }) {
+ Vue.set(state.diffLineCommentForms, lineCode, true);
+ },
+
+ [types.REMOVE_COMMENT_FORM_LINE](state, { lineCode }) {
+ Vue.delete(state.diffLineCommentForms, lineCode);
+ },
+
+ [types.ADD_CONTEXT_LINES](state, options) {
+ const { lineNumbers, contextLines, fileHash } = options;
+ const { bottom } = options.params;
+ const diffFile = findDiffFile(state.diffFiles, fileHash);
+ const { highlightedDiffLines, parallelDiffLines } = diffFile;
+
+ removeMatchLine(diffFile, lineNumbers, bottom);
+ const lines = addLineReferences(contextLines, lineNumbers, bottom);
+ addContextLines({
+ inlineLines: highlightedDiffLines,
+ parallelLines: parallelDiffLines,
+ contextLines: lines,
+ bottom,
+ lineNumbers,
+ });
+ },
+
+ [types.ADD_COLLAPSED_DIFFS](state, { file, data }) {
+ const normalizedData = convertObjectPropsToCamelCase(data, { deep: true });
+ const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash);
+
+ if (newFileData) {
+ const index = _.findIndex(state.diffFiles, f => f.fileHash === file.fileHash);
+ state.diffFiles.splice(index, 1, newFileData);
+ }
+ },
+
+ [types.EXPAND_ALL_FILES](state) {
+ const diffFiles = [];
+
+ state.diffFiles.forEach((file) => {
+ diffFiles.push({
+ ...file,
+ collapsed: false,
+ });
+ });
+
+ Object.assign(state, { diffFiles });
+ },
+};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
new file mode 100644
index 00000000000..da7ae16aaf1
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -0,0 +1,172 @@
+import _ from 'underscore';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import {
+ LINE_POSITION_LEFT,
+ LINE_POSITION_RIGHT,
+ TEXT_DIFF_POSITION_TYPE,
+ DIFF_NOTE_TYPE,
+ NEW_LINE_TYPE,
+ OLD_LINE_TYPE,
+ MATCH_LINE_TYPE,
+} from '../constants';
+
+export function findDiffFile(files, hash) {
+ return files.filter(file => file.fileHash === hash)[0];
+}
+
+export const getReversePosition = linePosition => {
+ if (linePosition === LINE_POSITION_RIGHT) {
+ return LINE_POSITION_LEFT;
+ }
+
+ return LINE_POSITION_RIGHT;
+};
+
+export function getNoteFormData(params) {
+ const {
+ note,
+ noteableType,
+ noteableData,
+ diffFile,
+ noteTargetLine,
+ diffViewType,
+ linePosition,
+ } = params;
+
+ const position = JSON.stringify({
+ base_sha: diffFile.diffRefs.baseSha,
+ start_sha: diffFile.diffRefs.startSha,
+ head_sha: diffFile.diffRefs.headSha,
+ old_path: diffFile.oldPath,
+ new_path: diffFile.newPath,
+ position_type: TEXT_DIFF_POSITION_TYPE,
+ old_line: noteTargetLine.oldLine,
+ new_line: noteTargetLine.newLine,
+ });
+
+ const postData = {
+ view: diffViewType,
+ line_type: linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE,
+ merge_request_diff_head_sha: diffFile.diffRefs.headSha,
+ in_reply_to_discussion_id: '',
+ note_project_id: '',
+ target_type: noteableData.targetType,
+ target_id: noteableData.id,
+ note: {
+ note,
+ position,
+ noteable_type: noteableType,
+ noteable_id: noteableData.id,
+ commit_id: '',
+ type: DIFF_NOTE_TYPE,
+ line_code: noteTargetLine.lineCode,
+ },
+ };
+
+ return {
+ endpoint: noteableData.create_note_path,
+ data: postData,
+ };
+}
+
+export const findIndexInInlineLines = (lines, lineNumbers) => {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+
+ return _.findIndex(
+ lines,
+ line => line.oldLine === oldLineNumber && line.newLine === newLineNumber,
+ );
+};
+
+export const findIndexInParallelLines = (lines, lineNumbers) => {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+
+ return _.findIndex(
+ lines,
+ line =>
+ line.left &&
+ line.right &&
+ line.left.oldLine === oldLineNumber &&
+ line.right.newLine === newLineNumber,
+ );
+};
+
+export function removeMatchLine(diffFile, lineNumbers, bottom) {
+ const indexForInline = findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers);
+ const indexForParallel = findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers);
+ const factor = bottom ? 1 : -1;
+
+ diffFile.highlightedDiffLines.splice(indexForInline + factor, 1);
+ diffFile.parallelDiffLines.splice(indexForParallel + factor, 1);
+}
+
+export function addLineReferences(lines, lineNumbers, bottom) {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+ const lineCount = lines.length;
+ let matchLineIndex = -1;
+
+ const linesWithNumbers = lines.map((l, index) => {
+ const line = convertObjectPropsToCamelCase(l);
+
+ if (line.type === MATCH_LINE_TYPE) {
+ matchLineIndex = index;
+ } else {
+ Object.assign(line, {
+ oldLine: bottom ? oldLineNumber + index + 1 : oldLineNumber + index - lineCount,
+ newLine: bottom ? newLineNumber + index + 1 : newLineNumber + index - lineCount,
+ });
+ }
+
+ return line;
+ });
+
+ if (matchLineIndex > -1) {
+ const line = linesWithNumbers[matchLineIndex];
+ const targetLine = bottom
+ ? linesWithNumbers[matchLineIndex - 1]
+ : linesWithNumbers[matchLineIndex + 1];
+
+ Object.assign(line, {
+ metaData: {
+ oldPos: targetLine.oldLine,
+ newPos: targetLine.newLine,
+ },
+ });
+ }
+
+ return linesWithNumbers;
+}
+
+export function addContextLines(options) {
+ const { inlineLines, parallelLines, contextLines, lineNumbers } = options;
+ const normalizedParallelLines = contextLines.map(line => ({
+ left: line,
+ right: line,
+ }));
+
+ if (options.bottom) {
+ inlineLines.push(...contextLines);
+ parallelLines.push(...normalizedParallelLines);
+ } else {
+ const inlineIndex = findIndexInInlineLines(inlineLines, lineNumbers);
+ const parallelIndex = findIndexInParallelLines(parallelLines, lineNumbers);
+ inlineLines.splice(inlineIndex, 0, ...contextLines);
+ parallelLines.splice(parallelIndex, 0, ...normalizedParallelLines);
+ }
+}
+
+export function trimFirstCharOfLineContent(line) {
+ if (!line.richText) {
+ return line;
+ }
+
+ const firstChar = line.richText.charAt(0);
+
+ if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
+ Object.assign(line, {
+ richText: line.richText.substring(1),
+ });
+ }
+
+ return line;
+}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 72f21f13860..b755458aa4b 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
+/* eslint-disable consistent-return, no-new */
import $ from 'jquery';
import Flash from './flash';
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index faaaf899a0d..eba58bedd6d 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -40,7 +40,7 @@
methods: {
onClick() {
// eslint-disable-next-line no-alert
- if (confirm('Are you sure you want to stop this environment?')) {
+ if (window.confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
$(this.$el).tooltip('dispose');
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 7fbba7e27cb..45889c2d604 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
+/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, one-var-declaration-per-line, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */
/* global fuzzaldrinPlus */
import $ from 'jquery';
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 1def38bb336..b0765747a36 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -148,7 +148,6 @@ export default {
if (!parentGroup.isOpen) {
if (parentGroup.children.length === 0) {
parentGroup.isChildrenLoading = true;
- // eslint-disable-next-line promise/catch-or-return
this.fetchGroups({
parentId: parentGroup.id,
})
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 955d9280728..14c74687ab4 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -117,7 +117,7 @@ export default {
class="btn btn-primary btn-sm btn-block"
@click="toggleIsSmall"
>
- {{ __('Commit') }}
+ {{ __('Commit…') }}
</button>
<p
class="text-center"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 3d59410cbc2..d0fb0e3d99e 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -34,6 +34,10 @@ export default {
type: String,
required: true,
},
+ actionBtnIcon: {
+ type: String,
+ required: true,
+ },
itemActionComponent: {
type: String,
required: true,
@@ -53,26 +57,21 @@ export default {
required: true,
},
},
- data() {
- return {
- showActionButton: false,
- };
- },
computed: {
titleText() {
return sprintf(__('%{title} changes'), {
title: this.title,
});
},
+ filesLength() {
+ return this.fileList.length;
+ },
},
methods: {
...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() {
this[this.action]();
},
- setShowActionButton(show) {
- this.showActionButton = show;
- },
},
};
</script>
@@ -83,8 +82,6 @@ export default {
>
<header
class="multi-file-commit-panel-header"
- @mouseenter="setShowActionButton(true)"
- @mouseleave="setShowActionButton(false)"
>
<div
class="multi-file-commit-panel-header-title"
@@ -95,24 +92,40 @@ export default {
:size="18"
/>
{{ titleText }}
- <span
- v-show="!showActionButton"
- class="ide-commit-file-count"
- >
- {{ fileList.length }}
- </span>
- <button
- v-show="showActionButton"
- type="button"
- class="btn btn-blank btn-link ide-staged-action-btn"
- @click="actionBtnClicked"
- >
- {{ actionBtnText }}
- </button>
+ <div class="d-flex ml-auto">
+ <button
+ v-tooltip
+ v-show="filesLength"
+ :class="{
+ 'd-flex': filesLength
+ }"
+ :title="actionBtnText"
+ type="button"
+ class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center"
+ data-placement="bottom"
+ data-container="body"
+ data-boundary="viewport"
+ @click="actionBtnClicked"
+ >
+ <icon
+ :name="actionBtnIcon"
+ :size="12"
+ class="ml-auto mr-auto"
+ />
+ </button>
+ <span
+ :class="{
+ 'rounded-right': !filesLength
+ }"
+ class="ide-commit-file-count order-0 rounded-left text-center"
+ >
+ {{ filesLength }}
+ </span>
+ </div>
</div>
</header>
<ul
- v-if="fileList.length"
+ v-if="filesLength"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 2254271c679..d376a004e84 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -38,14 +38,17 @@ export default {
return this.modifiedFilesLength ? 'multi-file-modified' : '';
},
additionsTooltip() {
- return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
+ return sprintf(n__('1 %{type} addition', '%{count} %{type} additions', this.addedFilesLength), {
type: this.title.toLowerCase(),
+ count: this.addedFilesLength,
});
},
modifiedTooltip() {
return sprintf(
- n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
- { type: this.title.toLowerCase() },
+ n__('1 %{type} modification', '%{count} %{type} modifications', this.modifiedFilesLength), {
+ type: this.title.toLowerCase(),
+ count: this.modifiedFilesLength,
+ },
);
},
titleTooltip() {
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 2ecf9af4bf0..5cda7967130 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions } from 'vuex';
+import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
@@ -11,6 +12,9 @@ export default {
StageButton,
UnstageButton,
},
+ directives: {
+ tooltip,
+ },
props: {
file: {
type: Object,
@@ -50,6 +54,9 @@ export default {
isActive() {
return this.activeFileKey === this.fullKey;
},
+ tooltipTitle() {
+ return this.file.path === this.file.name ? '' : this.file.path;
+ },
},
methods: {
...mapActions([
@@ -81,29 +88,30 @@ export default {
</script>
<template>
- <div
- :class="{
- 'is-active': isActive
- }"
- class="multi-file-commit-list-item"
- >
+ <div class="multi-file-commit-list-item position-relative">
<button
+ v-tooltip
+ :title="tooltipTitle"
+ :class="{
+ 'is-active': isActive
+ }"
type="button"
- class="multi-file-commit-list-path"
+ class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0"
@dblclick="fileAction"
@click="openFileInEditor"
>
- <span class="multi-file-commit-list-file-path">
+ <span class="multi-file-commit-list-file-path d-flex align-items-center">
<icon
:name="iconName"
:size="16"
:css-classes="iconClass"
- />{{ file.path }}
+ />{{ file.name }}
</span>
</button>
<component
:is="actionComponent"
:path="file.path"
+ class="d-flex position-absolute"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
index a786ec80ac2..7014b9f605e 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -25,15 +25,17 @@ export default {
<template>
<div
v-once
- class="multi-file-discard-btn"
+ class="multi-file-discard-btn dropdown"
>
<button
v-tooltip
:aria-label="__('Stage changes')"
:title="__('Stage changes')"
type="button"
- class="btn btn-blank append-right-5"
+ class="btn btn-blank append-right-5 d-flex align-items-center"
data-container="body"
+ data-boundary="viewport"
+ data-placement="bottom"
@click.stop="stageChange(path)"
>
<icon
@@ -43,17 +45,31 @@ export default {
</button>
<button
v-tooltip
- :aria-label="__('Discard changes')"
- :title="__('Discard changes')"
+ :title="__('More actions')"
type="button"
- class="btn btn-blank"
+ class="btn btn-blank d-flex align-items-center"
data-container="body"
- @click.stop="discardFileChanges(path)"
+ data-boundary="viewport"
+ data-placement="bottom"
+ data-toggle="dropdown"
+ data-display="static"
>
<icon
:size="12"
- name="remove"
+ name="more"
/>
</button>
+ <div class="dropdown-menu dropdown-menu-right">
+ <ul>
+ <li>
+ <button
+ type="button"
+ @click.stop="discardFileChanges(path)"
+ >
+ {{ __('Discard changes') }}
+ </button>
+ </li>
+ </ul>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
index 34b366f63ac..9cec73ec00e 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
@@ -32,8 +32,10 @@ export default {
:aria-label="__('Unstage changes')"
:title="__('Unstage changes')"
type="button"
- class="btn btn-blank"
+ class="btn btn-blank d-flex align-items-center"
data-container="body"
+ data-boundary="viewport"
+ data-placement="bottom"
@click="unstageChange(path)"
>
<icon
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 01df0019fd4..c2c678ff0be 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -93,23 +93,25 @@ export default {
:title="__('Unstaged')"
:key-prefix="$options.stageKeys.unstaged"
:file-list="changedFiles"
- :action-btn-text="__('Stage all')"
+ :action-btn-text="__('Stage all changes')"
:active-file-key="activeFileKey"
- class="is-first"
- icon-name="unstaged"
action="stageAllChanges"
+ action-btn-icon="mobile-issue-close"
item-action-component="stage-button"
+ class="is-first"
+ icon-name="unstaged"
/>
<commit-files-list
:title="__('Staged')"
:key-prefix="$options.stageKeys.staged"
:file-list="stagedFiles"
- :action-btn-text="__('Unstage all')"
+ :action-btn-text="__('Unstage all changes')"
:staged-list="true"
:active-file-key="activeFileKey"
- icon-name="staged"
action="unstageAllChanges"
+ action-btn-icon="history"
item-action-component="unstage-button"
+ icon-name="staged"
/>
</template>
<empty-state
diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js
index e74c4046330..f09930e8158 100644
--- a/app/assets/javascripts/ide/lib/diff/diff_worker.js
+++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js
@@ -1,8 +1,10 @@
import { computeDiff } from './diff';
+// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', (e) => {
const data = e.data;
+ // eslint-disable-next-line no-restricted-globals
self.postMessage({
path: data.path,
changes: computeDiff(data.originalContent, data.newContent),
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index 9f895d49f2e..e35595ab1fd 100644
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
@@ -12,5 +12,6 @@ export const defaultEditorOptions = {
export default [
{
readOnly: model => !!model.file.file_lock,
+ quickSuggestions: model => !(model.language === 'markdown'),
},
];
diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
index 0a1c253c637..fa35c215880 100644
--- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
+++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
@@ -1,6 +1,7 @@
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../utils';
+// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', e => {
const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
@@ -89,6 +90,7 @@ self.addEventListener('message', e => {
return acc;
}, {});
+ // eslint-disable-next-line no-restricted-globals
self.postMessage({
entries,
treeList: sortTree(treeList),
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index cdb75752b4e..bd90d0eaa32 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -91,7 +91,6 @@ export default class IntegrationSettingsForm {
}
}
- /* eslint-disable promise/catch-or-return, no-new */
/**
* Test Integration config
*/
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index e003fb1d127..35eaf21a836 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
+/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, prefer-arrow-callback, max-len, no-unused-vars */
import $ from 'jquery';
import _ from 'underscore';
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index bb8b3d91e40..0140960b367 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
+/* eslint-disable no-new, no-unused-vars, consistent-return, no-else-return */
/* global GitLab */
import $ from 'jquery';
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 5113ac6775d..8c225cd7d91 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
+/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-unused-vars, consistent-return, quotes, max-len */
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index e87a8ed7fea..b6364318537 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -226,7 +226,7 @@
.then(res => res.data)
.then(data => this.checkForSpam(data))
.then((data) => {
- if (location.pathname !== data.web_url) {
+ if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index 8cb0ab22bfb..597c6d69a81 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -38,7 +38,7 @@
},
deleteIssuable() {
// eslint-disable-next-line no-alert
- if (confirm('Issue will be removed! Are you sure?')) {
+ if (window.confirm('Issue will be removed! Are you sure?')) {
this.deleteLoading = true;
eventHub.$emit('delete.issuable');
diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue
index 1c2789f154a..ad0d40faf32 100644
--- a/app/assets/javascripts/issue_show/components/locked_warning.vue
+++ b/app/assets/javascripts/issue_show/components/locked_warning.vue
@@ -2,7 +2,7 @@
export default {
computed: {
currentPath() {
- return location.pathname;
+ return window.location.pathname;
},
},
};
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 8b01024b7d4..c10b1a2b233 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
+/* eslint-disable class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, func-names, max-len */
import $ from 'jquery';
import Sortable from 'sortablejs';
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 7d0ff53f366..dfc3f7a94c8 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread */
+/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty */
/* global Issuable */
/* global ListLabel */
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index dbbf1637a47..9482d131344 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -44,8 +44,8 @@ export default class LazyLoader {
requestAnimationFrame(() => this.checkElementsInView());
}
checkElementsInView() {
- const scrollTop = pageYOffset;
- const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
+ const scrollTop = window.pageYOffset;
+ const visHeight = scrollTop + window.innerHeight + SCROLL_THRESHOLD;
// Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => {
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
index 3873f4528ce..c28ed04f94f 100644
--- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
+++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
@@ -93,7 +93,7 @@ export default class LinkedTabs {
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
- history.replaceState({
+ window.history.replaceState({
url: newState,
}, document.title, newState);
return newState;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index d55d0585031..68f92c7f08a 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,10 +1,14 @@
import $ from 'jquery';
-import Cookies from 'js-cookie';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility';
+import { isObject } from './type_utility';
-export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
+export const getPagePath = (index = 0) => {
+ const page = $('body').attr('data-page') || '';
+
+ return page.split(':')[index];
+};
export const isInGroupsPage = () => getPagePath() === 'groups';
@@ -34,17 +38,18 @@ export const checkPageAndAction = (page, action) => {
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
-export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
-export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
-
-export const ajaxGet = url => axios.get(url, {
- params: { format: 'js' },
- responseType: 'text',
-}).then(({ data }) => {
- $.globalEval(data);
-});
-export const rstrip = (val) => {
+export const ajaxGet = url =>
+ axios
+ .get(url, {
+ params: { format: 'js' },
+ responseType: 'text',
+ })
+ .then(({ data }) => {
+ $.globalEval(data);
+ });
+
+export const rstrip = val => {
if (val) {
return val.replace(/\s+$/, '');
}
@@ -60,7 +65,7 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa
closestSubmit.disable();
}
// eslint-disable-next-line func-names
- return field.on(eventName, function () {
+ return field.on(eventName, function() {
if (rstrip($(this).val()) === '') {
return closestSubmit.disable();
}
@@ -79,7 +84,7 @@ export const handleLocationHash = () => {
const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`);
const fixedTabs = document.querySelector('.js-tabs-affix');
- const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck');
+ const fixedDiffStats = document.querySelector('.js-diff-files-changed');
const fixedNav = document.querySelector('.navbar-gitlab');
let adjustment = 0;
@@ -102,7 +107,7 @@ export const handleLocationHash = () => {
// Check if element scrolled into viewport from above or below
// Courtesy http://stackoverflow.com/a/7557433/414749
-export const isInViewport = (el) => {
+export const isInViewport = el => {
const rect = el.getBoundingClientRect();
return (
@@ -113,13 +118,13 @@ export const isInViewport = (el) => {
);
};
-export const parseUrl = (url) => {
+export const parseUrl = url => {
const parser = document.createElement('a');
parser.href = url;
return parser;
};
-export const parseUrlPathname = (url) => {
+export const parseUrlPathname = url => {
const parsedUrl = parseUrl(url);
// parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
// We have to make sure we always have an absolute path.
@@ -128,10 +133,14 @@ export const parseUrlPathname = (url) => {
// We can trust that each param has one & since values containing & will be encoded
// Remove the first character of search as it is always ?
-export const getUrlParamsArray = () => window.location.search.slice(1).split('&').map((param) => {
- const split = param.split('=');
- return [decodeURI(split[0]), split[1]].join('=');
-});
+export const getUrlParamsArray = () =>
+ window.location.search
+ .slice(1)
+ .split('&')
+ .map(param => {
+ const split = param.split('=');
+ return [decodeURI(split[0]), split[1]].join('=');
+ });
export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
@@ -141,18 +150,28 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
-export const scrollToElement = (element) => {
+export const contentTop = () => {
+ const perfBar = $('#js-peek').height() || 0;
+ const mrTabsHeight = $('.merge-request-tabs').height() || 0;
+ const headerHeight = $('.navbar-gitlab').height() || 0;
+ const diffFilesChanged = $('.js-diff-files-changed').height() || 0;
+
+ return perfBar + mrTabsHeight + headerHeight + diffFilesChanged;
+};
+
+export const scrollToElement = element => {
let $el = element;
if (!(element instanceof $)) {
$el = $(element);
}
const top = $el.offset().top;
- const mrTabsHeight = $('.merge-request-tabs').height() || 0;
- const headerHeight = $('.navbar-gitlab').height() || 0;
- return $('body, html').animate({
- scrollTop: top - mrTabsHeight - headerHeight,
- }, 200);
+ return $('body, html').animate(
+ {
+ scrollTop: top - contentTop(),
+ },
+ 200,
+ );
};
/**
@@ -197,7 +216,10 @@ export const insertText = (target, text) => {
// eslint-disable-next-line no-param-reassign
target.value = newText;
// eslint-disable-next-line no-param-reassign
- target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
+ target.selectionStart = selectionStart + insertedText.length;
+
+ // eslint-disable-next-line no-param-reassign
+ target.selectionEnd = selectionStart + insertedText.length;
// Trigger autosave
target.dispatchEvent(new Event('input'));
@@ -209,7 +231,8 @@ export const insertText = (target, text) => {
};
export const nodeMatchesSelector = (node, selector) => {
- const matches = Element.prototype.matches ||
+ const matches =
+ Element.prototype.matches ||
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
@@ -238,10 +261,10 @@ export const nodeMatchesSelector = (node, selector) => {
this will take in the headers from an API response and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
-export const normalizeHeaders = (headers) => {
+export const normalizeHeaders = headers => {
const upperCaseHeaders = {};
- Object.keys(headers || {}).forEach((e) => {
+ Object.keys(headers || {}).forEach(e => {
upperCaseHeaders[e.toUpperCase()] = headers[e];
});
@@ -252,11 +275,11 @@ export const normalizeHeaders = (headers) => {
this will take in the getAllResponseHeaders result and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
-export const normalizeCRLFHeaders = (headers) => {
+export const normalizeCRLFHeaders = headers => {
const headersObject = {};
const headersArray = headers.split('\n');
- headersArray.forEach((header) => {
+ headersArray.forEach(header => {
const keyValue = header.split(': ');
headersObject[keyValue[0]] = keyValue[1];
});
@@ -292,15 +315,13 @@ export const parseIntPagination = paginationInformation => ({
export const parseQueryStringIntoObject = (query = '') => {
if (query === '') return {};
- return query
- .split('&')
- .reduce((acc, element) => {
- const val = element.split('=');
- Object.assign(acc, {
- [val[0]]: decodeURIComponent(val[1]),
- });
- return acc;
- }, {});
+ return query.split('&').reduce((acc, element) => {
+ const val = element.split('=');
+ Object.assign(acc, {
+ [val[0]]: decodeURIComponent(val[1]),
+ });
+ return acc;
+ }, {});
};
/**
@@ -309,9 +330,13 @@ export const parseQueryStringIntoObject = (query = '') => {
*
* @param {Object} params
*/
-export const objectToQueryString = (params = {}) => Object.keys(params).map(param => `${param}=${params[param]}`).join('&');
+export const objectToQueryString = (params = {}) =>
+ Object.keys(params)
+ .map(param => `${param}=${params[param]}`)
+ .join('&');
-export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
+export const buildUrlWithCurrentLocation = param =>
+ (param ? `${window.location.pathname}${param}` : window.location.pathname);
/**
* Based on the current location and the string parameters provided
@@ -319,7 +344,7 @@ export const buildUrlWithCurrentLocation = param => (param ? `${window.location.
*
* @param {String} param
*/
-export const historyPushState = (newUrl) => {
+export const historyPushState = newUrl => {
window.history.pushState({}, document.title, newUrl);
};
@@ -368,7 +393,7 @@ export const backOff = (fn, timeout = 60000) => {
let timeElapsed = 0;
return new Promise((resolve, reject) => {
- const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
+ const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
const next = () => {
if (timeElapsed < timeout) {
@@ -444,7 +469,8 @@ export const resetFavicon = () => {
};
export const setCiStatusFavicon = pageUrl =>
- axios.get(pageUrl)
+ axios
+ .get(pageUrl)
.then(({ data }) => {
if (data && data.favicon) {
return setFaviconOverlay(data.favicon);
@@ -466,28 +492,38 @@ export const spriteIcon = (icon, className = '') => {
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
*/
-export const convertObjectPropsToCamelCase = (obj = {}) => {
+export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
if (obj === null) {
return {};
}
+ const initial = Array.isArray(obj) ? [] : {};
+
return Object.keys(obj).reduce((acc, prop) => {
const result = acc;
+ const val = obj[prop];
- result[convertToCamelCase(prop)] = obj[prop];
+ if (options.deep && (isObject(val) || Array.isArray(val))) {
+ result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(val, options);
+ } else {
+ result[convertToCamelCase(prop)] = obj[prop];
+ }
return acc;
- }, {});
+ }, initial);
};
-export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
+export const imagePath = imgUrl =>
+ `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
$(selector).on('focusin', function selectOnFocusCallback() {
- $(this).select().one('mouseup', (e) => {
- e.preventDefault();
- });
+ $(this)
+ .select()
+ .one('mouseup', e => {
+ e.preventDefault();
+ });
});
};
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index 914de9de940..6f42382246d 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -1,7 +1,4 @@
-import $ from 'jquery';
-import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie } from './common_utils';
-
-const isVueMRDiscussions = () => isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
+import { isInIssuePage, isInMRPage, isInEpicPage } from './common_utils';
export const addClassIfElementExists = (element, className) => {
if (element) {
@@ -9,4 +6,4 @@ export const addClassIfElementExists = (element, className) => {
}
};
-export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isVueMRDiscussions();
+export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isInMRPage();
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index 973d6119158..305ad3e5e26 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,4 +1,4 @@
-/* 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 */
+/* eslint-disable func-names, no-var, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, max-len */
function notificationGranted(message, opts, onclick) {
var notification;
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index a02c79b787e..f086d962221 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -12,7 +12,7 @@ export function formatRelevantDigits(number) {
let digitsLeft = '';
let relevantDigits = 0;
let formattedNumber = '';
- if (!isNaN(Number(number))) {
+ if (!Number.isNaN(Number(number))) {
digitsLeft = number.toString().split('.')[0];
switch (digitsLeft.length) {
case 1:
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 5a16adea4dc..ce0bc4d40e9 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
+/* eslint-disable func-names, no-var, no-param-reassign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, max-len, consistent-return, no-unused-vars, max-len */
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 5e786ee6935..5f25c6ce1ae 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -58,6 +58,14 @@ export const slugify = str => str.trim().toLowerCase();
export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3)}...`;
/**
+ * Truncate SHA to 8 characters
+ *
+ * @param {String} sha
+ * @returns {String}
+ */
+export const truncateSha = sha => sha.substr(0, 8);
+
+/**
* Capitalizes first character
*
* @param {String} text
@@ -98,3 +106,16 @@ export const convertToSentenceCase = string => {
return splitWord.join(' ');
};
+
+/**
+ * Splits camelCase or PascalCase words
+ * e.g. HelloWorld => Hello World
+ *
+ * @param {*} string
+*/
+export const splitCamelCase = string => (
+ string
+ .replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2')
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
+ .trim()
+);
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index f2323f57455..291655235d5 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */
+/* eslint-disable func-names, no-var, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */
import $ from 'jquery';
@@ -35,7 +35,7 @@ const LineHighlighter = function(options = {}) {
options.highlightLineClass = options.highlightLineClass || 'hll';
options.fileHolderSelector = options.fileHolderSelector || '.file-holder';
options.scrollFileHolder = options.scrollFileHolder || false;
- options.hash = options.hash || location.hash;
+ options.hash = options.hash || window.location.hash;
this.options = options;
this._hash = options.hash;
@@ -142,12 +142,14 @@ LineHighlighter.prototype.highlightLine = function(lineNumber) {
//
// range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) {
- var i, lineNumber, ref, ref1, results;
if (range[1]) {
- results = [];
- for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
+ const results = [];
+ const ref = range[0] <= range[1] ? range : range.reverse();
+
+ for (let lineNumber = range[0]; lineNumber <= ref[1]; lineNumber += 1) {
results.push(this.highlightLine(lineNumber));
}
+
return results;
} else {
return this.highlightLine(range[0]);
@@ -170,7 +172,7 @@ LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
//
// This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) {
- return history.pushState({
+ return window.history.pushState({
url: value
// We're using pushState instead of assigning location.hash directly to
// prevent the page from scrolling on the hashchange event
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
index e4ed8111824..81950515ab4 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */
+/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-param-reassign, max-len */
/* global ace */
import Vue from 'vue';
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
index 57e73e38d88..69208ac2d36 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign, comma-dangle */
+/* eslint-disable no-param-reassign */
import Vue from 'vue';
import actionsMixin from '../mixins/line_conflict_actions';
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index d8222ebec63..7bf2c56dd5d 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */
+/* eslint-disable func-names, no-var, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, comma-dangle, max-len, prefer-arrow-callback */
import $ from 'jquery';
import { __ } from '~/locale';
@@ -49,6 +49,7 @@ MergeRequest.prototype.initTabs = function() {
if (window.mrTabs) {
window.mrTabs.unbindEvents();
}
+
window.mrTabs = new MergeRequestTabs(this.opts);
};
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 65ab41559be..83d326ef68f 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,6 +1,7 @@
/* eslint-disable no-new, class-methods-use-this */
import $ from 'jquery';
+import Vue from 'vue';
import Cookies from 'js-cookie';
import axios from './lib/utils/axios_utils';
import flash from './flash';
@@ -8,6 +9,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils';
+import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { getLocationHash } from './lib/utils/url_utility';
import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
@@ -70,11 +72,13 @@ export default class MergeRequestTabs {
const navbar = document.querySelector('.navbar-gitlab');
const peek = document.getElementById('js-peek');
const paddingTop = 16;
+ this.commitsTab = document.querySelector('.tab-content .commits.tab-pane');
this.diffsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
+ this.eventHub = new Vue();
this.setUrl = setUrl !== undefined ? setUrl : true;
this.setCurrentAction = this.setCurrentAction.bind(this);
@@ -149,7 +153,9 @@ export default class MergeRequestTabs {
this.resetViewContainer();
this.destroyPipelinesView();
} else if (this.isDiffAction(action)) {
- this.loadDiff($target.attr('href'));
+ if (!isInVueNoteablePage()) {
+ this.loadDiff($target.attr('href'));
+ }
if (bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
}
@@ -157,6 +163,7 @@ export default class MergeRequestTabs {
this.expandViewContainer();
}
this.destroyPipelinesView();
+ this.commitsTab.classList.remove('active');
} else if (action === 'pipelines') {
this.resetViewContainer();
this.mountPipelinesView();
@@ -172,6 +179,8 @@ export default class MergeRequestTabs {
if (this.setUrl) {
this.setCurrentAction(action);
}
+
+ this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction());
}
scrollToElement(container) {
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 325fa570f37..6da04020881 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -18,13 +18,13 @@ export default class Milestone {
return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
const $target = $(e.target);
- location.hash = $target.attr('href');
+ window.location.hash = $target.attr('href');
this.loadTab($target);
});
}
// eslint-disable-next-line class-methods-use-this
loadInitialTab() {
- const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
+ const $target = $(`.js-milestone-tabs a[href="${window.location.hash}"]`);
if ($target.length) {
$target.tab('show');
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index d269c45203a..77acba6e355 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
+/* eslint-disable max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* global Issuable */
/* global ListMilestone */
@@ -16,10 +16,10 @@ export default class MilestoneSelect {
typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
- this.init(els, options);
+ MilestoneSelect.init(els, options);
}
- init(els, options) {
+ static init(els, options) {
let $els = $(els);
if (!els) {
@@ -224,7 +224,6 @@ export default class MilestoneSelect {
$selectBox.hide();
$value.css('display', '');
if (data.milestone != null) {
- data.milestone.full_path = this.currentProject.full_path;
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 21934021852..e1c8b6a6d4a 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -139,7 +139,7 @@ export default {
this.updateAspectRatio = true;
},
toggleAspectRatio() {
- this.updatedAspectRatios = this.updatedAspectRatios += 1;
+ this.updatedAspectRatios += 1;
if (this.store.getMetricsCount() === this.updatedAspectRatios) {
this.updateAspectRatio = !this.updateAspectRatio;
this.updatedAspectRatios = 0;
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 20400154100..e5680a0499f 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -154,7 +154,7 @@ export default {
point.x = e.clientX;
point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
- point.x = point.x += 7;
+ point.x += 7;
const firstTimeSeries = this.timeSeries[0];
const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index 282c5c24384..92fe98508ad 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -97,7 +97,7 @@ export default {
? this.deploymentFlagData.seriesIndex
: indexFromCoordinates;
const value = series.values[index] && series.values[index].value;
- if (isNaN(value)) {
+ if (Number.isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 4d3f1f1a7cc..ed3a27dd68b 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -73,7 +73,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
timeSeriesScaleX.ticks(d3.timeMinute, 60);
timeSeriesScaleY.domain(yDom);
- const defined = d => !isNaN(d.value) && d.value != null;
+ const defined = d => !Number.isNaN(d.value) && d.value != null;
const lineFunction = d3
.line()
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index e3c5bf06b3d..3c0c9995cc2 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -1,20 +1,32 @@
+import $ from 'jquery';
import Vue from 'vue';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import initDiffsApp from '../diffs';
import notesApp from '../notes/components/notes_app.vue';
import discussionCounter from '../notes/components/discussion_counter.vue';
-import store from '../notes/stores';
+import store from './stores';
+import MergeRequest from '../merge_request';
export default function initMrNotes() {
+ const mrShowNode = document.querySelector('.merge-request');
+ // eslint-disable-next-line no-new
+ new MergeRequest({
+ action: mrShowNode.dataset.mrAction,
+ });
+
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-mr-discussions',
+ name: 'MergeRequestDiscussions',
components: {
notesApp,
},
+ store,
data() {
- const notesDataset = document.getElementById('js-vue-mr-discussions')
- .dataset;
+ const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
const noteableData = JSON.parse(notesDataset.noteableData);
noteableData.noteableType = notesDataset.noteableType;
+ noteableData.targetType = notesDataset.targetType;
return {
noteableData,
@@ -22,12 +34,42 @@ export default function initMrNotes() {
notesData: JSON.parse(notesDataset.notesData),
};
},
+ computed: {
+ ...mapGetters(['discussionTabCounter']),
+ ...mapState({
+ activeTab: state => state.page.activeTab,
+ }),
+ },
+ watch: {
+ discussionTabCounter() {
+ this.updateDiscussionTabCounter();
+ },
+ },
+ mounted() {
+ this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge');
+ this.setActiveTab(window.mrTabs.getCurrentAction());
+
+ window.mrTabs.eventHub.$on('MergeRequestTabChange', tab => {
+ this.setActiveTab(tab);
+ });
+ $(document).on('visibilitychange', this.updateDiscussionTabCounter);
+ },
+ beforeDestroy() {
+ $(document).off('visibilitychange', this.updateDiscussionTabCounter);
+ },
+ methods: {
+ ...mapActions(['setActiveTab']),
+ updateDiscussionTabCounter() {
+ this.notesCountBadge.text(this.discussionTabCounter);
+ },
+ },
render(createElement) {
return createElement('notes-app', {
props: {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
+ shouldShow: this.activeTab === 'show',
},
});
},
@@ -36,6 +78,7 @@ export default function initMrNotes() {
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-discussion-counter',
+ name: 'DiscussionCounter',
components: {
discussionCounter,
},
@@ -44,4 +87,6 @@ export default function initMrNotes() {
return createElement('discussion-counter');
},
});
+
+ initDiffsApp(store);
}
diff --git a/app/assets/javascripts/mr_notes/stores/actions.js b/app/assets/javascripts/mr_notes/stores/actions.js
new file mode 100644
index 00000000000..426c6a00d5e
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/actions.js
@@ -0,0 +1,7 @@
+import types from './mutation_types';
+
+export default {
+ setActiveTab({ commit }, tab) {
+ commit(types.SET_ACTIVE_TAB, tab);
+ },
+};
diff --git a/app/assets/javascripts/mr_notes/stores/getters.js b/app/assets/javascripts/mr_notes/stores/getters.js
new file mode 100644
index 00000000000..b10e9f9f9f1
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/getters.js
@@ -0,0 +1,5 @@
+export default {
+ isLoggedIn(state, getters) {
+ return !!getters.getUserData.id;
+ },
+};
diff --git a/app/assets/javascripts/mr_notes/stores/index.js b/app/assets/javascripts/mr_notes/stores/index.js
new file mode 100644
index 00000000000..dd2019001db
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import notesModule from '~/notes/stores/modules';
+import diffsModule from '~/diffs/store/modules';
+import mrPageModule from './modules';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ modules: {
+ page: mrPageModule,
+ notes: notesModule,
+ diffs: diffsModule,
+ },
+});
diff --git a/app/assets/javascripts/mr_notes/stores/modules/index.js b/app/assets/javascripts/mr_notes/stores/modules/index.js
new file mode 100644
index 00000000000..660081f76c8
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/modules/index.js
@@ -0,0 +1,12 @@
+import actions from '../actions';
+import getters from '../getters';
+import mutations from '../mutations';
+
+export default {
+ state: {
+ activeTab: null,
+ },
+ actions,
+ getters,
+ mutations,
+};
diff --git a/app/assets/javascripts/mr_notes/stores/mutation_types.js b/app/assets/javascripts/mr_notes/stores/mutation_types.js
new file mode 100644
index 00000000000..105104361cf
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/mutation_types.js
@@ -0,0 +1,3 @@
+export default {
+ SET_ACTIVE_TAB: 'SET_ACTIVE_TAB',
+};
diff --git a/app/assets/javascripts/mr_notes/stores/mutations.js b/app/assets/javascripts/mr_notes/stores/mutations.js
new file mode 100644
index 00000000000..8175aa9488f
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/stores/mutations.js
@@ -0,0 +1,7 @@
+import types from './mutation_types';
+
+export default {
+ [types.SET_ACTIVE_TAB](state, tab) {
+ Object.assign(state, { activeTab: tab });
+ },
+};
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index c7a8aac79df..17370edeb0c 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
+/* eslint-disable func-names, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
import $ from 'jquery';
import Api from './api';
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index bd007c707f2..6a8591692f1 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
+/* eslint-disable func-names, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
import $ from 'jquery';
import { __ } from '../locale';
@@ -112,7 +112,8 @@ export default (function() {
fill: "#444"
});
ref = this.days;
- for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) {
+
+ for (mm = 0, len = ref.length; mm < len; mm += 1) {
day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
// Dates
@@ -285,7 +286,8 @@ export default (function() {
r = this.r;
ref = commit.parents;
results = [];
- for (i = j = 0, len = ref.length; j < len; i = (j += 1)) {
+
+ for (i = 0, len = ref.length; i < len; i += 1) {
parent = ref[i];
parentCommit = this.preparedCommits[parent[0]];
parentY = this.offsetY + this.unitTime * parentCommit.time;
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 40c08ee0ace..41ba5b28a1b 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */
+/* eslint-disable func-names, no-var, one-var, max-len, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */
import $ from 'jquery';
import RefSelectDropdown from './ref_select_dropdown';
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index a2f0a44863f..17ec20f1cc1 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
+/* eslint-disable no-var, no-return-assign */
export default class NewCommitForm {
constructor(form) {
this.form = form;
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 55f1d0b496c..2f752d2dcd6 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,10 +1,8 @@
-/* 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, class-methods-use-this */
+/* eslint-disable no-restricted-properties, func-names, no-var, wrap-iife, camelcase,
+no-unused-expressions, 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, vars-on-top,
+no-unused-vars, no-shadow, no-useless-escape, class-methods-use-this */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
@@ -32,7 +30,7 @@ import {
getPagePath,
scrollToElement,
isMetaKey,
- hasVueMRDiscussionsCookie,
+ isInMRPage,
} from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
@@ -47,21 +45,9 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export default class Notes {
- static initialize(
- notes_url,
- note_ids,
- last_fetched_at,
- view,
- enableGFM = true,
- ) {
+ static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
if (!this.instance) {
- this.instance = new Notes(
- notes_url,
- note_ids,
- last_fetched_at,
- view,
- enableGFM,
- );
+ this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM);
}
}
@@ -104,9 +90,7 @@ export default class Notes {
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
- this.$wrapperEl = hasVueMRDiscussionsCookie()
- ? $(document).find('.diffs')
- : $(document);
+ this.$wrapperEl = isInMRPage() ? $(document).find('.diffs') : $(document);
this.cleanBinding();
this.addBinding();
this.setPollingInterval();
@@ -146,55 +130,27 @@ export default class Notes {
// Reopen and close actions for Issue/MR combined with note form submit
this.$wrapperEl.on('click', '.js-comment-submit-button', this.postComment);
this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment);
- this.$wrapperEl.on(
- 'keyup input',
- '.js-note-text',
- this.updateTargetButtons,
- );
+ this.$wrapperEl.on('keyup input', '.js-note-text', this.updateTargetButtons);
// resolve a discussion
this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment);
// remove a note (in general)
this.$wrapperEl.on('click', '.js-note-delete', this.removeNote);
// delete note attachment
- this.$wrapperEl.on(
- 'click',
- '.js-note-attachment-delete',
- this.removeAttachment,
- );
+ this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment);
// reset main target form when clicking discard
this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm);
// update the file name when an attachment is selected
- this.$wrapperEl.on(
- 'change',
- '.js-note-attachment-input',
- this.updateFormAttachment,
- );
+ this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
// reply to diff/discussion notes
- this.$wrapperEl.on(
- 'click',
- '.js-discussion-reply-button',
- this.onReplyToDiscussionNote,
- );
+ this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
- this.$wrapperEl.on(
- 'click',
- '.js-add-image-diff-note-button',
- this.onAddImageDiffNote,
- );
+ this.$wrapperEl.on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
// hide diff note form
- this.$wrapperEl.on(
- 'click',
- '.js-close-discussion-note-form',
- this.cancelDiscussionForm,
- );
+ this.$wrapperEl.on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list
- this.$wrapperEl.on(
- 'click',
- '.system-note-commit-list-toggler',
- this.toggleCommitList,
- );
+ this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this));
@@ -205,16 +161,8 @@ export default class Notes {
this.$wrapperEl.on('issuable:change', this.refresh);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote);
- this.$wrapperEl.on(
- 'ajax:success',
- '.js-discussion-note-form',
- this.addDiscussionNote,
- );
- this.$wrapperEl.on(
- 'ajax:success',
- '.js-main-target-form',
- this.resetMainTargetForm,
- );
+ this.$wrapperEl.on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
+ this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
this.$wrapperEl.on(
'ajax:complete',
'.js-main-target-form',
@@ -224,8 +172,6 @@ export default class Notes {
this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText);
// When the URL fragment/hash has changed, `#note_xxx`
$(window).on('hashchange', this.onHashChange);
- this.boundGetContent = this.getContent.bind(this);
- document.addEventListener('refreshLegacyNotes', this.boundGetContent);
}
cleanBinding() {
@@ -249,21 +195,14 @@ export default class Notes {
this.$wrapperEl.off('ajax:success', '.js-main-target-form');
this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
- document.removeEventListener('refreshLegacyNotes', this.boundGetContent);
$(window).off('hashchange', this.onHashChange);
}
static initCommentTypeToggle(form) {
- const dropdownTrigger = form.querySelector(
- '.js-comment-type-dropdown .dropdown-toggle',
- );
- const dropdownList = form.querySelector(
- '.js-comment-type-dropdown .dropdown-menu',
- );
+ const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle');
+ const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu');
const noteTypeInput = form.querySelector('#note_type');
- const submitButton = form.querySelector(
- '.js-comment-type-dropdown .js-comment-submit-button',
- );
+ const submitButton = form.querySelector('.js-comment-type-dropdown .js-comment-submit-button');
const closeButton = form.querySelector('.js-note-target-close');
const reopenButton = form.querySelector('.js-note-target-reopen');
@@ -299,9 +238,7 @@ export default class Notes {
return;
}
myLastNote = $(
- `li.note[data-author-id='${
- gon.current_user_id
- }'][data-editable]:last`,
+ `li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`,
$textarea.closest('.note, .notes_holder, #notes'),
);
if (myLastNote.length) {
@@ -315,7 +252,7 @@ export default class Notes {
if (discussionNoteForm.length) {
if ($textarea.val() !== '') {
if (
- !confirm('Are you sure you want to cancel creating this comment?')
+ !window.confirm('Are you sure you want to cancel creating this comment?')
) {
return;
}
@@ -329,7 +266,7 @@ export default class Notes {
newText = $textarea.val();
if (originalText !== newText) {
if (
- !confirm('Are you sure you want to cancel editing this comment?')
+ !window.confirm('Are you sure you want to cancel editing this comment?')
) {
return;
}
@@ -398,8 +335,7 @@ export default class Notes {
if (shouldReset == null) {
shouldReset = true;
}
- nthInterval =
- this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
+ nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
if (shouldReset) {
this.pollingInterval = this.basePollingInterval;
} else if (this.pollingInterval < nthInterval) {
@@ -420,10 +356,7 @@ export default class Notes {
loadAwardsHandler()
.then(awardsHandler => {
- awardsHandler.addAwardToEmojiBar(
- votesBlock,
- noteEntity.commands_changes.emoji_award,
- );
+ awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
awardsHandler.scrollToAwards();
})
.catch(() => {
@@ -473,17 +406,10 @@ export default class Notes {
if (!noteEntity.valid) {
if (noteEntity.errors && noteEntity.errors.commands_only) {
- if (
- noteEntity.commands_changes &&
- Object.keys(noteEntity.commands_changes).length > 0
- ) {
+ if (noteEntity.commands_changes && Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
- this.addFlash(
- noteEntity.errors.commands_only,
- 'notice',
- this.parentTimeline.get(0),
- );
+ this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0));
this.refresh();
}
return;
@@ -491,7 +417,7 @@ export default class Notes {
const $note = $notesList.find(`#note_${noteEntity.id}`);
if (Notes.isNewNote(noteEntity, this.note_ids)) {
- if (hasVueMRDiscussionsCookie()) {
+ if (isInMRPage()) {
return;
}
@@ -519,8 +445,7 @@ export default class Notes {
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
const isTextareaUntouched =
- currentContent === initialContent ||
- currentContent === sanitizedNoteNote;
+ currentContent === initialContent || currentContent === sanitizedNoteNote;
if (isEditing && isTextareaUntouched) {
$textarea.val(noteEntity.note);
@@ -533,8 +458,6 @@ export default class Notes {
this.setupNewNote($updatedNote);
}
}
-
- Notes.refreshVueNotes();
}
isParallelView() {
@@ -552,13 +475,7 @@ export default class Notes {
}
this.note_ids.push(noteEntity.id);
- form =
- $form ||
- $(
- `.js-discussion-note-form[data-discussion-id="${
- noteEntity.discussion_id
- }"]`,
- );
+ form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
row =
form.length || !noteEntity.discussion_line_code
? form.closest('tr')
@@ -574,9 +491,7 @@ export default class Notes {
.first()
.find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
- discussionContainer = $(
- `.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
- );
+ discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes');
}
@@ -584,18 +499,12 @@ export default class Notes {
if (noteEntity.diff_discussion_html) {
var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
- if (
- !this.isParallelView() ||
- row.hasClass('js-temp-notes-holder') ||
- noteEntity.on_image
- ) {
+ if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
// insert the note and the reply button after the temp row
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
@@ -608,29 +517,15 @@ export default class Notes {
.find(contentContainerClass + ' .content')
.append($notes.closest('.content').children());
}
- }
- // Init discussion on 'Discussion' page if it is merge request page
- const page = $('body').attr('data-page');
- if (
- (page && page.indexOf('projects:merge_request') !== -1) ||
- !noteEntity.diff_discussion_html
- ) {
- if (!hasVueMRDiscussionsCookie()) {
- Notes.animateAppendNote(
- noteEntity.discussion_html,
- $('.main-notes-list'),
- );
- }
+ } else {
+ Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
}
} else {
// append new note to all matching discussions
Notes.animateAppendNote(noteEntity.html, discussionContainer);
}
- if (
- typeof gl.diffNotesCompileComponents !== 'undefined' &&
- noteEntity.discussion_resolvable
- ) {
+ if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
@@ -784,6 +679,7 @@ export default class Notes {
}
updateNoteError($parentTimeline) {
+ // eslint-disable-next-line no-new
new Flash(
'Your comment could not be updated! Please check your network connection and try again.',
);
@@ -939,9 +835,7 @@ export default class Notes {
form.removeClass('current-note-edit-form');
form.find('.js-finish-edit-warning').hide();
// Replace markdown textarea text with original note text.
- return form
- .find('.js-note-text')
- .val(form.find('form.edit-note').data('originalNote'));
+ return form.find('.js-note-text').val(form.find('form.edit-note').data('originalNote'));
}
/**
@@ -989,21 +883,15 @@ export default class Notes {
// The notes tr can contain multiple lists of notes, like on the parallel diff
// notesTr does not exist for image diffs
- if (
- notesTr.find('.discussion-notes').length > 1 ||
- notesTr.length === 0
- ) {
+ if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
const $diffFile = $notes.closest('.diff-file');
if ($diffFile.length > 0) {
- const removeBadgeEvent = new CustomEvent(
- 'removeBadge.imageDiff',
- {
- detail: {
- // badgeNumber's start with 1 and index starts with 0
- badgeNumber: $notes.index() + 1,
- },
+ const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
+ detail: {
+ // badgeNumber's start with 1 and index starts with 0
+ badgeNumber: $notes.index() + 1,
},
- );
+ });
$diffFile[0].dispatchEvent(removeBadgeEvent);
}
@@ -1017,7 +905,6 @@ export default class Notes {
})(this),
);
- Notes.refreshVueNotes();
Notes.checkMergeRequestStatus();
return this.updateNotesCount(-1);
}
@@ -1033,7 +920,7 @@ export default class Notes {
$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();
+ return $note.find('.diffs .current-note-edit-form').remove();
}
/**
@@ -1107,9 +994,7 @@ export default class Notes {
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
- form
- .removeClass('js-main-target-form')
- .addClass('discussion-form js-discussion-note-form');
+ form.removeClass('js-main-target-form').addClass('discussion-form js-discussion-note-form');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
var $commentBtn = form.find('comment-and-resolve-btn');
@@ -1119,9 +1004,7 @@ export default class Notes {
}
form.find('.js-note-text').focus();
- form
- .find('.js-comment-resolve-button')
- .attr('data-discussion-id', discussionID);
+ form.find('.js-comment-resolve-button').attr('data-discussion-id', discussionID);
}
/**
@@ -1154,9 +1037,7 @@ export default class Notes {
// Setup comment form
let newForm;
- const $noteContainer = $link
- .closest('.diff-viewer')
- .find('.note-container');
+ const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
const $form = $noteContainer.find('> .discussion-form');
if ($form.length === 0) {
@@ -1225,9 +1106,7 @@ export default class Notes {
notesContent = targetRow.find(notesContentSelector);
addForm = true;
} else {
- const isCurrentlyShown = targetRow
- .find('.content:not(:empty)')
- .is(':visible');
+ const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible');
const isForced = forceShow === true || forceShow === false;
const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
@@ -1392,9 +1271,7 @@ export default class Notes {
if ($note.find('.js-conflict-edit-warning').length === 0) {
const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
This comment has changed since you started editing, please review the
- <a href="#note_${
- noteEntity.id
- }" target="_blank" rel="noopener noreferrer">
+ <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer">
updated comment
</a>
to ensure information is not lost
@@ -1404,15 +1281,13 @@ export default class Notes {
}
updateNotesCount(updateCount) {
- return this.notesCountBadge.text(
- parseInt(this.notesCountBadge.text(), 10) + updateCount,
- );
+ return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
}
static renderPlaceholderComponent($container) {
const el = $container.find('.js-code-placeholder').get(0);
+ // eslint-disable-next-line no-new
new Vue({
- // eslint-disable-line no-new
el,
components: {
SkeletonLoadingContainer,
@@ -1483,9 +1358,7 @@ export default class Notes {
toggleCommitList(e) {
const $element = $(e.currentTarget);
- const $closestSystemCommitList = $element.siblings(
- '.system-note-commit-list',
- );
+ const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
$element
.find('.fa')
@@ -1518,9 +1391,7 @@ export default class Notes {
$systemNote.find('.note-text').addClass('system-note-commit-list');
$systemNote.find('.system-note-commit-list-toggler').show();
} else {
- $systemNote
- .find('.note-text')
- .addClass('system-note-commit-list hide-shade');
+ $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade');
}
});
}
@@ -1591,10 +1462,6 @@ export default class Notes {
return $updatedNote;
}
- static refreshVueNotes() {
- document.dispatchEvent(new CustomEvent('refreshVueNotes'));
- }
-
/**
* Get data from Form attributes to use for saving/submitting comment.
*/
@@ -1753,15 +1620,8 @@ export default class Notes {
.attr('id') === 'discussion';
const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form');
- const isDiscussionResolve = $submitBtn.hasClass(
- 'js-comment-resolve-button',
- );
- const {
- formData,
- formContent,
- formAction,
- formContentOriginal,
- } = this.getFormData($form);
+ const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
+ const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
let noteUniqueId;
let systemNoteUniqueId;
let hasQuickActions = false;
@@ -1827,7 +1687,6 @@ export default class Notes {
$closeBtn.text($closeBtn.data('originalText'));
- /* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
return axios
.post(`${formAction}?html=true`, formData)
@@ -1849,9 +1708,7 @@ export default class Notes {
// Reset cached commands list when command is applied
if (hasQuickActions) {
- $form
- .find('textarea.js-note-text')
- .trigger('clear-commands-cache.atwho');
+ $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
}
// Clear previous form errors
@@ -1896,12 +1753,8 @@ export default class Notes {
// append flash-container to the Notes list
if ($notesContainer.length) {
- $notesContainer.append(
- '<div class="flash-container" style="display: none;"></div>',
- );
+ $notesContainer.append('<div class="flash-container" style="display: none;"></div>');
}
-
- Notes.refreshVueNotes();
} else if (isMainForm) {
// Check if this was main thread comment
// Show final note element on UI and perform form and action buttons cleanup
@@ -1935,9 +1788,7 @@ export default class Notes {
// Show form again on UI on failure
if (isDiscussionForm && $notesContainer.length) {
- const replyButton = $notesContainer
- .parent()
- .find('.js-discussion-reply-button');
+ const replyButton = $notesContainer.parent().find('.js-discussion-reply-button');
this.replyToDiscussionNote(replyButton[0]);
$form = $notesContainer.parent().find('form');
}
@@ -1980,16 +1831,13 @@ export default class Notes {
// Show updated comment content temporarily
$noteBodyText.html(formContent);
- $editingNote
- .removeClass('is-editing fade-in-full')
- .addClass('being-posted fade-in-half');
+ $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half');
$editingNote
.find('.note-headline-meta a')
.html(
'<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>',
);
- /* eslint-disable promise/catch-or-return */
// Make request to update comment on server
axios
.post(`${formAction}?html=true`, formData)
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index ad6dd3d9a09..c6a524f68cb 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -7,10 +7,7 @@ import { __, sprintf } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
-import {
- capitalizeFirstCharacter,
- convertToCamelCase,
-} from '../../lib/utils/text_utility';
+import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase } from '../../lib/utils/text_utility';
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
@@ -56,21 +53,23 @@ export default {
]),
...mapState(['isToggleStateButtonLoading']),
noteableDisplayName() {
- return this.noteableType.replace(/_/g, ' ');
+ return splitCamelCase(this.noteableType).toLowerCase();
},
isLoggedIn() {
return this.getUserData.id;
},
commentButtonTitle() {
- return this.noteType === constants.COMMENT
- ? 'Comment'
- : 'Start discussion';
+ return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
+ },
+ startDiscussionDescription() {
+ let text = 'Discuss a specific suggestion or question';
+ if (this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE) {
+ text += ' that needs to be resolved';
+ }
+ return `${text}.`;
},
isOpen() {
- return (
- this.openState === constants.OPENED ||
- this.openState === constants.REOPENED
- );
+ return this.openState === constants.OPENED || this.openState === constants.REOPENED;
},
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
@@ -117,6 +116,9 @@ export default {
endpoint() {
return this.getNoteableData.create_note_path;
},
+ issuableTypeTitle() {
+ return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE ? 'merge request' : 'issue';
+ },
},
watch: {
note(newNote) {
@@ -129,9 +131,7 @@ export default {
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
- this.toggleIssueLocalState(
- isClosed ? constants.CLOSED : constants.REOPENED,
- );
+ this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED);
});
this.initAutoSave();
@@ -168,6 +168,7 @@ export default {
noteable_id: this.getNoteableData.id,
note: this.note,
},
+ merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
},
};
@@ -227,9 +228,7 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false);
Flash(
sprintf(
- __(
- 'Something went wrong while closing the %{issuable}. Please try again later',
- ),
+ __('Something went wrong while closing the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName },
),
);
@@ -242,9 +241,7 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false);
Flash(
sprintf(
- __(
- 'Something went wrong while reopening the %{issuable}. Please try again later',
- ),
+ __('Something went wrong while reopening the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName },
),
);
@@ -281,9 +278,7 @@ Please check your network connection and try again.`;
},
initAutoSave() {
if (this.isLoggedIn) {
- const noteableType = capitalizeFirstCharacter(
- convertToCamelCase(this.noteableType),
- );
+ const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
this.autosave = new Autosave($(this.$refs.textarea), [
'Note',
@@ -312,8 +307,8 @@ Please check your network connection and try again.`;
<div>
<note-signed-out-widget v-if="!isLoggedIn" />
<discussion-locked-widget
- v-else-if="isLocked(getNoteableData) && !canCreateNote"
- issuable-type="issue"
+ v-else-if="!canCreateNote"
+ :issuable-type="issuableTypeTitle"
/>
<ul
v-else-if="canCreateNote"
@@ -357,7 +352,7 @@ Please check your network connection and try again.`;
v-model="note"
:disabled="isSubmitting"
name="note[note]"
- class="note-textarea js-vue-comment-form
+ class="note-textarea js-vue-comment-form js-note-text
js-gfm-input js-autosize markdown-area js-vue-textarea"
data-supports-quick-actions="true"
aria-label="Description"
@@ -423,7 +418,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<div class="description">
<strong>Start discussion</strong>
<p>
- Discuss a specific suggestion or question.
+ {{ startDiscussionDescription }}
</p>
</div>
</button>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index cafb28910eb..d321f2ce15e 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,13 +1,15 @@
<script>
-import $ from 'jquery';
-import syntaxHighlight from '~/syntax_highlight';
+import { mapState, mapActions } from 'vuex';
import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import DiffFileHeader from './diff_file_header.vue';
+import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
export default {
components: {
DiffFileHeader,
+ SkeletonLoadingContainer,
},
props: {
discussion: {
@@ -15,7 +17,24 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ error: false,
+ };
+ },
computed: {
+ ...mapState({
+ noteableData: state => state.notes.noteableData,
+ }),
+ hasTruncatedDiffLines() {
+ return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0;
+ },
+ isDiscussionsExpanded() {
+ return true; // TODO: @fatihacet - Fix this.
+ },
+ isCollapsed() {
+ return this.diffFile.collapsed || false;
+ },
isImageDiff() {
return !this.diffFile.text;
},
@@ -23,36 +42,46 @@ export default {
const { text } = this.diffFile;
return text ? 'text-file' : 'js-image-file';
},
- diffRows() {
- return $(this.discussion.truncatedDiffLines);
- },
diffFile() {
- return convertObjectPropsToCamelCase(this.discussion.diffFile);
+ return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true });
},
imageDiffHtml() {
return this.discussion.imageDiffHtml;
},
+ currentUser() {
+ return this.noteableData.current_user;
+ },
+ userColorScheme() {
+ return window.gon.user_color_scheme;
+ },
+ normalizedDiffLines() {
+ const lines = this.discussion.truncatedDiffLines || [];
+
+ return lines.map(line => trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line)));
+ },
},
mounted() {
if (this.isImageDiff) {
const canCreateNote = false;
const renderCommentBadge = true;
- imageDiffHelper.initImageDiff(
- this.$refs.fileHolder,
- canCreateNote,
- renderCommentBadge,
- );
- } else {
- const fileHolder = $(this.$refs.fileHolder);
- this.$nextTick(() => {
- syntaxHighlight(fileHolder);
- });
+ imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
+ } else if (!this.hasTruncatedDiffLines) {
+ this.fetchDiff();
}
},
methods: {
+ ...mapActions(['fetchDiscussionDiffLines']),
rowTag(html) {
return html.outerHTML ? 'tr' : 'template';
},
+ fetchDiff() {
+ this.error = false;
+ this.fetchDiscussionDiffLines(this.discussion)
+ .then(this.highlight)
+ .catch(() => {
+ this.error = true;
+ });
+ },
},
};
</script>
@@ -63,23 +92,59 @@ export default {
:class="diffFileClass"
class="diff-file file-holder"
>
- <div class="js-file-title file-title file-title-flex-parent">
- <diff-file-header
- :diff-file="diffFile"
- />
- </div>
+ <diff-file-header
+ :diff-file="diffFile"
+ :current-user="currentUser"
+ :discussions-expanded="isDiscussionsExpanded"
+ :expanded="!isCollapsed"
+ />
<div
v-if="diffFile.text"
- class="diff-content code js-syntax-highlight"
+ :class="userColorScheme"
+ class="diff-content code"
>
<table>
- <component
- v-for="(html, index) in diffRows"
- :is="rowTag(html)"
- :class="html.className"
- :key="index"
- v-html="html.outerHTML"
- />
+ <tr
+ v-for="line in normalizedDiffLines"
+ :key="line.lineCode"
+ class="line_holder"
+ >
+ <td class="diff-line-num old_line">{{ line.oldLine }}</td>
+ <td class="diff-line-num new_line">{{ line.newLine }}</td>
+ <td
+ :class="line.type"
+ class="line_content"
+ v-html="line.richText"
+ >
+ </td>
+ </tr>
+ <tr
+ v-if="!hasTruncatedDiffLines"
+ class="line_holder line-holder-placeholder"
+ >
+ <td class="old_line diff-line-num"></td>
+ <td class="new_line diff-line-num"></td>
+ <td
+ v-if="error"
+ class="js-error-lazy-load-diff diff-loading-error-block"
+ >
+ Unable to load the diff
+ <button
+ class="btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button"
+ @click="fetchDiff"
+ >
+ Try again
+ </button>
+ </td>
+ <td
+ v-else
+ class="line_content js-success-lazy-load"
+ >
+ <span></span>
+ <skeleton-loading-container />
+ <span></span>
+ </td>
+ </tr>
<tr class="notes_holder">
<td
class="notes_line"
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 68e17ac8055..6385b75e557 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg';
@@ -48,10 +48,14 @@ export default {
this.nextDiscussionSvg = nextDiscussionSvg;
},
methods: {
- jumpToFirstDiscussion() {
- const el = document.querySelector(
- `[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`,
- );
+ ...mapActions(['expandDiscussion']),
+ jumpToFirstUnresolvedDiscussion() {
+ const discussionId = this.firstUnresolvedDiscussionId;
+ if (!discussionId) {
+ return;
+ }
+
+ const el = document.querySelector(`[data-discussion-id="${discussionId}"]`);
const activeTab = window.mrTabs.currentAction;
if (activeTab === 'commits' || activeTab === 'pipelines') {
@@ -59,6 +63,7 @@ export default {
}
if (el) {
+ this.expandDiscussion({ discussionId });
scrollToElement(el);
}
},
@@ -97,7 +102,7 @@ export default {
<a
v-tooltip
:href="resolveAllDiscussionsIssuePath"
- title="Resolve all discussions in new issue"
+ :title="s__('Resolve all discussions in new issue')"
data-container="body"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn">
<span v-html="mrIssueSvg"></span>
@@ -112,7 +117,7 @@ export default {
title="Jump to first unresolved discussion"
data-container="body"
class="btn btn-default discussion-next-btn"
- @click="jumpToFirstDiscussion">
+ @click="jumpToFirstUnresolvedDiscussion">
<span v-html="nextDiscussionSvg"></span>
</button>
</div>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 0bf4258a257..cdbbb342331 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -27,6 +27,10 @@ export default {
type: Number,
required: true,
},
+ noteUrl: {
+ type: String,
+ required: true,
+ },
accessLevel: {
type: String,
required: false,
@@ -48,6 +52,11 @@ export default {
type: Boolean,
required: true,
},
+ canResolve: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
resolvable: {
type: Boolean,
required: false,
@@ -125,7 +134,7 @@ export default {
{{ accessLevel }}
</span>
<div
- v-if="resolvable"
+ v-if="canResolve"
class="note-actions-item">
<button
v-tooltip
@@ -216,6 +225,15 @@ export default {
Report as abuse
</a>
</li>
+ <li>
+ <button
+ :data-clipboard-text="noteUrl"
+ type="button"
+ css-class="btn-default btn-transparent"
+ >
+ Copy link
+ </button>
+ </li>
<li v-if="canEdit">
<button
class="btn btn-transparent js-note-delete js-note-delete"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 864edcd2ec6..d2db68df98e 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -40,7 +40,7 @@ export default {
this.initTaskList();
if (this.isEditing) {
- this.initAutoSave(this.note.noteable_type);
+ this.initAutoSave(this.note);
}
},
updated() {
@@ -49,7 +49,7 @@ export default {
if (this.isEditing) {
if (!this.autosave) {
- this.initAutoSave(this.note.noteable_type);
+ this.initAutoSave(this.note);
} else {
this.setAutoSave();
}
@@ -72,7 +72,7 @@ export default {
this.$emit('handleFormUpdate', note, parentElement, callback);
},
formCancelHandler(shouldConfirm, isDirty) {
- this.$emit('cancelFormEdition', shouldConfirm, isDirty);
+ this.$emit('cancelForm', shouldConfirm, isDirty);
},
},
};
@@ -93,7 +93,7 @@ export default {
:note-body="noteBody"
:note-id="note.id"
@handleFormUpdate="handleFormUpdate"
- @cancelFormEdition="formCancelHandler"
+ @cancelForm="formCancelHandler"
/>
<textarea
v-if="canEdit"
@@ -105,6 +105,7 @@ export default {
:edited-at="note.last_edited_at"
:edited-by="note.last_edited_by"
action-text="Edited"
+ class="note_edited_ago"
/>
<note-awards-list
v-if="note.award_emoji.length"
diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue
index 2dc39d1a186..391bb2ae179 100644
--- a/app/assets/javascripts/notes/components/note_edited_text.vue
+++ b/app/assets/javascripts/notes/components/note_edited_text.vue
@@ -11,14 +11,20 @@ export default {
type: String,
required: true,
},
+ actionDetailText: {
+ type: String,
+ required: false,
+ default: '',
+ },
editedAt: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
editedBy: {
type: Object,
required: false,
- default: () => ({}),
+ default: null,
},
className: {
type: String,
@@ -33,13 +39,14 @@ export default {
<div :class="className">
{{ actionText }}
<template v-if="editedBy">
- {{ s__('ByAuthor|by') }}
+ by
<a
:href="editedBy.path"
class="js-vue-author author_link">
{{ editedBy.name }}
</a>
</template>
+ {{ actionDetailText }}
<time-ago-tooltip
:time="editedAt"
tooltip-placement="bottom"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 7254ef3357d..a62696b39b4 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -29,7 +29,7 @@ export default {
required: false,
default: 'Save comment',
},
- note: {
+ discussion: {
type: Object,
required: false,
default: () => ({}),
@@ -38,6 +38,11 @@ export default {
type: Boolean,
required: true,
},
+ lineCode: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -66,9 +71,7 @@ export default {
return this.getNotesDataByProp('markdownDocsPath');
},
quickActionsDocsPath() {
- return !this.isEditing
- ? this.getNotesDataByProp('quickActionsDocsPath')
- : undefined;
+ return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined;
},
currentUserId() {
return this.getUserDataByProp('id');
@@ -95,24 +98,17 @@ export default {
const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true;
- this.$emit(
- 'handleFormUpdate',
- this.updatedNoteBody,
- this.$refs.editNoteForm,
- () => {
- this.isSubmitting = false;
+ this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => {
+ this.isSubmitting = false;
- if (shouldResolve) {
- this.resolveHandler(beforeSubmitDiscussionState);
- }
- },
- );
+ if (shouldResolve) {
+ this.resolveHandler(beforeSubmitDiscussionState);
+ }
+ });
},
editMyLastNote() {
if (this.updatedNoteBody === '') {
- const lastNoteInDiscussion = this.getDiscussionLastNote(
- this.updatedNoteBody,
- );
+ const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion);
if (lastNoteInDiscussion) {
eventHub.$emit('enterEditMode', {
@@ -123,11 +119,7 @@ export default {
},
cancelHandler(shouldConfirm = false) {
// Sends information about confirm message and if the textarea has changed
- this.$emit(
- 'cancelFormEdition',
- shouldConfirm,
- this.noteBody !== this.updatedNoteBody,
- );
+ this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody);
},
},
};
@@ -136,7 +128,7 @@ export default {
<template>
<div
ref="editNoteForm"
- class="note-edit-form current-note-edit-form">
+ class="note-edit-form current-note-edit-form js-discussion-note-form">
<div
v-if="conflictWhileEditing"
class="js-conflict-edit-warning alert alert-danger">
@@ -150,7 +142,10 @@ export default {
to ensure information is not lost.
</div>
<div class="flash-container timeline-content"></div>
- <form class="edit-note common-note-form js-quick-submit gfm-form">
+ <form
+ :data-line-code="lineCode"
+ class="edit-note common-note-form js-quick-submit gfm-form"
+ >
<issue-warning
v-if="hasWarning(getNoteableData)"
@@ -170,7 +165,7 @@ export default {
:data-supports-quick-actions="!isEditing"
v-model="updatedNoteBody"
name="note[note]"
- class="note-textarea js-gfm-input
+ class="note-textarea js-gfm-input js-note-text
js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
@@ -184,19 +179,19 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
<button
:disabled="isDisabled"
type="button"
- class="js-vue-issue-save btn btn-save"
+ class="js-vue-issue-save btn btn-save js-comment-button "
@click="handleUpdate()">
{{ saveButtonTitle }}
</button>
<button
- v-if="note.resolvable"
+ v-if="discussion.resolvable"
class="btn btn-nr btn-default append-right-10 js-comment-resolve-button"
@click.prevent="handleUpdate(true)"
>
{{ resolveButtonTitle }}
</button>
<button
- class="btn btn-cancel note-edit-cancel"
+ class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
type="button"
@click="cancelHandler()">
Cancel
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index ffe3ba9c805..ee3580895df 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -20,11 +20,6 @@ export default {
required: false,
default: '',
},
- actionTextHtml: {
- type: String,
- required: false,
- default: '',
- },
noteId: {
type: Number,
required: true,
@@ -88,10 +83,8 @@ export default {
<template v-if="actionText">
{{ actionText }}
</template>
- <span
- v-if="actionTextHtml"
- class="system-note-message"
- v-html="actionTextHtml">
+ <span class="system-note-message">
+ <slot></slot>
</span>
<span class="system-note-separator">
&middot;
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index f9f5041a9f9..bee635398b3 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,7 +1,11 @@
<script>
+import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionsSvg from 'icons/_next_discussion.svg';
+import { convertObjectPropsToCamelCase, scrollToElement } from '~/lib/utils/common_utils';
+import { truncateSha } from '~/lib/utils/text_utility';
+import systemNote from '~/vue_shared/components/notes/system_note.vue';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -17,9 +21,9 @@ import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import tooltip from '../../vue_shared/directives/tooltip';
-import { scrollToElement } from '../../lib/utils/common_utils';
export default {
+ name: 'NoteableDiscussion',
components: {
noteableNote,
diffWithNote,
@@ -30,16 +34,32 @@ export default {
noteForm,
placeholderNote,
placeholderSystemNote,
+ systemNote,
},
directives: {
tooltip,
},
mixins: [autosave, noteable, resolvable],
props: {
- note: {
+ discussion: {
type: Object,
required: true,
},
+ renderHeader: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ renderDiffFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ alwaysExpanded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -53,19 +73,27 @@ export default {
'getNoteableData',
'discussionCount',
'resolvedDiscussionCount',
+ 'allDiscussions',
'unresolvedDiscussions',
]),
- discussion() {
+ transformedDiscussion() {
return {
- ...this.note.notes[0],
- truncatedDiffLines: this.note.truncated_diff_lines,
- diffFile: this.note.diff_file,
- diffDiscussion: this.note.diff_discussion,
- imageDiffHtml: this.note.image_diff_html,
+ ...this.discussion.notes[0],
+ truncatedDiffLines: this.discussion.truncated_diff_lines || [],
+ truncatedDiffLinesPath: this.discussion.truncated_diff_lines_path,
+ diffFile: this.discussion.diff_file,
+ diffDiscussion: this.discussion.diff_discussion,
+ imageDiffHtml: this.discussion.image_diff_html,
+ active: this.discussion.active,
+ discussionPath: this.discussion.discussion_path,
+ resolved: this.discussion.resolved,
+ resolvedBy: this.discussion.resolved_by,
+ resolvedByPush: this.discussion.resolved_by_push,
+ resolvedAt: this.discussion.resolved_at,
};
},
author() {
- return this.discussion.author;
+ return this.transformedDiscussion.author;
},
canReply() {
return this.getNoteableData.current_user.can_create_note;
@@ -74,7 +102,7 @@ export default {
return this.getNoteableData.create_note_path;
},
lastUpdatedBy() {
- const { notes } = this.note;
+ const { notes } = this.discussion;
if (notes.length > 1) {
return notes[notes.length - 1].author;
@@ -83,7 +111,7 @@ export default {
return null;
},
lastUpdatedAt() {
- const { notes } = this.note;
+ const { notes } = this.discussion;
if (notes.length > 1) {
return notes[notes.length - 1].created_at;
@@ -91,27 +119,40 @@ export default {
return null;
},
- hasUnresolvedDiscussion() {
- return this.unresolvedDiscussions.length > 0;
+ resolvedText() {
+ return this.transformedDiscussion.resolvedByPush ? 'Automatically resolved' : 'Resolved';
+ },
+ hasMultipleUnresolvedDiscussions() {
+ return this.unresolvedDiscussions.length > 1;
+ },
+ shouldRenderDiffs() {
+ const { diffDiscussion, diffFile } = this.transformedDiscussion;
+
+ return diffDiscussion && diffFile && this.renderDiffFile;
},
wrapperComponent() {
- return this.discussion.diffDiscussion && this.discussion.diffFile
- ? diffWithNote
- : 'div';
+ return this.shouldRenderDiffs ? diffWithNote : 'div';
+ },
+ wrapperComponentProps() {
+ if (this.shouldRenderDiffs) {
+ return { discussion: convertObjectPropsToCamelCase(this.discussion) };
+ }
+
+ return {};
},
wrapperClass() {
- return this.isDiffDiscussion ? '' : 'card';
+ return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
},
},
mounted() {
if (this.isReplying) {
- this.initAutoSave(this.discussion.noteable_type);
+ this.initAutoSave(this.transformedDiscussion);
}
},
updated() {
if (this.isReplying) {
if (!this.autosave) {
- this.initAutoSave(this.discussion.noteable_type);
+ this.initAutoSave(this.transformedDiscussion);
} else {
this.setAutoSave();
}
@@ -127,7 +168,9 @@ export default {
'toggleDiscussion',
'removePlaceholderNotes',
'toggleResolveNote',
+ 'expandDiscussion',
]),
+ truncateSha,
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
@@ -136,23 +179,25 @@ export default {
return placeholderNote;
}
+ if (note.system) {
+ return systemNote;
+ }
+
return noteableNote;
},
componentData(note) {
- return note.isPlaceholderNote ? this.note.notes[0] : note;
+ return note.isPlaceholderNote ? this.discussion.notes[0] : note;
},
toggleDiscussionHandler() {
- this.toggleDiscussion({ discussionId: this.note.id });
+ this.toggleDiscussion({ discussionId: this.discussion.id });
},
showReplyForm() {
this.isReplying = true;
},
cancelReplyForm(shouldConfirm) {
if (shouldConfirm && this.$refs.noteForm.isDirty) {
- const msg = 'Are you sure you want to cancel creating this comment?';
-
// eslint-disable-next-line no-alert
- if (!confirm(msg)) {
+ if (!window.confirm('Are you sure you want to cancel creating this comment?')) {
return;
}
}
@@ -161,18 +206,23 @@ export default {
this.isReplying = false;
},
saveReply(noteText, form, callback) {
+ const postData = {
+ in_reply_to_discussion_id: this.discussion.reply_id,
+ target_type: this.getNoteableData.targetType,
+ note: { note: noteText },
+ };
+
+ if (this.discussion.for_commit) {
+ postData.note_project_id = this.discussion.project_id;
+ }
+
const replyData = {
endpoint: this.newNotePath,
flashContainer: this.$el,
- data: {
- in_reply_to_discussion_id: this.note.reply_id,
- target_type: this.noteableType,
- target_id: this.discussion.noteable_id,
- note: { note: noteText },
- },
+ data: postData,
};
- this.isReplying = false;
+ this.isReplying = false;
this.saveNote(replyData)
.then(() => {
this.resetAutoSave();
@@ -190,15 +240,19 @@ Please check your network connection and try again.`;
});
});
},
- jumpToDiscussion() {
+ jumpToNextDiscussion() {
+ const discussionIds = this.allDiscussions.map(d => d.id);
const unresolvedIds = this.unresolvedDiscussions.map(d => d.id);
- const index = unresolvedIds.indexOf(this.note.id);
+ const currentIndex = discussionIds.indexOf(this.discussion.id);
+ const remainingAfterCurrent = discussionIds.slice(currentIndex + 1);
+ const nextIndex = _.findIndex(remainingAfterCurrent, id => unresolvedIds.indexOf(id) > -1);
- if (index >= 0 && index !== unresolvedIds.length) {
- const nextId = unresolvedIds[index + 1];
+ if (nextIndex > -1) {
+ const nextId = remainingAfterCurrent[nextIndex];
const el = document.querySelector(`[data-discussion-id="${nextId}"]`);
if (el) {
+ this.expandDiscussion({ discussionId: nextId });
scrollToElement(el);
}
}
@@ -208,9 +262,7 @@ Please check your network connection and try again.`;
</script>
<template>
- <li
- :data-discussion-id="note.id"
- class="note note-discussion timeline-entry">
+ <li class="note note-discussion timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
@@ -221,20 +273,52 @@ Please check your network connection and try again.`;
/>
</div>
<div class="timeline-content">
- <div class="discussion">
- <div class="discussion-header">
+ <div
+ :data-discussion-id="transformedDiscussion.discussion_id"
+ class="discussion js-discussion-container"
+ >
+ <div
+ v-if="renderHeader"
+ class="discussion-header"
+ >
<note-header
:author="author"
- :created-at="discussion.created_at"
- :note-id="discussion.id"
+ :created-at="transformedDiscussion.created_at"
+ :note-id="transformedDiscussion.id"
:include-toggle="true"
- :expanded="note.expanded"
- action-text="started a discussion"
- class="discussion"
+ :expanded="discussion.expanded"
@toggleHandler="toggleDiscussionHandler"
+ >
+ <template v-if="transformedDiscussion.diffDiscussion">
+ started a discussion on
+ <a :href="transformedDiscussion.discussionPath">
+ <template v-if="transformedDiscussion.active">
+ the diff
+ </template>
+ <template v-else>
+ an old version of the diff
+ </template>
+ </a>
+ </template>
+ <template v-else-if="discussion.for_commit">
+ started a discussion on commit
+ <a :href="discussion.discussion_path">
+ {{ truncateSha(discussion.commit_id) }}
+ </a>
+ </template>
+ <template v-else>
+ started a discussion
+ </template>
+ </note-header>
+ <note-edited-text
+ v-if="transformedDiscussion.resolved"
+ :edited-at="transformedDiscussion.resolvedAt"
+ :edited-by="transformedDiscussion.resolvedBy"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline"
/>
<note-edited-text
- v-if="lastUpdatedAt"
+ v-else-if="lastUpdatedAt"
:edited-at="lastUpdatedAt"
:edited-by="lastUpdatedBy"
action-text="Last updated"
@@ -242,17 +326,17 @@ Please check your network connection and try again.`;
/>
</div>
<div
- v-if="note.expanded"
+ v-if="discussion.expanded || alwaysExpanded"
class="discussion-body">
<component
:is="wrapperComponent"
- :discussion="discussion"
+ v-bind="wrapperComponentProps"
:class="wrapperClass"
>
<div class="discussion-notes">
<ul class="notes">
<component
- v-for="note in note.notes"
+ v-for="note in discussion.notes"
:is="componentName(note)"
:note="componentData(note)"
:key="note.id"
@@ -260,27 +344,28 @@ Please check your network connection and try again.`;
</ul>
<div
:class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder">
+ class="discussion-reply-holder"
+ >
<template v-if="!isReplying && canReply">
<div
class="btn-group d-flex discussion-with-resolve-btn"
role="group">
<div
- class="btn-group"
+ class="btn-group w-100"
role="group">
<button
type="button"
- class="js-vue-discussion-reply btn btn-text-field"
+ class="js-vue-discussion-reply btn btn-text-field mr-2"
title="Add a reply"
@click="showReplyForm">Reply...</button>
</div>
<div
- v-if="note.resolvable"
+ v-if="discussion.resolvable"
class="btn-group"
role="group">
<button
type="button"
- class="btn btn-default"
+ class="btn btn-default mr-2"
@click="resolveHandler()"
>
<i
@@ -292,7 +377,7 @@ Please check your network connection and try again.`;
</button>
</div>
<div
- v-if="note.resolvable"
+ v-if="discussion.resolvable"
class="btn-group discussion-actions"
role="group"
>
@@ -302,17 +387,17 @@ Please check your network connection and try again.`;
role="group">
<a
v-tooltip
- :href="note.resolve_with_issue_path"
+ :href="discussion.resolve_with_issue_path"
+ :title="s__('MergeRequests|Resolve this discussion in a new issue')"
class="new-issue-for-discussion btn
btn-default discussion-create-issue-btn"
- title="Resolve this discussion in a new issue"
data-container="body"
>
<span v-html="resolveDiscussionsSvg"></span>
</a>
</div>
<div
- v-if="hasUnresolvedDiscussion"
+ v-if="hasMultipleUnresolvedDiscussions"
class="btn-group"
role="group">
<button
@@ -320,7 +405,7 @@ Please check your network connection and try again.`;
class="btn btn-default discussion-next-btn"
title="Jump to next unresolved discussion"
data-container="body"
- @click="jumpToDiscussion"
+ @click="jumpToNextDiscussion"
>
<span v-html="nextDiscussionsSvg"></span>
</button>
@@ -331,11 +416,11 @@ Please check your network connection and try again.`;
<note-form
v-if="isReplying"
ref="noteForm"
- :note="note"
+ :discussion="discussion"
:is-editing="false"
save-button-title="Comment"
@handleFormUpdate="saveReply"
- @cancelFormEdition="cancelReplyForm" />
+ @cancelForm="cancelReplyForm" />
<note-signed-out-widget v-if="!canReply" />
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index ec3ee407f0a..4ebeb5599f2 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -12,6 +12,7 @@ import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
export default {
+ name: 'NoteableNote',
components: {
userAvatarLink,
noteHeader,
@@ -34,26 +35,31 @@ export default {
};
},
computed: {
- ...mapGetters(['targetNoteHash', 'getUserData']),
+ ...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData']),
author() {
return this.note.author;
},
classNameBindings() {
return {
+ [`note-row-${this.note.id}`]: true,
'is-editing': this.isEditing && !this.isRequesting,
'is-requesting being-posted': this.isRequesting,
'disabled-content': this.isDeleting,
- target: this.targetNoteHash === this.noteAnchorId,
+ target: this.isTarget,
};
},
+ canResolve() {
+ return this.note.resolvable && !!this.getUserData.id;
+ },
canReportAsAbuse() {
- return (
- this.note.report_abuse_path && this.author.id !== this.getUserData.id
- );
+ return this.note.report_abuse_path && this.author.id !== this.getUserData.id;
},
noteAnchorId() {
return `note_${this.note.id}`;
},
+ isTarget() {
+ return this.targetNoteHash === this.noteAnchorId;
+ },
},
created() {
@@ -65,19 +71,20 @@ export default {
});
},
+ mounted() {
+ if (this.isTarget) {
+ this.scrollToNoteIfNeeded($(this.$el));
+ }
+ },
+
methods: {
- ...mapActions([
- 'deleteNote',
- 'updateNote',
- 'toggleResolveNote',
- 'scrollToNoteIfNeeded',
- ]),
+ ...mapActions(['deleteNote', 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded']),
editHandler() {
this.isEditing = true;
},
deleteHandler() {
// eslint-disable-next-line no-alert
- if (confirm('Are you sure you want to delete this comment?')) {
+ if (window.confirm('Are you sure you want to delete this comment?')) {
this.isDeleting = true;
this.deleteNote(this.note)
@@ -85,9 +92,7 @@ export default {
this.isDeleting = false;
})
.catch(() => {
- Flash(
- 'Something went wrong while deleting your note. Please try again.',
- );
+ Flash('Something went wrong while deleting your note. Please try again.');
this.isDeleting = false;
});
}
@@ -96,7 +101,7 @@ export default {
const data = {
endpoint: this.note.path,
note: {
- target_type: this.noteableType,
+ target_type: this.getNoteableData.targetType,
target_id: this.note.noteable_id,
note: { note: noteText },
},
@@ -118,8 +123,7 @@ export default {
this.isRequesting = false;
this.isEditing = true;
this.$nextTick(() => {
- const msg =
- 'Something went wrong while editing your comment. Please try again.';
+ const msg = 'Something went wrong while editing your comment. Please try again.';
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
@@ -129,8 +133,7 @@ export default {
formCancelHandler(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert
- if (!confirm('Are you sure you want to cancel editing this comment?'))
- return;
+ if (!window.confirm('Are you sure you want to cancel editing this comment?')) return;
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
@@ -143,7 +146,7 @@ export default {
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
- this.$refs.noteBody.$refs.noteForm.note.note = noteText;
+ this.$refs.noteBody.note.note = noteText;
},
},
};
@@ -154,7 +157,9 @@ export default {
:id="noteAnchorId"
:class="classNameBindings"
:data-award-url="note.toggle_award_path"
- class="note timeline-entry">
+ :data-note-id="note.id"
+ class="note timeline-entry"
+ >
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
@@ -170,16 +175,17 @@ export default {
:author="author"
:created-at="note.created_at"
:note-id="note.id"
- action-text="commented"
/>
<note-actions
:author-id="author.id"
:note-id="note.id"
+ :note-url="note.noteable_note_url"
:access-level="note.human_access"
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
+ :can-resolve="note.current_user.can_resolve"
:report-abuse-path="note.report_abuse_path"
:resolvable="note.resolvable"
:is-resolved="note.resolved"
@@ -196,7 +202,7 @@ export default {
:can-edit="note.current_user.can_edit"
:is-editing="isEditing"
@handleFormUpdate="formUpdateHandler"
- @cancelFormEdition="formCancelHandler"
+ @cancelForm="formCancelHandler"
/>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index ebfc827ac57..17b5e8d1ae8 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,9 +1,7 @@
<script>
-import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash';
-import store from '../stores/';
import * as constants from '../constants';
import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue';
@@ -39,19 +37,23 @@ export default {
required: false,
default: () => ({}),
},
+ shouldShow: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
- store,
data() {
return {
isLoading: true,
};
},
computed: {
- ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
+ ...mapGetters(['discussions', 'getNotesDataByProp', 'discussionCount']),
noteableType() {
return this.noteableData.noteableType;
},
- allNotes() {
+ allDiscussions() {
if (this.isLoading) {
const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
@@ -59,36 +61,29 @@ export default {
isSkeletonNote: true,
});
}
- return this.notes;
+ return this.discussions;
},
},
created() {
this.setNotesData(this.notesData);
this.setNoteableData(this.noteableData);
this.setUserData(this.userData);
+ this.setTargetNoteHash(getLocationHash());
},
mounted() {
this.fetchNotes();
-
const parentElement = this.$el.parentElement;
- if (
- parentElement &&
- parentElement.classList.contains('js-vue-notes-event')
- ) {
+ if (parentElement && parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', event => {
const { awardName, noteId } = event.detail;
this.actionToggleAward({ awardName, noteId });
});
}
- document.addEventListener('refreshVueNotes', this.fetchNotes);
- },
- beforeDestroy() {
- document.removeEventListener('refreshVueNotes', this.fetchNotes);
},
methods: {
...mapActions({
- actionFetchNotes: 'fetchNotes',
+ fetchDiscussions: 'fetchDiscussions',
poll: 'poll',
actionToggleAward: 'toggleAward',
scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
@@ -97,28 +92,31 @@ export default {
setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash',
+ toggleDiscussion: 'toggleDiscussion',
}),
- getComponentName(note) {
- if (note.isSkeletonNote) {
+ getComponentName(discussion) {
+ if (discussion.isSkeletonNote) {
return skeletonLoadingContainer;
}
- if (note.isPlaceholderNote) {
- if (note.placeholderType === constants.SYSTEM_NOTE) {
+ if (discussion.isPlaceholderNote) {
+ if (discussion.placeholderType === constants.SYSTEM_NOTE) {
return placeholderSystemNote;
}
return placeholderNote;
- } else if (note.individual_note) {
- return note.notes[0].system ? systemNote : noteableNote;
+ } else if (discussion.individual_note) {
+ return discussion.notes[0].system ? systemNote : noteableNote;
}
return noteableDiscussion;
},
- getComponentData(note) {
- return note.individual_note ? note.notes[0] : note;
+ getComponentData(discussion) {
+ return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
},
fetchNotes() {
- return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath'))
- .then(() => this.initPolling())
+ return this.fetchDiscussions(this.getNotesDataByProp('discussionsPath'))
+ .then(() => {
+ this.initPolling();
+ })
.then(() => {
this.isLoading = false;
})
@@ -126,9 +124,7 @@ export default {
.then(() => this.checkLocationHash())
.catch(() => {
this.isLoading = false;
- Flash(
- 'Something went wrong while fetching comments. Please try again.',
- );
+ Flash('Something went wrong while fetching comments. Please try again.');
});
},
initPolling() {
@@ -143,11 +139,19 @@ export default {
},
checkLocationHash() {
const hash = getLocationHash();
- const element = document.getElementById(hash);
+ const noteId = hash && hash.replace(/^note_/, '');
- if (hash && element) {
- this.setTargetNoteHash(hash);
- this.scrollToNoteIfNeeded($(element));
+ if (noteId) {
+ this.discussions.forEach(discussion => {
+ if (discussion.notes) {
+ discussion.notes.forEach(note => {
+ if (`${note.id}` === `${noteId}`) {
+ // FIXME: this modifies the store state without using a mutation/action
+ Object.assign(discussion, { expanded: true });
+ }
+ });
+ }
+ });
}
},
},
@@ -155,16 +159,18 @@ export default {
</script>
<template>
- <div id="notes">
+ <div
+ v-if="shouldShow"
+ id="notes">
<ul
id="notes-list"
class="notes main-notes-list timeline">
<component
- v-for="note in allNotes"
- :is="getComponentName(note)"
- :note="getComponentData(note)"
- :key="note.id"
+ v-for="discussion in allDiscussions"
+ :is="getComponentName(discussion)"
+ v-bind="getComponentData(discussion)"
+ :key="discussion.id"
/>
</ul>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index 5b5b1e89058..2c3e07c0506 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -11,7 +11,7 @@ export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
export const ISSUE_NOTEABLE_TYPE = 'issue';
export const EPIC_NOTEABLE_TYPE = 'epic';
-export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
+export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const DESCRIPTION_TYPE = 'changed the description';
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index e4121f151db..eed3a82854d 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,46 +1,49 @@
import Vue from 'vue';
import notesApp from './components/notes_app.vue';
+import createStore from './stores';
-document.addEventListener(
- 'DOMContentLoaded',
- () =>
- new Vue({
- el: '#js-vue-notes',
- components: {
- notesApp,
- },
- data() {
- const notesDataset = document.getElementById('js-vue-notes').dataset;
- const parsedUserData = JSON.parse(notesDataset.currentUserData);
- const noteableData = JSON.parse(notesDataset.noteableData);
- let currentUserData = {};
+document.addEventListener('DOMContentLoaded', () => {
+ const store = createStore();
- noteableData.noteableType = notesDataset.noteableType;
+ return new Vue({
+ el: '#js-vue-notes',
+ components: {
+ notesApp,
+ },
+ store,
+ data() {
+ const notesDataset = document.getElementById('js-vue-notes').dataset;
+ const parsedUserData = JSON.parse(notesDataset.currentUserData);
+ const noteableData = JSON.parse(notesDataset.noteableData);
+ let currentUserData = {};
- if (parsedUserData) {
- currentUserData = {
- id: parsedUserData.id,
- name: parsedUserData.name,
- username: parsedUserData.username,
- avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
- path: parsedUserData.path,
- };
- }
+ noteableData.noteableType = notesDataset.noteableType;
+ noteableData.targetType = notesDataset.targetType;
- return {
- noteableData,
- currentUserData,
- notesData: JSON.parse(notesDataset.notesData),
+ if (parsedUserData) {
+ currentUserData = {
+ id: parsedUserData.id,
+ name: parsedUserData.name,
+ username: parsedUserData.username,
+ avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
+ path: parsedUserData.path,
};
- },
- render(createElement) {
- return createElement('notes-app', {
- props: {
- noteableData: this.noteableData,
- notesData: this.notesData,
- userData: this.currentUserData,
- },
- });
- },
- }),
-);
+ }
+
+ return {
+ noteableData,
+ currentUserData,
+ notesData: JSON.parse(notesDataset.notesData),
+ };
+ },
+ render(createElement) {
+ return createElement('notes-app', {
+ props: {
+ noteableData: this.noteableData,
+ notesData: this.notesData,
+ userData: this.currentUserData,
+ },
+ });
+ },
+ });
+});
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index 3dff715905f..36cc8d5d056 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -4,11 +4,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default {
methods: {
- initAutoSave(noteableType) {
+ initAutoSave(noteable) {
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [
'Note',
- capitalizeFirstCharacter(noteableType),
- this.note.id,
+ capitalizeFirstCharacter(noteable.noteable_type),
+ noteable.id,
]);
},
resetAutoSave() {
diff --git a/app/assets/javascripts/notes/mixins/noteable.js b/app/assets/javascripts/notes/mixins/noteable.js
index b68543d71c8..bf1cd6fe5a8 100644
--- a/app/assets/javascripts/notes/mixins/noteable.js
+++ b/app/assets/javascripts/notes/mixins/noteable.js
@@ -1,15 +1,10 @@
import * as constants from '../constants';
export default {
- props: {
- note: {
- type: Object,
- required: true,
- },
- },
computed: {
noteableType() {
- return constants.NOTEABLE_TYPE_MAPPING[this.note.noteable_type];
+ const note = this.discussion ? this.discussion.notes[0] : this.note;
+ return constants.NOTEABLE_TYPE_MAPPING[note.noteable_type];
},
},
};
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index f79049b85f6..cd8394e0619 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -2,42 +2,39 @@ import Flash from '~/flash';
import { __ } from '~/locale';
export default {
- props: {
- note: {
- type: Object,
- required: true,
- },
- },
computed: {
discussionResolved() {
- const { notes, resolved } = this.note;
+ if (this.discussion) {
+ const { notes, resolved } = this.discussion;
+
+ if (notes) {
+ // Decide resolved state using store. Only valid for discussions.
+ return notes.filter(note => !note.system).every(note => note.resolved);
+ }
- if (notes) {
- // Decide resolved state using store. Only valid for discussions.
- return notes.every(note => note.resolved && !note.system);
+ return resolved;
}
- return resolved;
+ return this.note.resolved;
},
resolveButtonTitle() {
if (this.updatedNoteBody) {
if (this.discussionResolved) {
- return __('Comment and unresolve discussion');
+ return __('Comment & unresolve discussion');
}
- return __('Comment and resolve discussion');
+ return __('Comment & resolve discussion');
}
- return this.discussionResolved
- ? __('Unresolve discussion')
- : __('Resolve discussion');
+
+ return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion');
},
},
methods: {
resolveHandler(resolvedState = false) {
this.isResolving = true;
- const endpoint = this.note.resolve_path || `${this.note.path}/resolve`;
const isResolved = this.discussionResolved || resolvedState;
const discussion = this.resolveAsThread;
+ const endpoint = discussion ? this.discussion.resolve_path : `${this.note.path}/resolve`;
this.toggleResolveNote({ endpoint, isResolved, discussion })
.then(() => {
@@ -45,9 +42,8 @@ export default {
})
.catch(() => {
this.isResolving = false;
- const msg = __(
- 'Something went wrong while resolving this discussion. Please try again.',
- );
+
+ const msg = __('Something went wrong while resolving this discussion. Please try again.');
Flash(msg, 'alert', this.$el);
});
},
diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
index 7c623aac6ed..ee7628840cf 100644
--- a/app/assets/javascripts/notes/services/notes_service.js
+++ b/app/assets/javascripts/notes/services/notes_service.js
@@ -5,7 +5,7 @@ import * as constants from '../constants';
Vue.use(VueResource);
export default {
- fetchNotes(endpoint) {
+ fetchDiscussions(endpoint) {
return Vue.http.get(endpoint);
},
deleteNote(endpoint) {
@@ -22,9 +22,7 @@ export default {
},
toggleResolveNote(endpoint, isResolved) {
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
- const method = isResolved
- ? UNRESOLVE_NOTE_METHOD_NAME
- : RESOLVE_NOTE_METHOD_NAME;
+ const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME;
return Vue.http[method](endpoint);
},
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index b2222476924..0a40b48257f 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import axios from '~/lib/utils/axios_utils';
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
@@ -12,20 +13,29 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll;
+export const expandDiscussion = ({ commit }, data) => commit(types.EXPAND_DISCUSSION, data);
+
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
+
export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
+
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
+
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
-export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
+
+export const setInitialNotes = ({ commit }, discussions) =>
+ commit(types.SET_INITIAL_DISCUSSIONS, discussions);
+
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
+
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
-export const fetchNotes = ({ commit }, path) =>
+export const fetchDiscussions = ({ commit }, path) =>
service
- .fetchNotes(path)
+ .fetchDiscussions(path)
.then(res => res.json())
- .then(res => {
- commit(types.SET_INITIAL_NOTES, res);
+ .then(discussions => {
+ commit(types.SET_INITIAL_DISCUSSIONS, discussions);
});
export const deleteNote = ({ commit }, note) =>
@@ -121,7 +131,8 @@ export const toggleIssueLocalState = ({ commit }, newState) => {
};
export const saveNote = ({ commit, dispatch }, noteData) => {
- const { note } = noteData.data.note;
+ // For MR discussuions we need to post as `note[note]` and issue we use `note.note`.
+ const note = noteData.data['note[note]'] || noteData.data.note.note;
let placeholderText = note;
const hasQuickActions = utils.hasQuickActions(placeholderText);
const replyId = noteData.data.in_reply_to_discussion_id;
@@ -192,7 +203,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
});
};
-const pollSuccessCallBack = (resp, commit, state, getters) => {
+const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
if (resp.notes && resp.notes.length) {
const { notesById } = getters;
@@ -200,10 +211,12 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note);
} else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
- const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
+ const discussion = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
+ } else if (note.type === constants.DIFF_NOTE) {
+ dispatch('fetchDiscussions', state.notesData.discussionsPath);
} else {
commit(types.ADD_NEW_NOTE, note);
}
@@ -218,13 +231,13 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
return resp;
};
-export const poll = ({ commit, state, getters }) => {
+export const poll = ({ commit, state, getters, dispatch }) => {
eTagPoll = new Poll({
resource: service,
method: 'poll',
data: state,
successCallback: resp =>
- resp.json().then(data => pollSuccessCallBack(data, commit, state, getters)),
+ resp.json().then(data => pollSuccessCallBack(data, commit, state, getters, dispatch)),
errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
});
@@ -285,5 +298,13 @@ export const scrollToNoteIfNeeded = (context, el) => {
}
};
+export const fetchDiscussionDiffLines = ({ commit }, discussion) =>
+ axios.get(discussion.truncatedDiffLinesPath).then(({ data }) => {
+ commit(types.SET_DISCUSSION_DIFF_LINES, {
+ discussionId: discussion.id,
+ diffLines: data.truncated_diff_lines,
+ });
+ });
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index bc373e0d0fc..ab28bb48e9e 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,58 +1,89 @@
import _ from 'underscore';
+import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
-export const notes = state => collapseSystemNotes(state.notes);
+export const discussions = state => collapseSystemNotes(state.discussions);
export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData;
+
export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData;
+
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
+
export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {};
-export const getUserDataByProp = state => prop =>
- state.userData && state.userData[prop];
+
+export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
export const notesById = state =>
- state.notes.reduce((acc, note) => {
+ state.discussions.reduce((acc, note) => {
note.notes.every(n => Object.assign(acc, { [n.id]: n }));
return acc;
}, {});
+export const discussionsByLineCode = state =>
+ state.discussions.reduce((acc, note) => {
+ if (note.diff_discussion && note.line_code && note.resolvable) {
+ // For context about line notes: there might be multiple notes with the same line code
+ const items = acc[note.line_code] || [];
+ items.push(note);
+
+ Object.assign(acc, { [note.line_code]: items });
+ }
+ return acc;
+ }, {});
+
+export const noteableType = state => {
+ const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
+
+ if (state.noteableData.noteableType === EPIC_NOTEABLE_TYPE) {
+ return EPIC_NOTEABLE_TYPE;
+ }
+
+ return state.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE;
+};
+
const reverseNotes = array => array.slice(0).reverse();
+
const isLastNote = (note, state) =>
- !note.system &&
- state.userData &&
- note.author &&
- note.author.id === state.userData.id;
+ !note.system && state.userData && note.author && note.author.id === state.userData.id;
export const getCurrentUserLastNote = state =>
- _.flatten(
- reverseNotes(state.notes).map(note => reverseNotes(note.notes)),
- ).find(el => isLastNote(el, state));
+ _.flatten(reverseNotes(state.discussions).map(note => reverseNotes(note.notes))).find(el =>
+ isLastNote(el, state),
+ );
export const getDiscussionLastNote = state => discussion =>
reverseNotes(discussion.notes).find(el => isLastNote(el, state));
export const discussionCount = state => {
- const discussions = state.notes.filter(n => !n.individual_note);
+ const filteredDiscussions = state.discussions.filter(n => !n.individual_note && n.resolvable);
- return discussions.length;
+ return filteredDiscussions.length;
};
export const unresolvedDiscussions = (state, getters) => {
const resolvedMap = getters.resolvedDiscussionsById;
- return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
+ return state.discussions.filter(n => !n.individual_note && !resolvedMap[n.id]);
+};
+
+export const allDiscussions = (state, getters) => {
+ const resolved = getters.resolvedDiscussionsById;
+ const unresolved = getters.unresolvedDiscussions;
+
+ return Object.values(resolved).concat(unresolved);
};
export const resolvedDiscussionsById = state => {
const map = {};
- state.notes.forEach(n => {
+ state.discussions.forEach(n => {
if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system);
@@ -71,5 +102,15 @@ export const resolvedDiscussionCount = (state, getters) => {
return Object.keys(resolvedMap).length;
};
+export const discussionTabCounter = state => {
+ let all = [];
+
+ state.discussions.forEach(discussion => {
+ all = all.concat(discussion.notes.filter(note => !note.system && !note.placeholder));
+ });
+
+ return all.length;
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js
index 9ed19bf171e..0f48b8880f4 100644
--- a/app/assets/javascripts/notes/stores/index.js
+++ b/app/assets/javascripts/notes/stores/index.js
@@ -3,24 +3,14 @@ import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
+import module from './modules';
Vue.use(Vuex);
-export default new Vuex.Store({
- state: {
- notes: [],
- targetNoteHash: null,
- lastFetchedAt: null,
-
- // View layer
- isToggleStateButtonLoading: false,
-
- // holds endpoints and permissions provided through haml
- notesData: {},
- userData: {},
- noteableData: {},
- },
- actions,
- getters,
- mutations,
-});
+export default () =>
+ new Vuex.Store({
+ state: module.state,
+ actions,
+ getters,
+ mutations,
+ });
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
new file mode 100644
index 00000000000..a978490c009
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -0,0 +1,26 @@
+import * as actions from '../actions';
+import * as getters from '../getters';
+import mutations from '../mutations';
+
+export default {
+ state: {
+ discussions: [],
+ targetNoteHash: null,
+ lastFetchedAt: null,
+
+ // View layer
+ isToggleStateButtonLoading: false,
+
+ // holds endpoints and permissions provided through haml
+ notesData: {
+ markdownDocsPath: '',
+ },
+ userData: {},
+ noteableData: {
+ current_user: {},
+ },
+ },
+ actions,
+ getters,
+ mutations,
+};
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index b455e23ecde..caead4cb860 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -1,11 +1,12 @@
export const ADD_NEW_NOTE = 'ADD_NEW_NOTE';
export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
export const DELETE_NOTE = 'DELETE_NOTE';
+export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA';
export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA';
-export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES';
+export const SET_INITIAL_DISCUSSIONS = 'SET_INITIAL_DISCUSSIONS';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH';
export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
@@ -13,6 +14,7 @@ export const TOGGLE_AWARD = 'TOGGLE_AWARD';
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
+export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index c8edc06349f..ea165709e61 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -6,8 +6,8 @@ import { isInMRPage } from '../../lib/utils/common_utils';
export default {
[types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note;
- const [exists] = state.notes.filter(n => n.id === note.discussion_id);
- const isDiscussion = type === constants.DISCUSSION_NOTE;
+ const [exists] = state.discussions.filter(n => n.id === note.discussion_id);
+ const isDiscussion = type === constants.DISCUSSION_NOTE || type === constants.DIFF_NOTE;
if (!exists) {
const noteData = {
@@ -25,42 +25,44 @@ export default {
noteData.resolve_with_issue_path = note.resolve_with_issue_path;
}
- state.notes.push(noteData);
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
+ state.discussions.push(noteData);
}
},
[types.ADD_NEW_REPLY_TO_DISCUSSION](state, note) {
- const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+ const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj) {
noteObj.notes.push(note);
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
}
},
[types.DELETE_NOTE](state, note) {
- const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+ const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj.individual_note) {
- state.notes.splice(state.notes.indexOf(noteObj), 1);
+ state.discussions.splice(state.discussions.indexOf(noteObj), 1);
} else {
const comment = utils.findNoteObjectById(noteObj.notes, note.id);
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1);
if (!noteObj.notes.length) {
- state.notes.splice(state.notes.indexOf(noteObj), 1);
+ state.discussions.splice(state.discussions.indexOf(noteObj), 1);
}
}
+ },
+
+ [types.EXPAND_DISCUSSION](state, { discussionId }) {
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
+ discussion.expanded = true;
},
[types.REMOVE_PLACEHOLDER_NOTES](state) {
- const { notes } = state;
+ const { discussions } = state;
- for (let i = notes.length - 1; i >= 0; i -= 1) {
- const note = notes[i];
+ for (let i = discussions.length - 1; i >= 0; i -= 1) {
+ const note = discussions[i];
const children = note.notes;
if (children.length && !note.individual_note) {
@@ -72,7 +74,7 @@ export default {
}
} else if (note.isPlaceholderNote) {
// remove placeholders from state root
- notes.splice(i, 1);
+ discussions.splice(i, 1);
}
}
},
@@ -88,29 +90,29 @@ export default {
[types.SET_USER_DATA](state, data) {
Object.assign(state, { userData: data });
},
- [types.SET_INITIAL_NOTES](state, notesData) {
- const notes = [];
+ [types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
+ const discussions = [];
- notesData.forEach(note => {
+ discussionsData.forEach(discussion => {
// To support legacy notes, should be very rare case.
- if (note.individual_note && note.notes.length > 1) {
- note.notes.forEach(n => {
- notes.push({
- ...note,
+ if (discussion.individual_note && discussion.notes.length > 1) {
+ discussion.notes.forEach(n => {
+ discussions.push({
+ ...discussion,
notes: [n], // override notes array to only have one item to mimick individual_note
});
});
} else {
- const oldNote = utils.findNoteObjectById(state.notes, note.id);
+ const oldNote = utils.findNoteObjectById(state.discussions, discussion.id);
- notes.push({
- ...note,
- expanded: oldNote ? oldNote.expanded : note.expanded,
+ discussions.push({
+ ...discussion,
+ expanded: oldNote ? oldNote.expanded : discussion.expanded,
});
}
});
- Object.assign(state, { notes });
+ Object.assign(state, { discussions });
},
[types.SET_LAST_FETCHED_AT](state, fetchedAt) {
@@ -122,17 +124,17 @@ export default {
},
[types.SHOW_PLACEHOLDER_NOTE](state, data) {
- let notesArr = state.notes;
- if (data.replyId) {
- notesArr = utils.findNoteObjectById(notesArr, data.replyId).notes;
+ let notesArr = state.discussions;
+
+ const existingDiscussion = utils.findNoteObjectById(notesArr, data.replyId);
+ if (existingDiscussion) {
+ notesArr = existingDiscussion.notes;
}
notesArr.push({
individual_note: true,
isPlaceholderNote: true,
- placeholderType: data.isSystemNote
- ? constants.SYSTEM_NOTE
- : constants.NOTE,
+ placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE,
notes: [
{
body: data.noteBody,
@@ -151,28 +153,23 @@ export default {
if (hasEmojiAwardedByCurrentUser.length) {
// If current user has awarded this emoji, remove it.
- note.award_emoji.splice(
- note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]),
- 1,
- );
+ note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 1);
} else {
note.award_emoji.push({
name: awardName,
user: { id, name, username },
});
}
-
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.TOGGLE_DISCUSSION](state, { discussionId }) {
- const discussion = utils.findNoteObjectById(state.notes, discussionId);
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
discussion.expanded = !discussion.expanded;
},
[types.UPDATE_NOTE](state, note) {
- const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+ const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj.individual_note) {
noteObj.notes.splice(0, 1, note);
@@ -180,24 +177,20 @@ export default {
const comment = utils.findNoteObjectById(noteObj.notes, note.id);
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
}
-
- // document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.UPDATE_DISCUSSION](state, noteData) {
const note = noteData;
let index = 0;
- state.notes.forEach((n, i) => {
+ state.discussions.forEach((n, i) => {
if (n.id === note.id) {
index = i;
}
});
note.expanded = true; // override expand flag to prevent collapse
- state.notes.splice(index, 1, note);
-
- document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
+ state.discussions.splice(index, 1, note);
},
[types.CLOSE_ISSUE](state) {
@@ -211,4 +204,15 @@ export default {
[types.TOGGLE_STATE_BUTTON_LOADING](state, value) {
Object.assign(state, { isToggleStateButtonLoading: value });
},
+
+ [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
+ const index = state.discussions.indexOf(discussion);
+
+ const discussionWithDiffLines = Object.assign({}, discussion, {
+ truncated_diff_lines: diffLines,
+ });
+
+ state.discussions.splice(index, 1, discussionWithDiffLines);
+ },
};
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index c334eaa90f8..6fc43af2623 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -1,4 +1,4 @@
-/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
+/* eslint-disable class-methods-use-this, no-unneeded-ternary */
import $ from 'jquery';
import { visitUrl } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
index 653e2502d01..ae72c8cb4d5 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */
+/* eslint-disable func-names, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */
import $ from 'jquery';
import _ from 'underscore';
@@ -36,7 +36,9 @@ export default (function() {
var author_graph, author_header;
author_header = _this.create_author_header(d);
$(".contributors-list").append(author_header);
- _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates);
+
+ author_graph = new ContributorsAuthorGraph(d.dates);
+ _this.authors[d.author_name] = author_graph;
return author_graph.draw();
};
})(this));
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
index 5316d3e9f3c..a02ec9e5f00 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
+/* eslint-disable func-names, max-len, no-restricted-syntax, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
import $ from 'jquery';
import _ from 'underscore';
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
index 77135ad1f0e..d12249bf612 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
+/* eslint-disable func-names, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
import _ from 'underscore';
export default {
@@ -111,10 +111,15 @@ export default {
parse_log_entry: function(log_entry, field, date_range) {
var parsed_entry;
parsed_entry = {};
+
parsed_entry.author_name = log_entry.author_name;
parsed_entry.author_email = log_entry.author_email;
parsed_entry.dates = {};
- parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
+
+ parsed_entry.commits = 0;
+ parsed_entry.additions = 0;
+ parsed_entry.deletions = 0;
+
_.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
return function(value, key) {
if (_this.in_range(value.date, date_range)) {
diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js
index 82143fa875a..56ab3fcdfcb 100644
--- a/app/assets/javascripts/pages/projects/init_blob.js
+++ b/app/assets/javascripts/pages/projects/init_blob.js
@@ -8,7 +8,8 @@ import initBlobBundle from '~/blob_edit/blob_bundle';
export default () => {
new LineHighlighter(); // eslint-disable-line no-new
- new BlobLinePermalinkUpdater( // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new BlobLinePermalinkUpdater(
document.querySelector('#blob-content-holder'),
'.diff-line-num[data-line-number]',
document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
@@ -19,12 +20,13 @@ export default () => {
new ShortcutsNavigation(); // eslint-disable-line no-new
- new ShortcutsBlob({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new ShortcutsBlob({
skipResetBindings: true,
fileBlobPermalinkUrl,
});
- new BlobForkSuggestion({ // eslint-disable-line no-new
+ new BlobForkSuggestion({
openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'),
forkButtons: document.querySelectorAll('.js-fork-suggestion-button'),
cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 28d8761b502..26ead75cec4 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -1,30 +1,15 @@
-import MergeRequest from '~/merge_request';
import ZenMode from '~/zen_mode';
-import initNotes from '~/init_notes';
import initIssuableSidebar from '~/init_issuable_sidebar';
-import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import ShortcutsIssuable from '~/shortcuts_issuable';
-import Diff from '~/diff';
import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initWidget from '../../../vue_merge_request_widget';
-export default function () {
- new Diff(); // eslint-disable-line no-new
+export default function() {
new ZenMode(); // eslint-disable-line no-new
-
initIssuableSidebar();
- initNotes();
- initDiffNotes();
initPipelines();
-
- const mrShowNode = document.querySelector('.merge-request');
-
- window.mergeRequest = new MergeRequest({
- action: mrShowNode.dataset.mrAction,
- });
-
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
howToMerge();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index e5b2827b50c..f61f4db78d5 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,4 +1,3 @@
-import { hasVueMRDiscussionsCookie } from '~/lib/utils/common_utils';
import initMrNotes from '~/mr_notes';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initShow from '../init_merge_request_show';
@@ -6,8 +5,5 @@ import initShow from '../init_merge_request_show';
document.addEventListener('DOMContentLoaded', () => {
initShow();
initSidebarBundle();
-
- if (hasVueMRDiscussionsCookie()) {
- initMrNotes();
- }
+ initMrNotes();
});
diff --git a/app/assets/javascripts/pages/projects/network/network.js b/app/assets/javascripts/pages/projects/network/network.js
index aa50dd4bb25..77368c47451 100644
--- a/app/assets/javascripts/pages/projects/network/network.js
+++ b/app/assets/javascripts/pages/projects/network/network.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
+/* eslint-disable func-names, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
import $ from 'jquery';
import BranchGraph from '../../../network/branch_graph';
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index c1e3425ec75..a853624e944 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
+/* eslint-disable func-names, no-var, no-return-assign, one-var,
+ one-var-declaration-per-line, object-shorthand, vars-on-top */
import $ from 'jquery';
import Cookies from 'js-cookie';
diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js
index a5c17ab322c..a52861c9efa 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/form.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/form.js
@@ -13,7 +13,7 @@ export default () => {
new ProtectedTagEditList();
initDeployKeys();
initSettingsPanels();
- new ProtectedBranchCreate(); // eslint-disable-line no-new
- new ProtectedBranchEditList(); // eslint-disable-line no-new
+ new ProtectedBranchCreate();
+ new ProtectedBranchEditList();
new DueDateSelectors();
};
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index 80a7114f94d..07f32210d93 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -6,7 +6,8 @@ import OAuthRememberMe from './oauth_remember_me';
document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new
new SigninTabsMemoizer(); // eslint-disable-line no-new
- new OAuthRememberMe({ // eslint-disable-line no-new
+
+ new OAuthRememberMe({
container: $('.omniauth-container'),
}).bindEvents();
});
diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
index 18c7b21cf8c..761618109a4 100644
--- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
+++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
@@ -17,7 +17,6 @@ export default class OAuthRememberMe {
$('#remember_me', this.container).on('click', this.toggleRememberMe);
}
- // eslint-disable-next-line class-methods-use-this
toggleRememberMe(event) {
const rememberMe = $(event.target).is(':checked');
diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
index d321892d2d2..1e7c29aefaa 100644
--- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
+++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
@@ -37,6 +37,11 @@ export default class SigninTabsMemoizer {
const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`);
if (tab) {
tab.click();
+ } else {
+ const firstTab = document.querySelector(`${this.tabSelector} a`);
+ if (firstTab) {
+ firstTab.click();
+ }
}
}
}
diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js
index 87213c94eda..97cf1aeaadc 100644
--- a/app/assets/javascripts/pages/sessions/new/username_validator.js
+++ b/app/assets/javascripts/pages/sessions/new/username_validator.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */
+/* eslint-disable comma-dangle, consistent-return, class-methods-use-this */
import $ from 'jquery';
import _ from 'underscore';
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index f88a6b27c18..a2ca03536f2 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -187,7 +187,7 @@ export default class UserTabs {
let newState = source;
newState = newState.replace(/\/+$/, '');
newState += this.windowLocation.search + this.windowLocation.hash;
- history.replaceState(
+ window.history.replaceState(
{
url: newState,
},
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index db0505a55fe..1f152ed438d 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -91,6 +91,7 @@ export default {
class="js-ci-action btn btn-blank
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
data-container="body"
+ data-boundary="viewport"
@click="onClickAction"
>
<icon :name="actionIcon"/>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index 04120d45834..e047d10ac93 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -87,6 +87,7 @@ export default {
data-toggle="dropdown"
data-container="body"
data-boundary="viewport"
+ data-display="static"
class="dropdown-menu-toggle build-content"
>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 4dd2fb9fbed..b9231c002fd 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -165,6 +165,7 @@ export default {
class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
data-placement="top"
data-toggle="dropdown"
+ data-display="static"
type="button"
aria-haspopup="true"
aria-expanded="false"
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 246a265ef2b..45670584679 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-var, object-shorthand, comma-dangle, prefer-arrow-callback */
+/* eslint-disable func-names, no-var, object-shorthand, prefer-arrow-callback */
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index 8f93156cdd1..c6d809d84a6 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */
+/* eslint-disable no-useless-escape, max-len, no-var, no-underscore-dangle, func-names, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */
import $ from 'jquery';
import 'cropper';
@@ -139,9 +139,10 @@ import _ from 'underscore';
var array, binary, i, k, len, v;
binary = atob(dataURL.split(',')[1]);
array = [];
- for (k = i = 0, len = binary.length; i < len; k = (i += 1)) {
- v = binary[k];
- array.push(binary.charCodeAt(k));
+
+ for (i = 0, len = binary.length; i < len; i += 1) {
+ v = binary[i];
+ array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], {
type: 'image/png'
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 0af34657d72..5d58d968d30 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,8 +1,5 @@
-/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
-
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
import flash from '../flash';
export default class Profile {
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 4c4acd487f8..bcdb3f739fe 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */
+/* eslint-disable func-names, no-var, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, prefer-template, no-unused-vars, no-return-assign */
import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
@@ -91,7 +91,8 @@ export default class ProjectFindFile {
var blobItemUrl, filePath, html, i, j, len, matches, results;
this.element.find(".tree-table > tbody").empty();
results = [];
- for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) {
+
+ for (i = 0, len = filePaths.length; i < len; i += 1) {
filePath = filePaths[i];
if (i === 20) {
break;
@@ -150,7 +151,7 @@ export default class ProjectFindFile {
}
goToTree() {
- return location.href = this.options.treeUrl;
+ return window.location.href = this.options.treeUrl;
}
goToBlob() {
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index d2d26d6f67e..5a0d2b642eb 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -2,7 +2,7 @@ import { visitUrl } from './lib/utils/url_utility';
export default function projectImport() {
setTimeout(() => {
- visitUrl(location.href);
+ visitUrl(window.location.href);
}, 5000);
}
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index cb2e6855d1d..240dde56325 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
+/* eslint-disable func-names, wrap-iife, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
import $ from 'jquery';
import Api from './api';
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index 1a75fdd75db..078ccbbbac2 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -107,7 +107,7 @@ export default class PrometheusMetrics {
if (data && data.success) {
stop(data);
} else {
- this.backOffRequestCounter = this.backOffRequestCounter += 1;
+ this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 2afcf4626b8..b27d635c6ac 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */
+/* eslint-disable func-names, no-var, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, no-param-reassign, max-len */
import $ from 'jquery';
import _ from 'underscore';
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 2da022fde63..2f4e4881f24 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
+/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, consistent-return, object-shorthand, prefer-template, quotes, class-methods-use-this, no-lonely-if, no-else-return, vars-on-top, max-len */
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
@@ -438,7 +438,7 @@ export default class SearchAutocomplete {
}
onClick(item, $el, e) {
- if (location.pathname.indexOf(item.url) !== -1) {
+ if (window.location.pathname.indexOf(item.url) !== -1) {
if (!e.metaKey) e.preventDefault();
if (!this.badgePresent) {
if (item.category === 'Projects') {
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index eecde4550f9..37b4a2a4c63 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -42,8 +42,8 @@ export default function initSettingsPanels() {
}
});
- if (location.hash) {
- const $target = $(location.hash);
+ if (window.location.hash) {
+ const $target = $(window.location.hash);
if ($target.length && $target.hasClass('settings')) {
expandSection($target);
}
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 1e246a56b85..8658081c6c2 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -13,8 +13,8 @@ export default class ShortcutsFindFile extends ShortcutsNavigation {
element === this.projectFindFile.inputElement[0] &&
(combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')
) {
- // when press up/down key in textbox, cusor prevent to move to home/end
- event.preventDefault();
+ // when press up/down key in textbox, cursor prevent to move to home/end
+ e.preventDefault();
return false;
}
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 193788f754f..e9451be31fd 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -9,12 +9,10 @@ export default class ShortcutsIssuable extends Shortcuts {
constructor(isMergeRequest) {
super();
- this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form');
-
Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
- Mousetrap.bind('r', this.replyWithSelectedText.bind(this));
+ Mousetrap.bind('r', ShortcutsIssuable.replyWithSelectedText);
Mousetrap.bind('e', ShortcutsIssuable.editIssue);
if (isMergeRequest) {
@@ -24,11 +22,16 @@ export default class ShortcutsIssuable extends Shortcuts {
}
}
- replyWithSelectedText() {
+ static replyWithSelectedText() {
+ const $replyField = $('.js-main-target-form .js-vue-comment-form');
const documentFragment = window.gl.utils.getSelectedFragment();
+ if (!$replyField.length) {
+ return false;
+ }
+
if (!documentFragment) {
- this.$replyField.focus();
+ $replyField.focus();
return false;
}
@@ -39,21 +42,22 @@ export default class ShortcutsIssuable extends Shortcuts {
return false;
}
- const quote = _.map(selected.split('\n'), val => `${(`> ${val}`).trim()}\n`);
+ const quote = _.map(selected.split('\n'), val => `${`> ${val}`.trim()}\n`);
// If replyField already has some content, add a newline before our quote
- const separator = (this.$replyField.val().trim() !== '' && '\n\n') || '';
- this.$replyField.val((a, current) => `${current}${separator}${quote.join('')}\n`)
+ const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
+ $replyField
+ .val((a, current) => `${current}${separator}${quote.join('')}\n`)
.trigger('input')
.trigger('change');
// Trigger autosize
const event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
- this.$replyField.get(0).dispatchEvent(event);
+ $replyField.get(0).dispatchEvent(event);
// Focus the input field
- this.$replyField.focus();
+ $replyField.focus();
return false;
}
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 1b866cca4b1..2b8d6207dea 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -54,7 +54,7 @@ export default {
updateConfidentialAttribute(confidential) {
this.service
.update('issue', { confidential })
- .then(() => location.reload())
+ .then(() => window.location.reload())
.catch(() => {
Flash(
__(
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index 10bedf8af1f..8bbc59f623a 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -76,7 +76,7 @@ export default {
.update(this.issuableType, {
discussion_locked: locked,
})
- .then(() => location.reload())
+ .then(() => window.location.reload())
.catch(() =>
Flash(
this.__(
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 3086e7d0fc9..655bf9198b7 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -75,7 +75,6 @@ function mountLockComponent(mediator) {
function mountParticipantsComponent(mediator) {
const el = document.querySelector('.js-sidebar-participants-entry-point');
- // eslint-disable-next-line no-new
if (!el) return;
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index d86557e870a..d9ca5e46770 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -80,7 +80,7 @@ export default class SidebarMediator {
return this.service.moveIssue(this.store.moveToProjectId)
.then(response => response.json())
.then((data) => {
- if (location.pathname !== data.web_url) {
+ if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
});
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index ae27c676fa0..99c93952e2a 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
+/* eslint-disable func-names, prefer-arrow-callback, consistent-return, */
import $ from 'jquery';
import { __ } from './locale';
diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js
index 81ec483f2d9..873a506a92f 100644
--- a/app/assets/javascripts/snippet/snippet_embed.js
+++ b/app/assets/javascripts/snippet/snippet_embed.js
@@ -1,5 +1,5 @@
export default () => {
- const { protocol, host, pathname } = location;
+ const { protocol, host, pathname } = window.location;
const shareBtn = document.querySelector('.js-share-btn');
const embedBtn = document.querySelector('.js-embed-btn');
const snippetUrlArea = document.querySelector('.js-snippet-url-area');
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index f52990ba232..37f3dd4b496 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */
+/* eslint-disable consistent-return, no-else-return */
import $ from 'jquery';
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index afbb958d058..85123a63a45 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */
+/* eslint-disable func-names, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */
import $ from 'jquery';
import { visitUrl } from './lib/utils/url_utility';
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index cd954f75613..7abe7a6be5f 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
+/* eslint-disable func-names, one-var, no-var, prefer-rest-params, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
/* global Issuable */
/* global emitSidebarEvent */
@@ -259,7 +259,7 @@ function UsersSelect(currentUser, els, options = {}) {
showDivider = 0;
if (firstUser) {
// Move current user to the front of the list
- for (index = j = 0, len = users.length; j < len; index = (j += 1)) {
+ for (index = 0, len = users.length; index < len; index += 1) {
obj = users[index];
if (obj.username === firstUser) {
users.splice(index, 1);
@@ -561,7 +561,8 @@ function UsersSelect(currentUser, els, options = {}) {
if (firstUser) {
// Move current user to the front of the list
ref = data.results;
- for (index = j = 0, len = ref.length; j < len; index = (j += 1)) {
+
+ for (index = 0, len = ref.length; index < len; index += 1) {
obj = ref[index];
if (obj.username === firstUser) {
data.results.splice(index, 1);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
index f012f9c6772..5e76f6b1cac 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
@@ -105,7 +105,7 @@ export default {
MRWidgetService.fetchMetrics(this.metricsUrl)
.then((res) => {
if (res.status === statusCodes.NO_CONTENT) {
- this.backOffRequestCounter = this.backOffRequestCounter += 1;
+ this.backOffRequestCounter += 1;
/* eslint-disable no-unused-expressions */
this.backOffRequestCounter < 3 ? next() : stop(res);
} else {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 985f44dee97..1a444c04a1d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -173,7 +173,7 @@
</a>
<clipboard-button
:title="__('Copy commit SHA to clipboard')"
- :text="mr.shortMergeCommitSha"
+ :text="mr.mergeCommitSha"
css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha"
/>
</p>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
new file mode 100644
index 00000000000..bf8628d18a6
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
@@ -0,0 +1,15 @@
+/*
+The squash-before-merge button is EE only, but it's located right in the middle
+of the readyToMerge state component template.
+
+If we didn't declare this component in CE, we'd need to maintain a separate copy
+of the readyToMergeState template in EE, which is pretty big and likely to change.
+
+Instead, in CE, we declare the component, but it's hidden and is configured to do nothing.
+In EE, the configuration extends this object to add a functioning squash-before-merge
+button.
+*/
+
+export default {
+ template: '',
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index bc4ba3d050b..e455c4d2cb5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -172,7 +172,7 @@ export default {
}
})
.catch(() => {
- createFlash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line
+ createFlash('Something went wrong while fetching the environments for this merge request. Please try again.');
});
},
fetchActionsContent() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 134aaacf9d2..c881cd496d1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -26,6 +26,7 @@ export default class MergeRequestStore {
this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
+ this.mergeCommitSha = data.merge_commit_sha;
this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index 0fdea651130..e6e92594b65 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -1,5 +1,7 @@
<script>
import { __ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+
/**
* Port of detail_behavior expand button.
*
@@ -12,6 +14,9 @@ import { __ } from '~/locale';
*/
export default {
name: 'ExpandButton',
+ components: {
+ Icon,
+ },
data() {
return {
isCollapsed: true,
@@ -22,6 +27,9 @@ export default {
return __('Click to expand text');
},
},
+ destroyed() {
+ this.isCollapsed = true;
+ },
methods: {
onClick() {
this.isCollapsed = !this.isCollapsed;
@@ -37,7 +45,10 @@ export default {
type="button"
class="text-expander btn-blank"
@click="onClick">
- ...
+ <icon
+ :size="12"
+ name="ellipsis_h"
+ />
</button>
<span v-if="!isCollapsed">
<slot name="expanded"></slot>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 05e8ed2da2c..7d26390d9bc 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -150,7 +150,7 @@
</div>
<div
v-show="previewMarkdown"
- class="md md-preview-holder md-preview"
+ class="md md-preview-holder md-preview js-vue-md-preview"
>
<div
ref="markdown-preview"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index ee3628b1e3f..83171ae50b8 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -71,7 +71,7 @@
class="md-header-tab"
>
<a
- class="js-preview-link"
+ class="js-preview-link js-md-preview-button"
href="#md-preview-holder"
tabindex="-1"
@click.prevent="previewMarkdownTab($event)"
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index 80e3db52cb0..2eb6c20b2c0 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -14,11 +14,12 @@
</template>
<script>
- import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
- export default {
- components: {
- skeletonLoadingContainer,
- },
- };
+export default {
+ name: 'SkeletonNote',
+ components: {
+ skeletonLoadingContainer,
+ },
+};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index aac10f84087..2122d0a508e 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -1,51 +1,75 @@
<script>
- /**
- * Common component to render a system note, icon and user information.
- *
- * This component needs to be used with a vuex store.
- * That vuex store needs to have a `targetNoteHash` getter
- *
- * @example
- * <system-note
- * :note="{
- * id: String,
- * author: Object,
- * createdAt: String,
- * note_html: String,
- * system_note_icon_name: String
- * }"
- * />
- */
- import { mapGetters } from 'vuex';
- import noteHeader from '~/notes/components/note_header.vue';
- import { spriteIcon } from '../../../lib/utils/common_utils';
+/**
+ * Common component to render a system note, icon and user information.
+ *
+ * This component needs to be used with a vuex store.
+ * That vuex store needs to have a `targetNoteHash` getter
+ *
+ * @example
+ * <system-note
+ * :note="{
+ * id: String,
+ * author: Object,
+ * createdAt: String,
+ * note_html: String,
+ * system_note_icon_name: String
+ * }"
+ * />
+ */
+import $ from 'jquery';
+import { mapGetters } from 'vuex';
+import noteHeader from '~/notes/components/note_header.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { spriteIcon } from '../../../lib/utils/common_utils';
- export default {
- name: 'SystemNote',
- components: {
- noteHeader,
+const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
+
+export default {
+ name: 'SystemNote',
+ components: {
+ Icon,
+ noteHeader,
+ },
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ expanded: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['targetNoteHash']),
+ noteAnchorId() {
+ return `note_${this.note.id}`;
+ },
+ isTargetNote() {
+ return this.targetNoteHash === this.noteAnchorId;
},
- props: {
- note: {
- type: Object,
- required: true,
- },
+ iconHtml() {
+ return spriteIcon(this.note.system_note_icon_name);
},
- computed: {
- ...mapGetters([
- 'targetNoteHash',
- ]),
- noteAnchorId() {
- return `note_${this.note.id}`;
- },
- isTargetNote() {
- return this.targetNoteHash === this.noteAnchorId;
- },
- iconHtml() {
- return spriteIcon(this.note.system_note_icon_name);
- },
+ toggleIcon() {
+ return this.expanded ? 'chevron-up' : 'chevron-down';
},
- };
+ // following 2 methods taken from code in `collapseLongCommitList` of notes.js:
+ actionTextHtml() {
+ return $(this.note.note_html)
+ .unwrap()
+ .html();
+ },
+ hasMoreCommits() {
+ return (
+ $(this.note.note_html)
+ .filter('ul')
+ .children().length > MAX_VISIBLE_COMMIT_LIST_COUNT
+ );
+ },
+ },
+};
</script>
<template>
@@ -64,8 +88,35 @@
:author="note.author"
:created-at="note.created_at"
:note-id="note.id"
- :action-text-html="note.note_html"
- />
+ >
+ <span v-html="actionTextHtml"></span>
+ </note-header>
+ </div>
+ <div class="note-body">
+ <div
+ :class="{
+ 'system-note-commit-list': hasMoreCommits,
+ 'hide-shade': expanded
+ }"
+ class="note-text"
+ v-html="note.note_html"
+ ></div>
+ <div
+ v-if="hasMoreCommits"
+ class="flex-list"
+ >
+ <div
+ class="system-note-commit-list-toggler flex-row"
+ @click="expanded = !expanded"
+ >
+ <Icon
+ :name="toggleIcon"
+ :size="8"
+ class="append-right-5"
+ />
+ <span>Toggle commit list</span>
+ </div>
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index b9693892f45..73b9131e5ba 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -28,7 +28,7 @@ Vue.http.interceptors.push((request, next) => {
response.headers.forEach((value, key) => {
headers[key] = value;
});
- // eslint-disable-next-line no-param-reassign
+
response.headers = headers;
});
});
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index f68a4f28714..0138c9be803 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len, class-methods-use-this */
+/* eslint-disable func-names, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len, class-methods-use-this */
// Zen Mode (full screen) textarea
//
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 5da0e672288..0d8e867f41d 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -128,11 +128,6 @@ table {
border-spacing: 0;
}
-.tooltip {
- // Fix bootstrap4 bug whereby tooltips flicker when they are hovered over their borders
- pointer-events: none;
-}
-
.popover {
font-size: 14px;
}
@@ -178,7 +173,10 @@ table {
display: none;
}
-.badge {
+// Add to .label so that old system notes that are saved to the db
+// will still receive the correct styling
+.badge,
+.label {
padding: 4px 5px;
font-size: 12px;
font-style: normal;
@@ -213,6 +211,15 @@ table {
&:not(:last-of-type) {
border-bottom: 1px solid $well-inner-border;
}
+
+ p,
+ ol,
+ ul,
+ .form-group {
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+ }
}
.badge.badge-gray {
@@ -264,15 +271,36 @@ pre code {
white-space: pre-wrap;
}
+.alert,
+.flash-notice {
+ border-radius: 0;
+}
+
+.alert-success {
+ background-color: $green-500;
+ border-color: $green-500;
+}
+
+.alert-info {
+ background-color: $blue-500;
+ border-color: $blue-500;
+}
+
+.alert-warning {
+ background-color: $orange-500;
+ border-color: $orange-500;
+}
+
.alert-danger {
background-color: $red-500;
border-color: $red-500;
}
+.alert-success,
+.alert-info,
.alert-warning,
.alert-danger,
.flash-notice {
- border-radius: 0;
color: $white-light;
h4,
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 14cd32da9eb..549a8730301 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -251,3 +251,12 @@ $skeleton-line-widths: (
transform: translateX(468px);
}
}
+
+.slide-down-enter-active {
+ transition: transform 0.2s;
+}
+
+.slide-down-enter,
+.slide-down-leave-to {
+ transform: translateY(-30%);
+}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 0de05548c68..1d4828be223 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -13,6 +13,7 @@
&.diff-collapsed {
padding: 5px;
+ line-height: 34px;
.click-to-expand {
cursor: pointer;
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 52c3f18a682..a6e324036ae 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -10,6 +10,20 @@
@extend .alert;
background-color: $blue-500;
margin: 0;
+
+ &.flash-notice-persistent {
+ background-color: $blue-100;
+ color: $gl-text-color;
+
+ a {
+ color: $gl-link-color;
+
+ &:hover {
+ color: $gl-link-hover-color;
+ text-decoration: none;
+ }
+ }
+ }
}
.flash-warning {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 03520f42997..2b2e6d69e33 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -201,6 +201,10 @@ label {
}
.gl-show-field-errors {
+ .form-control {
+ height: 34px;
+ }
+
.gl-field-success-outline {
border: 1px solid $green-600;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index db59c91e375..2fa71b23314 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -558,7 +558,7 @@
background: $white-light;
border-bottom: 1px solid $white-normal;
- .center-logo {
+ .mx-auto {
margin: 8px 0;
text-align: center;
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 55c0bc76f23..52b5f059f20 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -54,10 +54,6 @@ body {
&.limit-container-width {
max-width: $limited-layout-width;
}
-
- &.limit-container-width-sm {
- max-width: $limited-layout-width-sm;
- }
}
.alert-wrapper {
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 2d9e9e6a67d..9dbb04e5443 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -347,7 +347,7 @@
.empty-state .project-item-select-holder.btn-group {
float: none;
- display: inline-block;
+ justify-content: center;
.btn {
// overrides styles applied to plain `.empty-state .btn`
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 04d2a049f7d..f30f296d41f 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -244,10 +244,11 @@ $tooltip-font-size: 12px;
/*
* Padding
*/
-$gl-padding-24: 24px;
-$gl-padding: 16px;
-$gl-padding-8: 8px;
$gl-padding-4: 4px;
+$gl-padding-8: 8px;
+$gl-padding: 16px;
+$gl-padding-24: 24px;
+$gl-padding-32: 32px;
$gl-col-padding: 15px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px;
@@ -265,7 +266,6 @@ $header-height: 40px;
$ide-statusbar-height: 25px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
-$limited-layout-width-sm: 790px;
$container-text-max-width: 540px;
$gl-avatar-size: 40px;
$error-exclamation-point: $red-500;
@@ -834,3 +834,4 @@ $font-family-sans-serif: $regular_font;
$font-family-monospace: $monospace_font;
$input-line-height: 20px;
$btn-line-height: 20px;
+$table-accent-bg: $gray-light;
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index f0ac9b46f91..604f806dc58 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -111,7 +111,9 @@ $dark-il: #de935f;
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
@include dark-diff-match-line;
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index eba7919ada9..8e2720511da 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -111,7 +111,9 @@ $monokai-gi: #a6e22e;
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
@include dark-diff-match-line;
}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index ba53ef0352b..cd1f0f6650f 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -115,7 +115,9 @@ $solarized-dark-il: #2aa198;
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
@include dark-diff-match-line;
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index e9fccf1b58a..09c3ea36414 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -122,7 +122,9 @@ $solarized-light-il: #2aa198;
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
@include matchLine;
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index a4ca82de90e..49226ae8eac 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -135,10 +135,10 @@
}
.text-expander {
- display: inline-block;
+ display: inline-flex;
background: $white-light;
color: $gl-text-color-secondary;
- padding: 0 4px;
+ padding: 1px $gl-padding-4;
cursor: pointer;
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
@@ -180,6 +180,11 @@
.commit-content {
padding-right: 10px;
white-space: normal;
+
+ .commit-title {
+ display: flex;
+ align-items: center;
+ }
}
.commit-actions {
@@ -193,6 +198,10 @@
display: inline-flex;
}
+ .ci-status-icon svg {
+ vertical-align: text-bottom;
+ }
+
> .ci-status-link,
> .btn,
> .commit-sha-group {
@@ -249,7 +258,6 @@
.generic_commit_status {
a,
button {
- color: $gl-text-color;
vertical-align: baseline;
}
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 7b36bcb3c7d..2e007c52592 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -23,7 +23,8 @@
position: relative;
line-height: 35px;
display: flex;
- flex-grow: 1;
+ flex: 1 1;
+ min-width: 0;
@include media-breakpoint-up(sm) {
padding-left: 0;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index fbc97ec0c95..8e8a879be88 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -24,6 +24,10 @@
color: $gl-text-color;
border-radius: 0 0 3px 3px;
+ .code {
+ padding: 0;
+ }
+
.unfold {
cursor: pointer;
}
@@ -61,6 +65,7 @@
.diff-line-num {
width: 50px;
+ position: relative;
a {
transition: none;
@@ -77,6 +82,12 @@
span {
white-space: pre-wrap;
+
+ &.context-cell {
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ }
}
.line {
@@ -677,21 +688,21 @@
}
@include media-breakpoint-up(sm) {
- position: -webkit-sticky;
- position: sticky;
top: 24px;
background-color: $white-light;
- z-index: 190;
&.diff-files-changed-merge-request {
- top: 76px;
+ position: sticky;
+ top: 90px;
+ z-index: 190;
+ margin: $gl-padding 0;
+ padding: 0;
}
&.is-stuck {
padding-top: 0;
padding-bottom: 0;
border-bottom: 1px solid $white-dark;
- transform: translateY(16px);
.diff-stats-additions-deletions-expanded,
.inline-parallel-buttons {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index b42c232fd91..f9fd9f1ab8b 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -698,6 +698,8 @@
font-size: 14px;
line-height: 24px;
align-self: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.js-issuable-selector-wrap {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 73eb399d7bb..79cac7f4ff0 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -280,7 +280,7 @@
width: 150px;
flex-shrink: 0;
- .label {
+ .badge {
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 99fe4a578be..d96ba2107d1 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -46,6 +46,7 @@
.btn {
font-size: $gl-font-size;
+ max-height: 26px;
&[disabled] {
opacity: 0.3;
@@ -599,14 +600,12 @@
position: relative;
background: $gray-light;
color: $gl-text-color;
- z-index: 199;
.mr-version-menus-container {
- display: -webkit-flex;
display: flex;
- -webkit-align-items: center;
align-items: center;
padding: 16px;
+ z-index: 199;
}
.content-block {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 3849a04db5d..5e5696b1602 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -247,22 +247,6 @@
}
.discussion-with-resolve-btn {
- display: table;
- width: 100%;
- border-collapse: separate;
- table-layout: auto;
-
- .btn-group {
- display: table-cell;
- float: none;
- width: 1%;
-
- &:first-child {
- width: 100%;
- padding-right: 5px;
- }
- }
-
.discussion-actions {
display: table;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 299eda53140..25400d886fb 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -3,9 +3,17 @@
*/
@-webkit-keyframes targe3-note {
- from { background: $note-targe3-outside; }
- 50% { background: $note-targe3-inside; }
- to { background: $note-targe3-outside; }
+ from {
+ background: $note-targe3-outside;
+ }
+
+ 50% {
+ background: $note-targe3-inside;
+ }
+
+ to {
+ background: $note-targe3-outside;
+ }
}
ul.notes {
@@ -33,10 +41,12 @@ ul.notes {
.diff-content {
overflow: visible;
+ padding: 0;
}
}
- > li { // .timeline-entry
+ > li {
+ // .timeline-entry
padding: 0;
display: block;
position: relative;
@@ -153,7 +163,6 @@ ul.notes {
}
.note-header {
-
@include notes-media('max', map-get($grid-breakpoints, xs)) {
.inline {
display: block;
@@ -245,7 +254,6 @@ ul.notes {
.system-note-commit-list-toggler {
color: $gl-link-color;
- display: none;
padding: 10px 0 0;
cursor: pointer;
position: relative;
@@ -624,20 +632,18 @@ ul.notes {
.line_holder .is-over:not(.no-comment-btn) {
.add-diff-note {
opacity: 1;
+ z-index: 101;
}
}
.add-diff-note {
@include btn-comment-icon;
opacity: 0;
- margin-top: -2px;
margin-left: -55px;
position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
z-index: 10;
-
- .new & {
- margin-top: -10px;
- }
}
.discussion-body,
@@ -665,7 +671,6 @@ ul.notes {
background-color: $white-light;
}
-
a {
color: $gl-link-color;
}
@@ -771,3 +776,44 @@ ul.notes {
height: auto;
}
}
+
+// Vue refactored diff discussion adjustments
+.files {
+ .diff-discussions {
+ .note-discussion.timeline-entry {
+ padding-left: 0;
+
+ &:last-child {
+ border-bottom: 0;
+ }
+
+ > .timeline-entry-inner {
+ padding: 0;
+
+ > .timeline-content {
+ margin-left: 0;
+ }
+
+ > .timeline-icon {
+ display: none;
+ }
+ }
+
+ .discussion-body {
+ padding-top: 0;
+
+ .discussion-wrapper {
+ border-color: transparent;
+ }
+ }
+ }
+ }
+
+ .diff-comment-form {
+ display: block;
+ }
+
+ .add-diff-note svg {
+ margin-top: 4px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 4e1768f556a..52332ac97dd 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -1001,7 +1001,7 @@ button.mini-pipeline-graph-dropdown-toggle {
/**
* Center dropdown menu in mini graph
*/
- &.dropdown-menu {
+ .dropdown &.dropdown-menu {
transform: translate(-80%, 0);
@media (min-width: map-get($grid-breakpoints, md)) {
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index babe81cb0f7..a353f301d07 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -16,7 +16,7 @@
.application-theme {
label {
- margin: 0 $gl-padding $gl-padding 0;
+ margin: 0 $gl-padding-32 $gl-padding 0;
text-align: center;
}
@@ -24,7 +24,7 @@
font-size: 0;
height: 48px;
border-radius: 4px;
- min-width: 135px;
+ min-width: 112px;
margin-bottom: $gl-padding-8;
&.ui-indigo {
@@ -75,7 +75,8 @@
.syntax-theme {
label {
- margin-right: 20px;
+ margin-right: $gl-padding-32;
+ margin-bottom: $gl-padding;
text-align: center;
.preview {
@@ -84,7 +85,6 @@
img {
border-radius: 4px;
-
max-width: 100%;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 7ac0eaec645..aa83e5bdebc 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -858,7 +858,6 @@ pre.light-well {
.git-clone-holder {
width: 380px;
- height: 28px;
.btn-clipboard {
border: 1px solid $border-color;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 4b8a3db1d06..0a56153203c 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -540,36 +540,12 @@
margin-right: -$grid-size;
min-height: 60px;
- .multi-file-commit-list-item {
- margin-left: 0;
- margin-right: 0;
- }
-
&.form-text.text-muted {
margin-left: 0;
right: 0;
}
}
-.multi-file-commit-list-item {
- &.is-active {
- background-color: $white-normal;
- }
-
- .multi-file-discard-btn {
- display: none;
- margin-top: -2px;
- margin-left: auto;
- color: $gl-link-color;
- }
-
- &:hover {
- .multi-file-discard-btn {
- display: flex;
- }
- }
-}
-
.multi-file-addition,
.multi-file-addition-solid {
color: $green-500;
@@ -599,7 +575,7 @@
}
}
-.multi-file-commit-list-item,
+.multi-file-commit-list-path,
.ide-file-list .file {
display: flex;
align-items: center;
@@ -616,11 +592,9 @@
}
.multi-file-commit-list-path {
- padding: 0;
- background: none;
- border: 0;
- text-align: left;
- width: 100%;
+ &.is-active {
+ background-color: $white-normal;
+ }
&:hover,
&:focus {
@@ -635,7 +609,7 @@
}
.multi-file-commit-list-file-path {
- @include str-truncated(100%);
+ @include str-truncated(calc(100% - 30px));
&:hover {
text-decoration: underline;
@@ -646,6 +620,16 @@
}
}
+.multi-file-discard-btn {
+ top: 4px;
+ right: 8px;
+ bottom: 4px;
+
+ svg {
+ top: 0;
+ }
+}
+
.multi-file-commit-form {
position: relative;
background-color: $white-light;
@@ -840,18 +824,20 @@
}
.ide-staged-action-btn {
- margin-left: auto;
- line-height: 22px;
+ width: 22px;
+ margin-left: -1px;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+
+ > svg {
+ top: 0;
+ }
}
.ide-commit-file-count {
min-width: 22px;
- margin-left: auto;
background-color: $gray-light;
- border-radius: $border-radius-default;
border: 1px solid $white-dark;
- line-height: 20px;
- text-align: center;
}
.ide-commit-radios {
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 4abb145067a..2f28031b9c8 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -127,13 +127,6 @@
color: $gl-danger;
}
-.service-settings {
- input[type="radio"],
- input[type="checkbox"] {
- margin-top: 10px;
- }
-}
-
.integration-settings-form {
.card.card-body,
.info-well {
@@ -296,7 +289,8 @@
}
.btn-clipboard {
- margin-left: 5px;
+ background-color: $white-light;
+ border: 1px solid $theme-gray-200;
}
.deploy-token-help-block {
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index 2b47819303e..fb788c47ef1 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -9,7 +9,7 @@ class Admin::HooksController < Admin::ApplicationController
end
def create
- @hook = SystemHook.new(hook_params)
+ @hook = SystemHook.new(hook_params.to_h)
if @hook.save
redirect_to admin_hooks_path, notice: 'Hook was successfully created.'
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index d04eb192129..ba510968684 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -90,7 +90,7 @@ module IssuableActions
end
def discussions
- notes = issuable.notes
+ notes = issuable.discussion_notes
.inc_relations_for_view
.includes(:noteable)
.fresh
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index b6eb7d292fc..9d58656773d 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -1,6 +1,7 @@
module IssuesAction
extend ActiveSupport::Concern
include IssuableCollections
+ include IssuesCalendar
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def issues
@@ -17,18 +18,9 @@ module IssuesAction
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
def issues_calendar
- @issues = issuables_collection
- .non_archived
- .with_due_date
- .limit(100)
-
- respond_to do |format|
- format.ics { response.headers['Content-Disposition'] = 'inline' }
- end
+ render_issues_calendar(issuables_collection)
end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
private
diff --git a/app/controllers/concerns/issues_calendar.rb b/app/controllers/concerns/issues_calendar.rb
new file mode 100644
index 00000000000..671a204621d
--- /dev/null
+++ b/app/controllers/concerns/issues_calendar.rb
@@ -0,0 +1,24 @@
+module IssuesCalendar
+ extend ActiveSupport::Concern
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def render_issues_calendar(issuables)
+ @issues = issuables
+ .non_archived
+ .with_due_date
+ .limit(100)
+
+ respond_to do |format|
+ format.ics do
+ # NOTE: with text/calendar as Content-Type, the browser always downloads
+ # the content as a file (even ignoring the Content-Disposition
+ # header). We want to display the content inline when accessed
+ # from GitLab, similarly to the RSS feed.
+ if request.referer&.start_with?(::Settings.gitlab.base_url)
+ response.headers['Content-Type'] = 'text/plain'
+ end
+ end
+ end
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 0c34e49206a..fe9a030cdf2 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -237,10 +237,6 @@ module NotesActions
def use_note_serializer?
return false if params['html']
- if noteable.is_a?(MergeRequest)
- cookies[:vue_mr_discussions] == 'true'
- else
- noteable.discussions_rendered_on_frontend?
- end
+ noteable.discussions_rendered_on_frontend?
end
end
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index 170bca8b56f..16374146ae4 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -1,9 +1,15 @@
module UploadsActions
+ extend ActiveSupport::Concern
+
include Gitlab::Utils::StrongMemoize
include SendFileUpload
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
+ included do
+ prepend_before_action :set_html_format, only: :show
+ end
+
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
@@ -41,6 +47,13 @@ module UploadsActions
private
+ # Explicitly set the format.
+ # Otherwise rails 5 will set it from a file extension.
+ # See https://github.com/rails/rails/commit/84e8accd6fb83031e4c27e44925d7596655285f7#diff-2b8f2fbb113b55ca8e16001c393da8f1
+ def set_html_format
+ request.format = :html
+ end
+
def uploader_class
raise NotImplementedError
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 68d328fa797..ff133001b84 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -54,7 +54,7 @@ class DashboardController < Dashboard::ApplicationController
return unless @no_filters_set
respond_to do |format|
- format.html
+ format.html { render }
format.atom { head :bad_request }
end
end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index abc283d7aa9..6484a713f8e 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -7,6 +7,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep]
before_action :extract_ref_name_and_path
+ before_action :set_request_format, only: [:file]
before_action :validate_artifacts!
before_action :entry, only: [:file]
@@ -101,4 +102,12 @@ class Projects::ArtifactsController < Projects::ApplicationController
render_404 unless @entry.exists?
end
+
+ def set_request_format
+ request.format = :html if set_request_format?
+ end
+
+ def set_request_format?
+ request.format != :json
+ end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 7c65f1f5dfe..ebc61264b39 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -3,10 +3,11 @@ class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
include CreatesCommit
include RendersBlob
+ include NotesHelper
include ActionView::Helpers::SanitizeHelper
-
prepend_before_action :authenticate_user!, only: [:edit]
+ before_action :set_request_format, only: [:edit, :show, :update]
before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code!
@@ -92,6 +93,7 @@ class Projects::BlobController < Projects::ApplicationController
@lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines
@form = UnfoldForm.new(params)
+
@lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe)
if @form.bottom?
@@ -102,11 +104,50 @@ class Projects::BlobController < Projects::ApplicationController
@match_line = "@@ -#{line}+#{line} @@"
end
- render layout: false
+ # We can keep only 'render_diff_lines' from this conditional when
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/44988 is done
+ if rendered_for_merge_request?
+ render_diff_lines
+ else
+ render layout: false
+ end
end
private
+ # Converts a String array to Gitlab::Diff::Line array
+ def render_diff_lines
+ @lines.map! do |line|
+ # These are marked as context lines but are loaded from blobs.
+ # We also have context lines loaded from diffs in other places.
+ diff_line = Gitlab::Diff::Line.new(line, 'context', nil, nil, nil)
+ diff_line.rich_text = line
+ diff_line
+ end
+
+ add_match_line
+
+ render json: @lines
+ end
+
+ def add_match_line
+ return unless @form.unfold?
+
+ if @form.bottom? && @form.to < @blob.lines.size
+ old_pos = @form.to - @form.offset
+ new_pos = @form.to
+ elsif @form.since != 1
+ old_pos = new_pos = @form.since
+ end
+
+ # Match line is not needed when it reaches the top limit or bottom limit of the file.
+ return unless new_pos
+
+ @match_line = Gitlab::Diff::Line.new(@match_line, 'match', nil, old_pos, new_pos)
+
+ @form.bottom? ? @lines.push(@match_line) : @lines.unshift(@match_line)
+ end
+
def blob
@blob ||= @repository.blob_at(@commit.id, @path)
@@ -188,6 +229,18 @@ class Projects::BlobController < Projects::ApplicationController
.last_for_path(@repository, @ref, @path).sha
end
+ # In Rails 4.2 if params[:format] is empty, Rails set it to :html
+ # But since Rails 5.0 the framework now looks for an extension.
+ # E.g. for `blob/master/CHANGELOG.md` in Rails 4 the format would be `:html`, but in Rails 5 on it'd be `:md`
+ # This before_action explicitly sets the `:html` format for all requests unless `:format` is set by a client e.g. by JS for XHR requests.
+ def set_request_format
+ request.format = :html if set_request_format?
+ end
+
+ def set_request_format?
+ params[:id].present? && params[:format].blank? && request.format != "json"
+ end
+
def show_html
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index b7b36f770f5..cd7250b10fc 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -31,7 +31,10 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
- render
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/48097
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render
+ end
end
format.json do
branches = BranchesFinder.new(@repository, params).execute
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 7b7cb52d7ed..9e495061f4e 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -9,6 +9,7 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :assign_ref_vars
before_action :authorize_download_code!
before_action :set_commits
+ before_action :set_request_format, only: :show
def show
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
@@ -61,6 +62,19 @@ class Projects::CommitsController < Projects::ApplicationController
@commits = prepare_commits_for_rendering(@commits)
end
+ # Rails 5 sets request.format from the extension.
+ # Explicitly set to :html.
+ def set_request_format
+ request.format = :html if set_request_format?
+ end
+
+ # Rails 5 sets request.format from extension.
+ # In this case if the ref ends with `.atom`, it's expected to be the html response,
+ # not the atom one. So explicitly set request.format as :html to act like rails4.
+ def set_request_format?
+ request.format.to_s == "text/html" || @commits.ref.ends_with?("atom")
+ end
+
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42330')
end
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index 8e86af43fee..78b9d53a780 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -21,7 +21,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
def show
render json: {
- discussion_html: view_to_html_string('discussions/_diff_with_notes', discussion: discussion, expanded: true)
+ truncated_diff_lines: discussion.try(:truncated_diff_lines)
}
end
@@ -29,11 +29,6 @@ class Projects::DiscussionsController < Projects::ApplicationController
def render_discussion
if serialize_notes?
- # TODO - It is not needed to serialize notes when resolving
- # or unresolving discussions. We should remove this behavior
- # passing a parameter to DiscussionEntity to return an empty array
- # for notes.
- # Check issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/42853
prepare_notes_for_rendering(discussion.notes, merge_request)
render_json_with_discussions_serializer
else
@@ -44,7 +39,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
def render_json_with_discussions_serializer
render json:
DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user, note_entity: ProjectNoteEntity)
- .represent(discussion, context: self)
+ .represent(discussion, context: self, render_truncated_diff_lines: true)
end
# Legacy method used to render discussions notes when not using Vue on views.
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 35c36c725e2..7c897b2d86c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -4,6 +4,7 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableActions
include ToggleAwardEmoji
include IssuableCollections
+ include IssuesCalendar
include SpammableActions
prepend_before_action :authenticate_user!, only: [:new]
@@ -40,14 +41,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def calendar
- @issues = @issuables
- .non_archived
- .with_due_date
- .limit(100)
-
- respond_to do |format|
- format.ics { response.headers['Content-Disposition'] = 'inline' }
- end
+ render_issues_calendar(@issuables)
end
def new
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index fe8525a488c..48e02581d54 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -9,17 +9,21 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
before_action :define_diff_comment_vars
def show
- @environment = @merge_request.environments_for(current_user).last
-
- render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
+ render_diffs
end
def diff_for_path
- render_diff_for_path(@diffs)
+ render_diffs
end
private
+ def render_diffs
+ @environment = @merge_request.environments_for(current_user).last
+
+ render json: DiffsSerializer.new(current_user: current_user).represent(@diffs, additional_attributes)
+ end
+
def define_diff_vars
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
@compare = commit || find_merge_request_diff_compare
@@ -63,6 +67,19 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
end
end
+ def additional_attributes
+ {
+ environment: @environment,
+ merge_request: @merge_request,
+ merge_request_diff: @merge_request_diff,
+ merge_request_diffs: @merge_request_diffs,
+ start_version: @start_version,
+ start_sha: @start_sha,
+ commit: @commit,
+ latest_diff: @merge_request_diff&.latest?
+ }
+ end
+
def define_diff_comment_vars
@new_diff_note_attrs = {
noteable_type: 'MergeRequest',
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index b452bfd7e6f..a7c5f858c42 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -42,6 +42,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@noteable = @merge_request
@commits_count = @merge_request.commits_count
+ # TODO cleanup- Fatih Simon Create an issue to remove these after the refactoring
+ # we no longer render notes here. I see it will require a small frontend refactoring,
+ # since we gather some data from this collection.
@discussions = @merge_request.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
@@ -115,7 +118,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
format.json do
- render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+ render json: serializer.represent(@merge_request, serializer: 'basic')
end
end
rescue ActiveRecord::StaleObjectError
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a93b116c6fe..efb30ba4715 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -247,13 +247,13 @@ class ProjectsController < Projects::ApplicationController
if find_branches
branches = BranchesFinder.new(@repository, params).execute.take(100).map(&:name)
- options[s_('RefSwitcher|Branches')] = branches
+ options['Branches'] = branches
end
if find_tags && @repository.tag_count.nonzero?
tags = TagsFinder.new(@repository, params).execute.take(100).map(&:name)
- options[s_('RefSwitcher|Tags')] = tags
+ options['Tags'] = tags
end
# If reference is commit id - we should add it to branch/tag selectbox
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 35f4ff2f62f..9b7a35fb3b5 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -83,7 +83,7 @@ class NotesFinder
when "personal_snippet"
PersonalSnippet.all
else
- raise 'invalid target_type'
+ raise "invalid target_type '#{noteable_type}'"
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index d42284868c7..9f501ea55fb 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -157,7 +157,7 @@ module IssuablesHelper
output = ""
output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do
- author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline-block", tooltip: true)
+ author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline", tooltip: true)
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none")
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 5ff06b3e0fc..82a7931c557 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -86,6 +86,8 @@ module MergeRequestsHelper
end
def version_index(merge_request_diff)
+ return nil if @merge_request_diffs.empty?
+
@merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff)
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 7f67574a428..5459bb63397 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -174,11 +174,11 @@ module NotesHelper
discussion.resolved_by_push? ? 'Automatically resolved' : 'Resolved'
end
- def has_vue_discussions_cookie?
- cookies[:vue_mr_discussions] == 'true'
+ def rendered_for_merge_request?
+ params[:from_merge_request].present?
end
def serialize_notes?
- has_vue_discussions_cookie? && !params['html']
+ rendered_for_merge_request? || params['html'].nil?
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index daad829faa2..be3958c40a4 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -350,11 +350,15 @@ module ProjectsHelper
if allowed_protocols_present?
enabled_protocol
else
- if !current_user || current_user.require_ssh_key?
- gitlab_config.protocol
- else
- 'ssh'
- end
+ extra_default_clone_protocol
+ end
+ end
+
+ def extra_default_clone_protocol
+ if !current_user || current_user.require_ssh_key?
+ gitlab_config.protocol
+ else
+ 'ssh'
end
end
@@ -407,6 +411,7 @@ module ProjectsHelper
@ref || @repository.try(:root_ref)
end
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1235
def sanitize_repo_path(project, message)
return '' unless message.present?
@@ -500,4 +505,37 @@ module ProjectsHelper
"list-label"
end
end
+
+ def sidebar_settings_paths
+ %w[
+ projects#edit
+ project_members#index
+ integrations#show
+ services#edit
+ repository#show
+ ci_cd#show
+ badges#index
+ pages#show
+ ]
+ end
+
+ def sidebar_repository_paths
+ %w[
+ tree
+ blob
+ blame
+ edit_tree
+ new_tree
+ find_file
+ commit
+ commits
+ compare
+ projects/repositories
+ tags
+ branches
+ releases
+ graphs
+ network
+ ]
+ end
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 5ba3a4a322c..70509e9066d 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -59,8 +59,6 @@ module Emails
def merge_request_unmergeable_email(recipient_id, merge_request_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
- @reasons = MergeRequestPresenter.new(@merge_request, current_user: current_user).unmergeable_reasons
-
mail_answer_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id, reason))
end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index c702c4ee807..48137c2ed68 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -3,7 +3,7 @@ module Clusters
class Prometheus < ActiveRecord::Base
include PrometheusAdapter
- VERSION = "2.0.0".freeze
+ VERSION = '6.7.3'.freeze
self.table_name = 'clusters_applications_prometheus'
@@ -37,6 +37,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name,
chart: chart,
+ version: version,
values: values
)
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 44150b37708..b93c1145f82 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -107,6 +107,10 @@ module Issuable
false
end
+ def etag_caching_enabled?
+ false
+ end
+
def has_multiple_assignees?
assignees.count > 1
end
diff --git a/app/models/concerns/redis_cacheable.rb b/app/models/concerns/redis_cacheable.rb
index b5425295130..3bdc1330d23 100644
--- a/app/models/concerns/redis_cacheable.rb
+++ b/app/models/concerns/redis_cacheable.rb
@@ -48,7 +48,7 @@ module RedisCacheable
def cast_value_from_cache(attribute, value)
if Gitlab.rails5?
- self.class.type_for_attribute(attribute).cast(value)
+ self.class.type_for_attribute(attribute.to_s).cast(value)
else
self.class.column_for_attribute(attribute).type_cast_from_database(value)
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 92482a1a875..35a0ef00856 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -17,6 +17,10 @@ class Discussion
to: :first_note
+ def project_id
+ project&.id
+ end
+
def self.build(notes, context_noteable = nil)
notes.first.discussion_class(context_noteable).new(notes, context_noteable)
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d136700836d..d3df2da14e2 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -308,6 +308,10 @@ class Issue < ActiveRecord::Base
end
end
+ def etag_caching_enabled?
+ true
+ end
+
def discussions_rendered_on_frontend?
true
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 1cf04976602..7bbcaa121ca 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -85,11 +85,16 @@ class Label < ActiveRecord::Base
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
- (?<label_id>\d+(?!\S\w)\b) | # Integer-based label ID, or
- (?<label_name>
- [A-Za-z0-9_\-\?\.&]+ | # String-based single-word label title, or
- ".+?" # String-based multi-word label surrounded in quotes
- )
+ (?<label_id>\d+(?!\S\w)\b)
+ | # Integer-based label ID, or
+ (?<label_name>
+ # String-based single-word label title, or
+ [A-Za-z0-9_\-\?\.&]+
+ (?<!\.|\?)
+ |
+ # String-based multi-word label surrounded in quotes
+ ".+?"
+ )
)
}x
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 324065c1162..3df1130a6e2 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -128,8 +128,17 @@ class MergeRequest < ActiveRecord::Base
end
after_transition unchecked: :cannot_be_merged do |merge_request, transition|
- NotificationService.new.merge_request_unmergeable(merge_request)
- TodoService.new.merge_request_became_unmergeable(merge_request)
+ begin
+ # Merge request can become unmergeable due to many reasons.
+ # We only notify if it is due to conflict.
+ unless merge_request.project.repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch)
+ NotificationService.new.merge_request_unmergeable(merge_request)
+ TodoService.new.merge_request_became_unmergeable(merge_request)
+ end
+ rescue Gitlab::Git::CommandError
+ # Checking mergeability can trigger exception, e.g. non-utf8
+ # We ignore this type of errors.
+ end
end
def check_state?(merge_status)
@@ -1115,6 +1124,10 @@ class MergeRequest < ActiveRecord::Base
true
end
+ def discussions_rendered_on_frontend?
+ true
+ end
+
def update_project_counter_caches
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 41c04ae0571..abc40d9016e 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -384,6 +384,7 @@ class Note < ActiveRecord::Base
def expire_etag_cache
return unless noteable&.discussions_rendered_on_frontend?
+ return unless noteable&.etag_caching_enabled?
Gitlab::EtagCaching::Store.new.touch(etag_key)
end
diff --git a/app/models/project.rb b/app/models/project.rb
index e5fa1c4db7b..0d777515536 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -25,6 +25,7 @@ class Project < ActiveRecord::Base
include FastDestroyAll::Helpers
include WithUploads
include BatchDestroyDependentAssociations
+ extend Gitlab::Cache::RequestCache
extend Gitlab::ConfigHelper
@@ -2013,6 +2014,11 @@ class Project < ActiveRecord::Base
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
end
+ def any_lfs_file_locks?
+ lfs_file_locks.any?
+ end
+ request_cache(:any_lfs_file_locks?) { self.id }
+
private
def storage
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index ae0debbd3ac..a60b4c7fd0d 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -155,6 +155,7 @@ class ChatNotificationService < Service
end
def notify_for_ref?(data)
+ return true if data[:object_kind] == 'tag_push'
return true if data.dig(:object_attributes, :tag)
return true unless notify_only_default_branch?
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e4202505634..3089d0162ee 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -154,7 +154,10 @@ class Repository
# Returns a list of commits that are not present in any reference
def new_commits(newrev)
- refs = ::Gitlab::Git::RevList.new(raw, newrev: newrev).new_refs
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1233
+ refs = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ ::Gitlab::Git::RevList.new(raw, newrev: newrev).new_refs
+ end
refs.map { |sha| commit(sha.strip) }
end
@@ -847,7 +850,7 @@ class Repository
@root_ref_sha ||= commit(root_ref).sha
end
- delegate :merged_branch_names, :can_be_merged?, to: :raw_repository
+ delegate :merged_branch_names, to: :raw_repository
def merge_base(first_commit_id, second_commit_id)
first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 8ea5435d740..199bcf92b21 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -297,6 +297,7 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:build))
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
prevent(*create_read_update_admin_destroy(:environment))
+ prevent(*create_read_update_admin_destroy(:cluster))
prevent(*create_read_update_admin_destroy(:deployment))
end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 8d466c33510..eb54ab2cda6 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -20,17 +20,6 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
end
- def unmergeable_reasons
- strong_memoize(:unmergeable_reasons) do
- reasons = []
- reasons << "no commits" if merge_request.has_no_commits?
- reasons << "source branch is missing" unless merge_request.source_branch_exists?
- reasons << "target branch is missing" unless merge_request.target_branch_exists?
- reasons << "has merge conflicts" unless merge_request.project.repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch)
- reasons
- end
- end
-
def cancel_merge_when_pipeline_succeeds_path
if can_cancel_merge_when_pipeline_succeeds?(current_user)
cancel_merge_when_pipeline_succeeds_project_merge_request_path(project, merge_request)
diff --git a/app/serializers/blob_entity.rb b/app/serializers/blob_entity.rb
index ad039a2623d..b501fd5e964 100644
--- a/app/serializers/blob_entity.rb
+++ b/app/serializers/blob_entity.rb
@@ -3,11 +3,13 @@ class BlobEntity < Grape::Entity
expose :id, :path, :name, :mode
+ expose :readable_text?, as: :readable_text
+
expose :icon do |blob|
IconsHelper.file_type_icon_class('file', blob.mode, blob.name)
end
- expose :url do |blob|
+ expose :url, if: -> (*) { request.respond_to?(:ref) } do |blob|
project_blob_path(request.project, File.join(request.ref, blob.path))
end
end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index 6e68d275047..aa289a96975 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -1,25 +1,46 @@
class DiffFileEntity < Grape::Entity
+ include RequestAwareEntity
+ include BlobHelper
+ include CommitsHelper
include DiffHelper
include SubmoduleHelper
include BlobHelper
include IconsHelper
- include ActionView::Helpers::TagHelper
+ include TreeHelper
+ include ChecksCollaboration
+ include Gitlab::Utils::StrongMemoize
expose :submodule?, as: :submodule
expose :submodule_link do |diff_file|
- submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository).first
+ memoized_submodule_links(diff_file).first
+ end
+
+ expose :submodule_tree_url do |diff_file|
+ memoized_submodule_links(diff_file).last
end
- expose :blob_path do |diff_file|
- diff_file.blob.path
+ expose :blob, using: BlobEntity
+
+ expose :can_modify_blob do |diff_file|
+ merge_request = options[:merge_request]
+
+ if merge_request&.source_project && current_user
+ can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
+ else
+ false
+ end
end
- expose :blob_icon do |diff_file|
- blob_icon(diff_file.b_mode, diff_file.file_path)
+ expose :file_hash do |diff_file|
+ Digest::SHA1.hexdigest(diff_file.file_path)
end
expose :file_path
+ expose :too_large?, as: :too_large
+ expose :collapsed?, as: :collapsed
+ expose :new_file?, as: :new_file
+
expose :deleted_file?, as: :deleted_file
expose :renamed_file?, as: :renamed_file
expose :old_path
@@ -28,6 +49,36 @@ class DiffFileEntity < Grape::Entity
expose :a_mode
expose :b_mode
expose :text?, as: :text
+ expose :added_lines
+ expose :removed_lines
+ expose :diff_refs
+ expose :content_sha
+ expose :stored_externally?, as: :stored_externally
+ expose :external_storage
+
+ expose :load_collapsed_diff_url, if: -> (diff_file, options) { diff_file.text? && options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+ project = merge_request.target_project
+
+ next unless project
+
+ diff_for_path_namespace_project_merge_request_path(
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid,
+ old_path: diff_file.old_path,
+ new_path: diff_file.new_path,
+ file_identifier: diff_file.file_identifier
+ )
+ end
+
+ expose :formatted_external_url, if: -> (_, options) { options[:environment] } do |diff_file|
+ options[:environment].formatted_external_url
+ end
+
+ expose :external_url, if: -> (_, options) { options[:environment] } do |diff_file|
+ options[:environment].external_url_for(diff_file.new_path, diff_file.content_sha)
+ end
expose :old_path_html do |diff_file|
old_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
@@ -38,4 +89,64 @@ class DiffFileEntity < Grape::Entity
_, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
new_path
end
+
+ expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+
+ options = merge_request.persisted? ? { from_merge_request_iid: merge_request.iid } : {}
+
+ next unless merge_request.source_project
+
+ project_edit_blob_path(merge_request.source_project,
+ tree_join(merge_request.source_branch, diff_file.new_path),
+ options)
+ end
+
+ expose :view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+
+ project = merge_request.target_project
+
+ next unless project
+
+ project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path))
+ end
+
+ expose :replaced_view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image'
+ image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha
+
+ merge_request = options[:merge_request]
+ project = merge_request.target_project
+
+ next unless project
+
+ project_blob_path(project, tree_join(diff_file.old_content_sha, diff_file.old_path)) if image_diff && image_replaced
+ end
+
+ expose :context_lines_path, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
+ project_blob_diff_path(diff_file.repository.project, tree_join(diff_file.content_sha, diff_file.file_path))
+ end
+
+ # Used for inline diffs
+ expose :highlighted_diff_lines, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
+ diff_file.diff_lines_for_serializer
+ end
+
+ # Used for parallel diffs
+ expose :parallel_diff_lines, if: -> (diff_file, _) { diff_file.text? }
+
+ def current_user
+ request.current_user
+ end
+
+ def memoized_submodule_links(diff_file)
+ strong_memoize(:submodule_links) do
+ if diff_file.submodule?
+ submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository)
+ else
+ []
+ end
+ end
+ end
end
diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb
new file mode 100644
index 00000000000..bb804e5347a
--- /dev/null
+++ b/app/serializers/diffs_entity.rb
@@ -0,0 +1,65 @@
+class DiffsEntity < Grape::Entity
+ include DiffHelper
+ include RequestAwareEntity
+
+ expose :real_size
+ expose :size
+
+ expose :branch_name do |diffs|
+ merge_request&.source_branch
+ end
+
+ expose :target_branch_name do |diffs|
+ merge_request&.target_branch
+ end
+
+ expose :commit do |diffs|
+ options[:commit]
+ end
+
+ expose :merge_request_diff, using: MergeRequestDiffEntity do |diffs|
+ options[:merge_request_diff]
+ end
+
+ expose :start_version, using: MergeRequestDiffEntity do |diffs|
+ options[:start_version]
+ end
+
+ expose :latest_diff do |diffs|
+ options[:latest_diff]
+ end
+
+ expose :latest_version_path, if: -> (*) { merge_request } do |diffs|
+ diffs_project_merge_request_path(merge_request&.project, merge_request)
+ end
+
+ expose :added_lines do |diffs|
+ diffs.diff_files.sum(&:added_lines)
+ end
+
+ expose :removed_lines do |diffs|
+ diffs.diff_files.sum(&:removed_lines)
+ end
+
+ expose :render_overflow_warning do |diffs|
+ render_overflow_warning?(diffs.diff_files)
+ end
+
+ expose :email_patch_path, if: -> (*) { merge_request } do |diffs|
+ merge_request_path(merge_request, format: :patch)
+ end
+
+ expose :plain_diff_path, if: -> (*) { merge_request } do |diffs|
+ merge_request_path(merge_request, format: :diff)
+ end
+
+ expose :diff_files, using: DiffFileEntity
+
+ expose :merge_request_diffs, using: MergeRequestDiffEntity, if: -> (_, options) { options[:merge_request_diffs]&.any? } do |diffs|
+ options[:merge_request_diffs]
+ end
+
+ def merge_request
+ options[:merge_request]
+ end
+end
diff --git a/app/serializers/diffs_serializer.rb b/app/serializers/diffs_serializer.rb
new file mode 100644
index 00000000000..6771e10c5ac
--- /dev/null
+++ b/app/serializers/diffs_serializer.rb
@@ -0,0 +1,3 @@
+class DiffsSerializer < BaseSerializer
+ entity DiffsEntity
+end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index 718fb35e62d..63f28133a64 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -1,16 +1,31 @@
class DiscussionEntity < Grape::Entity
include RequestAwareEntity
+ include NotesHelper
expose :id, :reply_id
+ expose :position, if: -> (d, _) { d.diff_discussion? }
+ expose :line_code, if: -> (d, _) { d.diff_discussion? }
expose :expanded?, as: :expanded
+ expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? }
+ expose :project_id
expose :notes do |discussion, opts|
request.note_entity.represent(discussion.notes, opts)
end
+ expose :discussion_path do |discussion|
+ discussion_path(discussion)
+ end
+
expose :individual_note?, as: :individual_note
- expose :resolvable?, as: :resolvable
+ expose :resolvable do |discussion|
+ discussion.resolvable?
+ end
+
expose :resolved?, as: :resolved
+ expose :resolved_by_push?, as: :resolved_by_push
+ expose :resolved_by
+ expose :resolved_at
expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion|
resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id)
end
@@ -18,24 +33,17 @@ class DiscussionEntity < Grape::Entity
new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id)
end
- expose :diff_file, using: DiffFileEntity, if: -> (d, _) { defined? d.diff_file }
+ expose :diff_file, using: DiffFileEntity, if: -> (d, _) { d.diff_discussion? }
expose :diff_discussion?, as: :diff_discussion
- expose :truncated_diff_lines, if: -> (d, _) { (defined? d.diff_file) && d.diff_file.text? } do |discussion|
- options[:context].render_to_string(
- partial: "projects/diffs/line",
- collection: discussion.truncated_diff_lines,
- as: :line,
- locals: { diff_file: discussion.diff_file,
- discussion_expanded: true,
- plain: true },
- layout: false,
- formats: [:html]
- )
+ expose :truncated_diff_lines_path, if: -> (d, _) { !d.expanded? && !render_truncated_diff_lines? } do |discussion|
+ project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion)
end
- expose :image_diff_html, if: -> (d, _) { defined? d.diff_file } do |discussion|
+ expose :truncated_diff_lines, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) }
+
+ expose :image_diff_html, if: -> (d, _) { d.diff_discussion? && d.on_image? } do |discussion|
diff_file = discussion.diff_file
partial = diff_file.new_file? || diff_file.deleted_file? ? 'single_image_diff' : 'replaced_image_diff'
options[:context].render_to_string(
@@ -47,4 +55,17 @@ class DiscussionEntity < Grape::Entity
formats: [:html]
)
end
+
+ expose :for_commit?, as: :for_commit
+ expose :commit_id
+
+ private
+
+ def render_truncated_diff_lines?
+ options[:render_truncated_diff_lines]
+ end
+
+ def current_user
+ request.current_user
+ end
end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index e4aec977f01..1c06691026d 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -5,4 +5,8 @@ class MergeRequestBasicEntity < IssuableSidebarEntity
expose :state
expose :source_branch_exists?, as: :source_branch_exists
expose :rebase_in_progress?, as: :rebase_in_progress
+ expose :milestone, using: API::Entities::Milestone
+ expose :labels, using: LabelEntity
+ expose :assignee, using: API::Entities::UserBasic
+ expose :task_status, :task_status_short
end
diff --git a/app/serializers/merge_request_diff_entity.rb b/app/serializers/merge_request_diff_entity.rb
new file mode 100644
index 00000000000..32c761b45ac
--- /dev/null
+++ b/app/serializers/merge_request_diff_entity.rb
@@ -0,0 +1,46 @@
+class MergeRequestDiffEntity < Grape::Entity
+ include Gitlab::Routing
+ include GitHelper
+ include MergeRequestsHelper
+
+ expose :version_index do |merge_request_diff|
+ @merge_request_diffs = options[:merge_request_diffs]
+ diff = options[:merge_request_diff]
+
+ next unless diff.present?
+ next unless @merge_request_diffs.size > 1
+
+ version_index(merge_request_diff)
+ end
+
+ expose :created_at
+ expose :commits_count
+
+ expose :latest?, as: :latest
+
+ expose :short_commit_sha do |merge_request_diff|
+ short_sha(merge_request_diff.head_commit_sha)
+ end
+
+ expose :version_path do |merge_request_diff|
+ start_sha = options[:start_sha]
+ project = merge_request.target_project
+
+ next unless project
+
+ merge_request_version_path(project, merge_request, merge_request_diff, start_sha)
+ end
+
+ expose :compare_path do |merge_request_diff|
+ project = merge_request.target_project
+ diff = options[:merge_request_diff]
+
+ if project && diff
+ merge_request_version_path(project, merge_request, diff, merge_request_diff.head_commit_sha)
+ end
+ end
+
+ def merge_request
+ options[:merge_request]
+ end
+end
diff --git a/app/serializers/merge_request_user_entity.rb b/app/serializers/merge_request_user_entity.rb
new file mode 100644
index 00000000000..33fc7b724d5
--- /dev/null
+++ b/app/serializers/merge_request_user_entity.rb
@@ -0,0 +1,24 @@
+class MergeRequestUserEntity < UserEntity
+ include RequestAwareEntity
+ include BlobHelper
+ include TreeHelper
+
+ expose :can_fork do |user|
+ can?(user, :fork_project, request.project) if project
+ end
+
+ expose :can_create_merge_request do |user|
+ project && can?(user, :create_merge_request_in, project)
+ end
+
+ expose :fork_path, if: -> (*) { project } do |user|
+ params = edit_blob_fork_params("Edit")
+ project_forks_path(project, namespace_key: user.namespace.id, continue: params)
+ end
+
+ def project
+ return false unless request.respond_to?(:project) && request.project
+
+ request.project
+ end
+end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 8260c6c7b84..0426afc1b4a 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -120,12 +120,12 @@ class MergeRequestWidgetEntity < IssuableEntity
presenter(merge_request).can_cherry_pick_on_current_merge_request?
end
- expose :can_create_note do |issue|
- can?(request.current_user, :create_note, issue.project)
+ expose :can_create_note do |merge_request|
+ can?(request.current_user, :create_note, merge_request)
end
- expose :can_update do |issue|
- can?(request.current_user, :update_issue, issue)
+ expose :can_update do |merge_request|
+ can?(request.current_user, :update_merge_request, merge_request)
end
end
@@ -209,6 +209,10 @@ class MergeRequestWidgetEntity < IssuableEntity
commit_change_content_project_merge_request_path(merge_request.project, merge_request)
end
+ expose :preview_note_path do |merge_request|
+ preview_markdown_path(merge_request.project, quick_actions_target_type: 'MergeRequest', quick_actions_target_id: merge_request.id)
+ end
+
expose :merge_commit_path do |merge_request|
if merge_request.merge_commit_sha
project_commit_path(merge_request.project, merge_request.merge_commit_sha)
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index 06d603b277e..ce0c31b5806 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -1,5 +1,6 @@
class NoteEntity < API::Entities::Note
include RequestAwareEntity
+ include NotesHelper
expose :type
@@ -15,16 +16,21 @@ class NoteEntity < API::Entities::Note
expose :current_user do
expose :can_edit do |note|
- Ability.allowed?(request.current_user, :admin_note, note)
+ can?(current_user, :admin_note, note)
end
expose :can_award_emoji do |note|
- Ability.allowed?(request.current_user, :award_emoji, note)
+ can?(current_user, :award_emoji, note)
+ end
+
+ expose :can_resolve do |note|
+ note.resolvable? && can?(current_user, :resolve_note, note)
end
end
expose :resolved?, as: :resolved
expose :resolvable?, as: :resolvable
+
expose :resolved_by, using: NoteUserEntity
expose :system_note_icon_name, if: -> (note, _) { note.system? } do |note|
@@ -42,5 +48,23 @@ class NoteEntity < API::Entities::Note
new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note))
end
+ expose :noteable_note_url do |note|
+ noteable_note_url(note)
+ end
+
+ expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+ resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
+ end
+
+ expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+ new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
+ end
+
expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
+
+ private
+
+ def current_user
+ request.current_user
+ end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 925775aea0b..9bdbb2c0d99 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -25,14 +25,12 @@ module Ci
valid = true
- if Feature.enabled?('ci_job_request_with_tags_matcher')
- # pick builds that does not have other tags than runner's one
- builds = builds.matches_tag_ids(runner.tags.ids)
+ # pick builds that does not have other tags than runner's one
+ builds = builds.matches_tag_ids(runner.tags.ids)
- # pick builds that have at least one tag
- unless runner.run_untagged?
- builds = builds.with_any_tags
- end
+ # pick builds that have at least one tag
+ unless runner.run_untagged?
+ builds = builds.with_any_tags
end
builds.find do |build|
diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb
index 26eb274f4d5..455f761ca9b 100644
--- a/app/services/concerns/issues/resolve_discussions.rb
+++ b/app/services/concerns/issues/resolve_discussions.rb
@@ -14,7 +14,6 @@ module Issues
def merge_request_to_resolve_discussions_of
strong_memoize(:merge_request_to_resolve_discussions_of) do
MergeRequestsFinder.new(current_user, project_id: project.id)
- .execute
.find_by(iid: merge_request_to_resolve_discussions_of_iid)
end
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 3e38a8a12d4..aa60661f7f2 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -11,7 +11,7 @@ module Projects
order: { due_date: :asc, title: :asc }
}
- finder_params[:group_ids] = @project.group.self_and_ancestors.select(:id) if @project.group
+ finder_params[:group_ids] = @project.group.self_and_ancestors_ids if @project.group
MilestonesFinder.new(finder_params).execute.select([:iid, :title])
end
diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb
index 09afc63a5aa..3639375d474 100644
--- a/app/uploaders/favicon_uploader.rb
+++ b/app/uploaders/favicon_uploader.rb
@@ -1,17 +1,6 @@
class FaviconUploader < AttachmentUploader
EXTENSION_WHITELIST = %w[png ico].freeze
- include CarrierWave::MiniMagick
-
- version :favicon_main do
- process resize_to_fill: [32, 32]
- process convert: 'png'
-
- def full_filename(filename)
- filename_for_different_format(super(filename), 'png')
- end
- end
-
def extension_whitelist
EXTENSION_WHITELIST
end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 94db374040c..a0861870ba4 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -25,7 +25,7 @@
= f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label'
.col-sm-10
- if @appearance.favicon?
- = image_tag @appearance.favicon.favicon_main.url, class: 'appearance-light-logo-preview'
+ = image_tag @appearance.favicon_url, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
= link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
@@ -33,9 +33,9 @@
= f.hidden_field :favicon_cache
= f.file_field :favicon, class: ''
.hint
- Maximum file size is 1MB. Allowed image formats are #{favicon_extension_whitelist}.
+ Maximum file size is 1MB. Image size must be 32x32px. Allowed image formats are #{favicon_extension_whitelist}.
%br
- The resulting favicons will be cropped to be square and scaled down to a size of 32x32 px.
+ Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior.
%fieldset.sign-in
%legend
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 087af61235b..58c585a29ff 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -3,8 +3,8 @@
%li.nav-item
= link_to "Crowd", "#crowd", class: 'nav-link active', 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
- %li.nav-item{ class: active_when(i.zero? && !crowd_enabled?) }
- = link_to server['label'], "##{server['provider_name']}", class: 'nav-link', 'data-toggle' => 'tab'
+ %li.nav-item
+ = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && !crowd_enabled?)}", 'data-toggle' => 'tab'
- if password_authentication_enabled_for_web?
%li.nav-item
= link_to 'Standard', '#login-pane', class: 'nav-link', 'data-toggle' => 'tab'
diff --git a/app/views/email_rejection_mailer/rejection.html.haml b/app/views/email_rejection_mailer/rejection.html.haml
index 7f7d841fe21..c4ae7befe4e 100644
--- a/app/views/email_rejection_mailer/rejection.html.haml
+++ b/app/views/email_rejection_mailer/rejection.html.haml
@@ -2,3 +2,4 @@
Unfortunately, your email message to GitLab could not be processed.
= markdown @reason
+= render_if_exists 'shared/additional_email_text'
diff --git a/app/views/email_rejection_mailer/rejection.text.haml b/app/views/email_rejection_mailer/rejection.text.haml
index af518b5b583..0e13b2a6473 100644
--- a/app/views/email_rejection_mailer/rejection.text.haml
+++ b/app/views/email_rejection_mailer/rejection.text.haml
@@ -1,3 +1,4 @@
Unfortunately, your email message to GitLab could not be processed.
\
= @reason
+= render_if_exists 'shared/additional_email_text'
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 845f4046d0d..6abb56ba6d2 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,6 +1,6 @@
- if current_user
.dropdown
- %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
+ %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' }
= icon('globe')
%span.light Visibility:
- if params[:visibility_level].present?
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 8b0ef3cd87a..5a88619f769 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -18,7 +18,7 @@
- if can_create_subgroups
.btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } }
%input.btn.btn-success.dropdown-primary.js-new-group-child{ type: "button", value: new_project_label, data: { action: "new-project" } }
- %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown" } }
+ %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } }
= icon("caret-down", class: "dropdown-btn-icon")
%ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } }
%li.droplab-item-selected{ role: "button", data: { value: "new-project", text: new_project_label } }
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index de8369ed7b9..b32b602ceb3 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -443,8 +443,6 @@
.col-md-6
.alert.alert-success
= lorem
- .alert.alert-primary
- = lorem
.alert.alert-info
= lorem
.col-md-6
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index f311ac98ac6..cc672a5ea7c 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -20,7 +20,7 @@
- else
.input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
- .input-group-text
+ .input-group-text.border-0
#{user_url(current_user.username)}/
= hidden_field_tag :namespace_id, value: current_user.namespace_id
.form-group.col-12.col-sm-6.project-path
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index db8137cc248..d35df706036 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -1,5 +1,5 @@
%li.header-new.dropdown
- = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
+ = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do
= sprite_icon('plus-square', size: 16)
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index fdb07ce6fc5..9f8b3b86474 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -34,8 +34,10 @@
= link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do
%span= _('Cycle Analytics')
+ = render_if_exists 'projects/sidebar/security_dashboard'
+
- if project_nav_tab? :files
- = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do
+ = nav_link(controller: sidebar_repository_paths) do
= link_to project_tree_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
= sprite_icon('doc_text')
@@ -43,7 +45,7 @@
= _('Repository')
%ul.sidebar-sub-level-items
- = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network), html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: sidebar_repository_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to project_tree_path(@project) do
%strong.fly-out-top-item-name
= _('Repository')
@@ -80,6 +82,8 @@
= link_to charts_project_graph_path(@project, current_ref) do
= _('Charts')
+ = render_if_exists 'projects/sidebar/repository_locked_files'
+
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), class: 'shortcuts-issues' do
@@ -92,7 +96,7 @@
= number_with_delimiter(@project.open_issues_count(current_user))
%ul.sidebar-sub-level-items
- = nav_link(controller: :issues, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :issues, action: :index, html_options: { class: "fly-out-top-item" } ) do
= link_to project_issues_path(@project) do
%strong.fly-out-top-item-name
= _('Issues')
@@ -115,6 +119,8 @@
%span
= _('Labels')
+ = render_if_exists 'projects/sidebar/issues_service_desk'
+
= nav_link(controller: :milestones) do
= link_to project_milestones_path(@project), title: 'Milestones' do
%span
@@ -278,7 +284,7 @@
= _('Snippets')
- if project_nav_tab? :settings
- = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show]) do
+ = nav_link(path: sidebar_settings_paths) do
= link_to edit_project_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
= sprite_icon('settings')
@@ -288,7 +294,7 @@
%ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project)
- if can_edit
- = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(path: sidebar_settings_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to edit_project_path(@project) do
%strong.fly-out-top-item-name
= _('Settings')
@@ -326,6 +332,8 @@
%span
= _('Pages')
+ = render_if_exists 'projects/sidebar/settings_audit_events'
+
- else
= nav_link(controller: :project_members) do
= link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do
diff --git a/app/views/notify/merge_request_unmergeable_email.html.haml b/app/views/notify/merge_request_unmergeable_email.html.haml
index 578fa1fbce7..7ec0c1ef390 100644
--- a/app/views/notify/merge_request_unmergeable_email.html.haml
+++ b/app/views/notify/merge_request_unmergeable_email.html.haml
@@ -1,6 +1,2 @@
%p
- Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} can no longer be merged due to the following #{'reason'.pluralize(@reasons.count)}:
-
- %ul
- - @reasons.each do |reason|
- %li= reason
+ Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} can no longer be merged due to conflict.
diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml
index e4f9f1bf5e7..dcdd6db69d6 100644
--- a/app/views/notify/merge_request_unmergeable_email.text.haml
+++ b/app/views/notify/merge_request_unmergeable_email.text.haml
@@ -1,7 +1,4 @@
-Merge Request #{@merge_request.to_reference} can no longer be merged due to the following #{'reason'.pluralize(@reasons.count)}:
-
-- @reasons.each do |reason|
- * #{reason}
+Merge Request #{@merge_request.to_reference} can no longer be merged due to conflict.
Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 0a9adc6f243..dd6a84e503d 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -9,6 +9,8 @@
%p
Assignee: #{@merge_request.assignee_name}
+= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request
+
- if @merge_request.description
%div
= markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb
index 7d98400e6fe..d5b8f8d764f 100644
--- a/app/views/notify/new_merge_request_email.text.erb
+++ b/app/views/notify/new_merge_request_email.text.erb
@@ -5,6 +5,6 @@ New Merge Request <%= @merge_request.to_reference %>
<%= merge_path_description(@merge_request, 'to') %>
Author: <%= @merge_request.author_name %>
Assignee: <%= @merge_request.assignee_name %>
+<%= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request %>
<%= @merge_request.description %>
-
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index cfbd0459e3e..6f957533287 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -16,7 +16,7 @@
- else
.input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
- .input-group-text
+ .input-group-text.border-0
#{user_url(current_user.username)}/
= f.hidden_field :namespace_id, value: current_user.namespace_id
.form-group.project-path.col-sm-6
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 1b150ec3e5c..0a0b3ce1d6f 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -11,6 +11,7 @@
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }<
+ = render_if_exists 'projects/blob/header_file_locks_link'
= edit_blob_button
= ide_edit_button
- if current_user
@@ -18,3 +19,4 @@
= delete_blob_link
= render 'projects/fork_suggestion'
+= render_if_exists 'projects/blob/header_file_locks', project: @project, path: @path
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index c75093c4c24..f7551434d47 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -3,7 +3,7 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- archive_prefix = "#{project.path}-#{ref.tr('/', '-')}"
.project-action-button.dropdown.inline>
- %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') }
+ %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static' }
= sprite_icon('download')
= icon("caret-down")
%span.sr-only= _('Select Archive Format')
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 84245d72f4a..8b9c52f0802 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -8,7 +8,7 @@
- if show_menu
.project-action-button.dropdown.inline
- %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') }
+ %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...'), 'data-display' => 'static' }
= icon('plus')
= icon("caret-down")
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
diff --git a/app/views/projects/clusters/_sidebar.html.haml b/app/views/projects/clusters/_sidebar.html.haml
index 73cd7c50922..3d10348212f 100644
--- a/app/views/projects/clusters/_sidebar.html.haml
+++ b/app/views/projects/clusters/_sidebar.html.haml
@@ -1,7 +1,9 @@
+- clusters_help_url = help_page_path('user/project/clusters/index.md')
+- help_link_start = "<a href=\"%{url}\" target=\"_blank\" rel=\"noopener noreferrer\">".html_safe
+- help_link_end = '</a>'.html_safe
%h4.prepend-top-0
= s_('ClusterIntegration|Kubernetes cluster integration')
%p
= s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
%p
- - link = link_to(_('Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Learn more about %{link_to_documentation}').html_safe % { link_to_documentation: link }
+ = s_('ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: clusters_help_url }, help_link_end: help_link_end }
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index 59c4eeec17a..b8e40b0a38b 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -1,4 +1,10 @@
= javascript_include_tag 'https://apis.google.com/js/api.js'
+- external_link_icon = icon('external-link')
+- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones'
+- machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types'
+- pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype'
+- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
+- help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon }
%p
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
@@ -7,15 +13,15 @@
= form_for @cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster)
.form-group
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group
- = field.label :environment_scope, s_('ClusterIntegration|Environment scope')
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light'
= field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
= field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
.form-group
- = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project')
+ = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'), class: 'label-light'
.js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
= provider_gcp_field.hidden_field :gcp_project_id
.dropdown
@@ -26,8 +32,7 @@
%span.form-text.text-muted &nbsp;
.form-group
- = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
- = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone'), class: 'label-light'
.js-gcp-zone-dropdown-entry-point
= provider_gcp_field.hidden_field :zone
.dropdown
@@ -35,13 +40,15 @@
%span.dropdown-toggle-text
= _('Select project to choose zone')
= icon('chevron-down')
+ %p.form-text.text-muted
+ = s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end }
.form-group
- = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
+ = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes'), class: 'label-light'
= provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
.form-group
- = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
+ = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-light'
.js-gcp-machine-type-dropdown-entry-point
= provider_gcp_field.hidden_field :machine_type
.dropdown
@@ -49,6 +56,8 @@
%span.dropdown-toggle-text
= _('Select project and zone to choose machine type')
= icon('chevron-down')
+ %p.form-text.text-muted
+ = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
.form-group
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml
index 55a42ac4847..96c7a648676 100644
--- a/app/views/projects/clusters/gcp/login.html.haml
+++ b/app/views/projects/clusters/gcp/login.html.haml
@@ -16,5 +16,6 @@
= _('or')
= link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer')
- else
- - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer')
- = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
+ .settings-message.text-center
+ - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer')
+ = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 12b27eb9b66..90e55fd0fb0 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -34,7 +34,8 @@
.d-block.d-sm-none
= render_commit_status(commit, ref: ref)
- if commit.description?
- %button.text-expander.d-none.d-sm-inline-block.js-toggle-button{ type: "button" } ...
+ %button.text-expander.js-toggle-button
+ = sprite_icon('ellipsis_h', size: 12)
.commiter
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
diff --git a/app/views/projects/deploy_tokens/_index.html.haml b/app/views/projects/deploy_tokens/_index.html.haml
index 50e5950ced4..725720d2222 100644
--- a/app/views/projects/deploy_tokens/_index.html.haml
+++ b/app/views/projects/deploy_tokens/_index.html.haml
@@ -10,9 +10,8 @@
.settings-content
- if @new_deploy_token.persisted?
= render 'projects/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
- - else
- %h5.prepend-top-0
- = s_('DeployTokens|Add a deploy token')
- = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens
- %hr
+ %h5.prepend-top-0
+ = s_('DeployTokens|Add a deploy token')
+ = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens
+ %hr
= render 'projects/deploy_tokens/table', project: @project, active_tokens: @deploy_tokens
diff --git a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
index 1e715681e59..5dd9ffba074 100644
--- a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
+++ b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
@@ -1,14 +1,18 @@
-.created-deploy-token-container
- %h5.prepend-top-0
- = s_('DeployTokens|Your New Deploy Token')
+.created-deploy-token-container.info-well
+ .well-segment
+ %h5.prepend-top-0
+ = s_('DeployTokens|Your New Deploy Token')
- .form-group
- = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
- = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left')
- %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.")
+ .form-group
+ .input-group
+ = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
+ .input-group-append
+ = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left')
+ %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.")
- .form-group
- = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
- = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left')
- %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.")
-%hr
+ .form-group
+ .input-group
+ = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
+ .input-group-append
+ = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left')
+ %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.")
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 69753427d17..d47dc3d8143 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,3 +1,4 @@
+- @content_class = "limit-container-width" unless fluid_layout
- @no_container = true
- breadcrumb_title _("Details")
@@ -6,7 +7,7 @@
= render "home_panel"
.project-empty-note-panel
- %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] }
+ %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
.prepend-top-20
%h4
= _('The repository for this project is empty')
@@ -36,7 +37,7 @@
= render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
- if can?(current_user, :push_code, @project)
- %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] }
+ %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
.prepend-top-20
.empty_wrapper
%h3#repo-command-line-instructions.page-title-empty
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 816f2fa816d..665968a64e1 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -8,5 +8,6 @@
%section.js-vue-notes-event
#js-vue-notes{ data: { notes_data: notes_data(@issue),
noteable_data: serialize_issuable(@issue),
- noteable_type: 'issue',
+ noteable_type: 'Issue',
+ target_type: 'issue',
current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 76438fae663..a678cb6f058 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -18,7 +18,7 @@
%button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } }
= value
- %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
+ %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' }, display: 'static' } }
= icon('caret-down')
.droplab-dropdown
@@ -40,7 +40,7 @@
%label{ for: 'new-branch-name' }
= _('Branch name')
%input#new-branch-name.js-branch-name.form-control{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
- %span.js-branch-message.form-text.text-muted
+ %span.js-branch-message.form-text
.form-group
%label{ for: 'source-name' }
diff --git a/app/views/projects/merge_requests/diffs/_version_controls.html.haml b/app/views/projects/merge_requests/diffs/_version_controls.html.haml
index 1c26f0405d2..52bf584d550 100644
--- a/app/views/projects/merge_requests/diffs/_version_controls.html.haml
+++ b/app/views/projects/merge_requests/diffs/_version_controls.html.haml
@@ -3,7 +3,7 @@
.mr-version-menus-container.content-block
Changes between
%span.dropdown.inline.mr-version-dropdown
- %a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} }
+ %a.dropdown-toggle.btn.btn-default{ data: { toggle: :dropdown, display: 'static' } }
%span
- if @merge_request_diff.latest?
latest version
@@ -36,7 +36,7 @@
- if @merge_request_diff.base_commit_sha
and
%span.dropdown.inline.mr-version-compare-dropdown
- %a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} }
+ %a.btn.btn-default.dropdown-toggle{ data: { toggle: :dropdown, display: 'static' } }
- if @start_version
version #{version_index(@start_version)}
- else
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 2f1877a15c2..4fe0ae17ec5 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -52,24 +52,7 @@
Changes
%span.badge.badge-pill= @merge_request.diff_size
- - if has_vue_discussions_cookie?
- #js-vue-discussion-counter
- - else
- #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true }
- %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
- %div
- .line-resolve-all{ "v-show" => "discussionCount > 0",
- ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
- %span.line-resolve-btn.is-disabled{ type: "button",
- ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- %template{ 'v-if' => 'resolvedDiscussionCount === discussionCount' }
- = render 'shared/icons/icon_status_success_solid.svg'
- %template{ 'v-else' => '' }
- = render 'shared/icons/icon_resolve_discussion.svg'
- %span.line-resolve-text
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
- = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
- = render "discussions/jump_to_next"
+ #js-vue-discussion-counter
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
@@ -77,20 +60,20 @@
%section.col-md-12
%script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe
.issuable-discussion.js-vue-notes-event
- = render "projects/merge_requests/discussion"
- - if has_vue_discussions_cookie?
- #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
- noteable_data: serialize_issuable(@merge_request),
- noteable_type: 'merge_request',
- current_user_data: UserSerializer.new.represent(current_user).to_json} }
+ #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
+ noteable_data: serialize_issuable(@merge_request),
+ noteable_type: 'MergeRequest',
+ target_type: 'merge_request',
+ current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json} }
#commits.commits.tab-pane
-# This tab is always loaded via AJAX
#pipelines.pipelines.tab-pane
- if @pipelines.any?
= render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request)
- #diffs.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked? } }
- -# This tab is always loaded via AJAX
+ #js-diffs-app.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked?,
+ endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', request.query_parameters),
+ current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json } }
.mr-loading-status
= spinner
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index b478fbbb15e..f7b04c436a6 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -69,6 +69,8 @@
.wiki
= markdown_field(@milestone, :description)
+ = render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project
+
- if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero?
.alert.alert-success.prepend-top-default
%span Assign some issues to this milestone.
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index aa53fc3ea28..04131a90a57 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -29,7 +29,7 @@
= link_to @commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha js-details-short"
= link_to("#", class: "js-details-expand d-none d-sm-none d-md-inline") do
%span.text-expander
- \...
+ = sprite_icon('ellipsis_h', size: 12)
%span.js-details-content.hide
= link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full"
= clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard")
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index d07bb661615..506bf54b3f8 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -8,6 +8,8 @@
row.find("td.tree-time-ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
row.find("td.tree-commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
+ = render_if_exists 'projects/refs/logs_tree_lock_label', lock_label: content_data[:lock_label]
+
- if @more_log_url
:plain
if($('#tree-slider').length) {
diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml
index acbab8b85c9..16e48814578 100644
--- a/app/views/projects/services/_index.html.haml
+++ b/app/views/projects/services/_index.html.haml
@@ -8,7 +8,7 @@
%colgroup
%col
%col
- %col.d-none.d-sm-block
+ %col
%col{ width: "120" }
%thead
%tr
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 35c7dc2984a..d80d2957466 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,4 +1,4 @@
-- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
+- @content_class = "limit-container-width" unless fluid_layout
- page_title _("Edit"), @page.title.capitalize, _("Wiki")
= wiki_page_errors(@error)
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 909987d8090..8c2cbd495a0 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,4 +1,4 @@
-- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
+- @content_class = "limit-container-width" unless fluid_layout
- page_title s_("WikiClone|Git Access"), _("Wiki")
.wiki-page-header.has-sidebar-toggle
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index ff72c8bb75d..a08973c7f32 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,4 +1,4 @@
-- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
+- @content_class = "limit-container-width" unless fluid_layout
- breadcrumb_title @page.title.capitalize
- wiki_breadcrumb_dropdown_links(@page.slug)
- page_title @page.title.capitalize, _("Wiki")
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 5fc02ba3160..3655c2a1d42 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -11,7 +11,7 @@
%span
= default_clone_protocol.upcase
= icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown
+ %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown
%li
= ssh_clone_button(project)
%li
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index a112a9f1f7e..daee691e358 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -9,7 +9,7 @@
None
%a{ href: "#",
"v-for" => "label in issue.labels" }
- %span.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
+ .badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
{{ label.title }}
- if can_admin_issue?
.selectbox
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 955b8866c2c..37625a4a163 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -1,6 +1,8 @@
- project = @target_project || @project
- extra_class = extra_class || ''
- show_menu_above = show_menu_above || false
+- selected = local_assigns.fetch(:selected, nil)
+
- selected_text = selected.try(:title) || params[:milestone_title]
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone")
- if selected.present? || params[:milestone_title].present?
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index ed3ef6155db..8a13c7a3b83 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -50,7 +50,7 @@
- data[:multi_select] = true
- data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- - data['max-select'] = dropdown_options[:data][:'max-select']
+ - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
= dropdown_tag(title, options: options)
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index 01fbc163a14..bd87bb38e77 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -25,7 +25,10 @@
= form.hidden_field :label_ids, multiple: true, value: ''
.col-sm-10{ class: "#{"col-md-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
.issuable-form-select-holder
- = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
+ = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
+
+ = render_if_exists "shared/issuable/form/weight", issuable: issuable, form: form
+
- if has_due_date
.col-lg-6
.form-group.row
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index 608dd35182d..922805958a5 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -2,10 +2,10 @@
.form-group.row
= f.label :start_date, "Start Date", class: "col-form-label col-sm-2"
.col-sm-10
- = f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date"
+ = f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date", autocomplete: 'off'
%a.inline.float-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
.form-group.row
= f.label :due_date, "Due Date", class: "col-form-label col-sm-2"
.col-sm-10
- = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
+ = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date", autocomplete: 'off'
%a.inline.float-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index ca6e3602f05..d4e8f30e458 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -36,8 +36,6 @@
= note.author.to_reference
%span.note-headline-light
%span.note-headline-meta
- - unless note.system
- commented
- if note.system
%span.system-note-message
= markdown_field(note, :note)
@@ -61,7 +59,7 @@
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
- .system-note-commit-list-toggler
+ .system-note-commit-list-toggler.hide
Toggle commit list
%i.fa.fa-angle-down
- if note.attachment.url
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index b98d5339d2d..e0832fd9136 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -1,14 +1,13 @@
- issuable = @issue || @merge_request
- discussion_locked = issuable&.discussion_locked?
-- unless has_vue_discussions_cookie?
- %ul#notes-list.notes.main-notes-list.timeline
- = render "shared/notes/notes"
+%ul#notes-list.notes.main-notes-list.timeline
+ = render "shared/notes/notes"
= render 'shared/notes/edit_form', project: @project
- if can_create_note?
- %ul.notes.notes-form.timeline{ :class => ('hidden' if has_vue_discussions_cookie?) }
+ %ul.notes.notes-form.timeline
%li.timeline-entry
.timeline-entry-inner
.flash-container.timeline-content
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index e99d8d0973f..09ddf732ada 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -6,14 +6,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
= icon("caret-down")
diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml
index e50b7fa68dd..96527fcb4f2 100644
--- a/app/views/shared/runners/show.html.haml
+++ b/app/views/shared/runners/show.html.haml
@@ -21,17 +21,17 @@
%th Value
%tr
%td Active
- %td= @runner.active? ? _('Yes') : _('No')
+ %td= @runner.active? ? 'Yes' : 'No'
%tr
%td Protected
- %td= @runner.ref_protected? ? _('Yes') : _('No')
+ %td= @runner.active? ? _('Yes') : _('No')
%tr
- %td= _('Can run untagged jobs')
- %td= @runner.run_untagged? ? _('Yes') : _('No')
+ %td Can run untagged jobs
+ %td= @runner.run_untagged? ? 'Yes' : 'No'
- unless @runner.group_type?
%tr
- %td= _('Locked to this project')
- %td= @runner.locked? ? _('Yes') : _('No')
+ %td Locked to this project
+ %td= @runner.locked? ? 'Yes' : 'No'
%tr
%td Tags
%td
@@ -60,7 +60,7 @@
%td Description
%td= @runner.description
%tr
- %td= _('Maximum job timeout')
+ %td Maximum job timeout
%td= @runner.maximum_timeout_human_readable
%tr
%td Last contact
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index db48bb7e8b8..dbb215f1964 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -8,28 +8,12 @@ class RepositoryForkWorker
target_project_id = args.shift
target_project = Project.find(target_project_id)
- # By v10.8, we should've drained the queue of all jobs using the old arguments.
- # We can remove the else clause if we're no longer logging the message in that clause.
- # See https://gitlab.com/gitlab-org/gitaly/issues/1110
- if args.empty?
- source_project = target_project.forked_from_project
- unless source_project
- return target_project.mark_import_as_failed('Source project cannot be found.')
- end
-
- fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
- else
- Rails.logger.info("Project #{target_project.id} is being forked using old-style arguments.")
-
- source_repository_storage_path, source_disk_path = *args
-
- source_repository_storage_name = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- Gitlab.config.repositories.storages.find do |_, info|
- info.legacy_disk_path == source_repository_storage_path
- end&.first || raise("no shard found for path '#{source_repository_storage_path}'")
- end
- fork_repository(target_project, source_repository_storage_name, source_disk_path)
+ source_project = target_project.forked_from_project
+ unless source_project
+ return target_project.mark_import_as_failed('Source project cannot be found.')
end
+
+ fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
end
private