summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorSean McGivern <sean@gitlab.com>2018-07-06 11:41:03 +0100
committerSean McGivern <sean@gitlab.com>2018-07-06 11:41:03 +0100
commit827712e78ebb645bc7250f927de99c5f3395368f (patch)
treebb29b6bfc776ed5b99ab84918c33708e12df64ea /app
parent0d9ef34a2541a2adf00677132eac3637de33b6d4 (diff)
parentb0fa01fce3822da94aee6264829841996beb6df3 (diff)
downloadgitlab-ce-827712e78ebb645bc7250f927de99c5f3395368f.tar.gz
Merge branch 'master' into satishperala/gitlab-ce-20720_webhooks_full_image_url
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/activities.js2
-rw-r--r--app/assets/javascripts/ajax_loading_spinner.js2
-rw-r--r--app/assets/javascripts/api.js20
-rw-r--r--app/assets/javascripts/autosave.js6
-rw-r--r--app/assets/javascripts/awards_handler.js191
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js6
-rw-r--r--app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js2
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js2
-rw-r--r--app/assets/javascripts/blob/pdf/index.js1
-rw-r--r--app/assets/javascripts/blob/stl_viewer.js2
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js3
-rw-r--r--app/assets/javascripts/boards/components/board.js2
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue128
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js2
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js18
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js196
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue202
-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/footer.vue (renamed from app/assets/javascripts/boards/components/modal/footer.js)78
-rw-r--r--app/assets/javascripts/boards/components/modal/header.js79
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue82
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js171
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue178
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js159
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue161
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.js54
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.vue56
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js46
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue49
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js73
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue91
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js1
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js5
-rw-r--r--app/assets/javascripts/boards/index.js10
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js1
-rw-r--r--app/assets/javascripts/boards/models/issue.js3
-rw-r--r--app/assets/javascripts/boards/models/list.js62
-rw-r--r--app/assets/javascripts/boards/models/milestone.js2
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js2
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js2
-rw-r--r--app/assets/javascripts/build_artifacts.js2
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js2
-rw-r--r--app/assets/javascripts/commit/image_file.js8
-rw-r--r--app/assets/javascripts/compare_autocomplete.js2
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js2
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js2
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js7
-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.js32
-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.vue216
-rw-r--r--app/assets/javascripts/diffs/components/changed_files.vue184
-rw-r--r--app/assets/javascripts/diffs/components/changed_files_dropdown.vue126
-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.vue62
-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.vue262
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue105
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue202
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue114
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue145
-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_comment_row.vue82
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue104
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue65
-rw-r--r--app/assets/javascripts/diffs/components/no_changes.vue49
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue129
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue150
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue85
-rw-r--r--app/assets/javascripts/diffs/constants.js27
-rw-r--r--app/assets/javascripts/diffs/index.js41
-rw-r--r--app/assets/javascripts/diffs/mixins/changed_files.js38
-rw-r--r--app/assets/javascripts/diffs/store/actions.js95
-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.js26
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js10
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js80
-rw-r--r--app/assets/javascripts/diffs/store/utils.js175
-rw-r--r--app/assets/javascripts/dispatcher.js8
-rw-r--r--app/assets/javascripts/due_date_select.js41
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue2
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js6
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js4
-rw-r--r--app/assets/javascripts/filtered_search/recent_searches_root.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js24
-rw-r--r--app/assets/javascripts/gl_dropdown.js13
-rw-r--r--app/assets/javascripts/gl_form.js22
-rw-r--r--app/assets/javascripts/groups/components/app.vue1
-rw-r--r--app/assets/javascripts/groups/index.js4
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue26
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue11
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue6
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue6
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue10
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue69
-rw-r--r--app/assets/javascripts/ide/components/file_finder/item.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide.vue7
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue1
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue39
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue2
-rw-r--r--app/assets/javascripts/ide/ide_router.js8
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff_worker.js2
-rw-r--r--app/assets/javascripts/ide/services/index.js40
-rw-r--r--app/assets/javascripts/ide/stores/actions.js3
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js33
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js44
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js66
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js64
-rw-r--r--app/assets/javascripts/ide/stores/getters.js5
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js41
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js17
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/actions.js19
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js59
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js3
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js5
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js4
-rw-r--r--app/assets/javascripts/image_diff/helpers/dom_helper.js3
-rw-r--r--app/assets/javascripts/image_diff/helpers/utils_helper.js3
-rw-r--r--app/assets/javascripts/init_notes.js4
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js1
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js2
-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/job.js2
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js2
-rw-r--r--app/assets/javascripts/label_manager.js2
-rw-r--r--app/assets/javascripts/labels_select.js11
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js165
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js39
-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.js10
-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_conflicts/merge_conflict_store.js4
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js2
-rw-r--r--app/assets/javascripts/merge_request.js3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js175
-rw-r--r--app/assets/javascripts/milestone_select.js5
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue53
-rw-r--r--app/assets/javascripts/monitoring/services/monitoring_service.js19
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js5
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js4
-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.js57
-rw-r--r--app/assets/javascripts/new_branch_form.js4
-rw-r--r--app/assets/javascripts/new_commit_form.js2
-rw-r--r--app/assets/javascripts/notes.js285
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue51
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue194
-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_awards_list.vue2
-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.vue55
-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.vue51
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue106
-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.js8
-rw-r--r--app/assets/javascripts/notes/stores/actions.js44
-rw-r--r--app/assets/javascripts/notes/stores/getters.js75
-rw-r--r--app/assets/javascripts/notes/stores/index.js26
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js27
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js5
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js100
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue2
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js16
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/login/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/clusters/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js11
-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.js2
-rw-r--r--app/assets/javascripts/pages/projects/index.js14
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js8
-rw-r--r--app/assets/javascripts/pages/projects/init_form.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/form.js2
-rw-r--r--app/assets/javascripts/pages/projects/jobs/terminal/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js2
-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/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.js4
-rw-r--r--app/assets/javascripts/pages/projects/tags/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/wikis.js2
-rw-r--r--app/assets/javascripts/pages/search/show/search.js2
-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/snippets/form.js10
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js5
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue3
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/blank_state.vue24
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue32
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue40
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue124
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.vue62
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue64
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue506
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue68
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.vue30
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue130
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue446
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue37
-rw-r--r--app/assets/javascripts/pipelines/components/time_ago.vue94
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js11
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js5
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js3
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js2
-rw-r--r--app/assets/javascripts/preview_markdown.js4
-rw-r--r--app/assets/javascripts/profile/gl_crop.js23
-rw-r--r--app/assets/javascripts/profile/profile.js15
-rw-r--r--app/assets/javascripts/project_find_file.js7
-rw-r--r--app/assets/javascripts/project_select.js7
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue2
-rw-r--r--app/assets/javascripts/projects_dropdown/index.js2
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js34
-rw-r--r--app/assets/javascripts/registry/index.js2
-rw-r--r--app/assets/javascripts/registry/stores/actions.js2
-rw-r--r--app/assets/javascripts/right_sidebar.js2
-rw-r--r--app/assets/javascripts/search_autocomplete.js8
-rw-r--r--app/assets/javascripts/shared/milestones/form.js11
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js24
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js1
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue98
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js1
-rw-r--r--app/assets/javascripts/single_file_diff.js2
-rw-r--r--app/assets/javascripts/smart_interval.js9
-rw-r--r--app/assets/javascripts/syntax_highlight.js2
-rw-r--r--app/assets/javascripts/test_utils/simulate_drag.js4
-rw-r--r--app/assets/javascripts/tree.js2
-rw-r--r--app/assets/javascripts/u2f/authenticate.js8
-rw-r--r--app/assets/javascripts/users_select.js13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue4
-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.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue6
-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/components/sidebar/toggle_sidebar.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.vue2
-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.scss37
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss18
-rw-r--r--app/assets/stylesheets/framework.scss3
-rw-r--r--app/assets/stylesheets/framework/animations.scss9
-rw-r--r--app/assets/stylesheets/framework/awards.scss4
-rw-r--r--app/assets/stylesheets/framework/blocks.scss6
-rw-r--r--app/assets/stylesheets/framework/common.scss5
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss18
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1
-rw-r--r--app/assets/stylesheets/framework/files.scss30
-rw-r--r--app/assets/stylesheets/framework/flash.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss12
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss10
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss5
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/framework/wells.scss4
-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/highlight/white_base.scss4
-rw-r--r--app/assets/stylesheets/pages/boards.scss4
-rw-r--r--app/assets/stylesheets/pages/commits.scss14
-rw-r--r--app/assets/stylesheets/pages/diff.scss34
-rw-r--r--app/assets/stylesheets/pages/environments.scss17
-rw-r--r--app/assets/stylesheets/pages/issuable.scss1
-rw-r--r--app/assets/stylesheets/pages/labels.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss9
-rw-r--r--app/assets/stylesheets/pages/milestone.scss36
-rw-r--r--app/assets/stylesheets/pages/note_form.scss16
-rw-r--r--app/assets/stylesheets/pages/notes.scss72
-rw-r--r--app/assets/stylesheets/pages/repo.scss42
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/pages/settings.scss29
-rw-r--r--app/assets/stylesheets/pages/todos.scss16
-rw-r--r--app/assets/stylesheets/performance_bar.scss3
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/admin/groups_controller.rb4
-rw-r--r--app/controllers/admin/hooks_controller.rb3
-rw-r--r--app/controllers/admin/users_controller.rb4
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/notes_actions.rb6
-rw-r--r--app/controllers/concerns/preview_markdown.rb1
-rw-r--r--app/controllers/concerns/todos_actions.rb12
-rw-r--r--app/controllers/concerns/uploads_actions.rb10
-rw-r--r--app/controllers/dashboard/projects_controller.rb5
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/groups/uploads_controller.rb4
-rw-r--r--app/controllers/health_controller.rb3
-rw-r--r--app/controllers/metrics_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb4
-rw-r--r--app/controllers/projects/blob_controller.rb44
-rw-r--r--app/controllers/projects/clusters/gcp_controller.rb76
-rw-r--r--app/controllers/projects/clusters/user_controller.rb40
-rw-r--r--app/controllers/projects/clusters_controller.rb113
-rw-r--r--app/controllers/projects/commits_controller.rb14
-rw-r--r--app/controllers/projects/discussions_controller.rb9
-rw-r--r--app/controllers/projects/environments_controller.rb10
-rw-r--r--app/controllers/projects/hooks_controller.rb3
-rw-r--r--app/controllers/projects/jobs_controller.rb30
-rw-r--r--app/controllers/projects/lfs_api_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb25
-rw-r--r--app/controllers/projects/merge_requests_controller.rb3
-rw-r--r--app/controllers/projects/milestones_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb4
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb2
-rw-r--r--app/controllers/projects/todos_controller.rb14
-rw-r--r--app/controllers/projects/uploads_controller.rb4
-rw-r--r--app/controllers/projects/wikis_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb3
-rw-r--r--app/controllers/sessions_controller.rb49
-rw-r--r--app/finders/issuable_finder.rb4
-rw-r--r--app/finders/merge_requests_finder.rb2
-rw-r--r--app/finders/notes_finder.rb2
-rw-r--r--app/finders/pipelines_finder.rb9
-rw-r--r--app/finders/todos_finder.rb52
-rw-r--r--app/finders/user_recent_events_finder.rb2
-rw-r--r--app/graphql/gitlab_schema.rb3
-rw-r--r--app/graphql/resolvers/concerns/resolves_pipelines.rb23
-rw-r--r--app/graphql/resolvers/merge_request_pipelines_resolver.rb16
-rw-r--r--app/graphql/resolvers/project_pipelines_resolver.rb11
-rw-r--r--app/graphql/types/base_object.rb1
-rw-r--r--app/graphql/types/ci/pipeline_status_enum.rb9
-rw-r--r--app/graphql/types/ci/pipeline_type.rb31
-rw-r--r--app/graphql/types/merge_request_type.rb8
-rw-r--r--app/graphql/types/permission_types/base_permission_type.rb38
-rw-r--r--app/graphql/types/permission_types/ci/pipeline.rb11
-rw-r--r--app/graphql/types/permission_types/merge_request.rb17
-rw-r--r--app/graphql/types/permission_types/project.rb20
-rw-r--r--app/graphql/types/project_type.rb7
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb2
-rw-r--r--app/helpers/groups_helper.rb6
-rw-r--r--app/helpers/issuables_helper.rb13
-rw-r--r--app/helpers/merge_requests_helper.rb4
-rw-r--r--app/helpers/notes_helper.rb16
-rw-r--r--app/helpers/projects_helper.rb59
-rw-r--r--app/helpers/todos_helper.rb10
-rw-r--r--app/helpers/users_helper.rb8
-rw-r--r--app/mailers/emails/merge_requests.rb2
-rw-r--r--app/models/application_setting.rb13
-rw-r--r--app/models/board.rb4
-rw-r--r--app/models/ci/build.rb20
-rw-r--r--app/models/ci/build_runner_session.rb25
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/ci/runner.rb34
-rw-r--r--app/models/concerns/issuable.rb10
-rw-r--r--app/models/concerns/sortable.rb4
-rw-r--r--app/models/discussion.rb4
-rw-r--r--app/models/group.rb8
-rw-r--r--app/models/hooks/web_hook_log.rb5
-rw-r--r--app/models/issue.rb12
-rw-r--r--app/models/label.rb15
-rw-r--r--app/models/merge_request.rb38
-rw-r--r--app/models/merge_request_diff.rb27
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/note.rb5
-rw-r--r--app/models/project.rb15
-rw-r--r--app/models/project_auto_devops.rb4
-rw-r--r--app/models/project_services/bamboo_service.rb22
-rw-r--r--app/models/project_services/kubernetes_service.rb2
-rw-r--r--app/models/project_team.rb2
-rw-r--r--app/models/repository.rb14
-rw-r--r--app/models/service.rb2
-rw-r--r--app/models/todo.rb11
-rw-r--r--app/models/user.rb4
-rw-r--r--app/policies/ci/build_policy.rb6
-rw-r--r--app/policies/group_policy.rb13
-rw-r--r--app/presenters/merge_request_presenter.rb15
-rw-r--r--app/presenters/project_presenter.rb5
-rw-r--r--app/serializers/blob_entity.rb4
-rw-r--r--app/serializers/build_details_entity.rb2
-rw-r--r--app/serializers/diff_file_entity.rb128
-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_diff_entity.rb46
-rw-r--r--app/serializers/merge_request_user_entity.rb24
-rw-r--r--app/serializers/merge_request_widget_entity.rb14
-rw-r--r--app/serializers/note_entity.rb28
-rw-r--r--app/serializers/runner_entity.rb2
-rw-r--r--app/services/base_count_service.rb6
-rw-r--r--app/services/ci/register_job_service.rb14
-rw-r--r--app/services/concerns/issues/resolve_discussions.rb1
-rw-r--r--app/services/issues/base_service.rb5
-rw-r--r--app/services/issues/move_service.rb3
-rw-r--r--app/services/labels/find_or_create_service.rb8
-rw-r--r--app/services/merge_requests/delete_non_latest_diffs_service.rb18
-rw-r--r--app/services/merge_requests/merge_request_diff_cache_service.rb17
-rw-r--r--app/services/merge_requests/post_merge_service.rb7
-rw-r--r--app/services/merge_requests/rebase_service.rb8
-rw-r--r--app/services/merge_requests/reload_diffs_service.rb43
-rw-r--r--app/services/metrics_service.rb3
-rw-r--r--app/services/projects/count_service.rb6
-rw-r--r--app/services/projects/create_service.rb16
-rw-r--r--app/services/projects/open_issues_count_service.rb32
-rw-r--r--app/services/todo_service.rb30
-rw-r--r--app/services/web_hook_service.rb2
-rw-r--r--app/uploaders/attachment_uploader.rb2
-rw-r--r--app/uploaders/avatar_uploader.rb2
-rw-r--r--app/uploaders/favicon_uploader.rb2
-rw-r--r--app/uploaders/file_mover.rb2
-rw-r--r--app/uploaders/file_uploader.rb32
-rw-r--r--app/uploaders/gitlab_uploader.rb2
-rw-r--r--app/uploaders/job_artifact_uploader.rb2
-rw-r--r--app/uploaders/legacy_artifact_uploader.rb2
-rw-r--r--app/uploaders/lfs_object_uploader.rb2
-rw-r--r--app/uploaders/namespace_file_uploader.rb2
-rw-r--r--app/uploaders/object_storage.rb2
-rw-r--r--app/uploaders/personal_file_uploader.rb2
-rw-r--r--app/uploaders/records_uploads.rb2
-rw-r--r--app/uploaders/uploader_helper.rb2
-rw-r--r--app/uploaders/workhorse.rb2
-rw-r--r--app/validators/abstract_path_validator.rb2
-rw-r--r--app/validators/certificate_fingerprint_validator.rb2
-rw-r--r--app/validators/certificate_key_validator.rb2
-rw-r--r--app/validators/certificate_validator.rb2
-rw-r--r--app/validators/cluster_name_validator.rb2
-rw-r--r--app/validators/color_validator.rb2
-rw-r--r--app/validators/cron_timezone_validator.rb2
-rw-r--r--app/validators/cron_validator.rb2
-rw-r--r--app/validators/duration_validator.rb2
-rw-r--r--app/validators/email_validator.rb2
-rw-r--r--app/validators/key_restriction_validator.rb2
-rw-r--r--app/validators/line_code_validator.rb2
-rw-r--r--app/validators/namespace_name_validator.rb2
-rw-r--r--app/validators/namespace_path_validator.rb2
-rw-r--r--app/validators/project_path_validator.rb2
-rw-r--r--app/validators/public_url_validator.rb2
-rw-r--r--app/validators/top_level_group_validator.rb2
-rw-r--r--app/validators/url_validator.rb2
-rw-r--r--app/validators/variable_duplicates_validator.rb6
-rw-r--r--app/views/admin/application_settings/show.html.haml3
-rw-r--r--app/views/admin/dashboard/index.html.haml8
-rw-r--r--app/views/admin/identities/_form.html.haml4
-rw-r--r--app/views/admin/identities/_identity.html.haml6
-rw-r--r--app/views/admin/identities/edit.html.haml4
-rw-r--r--app/views/admin/identities/index.html.haml10
-rw-r--r--app/views/admin/identities/new.html.haml4
-rw-r--r--app/views/admin/labels/_form.html.haml10
-rw-r--r--app/views/admin/labels/_label.html.haml4
-rw-r--r--app/views/admin/labels/edit.html.haml8
-rw-r--r--app/views/admin/labels/index.html.haml8
-rw-r--r--app/views/admin/labels/new.html.haml4
-rw-r--r--app/views/admin/runners/_runner.html.haml4
-rw-r--r--app/views/admin/runners/show.html.haml4
-rw-r--r--app/views/dashboard/todos/index.html.haml48
-rw-r--r--app/views/devise/sessions/_new_base.html.haml4
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml4
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml2
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/groups/merge_requests.html.haml2
-rw-r--r--app/views/groups/new.html.haml46
-rw-r--r--app/views/help/_shortcuts.html.haml6
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml1
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml33
-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/profiles/keys/_form.html.haml6
-rw-r--r--app/views/profiles/keys/index.html.haml9
-rw-r--r--app/views/projects/_export.html.haml2
-rw-r--r--app/views/projects/_new_project_fields.html.haml10
-rw-r--r--app/views/projects/clusters/_dropdown.html.haml12
-rw-r--r--app/views/projects/clusters/_gcp_signup_offer_banner.html.haml2
-rw-r--r--app/views/projects/clusters/_integration_form.html.haml35
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml8
-rw-r--r--app/views/projects/clusters/gcp/_header.html.haml2
-rw-r--r--app/views/projects/clusters/gcp/login.html.haml20
-rw-r--r--app/views/projects/clusters/gcp/new.html.haml10
-rw-r--r--app/views/projects/clusters/new.html.haml35
-rw-r--r--app/views/projects/clusters/user/_form.html.haml13
-rw-r--r--app/views/projects/clusters/user/_header.html.haml2
-rw-r--r--app/views/projects/clusters/user/new.html.haml11
-rw-r--r--app/views/projects/deploy_tokens/_index.html.haml2
-rw-r--r--app/views/projects/deploy_tokens/_revoke_modal.html.haml2
-rw-r--r--app/views/projects/deployments/_commit.html.haml2
-rw-r--r--app/views/projects/deployments/_deployment.html.haml8
-rw-r--r--app/views/projects/deployments/_rollback.haml4
-rw-r--r--app/views/projects/edit.html.haml8
-rw-r--r--app/views/projects/environments/_external_url.html.haml2
-rw-r--r--app/views/projects/environments/_metrics_button.html.haml2
-rw-r--r--app/views/projects/environments/_terminal_button.html.haml2
-rw-r--r--app/views/projects/environments/empty.html.haml14
-rw-r--r--app/views/projects/environments/metrics.html.haml9
-rw-r--r--app/views/projects/environments/terminal.html.haml2
-rw-r--r--app/views/projects/graphs/charts.html.haml2
-rw-r--r--app/views/projects/issues/_discussion.html.haml3
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml8
-rw-r--r--app/views/projects/jobs/terminal.html.haml11
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml37
-rw-r--r--app/views/projects/merge_requests/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml36
-rw-r--r--app/views/projects/mirrors/_push.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml2
-rw-r--r--app/views/projects/runners/_runner.html.haml2
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml4
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml2
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml4
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml51
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml28
-rw-r--r--app/views/projects/settings/integrations/_project_hook.html.haml8
-rw-r--r--app/views/projects/settings/integrations/show.html.haml4
-rw-r--r--app/views/projects/settings/members/show.html.haml2
-rw-r--r--app/views/projects/settings/repository/show.html.haml4
-rw-r--r--app/views/projects/snippets/_actions.html.haml26
-rw-r--r--app/views/projects/snippets/edit.html.haml6
-rw-r--r--app/views/projects/snippets/index.html.haml4
-rw-r--r--app/views/projects/snippets/new.html.haml8
-rw-r--r--app/views/projects/snippets/show.html.haml4
-rw-r--r--app/views/shared/_label.html.haml10
-rw-r--r--app/views/shared/_milestone_expired.html.haml9
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml16
-rw-r--r--app/views/shared/_user_dropdown_contributing_link.html.haml5
-rw-r--r--app/views/shared/boards/_show.html.haml9
-rw-r--r--app/views/shared/boards/components/_board.html.haml12
-rw-r--r--app/views/shared/boards/components/_sidebar.html.haml3
-rw-r--r--app/views/shared/boards/components/sidebar/_due_date.html.haml12
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml8
-rw-r--r--app/views/shared/boards/components/sidebar/_milestone.html.haml12
-rw-r--r--app/views/shared/empty_states/_issues.html.haml2
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml14
-rw-r--r--app/views/shared/issuable/form/_title.html.haml2
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml4
-rw-r--r--app/views/shared/milestones/_milestone.html.haml105
-rw-r--r--app/views/shared/notes/_form.html.haml2
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml7
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/runners/show.html.haml16
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml9
-rw-r--r--app/views/u2f/_authenticate.html.haml3
-rw-r--r--app/workers/admin_email_worker.rb2
-rw-r--r--app/workers/all_queues.yml5
-rw-r--r--app/workers/archive_trace_worker.rb4
-rw-r--r--app/workers/authorized_projects_worker.rb2
-rw-r--r--app/workers/background_migration_worker.rb2
-rw-r--r--app/workers/build_coverage_worker.rb2
-rw-r--r--app/workers/build_finished_worker.rb2
-rw-r--r--app/workers/build_hooks_worker.rb2
-rw-r--r--app/workers/build_queue_worker.rb2
-rw-r--r--app/workers/build_success_worker.rb2
-rw-r--r--app/workers/build_trace_sections_worker.rb2
-rw-r--r--app/workers/ci/archive_traces_cron_worker.rb3
-rw-r--r--app/workers/ci/build_trace_chunk_flush_worker.rb2
-rw-r--r--app/workers/cluster_install_app_worker.rb2
-rw-r--r--app/workers/cluster_provision_worker.rb2
-rw-r--r--app/workers/cluster_wait_for_app_installation_worker.rb2
-rw-r--r--app/workers/cluster_wait_for_ingress_ip_address_worker.rb2
-rw-r--r--app/workers/concerns/application_worker.rb2
-rw-r--r--app/workers/concerns/cluster_applications.rb2
-rw-r--r--app/workers/concerns/cluster_queue.rb2
-rw-r--r--app/workers/concerns/cronjob_queue.rb2
-rw-r--r--app/workers/concerns/each_shard_worker.rb31
-rw-r--r--app/workers/concerns/exception_backtrace.rb2
-rw-r--r--app/workers/concerns/gitlab/github_import/queue.rb2
-rw-r--r--app/workers/concerns/mail_scheduler_queue.rb2
-rw-r--r--app/workers/concerns/new_issuable.rb2
-rw-r--r--app/workers/concerns/object_storage_queue.rb2
-rw-r--r--app/workers/concerns/pipeline_background_queue.rb2
-rw-r--r--app/workers/concerns/pipeline_queue.rb2
-rw-r--r--app/workers/concerns/project_import_options.rb2
-rw-r--r--app/workers/concerns/project_start_import.rb2
-rw-r--r--app/workers/concerns/repository_check_queue.rb2
-rw-r--r--app/workers/concerns/waitable_worker.rb2
-rw-r--r--app/workers/create_gpg_signature_worker.rb2
-rw-r--r--app/workers/create_note_diff_file_worker.rb2
-rw-r--r--app/workers/create_pipeline_worker.rb2
-rw-r--r--app/workers/delete_diff_files_worker.rb17
-rw-r--r--app/workers/delete_merged_branches_worker.rb2
-rw-r--r--app/workers/delete_user_worker.rb2
-rw-r--r--app/workers/email_receiver_worker.rb2
-rw-r--r--app/workers/emails_on_push_worker.rb2
-rw-r--r--app/workers/expire_build_artifacts_worker.rb2
-rw-r--r--app/workers/expire_build_instance_artifacts_worker.rb2
-rw-r--r--app/workers/expire_job_cache_worker.rb2
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb2
-rw-r--r--app/workers/git_garbage_collect_worker.rb2
-rw-r--r--app/workers/gitlab_shell_worker.rb2
-rw-r--r--app/workers/gitlab_usage_ping_worker.rb2
-rw-r--r--app/workers/group_destroy_worker.rb2
-rw-r--r--app/workers/import_export_project_cleanup_worker.rb2
-rw-r--r--app/workers/invalid_gpg_signature_update_worker.rb2
-rw-r--r--app/workers/irker_worker.rb17
-rw-r--r--app/workers/issue_due_scheduler_worker.rb2
-rw-r--r--app/workers/mail_scheduler/issue_due_worker.rb2
-rw-r--r--app/workers/mail_scheduler/notification_service_worker.rb2
-rw-r--r--app/workers/merge_worker.rb2
-rw-r--r--app/workers/namespaceless_project_destroy_worker.rb2
-rw-r--r--app/workers/new_issue_worker.rb2
-rw-r--r--app/workers/new_merge_request_worker.rb2
-rw-r--r--app/workers/new_note_worker.rb2
-rw-r--r--app/workers/object_storage/background_move_worker.rb2
-rw-r--r--app/workers/object_storage_upload_worker.rb2
-rw-r--r--app/workers/pages_domain_verification_cron_worker.rb2
-rw-r--r--app/workers/pages_domain_verification_worker.rb2
-rw-r--r--app/workers/pages_worker.rb2
-rw-r--r--app/workers/pipeline_hooks_worker.rb2
-rw-r--r--app/workers/pipeline_metrics_worker.rb2
-rw-r--r--app/workers/pipeline_notification_worker.rb2
-rw-r--r--app/workers/pipeline_process_worker.rb2
-rw-r--r--app/workers/pipeline_schedule_worker.rb2
-rw-r--r--app/workers/pipeline_success_worker.rb2
-rw-r--r--app/workers/pipeline_update_worker.rb2
-rw-r--r--app/workers/plugin_worker.rb2
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/process_commit_worker.rb2
-rw-r--r--app/workers/project_cache_worker.rb2
-rw-r--r--app/workers/project_destroy_worker.rb2
-rw-r--r--app/workers/project_export_worker.rb2
-rw-r--r--app/workers/project_migrate_hashed_storage_worker.rb2
-rw-r--r--app/workers/project_service_worker.rb2
-rw-r--r--app/workers/propagate_service_template_worker.rb2
-rw-r--r--app/workers/prune_old_events_worker.rb2
-rw-r--r--app/workers/prune_web_hook_logs_worker.rb26
-rw-r--r--app/workers/reactive_caching_worker.rb2
-rw-r--r--app/workers/rebase_worker.rb2
-rw-r--r--app/workers/remove_expired_group_links_worker.rb2
-rw-r--r--app/workers/remove_expired_members_worker.rb2
-rw-r--r--app/workers/remove_old_web_hook_logs_worker.rb2
-rw-r--r--app/workers/remove_unreferenced_lfs_objects_worker.rb2
-rw-r--r--app/workers/repository_archive_cache_worker.rb2
-rw-r--r--app/workers/repository_check/batch_worker.rb19
-rw-r--r--app/workers/repository_check/clear_worker.rb2
-rw-r--r--app/workers/repository_check/dispatch_worker.rb15
-rw-r--r--app/workers/repository_check/single_repository_worker.rb2
-rw-r--r--app/workers/repository_fork_worker.rb2
-rw-r--r--app/workers/repository_import_worker.rb2
-rw-r--r--app/workers/repository_remove_remote_worker.rb2
-rw-r--r--app/workers/repository_update_remote_mirror_worker.rb2
-rw-r--r--app/workers/requests_profiles_worker.rb2
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb2
-rw-r--r--app/workers/schedule_update_user_activity_worker.rb2
-rw-r--r--app/workers/stage_update_worker.rb2
-rw-r--r--app/workers/storage_migrator_worker.rb2
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb2
-rw-r--r--app/workers/stuck_import_jobs_worker.rb2
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb2
-rw-r--r--app/workers/system_hook_push_worker.rb2
-rw-r--r--app/workers/trending_projects_worker.rb2
-rw-r--r--app/workers/update_head_pipeline_for_merge_request_worker.rb2
-rw-r--r--app/workers/update_merge_requests_worker.rb2
-rw-r--r--app/workers/update_user_activity_worker.rb2
-rw-r--r--app/workers/upload_checksum_worker.rb2
-rw-r--r--app/workers/wait_for_cluster_creation_worker.rb2
-rw-r--r--app/workers/web_hook_worker.rb2
703 files changed, 10215 insertions, 4360 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/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js
index bd08308904c..54e86f329e4 100644
--- a/app/assets/javascripts/ajax_loading_spinner.js
+++ b/app/assets/javascripts/ajax_loading_spinner.js
@@ -26,7 +26,7 @@ export default class AjaxLoadingSpinner {
}
static toggleLoadingIcon(iconElement) {
- const classList = iconElement.classList;
+ const { classList } = iconElement;
classList.toggle(iconElement.dataset.icon);
classList.toggle('fa-spinner');
classList.toggle('fa-spin');
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 000938e475f..0ca0e8f35dd 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -150,14 +150,15 @@ const Api = {
},
// Return group projects list. Filtered by query
- groupProjects(groupId, query, callback) {
+ groupProjects(groupId, query, options, callback) {
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
+ const defaults = {
+ search: query,
+ per_page: 20,
+ };
return axios
.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
+ params: Object.assign({}, defaults, options),
})
.then(({ data }) => callback(data));
},
@@ -243,6 +244,15 @@ const Api = {
});
},
+ createBranch(id, { ref, branch }) {
+ const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));
+
+ return axios.post(url, {
+ ref,
+ branch,
+ });
+ },
+
buildUrl(url) {
let urlRoot = '';
if (gon.relative_url_root != null) {
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/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index 75834ba351d..00419e80cbb 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -52,7 +52,7 @@ export default function initCopyToClipboard() {
* data types to the intended values.
*/
$(document).on('copy', 'body > textarea[readonly]', (e) => {
- const clipboardData = e.originalEvent.clipboardData;
+ const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const text = e.target.value;
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index 1ea6dd909e9..5d7a3bed301 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';
@@ -321,7 +321,7 @@ export class CopyAsGFM {
}
static copyAsGFM(e, transformer) {
- const clipboardData = e.originalEvent.clipboardData;
+ const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const documentFragment = getSelectedFragment();
@@ -338,7 +338,7 @@ export class CopyAsGFM {
}
static pasteGFM(e) {
- const clipboardData = e.originalEvent.clipboardData;
+ const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const text = clipboardData.getData('text/plain');
diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
index 766039404ce..7986287f7e7 100644
--- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
@@ -84,7 +84,7 @@ class BalsamiqViewer {
renderTemplate(preview) {
const resource = this.getResource(preview.resourceID);
const name = BalsamiqViewer.parseTitle(resource);
- const image = preview.image;
+ const { image } = preview;
const template = PREVIEW_TEMPLATE({
name,
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index 06ef86ecb77..b88e69a07bf 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -12,7 +12,7 @@ export default function loadBalsamiqFile() {
if (!(viewer instanceof Element)) return;
- const endpoint = viewer.dataset.endpoint;
+ const { endpoint } = viewer.dataset;
const balsamiqViewer = new BalsamiqViewer(viewer);
balsamiqViewer.loadFile(endpoint).catch(onError);
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/stl_viewer.js b/app/assets/javascripts/blob/stl_viewer.js
index 63236b6477f..339906adc34 100644
--- a/app/assets/javascripts/blob/stl_viewer.js
+++ b/app/assets/javascripts/blob/stl_viewer.js
@@ -5,7 +5,7 @@ export default () => {
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
el.addEventListener('click', (e) => {
- const target = e.target;
+ const { target } = e;
e.preventDefault();
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 7920e08e4d8..a2355d7fd5c 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, space-before-function-paren, one-var */
+/* eslint-disable comma-dangle */
import Sortable from 'sortablejs';
import Vue from 'vue';
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index b7d3574bc80..0398102ad02 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,78 +1,78 @@
<script>
-/* eslint-disable vue/require-default-prop */
-import './issue_card_inner';
-import eventHub from '../eventhub';
+ /* eslint-disable vue/require-default-prop */
+ import IssueCardInner from './issue_card_inner.vue';
+ import eventHub from '../eventhub';
-const Store = gl.issueBoards.BoardsStore;
+ const Store = gl.issueBoards.BoardsStore;
-export default {
- name: 'BoardsIssueCard',
- components: {
- 'issue-card-inner': gl.issueBoards.IssueCardInner,
- },
- props: {
- list: {
- type: Object,
- default: () => ({}),
+ export default {
+ name: 'BoardsIssueCard',
+ components: {
+ IssueCardInner,
},
- issue: {
- type: Object,
- default: () => ({}),
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
+ },
+ issue: {
+ type: Object,
+ default: () => ({}),
+ },
+ issueLinkBase: {
+ type: String,
+ default: '',
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ index: {
+ type: Number,
+ default: 0,
+ },
+ rootPath: {
+ type: String,
+ default: '',
+ },
+ groupId: {
+ type: Number,
+ },
},
- issueLinkBase: {
- type: String,
- default: '',
+ data() {
+ return {
+ showDetail: false,
+ detailIssue: Store.detail,
+ };
},
- disabled: {
- type: Boolean,
- default: false,
+ computed: {
+ issueDetailVisible() {
+ return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+ },
},
- index: {
- type: Number,
- default: 0,
- },
- rootPath: {
- type: String,
- default: '',
- },
- groupId: {
- type: Number,
- },
- },
- data() {
- return {
- showDetail: false,
- detailIssue: Store.detail,
- };
- },
- computed: {
- issueDetailVisible() {
- return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
- },
- },
- methods: {
- mouseDown() {
- this.showDetail = true;
- },
- mouseMove() {
- this.showDetail = false;
- },
- showIssue(e) {
- if (e.target.classList.contains('js-no-trigger')) return;
-
- if (this.showDetail) {
+ methods: {
+ mouseDown() {
+ this.showDetail = true;
+ },
+ mouseMove() {
this.showDetail = false;
+ },
+ showIssue(e) {
+ if (e.target.classList.contains('js-no-trigger')) return;
+
+ if (this.showDetail) {
+ this.showDetail = false;
- if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
- eventHub.$emit('clearDetailIssue');
- } else {
- eventHub.$emit('newDetailIssue', this.issue);
- Store.detail.list = this.list;
+ if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
+ eventHub.$emit('clearDetailIssue');
+ } else {
+ eventHub.$emit('newDetailIssue', this.issue);
+ Store.detail.list = this.list;
+ }
}
- }
+ },
},
- },
-};
+ };
</script>
<template>
diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js
index 4dd9aebeed9..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';
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 82fe6b0c5fb..371be109229 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';
@@ -6,13 +6,13 @@ import Flash from '../../flash';
import { __ } from '../../locale';
import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub';
-import assigneeTitle from '../../sidebar/components/assignees/assignee_title.vue';
-import assignees from '../../sidebar/components/assignees/assignees.vue';
+import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue';
+import Assignees from '../../sidebar/components/assignees/assignees.vue';
import DueDateSelectors from '../../due_date_select';
-import './sidebar/remove_issue';
+import RemoveBtn from './sidebar/remove_issue.vue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
-import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
+import Subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
import MilestoneSelect from '../../milestone_select';
const Store = gl.issueBoards.BoardsStore;
@@ -22,10 +22,10 @@ window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardSidebar = Vue.extend({
components: {
- assigneeTitle,
- assignees,
- removeBtn: gl.issueBoards.RemoveIssueBtn,
- subscriptions,
+ AssigneeTitle,
+ Assignees,
+ RemoveBtn,
+ Subscriptions,
},
props: {
currentUser: {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
deleted file mode 100644
index f7d7b910e2f..00000000000
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ /dev/null
@@ -1,196 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import eventHub from '../eventhub';
-
-const Store = gl.issueBoards.BoardsStore;
-
-window.gl = window.gl || {};
-window.gl.issueBoards = window.gl.issueBoards || {};
-
-gl.issueBoards.IssueCardInner = Vue.extend({
- components: {
- UserAvatarLink,
- },
- props: {
- issue: {
- type: Object,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- list: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- rootPath: {
- type: String,
- required: true,
- },
- updateFilters: {
- type: Boolean,
- required: false,
- default: false,
- },
- groupId: {
- type: Number,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- limitBeforeCounter: 3,
- maxRender: 4,
- maxCounter: 99,
- };
- },
- computed: {
- numberOverLimit() {
- return this.issue.assignees.length - this.limitBeforeCounter;
- },
- assigneeCounterTooltip() {
- return `${this.assigneeCounterLabel} more`;
- },
- assigneeCounterLabel() {
- if (this.numberOverLimit > this.maxCounter) {
- return `${this.maxCounter}+`;
- }
-
- return `+${this.numberOverLimit}`;
- },
- shouldRenderCounter() {
- if (this.issue.assignees.length <= this.maxRender) {
- return false;
- }
-
- return this.issue.assignees.length > this.numberOverLimit;
- },
- issueId() {
- if (this.issue.iid) {
- return `#${this.issue.iid}`;
- }
- return false;
- },
- showLabelFooter() {
- return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
- },
- },
- methods: {
- isIndexLessThanlimit(index) {
- return index < this.limitBeforeCounter;
- },
- shouldRenderAssignee(index) {
- // Eg. maxRender is 4,
- // Render up to all 4 assignees if there are only 4 assigness
- // Otherwise render up to the limitBeforeCounter
- if (this.issue.assignees.length <= this.maxRender) {
- return index < this.maxRender;
- }
-
- return index < this.limitBeforeCounter;
- },
- assigneeUrl(assignee) {
- return `${this.rootPath}${assignee.username}`;
- },
- assigneeUrlTitle(assignee) {
- return `Assigned to ${assignee.name}`;
- },
- avatarUrlTitle(assignee) {
- return `Avatar for ${assignee.name}`;
- },
- showLabel(label) {
- if (!label.id) return false;
- return true;
- },
- filterByLabel(label, e) {
- if (!this.updateFilters) return;
-
- const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
- const labelTitle = encodeURIComponent(label.title);
- const param = `label_name[]=${labelTitle}`;
- const labelIndex = filterPath.indexOf(param);
- $(e.currentTarget).tooltip('hide');
-
- if (labelIndex === -1) {
- filterPath.push(param);
- } else {
- filterPath.splice(labelIndex, 1);
- }
-
- gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
-
- Store.updateFiltersUrl();
-
- eventHub.$emit('updateTokens');
- },
- labelStyle(label) {
- return {
- backgroundColor: label.color,
- color: label.textColor,
- };
- },
- },
- template: `
- <div>
- <div class="board-card-header">
- <h4 class="board-card-title">
- <i
- class="fa fa-eye-slash confidential-icon"
- v-if="issue.confidential"
- aria-hidden="true"
- />
- <a
- class="js-no-trigger"
- :href="issue.path"
- :title="issue.title">{{ issue.title }}</a>
- <span
- class="board-card-number"
- v-if="issueId"
- >
- {{ issue.referencePath }}
- </span>
- </h4>
- <div class="board-card-assignee">
- <user-avatar-link
- v-for="(assignee, index) in issue.assignees"
- :key="assignee.id"
- v-if="shouldRenderAssignee(index)"
- class="js-no-trigger"
- :link-href="assigneeUrl(assignee)"
- :img-alt="avatarUrlTitle(assignee)"
- :img-src="assignee.avatar"
- :tooltip-text="assigneeUrlTitle(assignee)"
- tooltip-placement="bottom"
- />
- <span
- class="avatar-counter has-tooltip"
- :title="assigneeCounterTooltip"
- v-if="shouldRenderCounter"
- >
- {{ assigneeCounterLabel }}
- </span>
- </div>
- </div>
- <div
- class="board-card-footer"
- v-if="showLabelFooter"
- >
- <button
- class="badge color-label has-tooltip"
- v-for="label in issue.labels"
- type="button"
- v-if="showLabel(label)"
- @click="filterByLabel(label, $event)"
- :style="labelStyle(label)"
- :title="label.description"
- data-container="body">
- {{ label.title }}
- </button>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
new file mode 100644
index 00000000000..d50641dc3a9
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -0,0 +1,202 @@
+<script>
+ import $ from 'jquery';
+ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+ import eventHub from '../eventhub';
+ import tooltip from '../../vue_shared/directives/tooltip';
+
+ const Store = gl.issueBoards.BoardsStore;
+
+ export default {
+ components: {
+ UserAvatarLink,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ updateFilters: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ groupId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ limitBeforeCounter: 3,
+ maxRender: 4,
+ maxCounter: 99,
+ };
+ },
+ computed: {
+ numberOverLimit() {
+ return this.issue.assignees.length - this.limitBeforeCounter;
+ },
+ assigneeCounterTooltip() {
+ return `${this.assigneeCounterLabel} more`;
+ },
+ assigneeCounterLabel() {
+ if (this.numberOverLimit > this.maxCounter) {
+ return `${this.maxCounter}+`;
+ }
+
+ return `+${this.numberOverLimit}`;
+ },
+ shouldRenderCounter() {
+ if (this.issue.assignees.length <= this.maxRender) {
+ return false;
+ }
+
+ return this.issue.assignees.length > this.numberOverLimit;
+ },
+ issueId() {
+ if (this.issue.iid) {
+ return `#${this.issue.iid}`;
+ }
+ return false;
+ },
+ showLabelFooter() {
+ return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
+ },
+ },
+ methods: {
+ isIndexLessThanlimit(index) {
+ return index < this.limitBeforeCounter;
+ },
+ shouldRenderAssignee(index) {
+ // Eg. maxRender is 4,
+ // Render up to all 4 assignees if there are only 4 assigness
+ // Otherwise render up to the limitBeforeCounter
+ if (this.issue.assignees.length <= this.maxRender) {
+ return index < this.maxRender;
+ }
+
+ return index < this.limitBeforeCounter;
+ },
+ assigneeUrl(assignee) {
+ return `${this.rootPath}${assignee.username}`;
+ },
+ assigneeUrlTitle(assignee) {
+ return `Assigned to ${assignee.name}`;
+ },
+ avatarUrlTitle(assignee) {
+ return `Avatar for ${assignee.name}`;
+ },
+ showLabel(label) {
+ if (!label.id) return false;
+ return true;
+ },
+ filterByLabel(label, e) {
+ if (!this.updateFilters) return;
+
+ const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
+ const labelTitle = encodeURIComponent(label.title);
+ const param = `label_name[]=${labelTitle}`;
+ const labelIndex = filterPath.indexOf(param);
+ $(e.currentTarget).tooltip('hide');
+
+ if (labelIndex === -1) {
+ filterPath.push(param);
+ } else {
+ filterPath.splice(labelIndex, 1);
+ }
+
+ gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
+
+ Store.updateFiltersUrl();
+
+ eventHub.$emit('updateTokens');
+ },
+ labelStyle(label) {
+ return {
+ backgroundColor: label.color,
+ color: label.textColor,
+ };
+ },
+ },
+ };
+</script>
+<template>
+ <div>
+ <div class="board-card-header">
+ <h4 class="board-card-title">
+ <i
+ v-if="issue.confidential"
+ class="fa fa-eye-slash confidential-icon"
+ aria-hidden="true"
+ ></i>
+ <a
+ :href="issue.path"
+ :title="issue.title"
+ class="js-no-trigger">{{ issue.title }}</a>
+ <span
+ v-if="issueId"
+ class="board-card-number"
+ >
+ {{ issue.referencePath }}
+ </span>
+ </h4>
+ <div class="board-card-assignee">
+ <user-avatar-link
+ v-for="(assignee, index) in issue.assignees"
+ v-if="shouldRenderAssignee(index)"
+ :key="assignee.id"
+ :link-href="assigneeUrl(assignee)"
+ :img-alt="avatarUrlTitle(assignee)"
+ :img-src="assignee.avatar"
+ :tooltip-text="assigneeUrlTitle(assignee)"
+ class="js-no-trigger"
+ tooltip-placement="bottom"
+ />
+ <span
+ v-tooltip
+ v-if="shouldRenderCounter"
+ :title="assigneeCounterTooltip"
+ class="avatar-counter"
+ >
+ {{ assigneeCounterLabel }}
+ </span>
+ </div>
+ </div>
+ <div
+ v-if="showLabelFooter"
+ class="board-card-footer"
+ >
+ <button
+ v-tooltip
+ v-for="label in issue.labels"
+ v-if="showLabel(label)"
+ :key="label.id"
+ :style="labelStyle(label)"
+ :title="label.description"
+ class="badge color-label"
+ type="button"
+ data-container="body"
+ @click="filterByLabel(label, $event)"
+ >
+ {{ label.title }}
+ </button>
+ </div>
+ </div>
+</template>
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/footer.js b/app/assets/javascripts/boards/components/modal/footer.vue
index 2745ca219ad..d4affc8c3de 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -1,14 +1,14 @@
-import Vue from 'vue';
+<script>
import Flash from '../../../flash';
import { __ } from '../../../locale';
-import './lists_dropdown';
+import ListsDropdown from './lists_dropdown.vue';
import { pluralize } from '../../../lib/utils/text_utility';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
-gl.issueBoards.ModalFooter = Vue.extend({
+export default {
components: {
- 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
+ ListsDropdown,
},
mixins: [modalMixin],
data() {
@@ -28,23 +28,29 @@ gl.issueBoards.ModalFooter = Vue.extend({
},
},
methods: {
+ buildUpdateRequest(list) {
+ return {
+ add_label_ids: [list.label.id],
+ };
+ },
addIssues() {
const firstListIndex = 1;
const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.id);
+ const req = this.buildUpdateRequest(list);
// Post the data to the backend
- gl.boardService.bulkUpdate(issueIds, {
- add_label_ids: [list.label.id],
- }).catch(() => {
- Flash(__('Failed to update issues, please try again.'));
+ gl.boardService
+ .bulkUpdate(issueIds, req)
+ .catch(() => {
+ Flash(__('Failed to update issues, please try again.'));
- selectedIssues.forEach((issue) => {
- list.removeIssue(issue);
- list.issuesSize -= 1;
+ selectedIssues.forEach((issue) => {
+ list.removeIssue(issue);
+ list.issuesSize -= 1;
+ });
});
- });
// Add the issues on the frontend
selectedIssues.forEach((issue) => {
@@ -55,28 +61,32 @@ gl.issueBoards.ModalFooter = Vue.extend({
this.toggleModal(false);
},
},
- template: `
- <footer
- class="form-actions add-issues-footer">
- <div class="float-left">
- <button
- class="btn btn-success"
- type="button"
- :disabled="submitDisabled"
- @click="addIssues">
- {{ submitText }}
- </button>
- <span class="inline add-issues-footer-to-list">
- to list
- </span>
- <lists-dropdown></lists-dropdown>
- </div>
+};
+</script>
+<template>
+ <footer
+ class="form-actions add-issues-footer"
+ >
+ <div class="float-left">
<button
- class="btn btn-default float-right"
+ :disabled="submitDisabled"
+ class="btn btn-success"
type="button"
- @click="toggleModal(false)">
- Cancel
+ @click="addIssues"
+ >
+ {{ submitText }}
</button>
- </footer>
- `,
-});
+ <span class="inline add-issues-footer-to-list">
+ to list
+ </span>
+ <lists-dropdown/>
+ </div>
+ <button
+ class="btn btn-default float-right"
+ type="button"
+ @click="toggleModal(false)"
+ >
+ Cancel
+ </button>
+ </footer>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js
deleted file mode 100644
index 5e511bb8935..00000000000
--- a/app/assets/javascripts/boards/components/modal/header.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import Vue from 'vue';
-import modalFilters from './filters';
-import './tabs';
-import ModalStore from '../../stores/modal_store';
-import modalMixin from '../../mixins/modal_mixins';
-
-gl.issueBoards.ModalHeader = Vue.extend({
- components: {
- 'modal-tabs': gl.issueBoards.ModalTabs,
- modalFilters,
- },
- mixins: [modalMixin],
- props: {
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- computed: {
- selectAllText() {
- if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
- return 'Select all';
- }
-
- return 'Deselect all';
- },
- showSearch() {
- return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
- },
- },
- methods: {
- toggleAll() {
- this.$refs.selectAllBtn.blur();
-
- ModalStore.toggleAll();
- },
- },
- template: `
- <div>
- <header class="add-issues-header form-actions">
- <h2>
- Add issues
- <button
- type="button"
- class="close"
- data-dismiss="modal"
- aria-label="Close"
- @click="toggleModal(false)">
- <span aria-hidden="true">×</span>
- </button>
- </h2>
- </header>
- <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
- <div
- class="add-issues-search append-bottom-10"
- v-if="showSearch">
- <modal-filters :store="filter" />
- <button
- type="button"
- class="btn btn-success btn-inverted prepend-left-10"
- ref="selectAllBtn"
- @click="toggleAll">
- {{ selectAllText }}
- </button>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
new file mode 100644
index 00000000000..979fb4d7199
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -0,0 +1,82 @@
+<script>
+ import ModalFilters from './filters';
+ import ModalTabs from './tabs.vue';
+ import ModalStore from '../../stores/modal_store';
+ import modalMixin from '../../mixins/modal_mixins';
+
+ export default {
+ components: {
+ ModalTabs,
+ ModalFilters,
+ },
+ mixins: [modalMixin],
+ props: {
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectAllText() {
+ if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
+ return 'Select all';
+ }
+
+ return 'Deselect all';
+ },
+ showSearch() {
+ return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
+ },
+ },
+ methods: {
+ toggleAll() {
+ this.$refs.selectAllBtn.blur();
+
+ ModalStore.toggleAll();
+ },
+ },
+ };
+</script>
+<template>
+ <div>
+ <header class="add-issues-header form-actions">
+ <h2>
+ Add issues
+ <button
+ type="button"
+ class="close"
+ data-dismiss="modal"
+ aria-label="Close"
+ @click="toggleModal(false)"
+ >
+ <span aria-hidden="true">×</span>
+ </button>
+ </h2>
+ </header>
+ <modal-tabs v-if="!loading && issuesCount > 0"/>
+ <div
+ v-if="showSearch"
+ class="add-issues-search append-bottom-10">
+ <modal-filters :store="filter" />
+ <button
+ ref="selectAllBtn"
+ type="button"
+ class="btn btn-success btn-inverted prepend-left-10"
+ @click="toggleAll"
+ >
+ {{ selectAllText }}
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
deleted file mode 100644
index c8b2f45f177..00000000000
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ /dev/null
@@ -1,171 +0,0 @@
-/* global ListIssue */
-
-import Vue from 'vue';
-import queryData from '~/boards/utils/query_data';
-import loadingIcon from '~/vue_shared/components/loading_icon.vue';
-import './header';
-import './list';
-import './footer';
-import './empty_state';
-import ModalStore from '../../stores/modal_store';
-
-gl.issueBoards.IssuesModal = Vue.extend({
- components: {
- 'modal-header': gl.issueBoards.ModalHeader,
- 'modal-list': gl.issueBoards.ModalList,
- 'modal-footer': gl.issueBoards.ModalFooter,
- 'empty-state': gl.issueBoards.ModalEmptyState,
- loadingIcon,
- },
- props: {
- newIssuePath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- computed: {
- showList() {
- if (this.activeTab === 'selected') {
- return this.selectedIssues.length > 0;
- }
-
- return this.issuesCount > 0;
- },
- showEmptyState() {
- if (!this.loading && this.issuesCount === 0) {
- return true;
- }
-
- return this.activeTab === 'selected' && this.selectedIssues.length === 0;
- },
- },
- watch: {
- page() {
- this.loadIssues();
- },
- showAddIssuesModal() {
- if (this.showAddIssuesModal && !this.issues.length) {
- this.loading = true;
- const loadingDone = () => {
- this.loading = false;
- };
-
- this.loadIssues()
- .then(loadingDone)
- .catch(loadingDone);
- } else if (!this.showAddIssuesModal) {
- this.issues = [];
- this.selectedIssues = [];
- this.issuesCount = false;
- }
- },
- filter: {
- handler() {
- if (this.$el.tagName) {
- this.page = 1;
- this.filterLoading = true;
- const loadingDone = () => {
- this.filterLoading = false;
- };
-
- this.loadIssues(true)
- .then(loadingDone)
- .catch(loadingDone);
- }
- },
- deep: true,
- },
- },
- created() {
- this.page = 1;
- },
- methods: {
- loadIssues(clearIssues = false) {
- if (!this.showAddIssuesModal) return false;
-
- return gl.boardService.getBacklog(queryData(this.filter.path, {
- page: this.page,
- per: this.perPage,
- }))
- .then(res => res.data)
- .then((data) => {
- if (clearIssues) {
- this.issues = [];
- }
-
- data.issues.forEach((issueObj) => {
- const issue = new ListIssue(issueObj);
- const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
- issue.selected = !!foundSelectedIssue;
-
- this.issues.push(issue);
- });
-
- this.loadingNewPage = false;
-
- if (!this.issuesCount) {
- this.issuesCount = data.size;
- }
- }).catch(() => {
- // TODO: handle request error
- });
- },
- },
- template: `
- <div
- class="add-issues-modal"
- v-if="showAddIssuesModal">
- <div class="add-issues-container">
- <modal-header
- :project-id="projectId"
- :milestone-path="milestonePath"
- :label-path="labelPath">
- </modal-header>
- <modal-list
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :empty-state-svg="emptyStateSvg"
- v-if="!loading && showList && !filterLoading"></modal-list>
- <empty-state
- v-if="showEmptyState"
- :new-issue-path="newIssuePath"
- :empty-state-svg="emptyStateSvg"></empty-state>
- <section
- class="add-issues-list text-center"
- v-if="loading || filterLoading">
- <div class="add-issues-list-loading">
- <loading-icon />
- </div>
- </section>
- <modal-footer></modal-footer>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
new file mode 100644
index 00000000000..33e72a6782e
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -0,0 +1,178 @@
+<script>
+ /* global ListIssue */
+ import queryData from '~/boards/utils/query_data';
+ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+ import ModalHeader from './header.vue';
+ import ModalList from './list.vue';
+ import ModalFooter from './footer.vue';
+ import EmptyState from './empty_state.vue';
+ import ModalStore from '../../stores/modal_store';
+
+ export default {
+ components: {
+ EmptyState,
+ ModalHeader,
+ ModalList,
+ ModalFooter,
+ loadingIcon,
+ },
+ props: {
+ newIssuePath: {
+ type: String,
+ required: true,
+ },
+ emptyStateSvg: {
+ type: String,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ showList() {
+ if (this.activeTab === 'selected') {
+ return this.selectedIssues.length > 0;
+ }
+
+ return this.issuesCount > 0;
+ },
+ showEmptyState() {
+ if (!this.loading && this.issuesCount === 0) {
+ return true;
+ }
+
+ return this.activeTab === 'selected' && this.selectedIssues.length === 0;
+ },
+ },
+ watch: {
+ page() {
+ this.loadIssues();
+ },
+ showAddIssuesModal() {
+ if (this.showAddIssuesModal && !this.issues.length) {
+ this.loading = true;
+ const loadingDone = () => {
+ this.loading = false;
+ };
+
+ this.loadIssues()
+ .then(loadingDone)
+ .catch(loadingDone);
+ } else if (!this.showAddIssuesModal) {
+ this.issues = [];
+ this.selectedIssues = [];
+ this.issuesCount = false;
+ }
+ },
+ filter: {
+ handler() {
+ if (this.$el.tagName) {
+ this.page = 1;
+ this.filterLoading = true;
+ const loadingDone = () => {
+ this.filterLoading = false;
+ };
+
+ this.loadIssues(true)
+ .then(loadingDone)
+ .catch(loadingDone);
+ }
+ },
+ deep: true,
+ },
+ },
+ created() {
+ this.page = 1;
+ },
+ methods: {
+ loadIssues(clearIssues = false) {
+ if (!this.showAddIssuesModal) return false;
+
+ return gl.boardService
+ .getBacklog(
+ queryData(this.filter.path, {
+ page: this.page,
+ per: this.perPage,
+ }),
+ )
+ .then(res => res.data)
+ .then(data => {
+ if (clearIssues) {
+ this.issues = [];
+ }
+
+ data.issues.forEach(issueObj => {
+ const issue = new ListIssue(issueObj);
+ const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
+ issue.selected = !!foundSelectedIssue;
+
+ this.issues.push(issue);
+ });
+
+ this.loadingNewPage = false;
+
+ if (!this.issuesCount) {
+ this.issuesCount = data.size;
+ }
+ })
+ .catch(() => {
+ // TODO: handle request error
+ });
+ },
+ },
+ };
+</script>
+<template>
+ <div
+ v-if="showAddIssuesModal"
+ class="add-issues-modal">
+ <div class="add-issues-container">
+ <modal-header
+ :project-id="projectId"
+ :milestone-path="milestonePath"
+ :label-path="labelPath"
+ />
+ <modal-list
+ v-if="!loading && showList && !filterLoading"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ :empty-state-svg="emptyStateSvg"
+ />
+ <empty-state
+ v-if="showEmptyState"
+ :new-issue-path="newIssuePath"
+ :empty-state-svg="emptyStateSvg"
+ />
+ <section
+ v-if="loading || filterLoading"
+ class="add-issues-list text-center"
+ >
+ <div class="add-issues-list-loading">
+ <loading-icon />
+ </div>
+ </section>
+ <modal-footer/>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js
deleted file mode 100644
index 11061c72a7b..00000000000
--- a/app/assets/javascripts/boards/components/modal/list.js
+++ /dev/null
@@ -1,159 +0,0 @@
-import Vue from 'vue';
-import bp from '../../../breakpoints';
-import ModalStore from '../../stores/modal_store';
-
-gl.issueBoards.ModalList = Vue.extend({
- components: {
- 'issue-card-inner': gl.issueBoards.IssueCardInner,
- },
- props: {
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- computed: {
- loopIssues() {
- if (this.activeTab === 'all') {
- return this.issues;
- }
-
- return this.selectedIssues;
- },
- groupedIssues() {
- const groups = [];
- this.loopIssues.forEach((issue, i) => {
- const index = i % this.columns;
-
- if (!groups[index]) {
- groups.push([]);
- }
-
- groups[index].push(issue);
- });
-
- return groups;
- },
- },
- watch: {
- activeTab() {
- if (this.activeTab === 'all') {
- ModalStore.purgeUnselectedIssues();
- }
- },
- },
- mounted() {
- this.scrollHandlerWrapper = this.scrollHandler.bind(this);
- this.setColumnCountWrapper = this.setColumnCount.bind(this);
- this.setColumnCount();
-
- this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
- window.addEventListener('resize', this.setColumnCountWrapper);
- },
- beforeDestroy() {
- this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
- window.removeEventListener('resize', this.setColumnCountWrapper);
- },
- methods: {
- scrollHandler() {
- const currentPage = Math.floor(this.issues.length / this.perPage);
-
- if (
- this.scrollTop() > this.scrollHeight() - 100 &&
- !this.loadingNewPage &&
- currentPage === this.page
- ) {
- this.loadingNewPage = true;
- this.page += 1;
- }
- },
- toggleIssue(e, issue) {
- if (e.target.tagName !== 'A') {
- ModalStore.toggleIssue(issue);
- }
- },
- listHeight() {
- return this.$refs.list.getBoundingClientRect().height;
- },
- scrollHeight() {
- return this.$refs.list.scrollHeight;
- },
- scrollTop() {
- return this.$refs.list.scrollTop + this.listHeight();
- },
- showIssue(issue) {
- if (this.activeTab === 'all') return true;
-
- const index = ModalStore.selectedIssueIndex(issue);
-
- return index !== -1;
- },
- setColumnCount() {
- const breakpoint = bp.getBreakpointSize();
-
- if (breakpoint === 'lg' || breakpoint === 'md') {
- this.columns = 3;
- } else if (breakpoint === 'sm') {
- this.columns = 2;
- } else {
- this.columns = 1;
- }
- },
- },
- template: `
- <section
- class="add-issues-list add-issues-list-columns"
- ref="list">
- <div
- class="empty-state add-issues-empty-state-filter text-center"
- v-if="issuesCount > 0 && issues.length === 0">
- <div
- class="svg-content">
- <img :src="emptyStateSvg"/>
- </div>
- <div class="text-content">
- <h4>
- There are no issues to show.
- </h4>
- </div>
- </div>
- <div
- v-for="group in groupedIssues"
- class="add-issues-list-column">
- <div
- v-for="issue in group"
- v-if="showIssue(issue)"
- class="board-card-parent">
- <div
- class="board-card"
- :class="{ 'is-active': issue.selected }"
- @click="toggleIssue($event, issue)">
- <issue-card-inner
- :issue="issue"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath">
- </issue-card-inner>
- <span
- :aria-label="'Issue #' + issue.id + ' selected'"
- aria-checked="true"
- v-if="issue.selected"
- class="issue-card-selected text-center">
- <i class="fa fa-check"></i>
- </span>
- </div>
- </div>
- </div>
- </section>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
new file mode 100644
index 00000000000..a58b5afe970
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -0,0 +1,161 @@
+<script>
+ import bp from '../../../breakpoints';
+ import ModalStore from '../../stores/modal_store';
+ import IssueCardInner from '../issue_card_inner.vue';
+
+ export default {
+ components: {
+ IssueCardInner,
+ },
+ props: {
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ emptyStateSvg: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ loopIssues() {
+ if (this.activeTab === 'all') {
+ return this.issues;
+ }
+
+ return this.selectedIssues;
+ },
+ groupedIssues() {
+ const groups = [];
+ this.loopIssues.forEach((issue, i) => {
+ const index = i % this.columns;
+
+ if (!groups[index]) {
+ groups.push([]);
+ }
+
+ groups[index].push(issue);
+ });
+
+ return groups;
+ },
+ },
+ watch: {
+ activeTab() {
+ if (this.activeTab === 'all') {
+ ModalStore.purgeUnselectedIssues();
+ }
+ },
+ },
+ mounted() {
+ this.scrollHandlerWrapper = this.scrollHandler.bind(this);
+ this.setColumnCountWrapper = this.setColumnCount.bind(this);
+ this.setColumnCount();
+
+ this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
+ window.addEventListener('resize', this.setColumnCountWrapper);
+ },
+ beforeDestroy() {
+ this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
+ window.removeEventListener('resize', this.setColumnCountWrapper);
+ },
+ methods: {
+ scrollHandler() {
+ const currentPage = Math.floor(this.issues.length / this.perPage);
+
+ if (
+ this.scrollTop() > this.scrollHeight() - 100 &&
+ !this.loadingNewPage &&
+ currentPage === this.page
+ ) {
+ this.loadingNewPage = true;
+ this.page += 1;
+ }
+ },
+ toggleIssue(e, issue) {
+ if (e.target.tagName !== 'A') {
+ ModalStore.toggleIssue(issue);
+ }
+ },
+ listHeight() {
+ return this.$refs.list.getBoundingClientRect().height;
+ },
+ scrollHeight() {
+ return this.$refs.list.scrollHeight;
+ },
+ scrollTop() {
+ return this.$refs.list.scrollTop + this.listHeight();
+ },
+ showIssue(issue) {
+ if (this.activeTab === 'all') return true;
+
+ const index = ModalStore.selectedIssueIndex(issue);
+
+ return index !== -1;
+ },
+ setColumnCount() {
+ const breakpoint = bp.getBreakpointSize();
+
+ if (breakpoint === 'lg' || breakpoint === 'md') {
+ this.columns = 3;
+ } else if (breakpoint === 'sm') {
+ this.columns = 2;
+ } else {
+ this.columns = 1;
+ }
+ },
+ },
+ };
+</script>
+<template>
+ <section
+ ref="list"
+ class="add-issues-list add-issues-list-columns">
+ <div
+ v-if="issuesCount > 0 && issues.length === 0"
+ class="empty-state add-issues-empty-state-filter text-center">
+ <div class="svg-content">
+ <img :src="emptyStateSvg" />
+ </div>
+ <div class="text-content">
+ <h4>
+ There are no issues to show.
+ </h4>
+ </div>
+ </div>
+ <div
+ v-for="(group, index) in groupedIssues"
+ :key="index"
+ class="add-issues-list-column">
+ <div
+ v-for="issue in group"
+ v-if="showIssue(issue)"
+ :key="issue.id"
+ class="board-card-parent">
+ <div
+ :class="{ 'is-active': issue.selected }"
+ class="board-card"
+ @click="toggleIssue($event, issue)">
+ <issue-card-inner
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"/>
+ <span
+ v-if="issue.selected"
+ :aria-label="'Issue #' + issue.id + ' selected'"
+ aria-checked="true"
+ class="issue-card-selected text-center">
+ <i class="fa fa-check"></i>
+ </span>
+ </div>
+ </div>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
deleted file mode 100644
index e644de2d4fc..00000000000
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import Vue from 'vue';
-import ModalStore from '../../stores/modal_store';
-
-gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
- data() {
- return {
- modal: ModalStore.store,
- state: gl.issueBoards.BoardsStore.state,
- };
- },
- computed: {
- selected() {
- return this.modal.selectedList || this.state.lists[1];
- },
- },
- destroyed() {
- this.modal.selectedList = null;
- },
- template: `
- <div class="dropdown inline">
- <button
- class="dropdown-menu-toggle"
- type="button"
- data-toggle="dropdown"
- aria-expanded="false">
- <span
- class="dropdown-label-box"
- :style="{ backgroundColor: selected.label.color }">
- </span>
- {{ selected.title }}
- <i class="fa fa-chevron-down"></i>
- </button>
- <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
- <ul>
- <li
- v-for="list in state.lists"
- v-if="list.type == 'label'">
- <a
- href="#"
- role="button"
- :class="{ 'is-active': list.id == selected.id }"
- @click.prevent="modal.selectedList = list">
- <span
- class="dropdown-label-box"
- :style="{ backgroundColor: list.label.color }">
- </span>
- {{ list.title }}
- </a>
- </li>
- </ul>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
new file mode 100644
index 00000000000..6a5a39099bd
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
@@ -0,0 +1,56 @@
+<script>
+import ModalStore from '../../stores/modal_store';
+
+export default {
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ selected() {
+ return this.modal.selectedList || this.state.lists[1];
+ },
+ },
+ destroyed() {
+ this.modal.selectedList = null;
+ },
+};
+</script>
+<template>
+ <div class="dropdown inline">
+ <button
+ class="dropdown-menu-toggle"
+ type="button"
+ data-toggle="dropdown"
+ aria-expanded="false">
+ <span
+ :style="{ backgroundColor: selected.label.color }"
+ class="dropdown-label-box">
+ </span>
+ {{ selected.title }}
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
+ <ul>
+ <li
+ v-for="(list, i) in state.lists"
+ v-if="list.type == 'label'"
+ :key="i">
+ <a
+ :class="{ 'is-active': list.id == selected.id }"
+ href="#"
+ role="button"
+ @click.prevent="modal.selectedList = list">
+ <span
+ :style="{ backgroundColor: list.label.color }"
+ class="dropdown-label-box">
+ </span>
+ {{ list.title }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
deleted file mode 100644
index 9d331de8e22..00000000000
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import Vue from 'vue';
-import ModalStore from '../../stores/modal_store';
-import modalMixin from '../../mixins/modal_mixins';
-
-gl.issueBoards.ModalTabs = Vue.extend({
- mixins: [modalMixin],
- data() {
- return ModalStore.store;
- },
- computed: {
- selectedCount() {
- return ModalStore.selectedCount();
- },
- },
- destroyed() {
- this.activeTab = 'all';
- },
- template: `
- <div class="top-area prepend-top-10 append-bottom-10">
- <ul class="nav-links issues-state-filters">
- <li :class="{ 'active': activeTab == 'all' }">
- <a
- href="#"
- role="button"
- @click.prevent="changeTab('all')">
- Open issues
- <span class="badge badge-pill">
- {{ issuesCount }}
- </span>
- </a>
- </li>
- <li :class="{ 'active': activeTab == 'selected' }">
- <a
- href="#"
- role="button"
- @click.prevent="changeTab('selected')">
- Selected issues
- <span class="badge badge-pill">
- {{ selectedCount }}
- </span>
- </a>
- </li>
- </ul>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue
new file mode 100644
index 00000000000..d926b080094
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/tabs.vue
@@ -0,0 +1,49 @@
+<script>
+ import ModalStore from '../../stores/modal_store';
+ import modalMixin from '../../mixins/modal_mixins';
+
+ export default {
+ mixins: [modalMixin],
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectedCount() {
+ return ModalStore.selectedCount();
+ },
+ },
+ destroyed() {
+ this.activeTab = 'all';
+ },
+ };
+</script>
+<template>
+ <div class="top-area prepend-top-10 append-bottom-10">
+ <ul class="nav-links issues-state-filters">
+ <li :class="{ 'active': activeTab == 'all' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('all')"
+ >
+ Open issues
+ <span class="badge badge-pill">
+ {{ issuesCount }}
+ </span>
+ </a>
+ </li>
+ <li :class="{ 'active': activeTab == 'selected' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('selected')"
+ >
+ Selected issues
+ <span class="badge badge-pill">
+ {{ selectedCount }}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+</template>
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/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
deleted file mode 100644
index 0a0820ec5fd..00000000000
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import Vue from 'vue';
-import Flash from '../../../flash';
-import { __ } from '../../../locale';
-
-const Store = gl.issueBoards.BoardsStore;
-
-window.gl = window.gl || {};
-window.gl.issueBoards = window.gl.issueBoards || {};
-
-gl.issueBoards.RemoveIssueBtn = Vue.extend({
- props: {
- issue: {
- type: Object,
- required: true,
- },
- list: {
- type: Object,
- required: true,
- },
- },
- computed: {
- updateUrl() {
- return this.issue.path;
- },
- },
- methods: {
- removeIssue() {
- const issue = this.issue;
- const lists = issue.getLists();
- const listLabelIds = lists.map(list => list.label.id);
-
- let labelIds = issue.labels
- .map(label => label.id)
- .filter(id => !listLabelIds.includes(id));
- if (labelIds.length === 0) {
- labelIds = [''];
- }
-
- const data = {
- issue: {
- label_ids: labelIds,
- },
- };
-
- // Post the remove data
- Vue.http.patch(this.updateUrl, data).catch(() => {
- Flash(__('Failed to remove issue from board, please try again.'));
-
- lists.forEach((list) => {
- list.addIssue(issue);
- });
- });
-
- // Remove from the frontend store
- lists.forEach((list) => {
- list.removeIssue(issue);
- });
-
- Store.detail.issue = {};
- },
- },
- template: `
- <div
- class="block list">
- <button
- class="btn btn-default btn-block"
- type="button"
- @click="removeIssue">
- Remove from board
- </button>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
new file mode 100644
index 00000000000..90d4c710daf
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
@@ -0,0 +1,91 @@
+<script>
+ import Vue from 'vue';
+ import Flash from '../../../flash';
+ import { __ } from '../../../locale';
+
+ const Store = gl.issueBoards.BoardsStore;
+
+ export default Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ updateUrl() {
+ return this.issue.path;
+ },
+ },
+ methods: {
+ removeIssue() {
+ const { issue } = this;
+ const lists = issue.getLists();
+ const req = this.buildPatchRequest(issue, lists);
+
+ const data = {
+ issue: this.seedPatchRequest(issue, req),
+ };
+
+ if (data.issue.label_ids.length === 0) {
+ data.issue.label_ids = [''];
+ }
+
+ // Post the remove data
+ Vue.http.patch(this.updateUrl, data).catch(() => {
+ Flash(__('Failed to remove issue from board, please try again.'));
+
+ lists.forEach(list => {
+ list.addIssue(issue);
+ });
+ });
+
+ // Remove from the frontend store
+ lists.forEach(list => {
+ list.removeIssue(issue);
+ });
+
+ Store.detail.issue = {};
+ },
+ /**
+ * Build the default patch request.
+ */
+ buildPatchRequest(issue, lists) {
+ const listLabelIds = lists.map(list => list.label.id);
+
+ const labelIds = issue.labels
+ .map(label => label.id)
+ .filter(id => !listLabelIds.includes(id));
+
+ return {
+ label_ids: labelIds,
+ };
+ },
+ /**
+ * Seed the given patch request.
+ *
+ * (This is overridden in EE)
+ */
+ seedPatchRequest(issue, req) {
+ return req;
+ },
+ },
+ });
+</script>
+<template>
+ <div
+ class="block list"
+ >
+ <button
+ class="btn btn-default btn-block"
+ type="button"
+ @click="removeIssue"
+ >
+ Remove from board
+ </button>
+ </div>
+</template>
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/filters/due_date_filters.js b/app/assets/javascripts/boards/filters/due_date_filters.js
index 70132dbfa6f..9eaa0cd227d 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js
@@ -1,8 +1,7 @@
-/* global dateFormat */
-
import Vue from 'vue';
+import dateFormat from 'dateformat';
-Vue.filter('due-date', (value) => {
+Vue.filter('due-date', value => {
const date = new Date(value);
return dateFormat(date, 'mmm d, yyyy', true);
});
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index cdad8d238e3..200d1923635 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';
@@ -25,7 +25,7 @@ import './filters/due_date_filters';
import './components/board';
import './components/board_sidebar';
import './components/new_list_dropdown';
-import './components/modal/index';
+import BoardAddIssuesModal from './components/modal/index.vue';
import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first
export default () => {
@@ -49,7 +49,7 @@ export default () => {
components: {
'board': gl.issueBoards.Board,
'board-sidebar': gl.issueBoards.BoardSidebar,
- 'board-add-issues-modal': gl.issueBoards.IssuesModal,
+ BoardAddIssuesModal,
},
data: {
state: Store.state,
@@ -121,7 +121,7 @@ export default () => {
this.filterManager.updateTokens();
},
updateDetailIssue(newIssue) {
- const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint;
+ const { sidebarInfoEndpoint } = newIssue;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
@@ -144,7 +144,7 @@ export default () => {
Store.detail.issue = {};
},
toggleSubscription(id) {
- const issue = Store.detail.issue;
+ const { issue } = Store.detail;
if (issue.id === id && issue.toggleSubscriptionEndpoint) {
issue.setFetchingState('subscriptions', true);
BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
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..c7cfb72067c 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -1,9 +1,10 @@
-/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
+/* eslint-disable no-unused-vars, comma-dangle */
/* global ListLabel */
/* global ListMilestone */
/* global ListAssignee */
import Vue from 'vue';
+import '~/vue_shared/models/label';
import IssueProject from './project';
class ListIssue {
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 1f0fe7f9e85..4f05a0e4282 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';
@@ -7,6 +7,24 @@ import queryData from '../utils/query_data';
const PER_PAGE = 20;
+const TYPES = {
+ backlog: {
+ isPreset: true,
+ isExpandable: true,
+ isBlank: false,
+ },
+ closed: {
+ isPreset: true,
+ isExpandable: true,
+ isBlank: false,
+ },
+ blank: {
+ isPreset: true,
+ isExpandable: false,
+ isBlank: true,
+ },
+};
+
class List {
constructor(obj, defaultAvatar) {
this.id = obj.id;
@@ -14,8 +32,10 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
- this.preset = ['backlog', 'closed', 'blank'].indexOf(this.type) > -1;
- this.isExpandable = ['backlog', 'closed'].indexOf(this.type) > -1;
+
+ const typeInfo = this.getTypeInfo(this.type);
+ this.preset = !!typeInfo.isPreset;
+ this.isExpandable = !!typeInfo.isExpandable;
this.isExpanded = true;
this.page = 1;
this.loading = true;
@@ -31,7 +51,7 @@ class List {
this.title = this.assignee.name;
}
- if (this.type !== 'blank' && this.id) {
+ if (!typeInfo.isBlank && this.id) {
this.getIssues().catch(() => {
// TODO: handle request error
});
@@ -107,7 +127,7 @@ class List {
return gl.boardService
.getIssuesForList(this.id, data)
.then(res => res.data)
- .then((data) => {
+ .then(data => {
this.loading = false;
this.issuesSize = data.size;
@@ -126,18 +146,7 @@ class List {
return gl.boardService
.newIssue(this.id, issue)
.then(res => res.data)
- .then((data) => {
- issue.id = data.id;
- issue.iid = data.iid;
- issue.project = data.project;
- issue.path = data.real_path;
- issue.referencePath = data.reference_path;
-
- if (this.issuesSize > 1) {
- const moveBeforeId = this.issues[1].id;
- gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
- }
- });
+ .then(data => this.onNewIssueResponse(issue, data));
}
createIssues(data) {
@@ -217,6 +226,25 @@ class List {
return !matchesRemove;
});
}
+
+ getTypeInfo (type) {
+ return TYPES[type] || {};
+ }
+
+ onNewIssueResponse (issue, data) {
+ issue.id = data.id;
+ issue.iid = data.iid;
+ issue.project = data.project;
+ issue.path = data.real_path;
+ issue.referencePath = data.reference_path;
+
+ if (this.issuesSize > 1) {
+ const moveBeforeId = this.issues[1].id;
+ gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
+ }
+ }
}
window.List = List;
+
+export default List;
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 ffe86468b12..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';
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index a4220cd840d..0d9ac367a70 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -26,7 +26,7 @@ class ModalStore {
toggleIssue(issueObj) {
const issue = issueObj;
- const selected = issue.selected;
+ const { selected } = issue;
issue.selected = !selected;
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/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index e42a3632e79..8139aa69fc7 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -81,7 +81,7 @@ export default class Clusters {
}
initApplications() {
- const store = this.store;
+ const { store } = this;
const el = document.querySelector('#js-cluster-applications');
this.applications = new Vue({
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 7f3d04655a7..410580b4c25 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) {
@@ -122,7 +122,7 @@ export default class ImageFile {
return $('.swipe.view', this.file).each((function(_this) {
return function(index, view) {
var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
- ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
+ ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref;
$swipeFrame = $('.swipe-frame', view);
$swipeWrap = $('.swipe-wrap', view);
$swipeBar = $('.swipe-bar', view);
@@ -159,7 +159,7 @@ export default class ImageFile {
return $('.onion-skin.view', this.file).each((function(_this) {
return function(index, view) {
var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
- ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
+ ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref;
$frame = $('.onion-skin-frame', view);
$frameAdded = $('.frame.added', view);
$track = $('.drag-track', view);
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 f77a5730b77..02aa507ba03 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -281,7 +281,7 @@ export default class CreateMergeRequestDropdown {
if (event.target === this.branchInput) {
target = 'branch';
- value = this.branchInput.value;
+ ({ value } = this.branchInput);
} else if (event.target === this.refInput) {
target = 'ref';
value =
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index 40f7c2fe5f3..5528d2a542b 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -111,7 +111,7 @@ const DiffNoteAvatars = Vue.extend({
});
},
addNoCommentClass() {
- const notesCount = this.notesCount;
+ const { notesCount } = this;
$(this.$el).closest('.js-avatar-container')
.toggleClass('no-comment-btn', notesCount > 0)
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..2b893e35b6d 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],
@@ -74,7 +73,7 @@ const JumpToDiscussion = Vue.extend({
}).toArray();
};
- const discussions = this.discussions;
+ const { discussions } = this;
if (activeTab === 'diffs') {
discussionsSelector = '.diffs .notes[data-discussion-id]';
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..7dcf3594471 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 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 projectPathHolder =
+ document.querySelector('.merge-request') || document.querySelector('.commit-box');
+ const { projectPath } = projectPathHolder.dataset;
+ 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..eb0985e5603
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -0,0 +1,216 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import eventHub from '../../notes/event_hub';
+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,
+ },
+ projectPath: {
+ 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', 'isNotesFetched']),
+ 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() {
+ // When the shouldShow property changed to true, the route is rendered for the first time
+ // and if we have the isLoading as true this means we didn't fetch the data
+ if (this.isLoading) {
+ this.fetchData();
+ }
+
+ this.adjustView();
+ },
+ },
+ mounted() {
+ this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath });
+
+ if (this.shouldShow) {
+ this.fetchData();
+ }
+ },
+ created() {
+ this.adjustView();
+ },
+ methods: {
+ ...mapActions(['setBaseConfig', 'fetchDiffFiles']),
+ fetchData() {
+ this.fetchDiffFiles().catch(() => {
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
+
+ if (!this.isNotesFetched) {
+ eventHub.$emit('fetchNotesData');
+ }
+ },
+ 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-show="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..b38d217fbe3
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
@@ -0,0 +1,126 @@
+<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>
+ <div class="dropdown-content">
+ <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>
+ </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..b6af49c7e2e
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -0,0 +1,62 @@
+<script>
+import { mapGetters, mapState } from 'vuex';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import { diffModes } from '~/ide/constants';
+import InlineDiffView from './inline_diff_view.vue';
+import ParallelDiffView from './parallel_diff_view.vue';
+
+export default {
+ components: {
+ InlineDiffView,
+ ParallelDiffView,
+ DiffViewer,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState({
+ projectPath: state => state.diffs.projectPath,
+ endpoint: state => state.diffs.endpoint,
+ }),
+ ...mapGetters(['isInlineView', 'isParallelView']),
+ diffMode() {
+ const diffModeKey = Object.keys(diffModes).find(key => this.diffFile[`${key}File`]);
+ return diffModes[diffModeKey] || diffModes.replaced;
+ },
+ isTextFile() {
+ return this.diffFile.text;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="diff-content">
+ <div class="diff-viewer">
+ <template v-if="isTextFile">
+ <inline-diff-view
+ v-show="isInlineView"
+ :diff-file="diffFile"
+ :diff-lines="diffFile.highlightedDiffLines || []"
+ />
+ <parallel-diff-view
+ v-show="isParallelView"
+ :diff-file="diffFile"
+ :diff-lines="diffFile.parallelDiffLines || []"
+ />
+ </template>
+ <diff-viewer
+ v-else
+ :diff-mode="diffMode"
+ :new-path="diffFile.newPath"
+ :new-sha="diffFile.diffRefs.headSha"
+ :old-path="diffFile.oldPath"
+ :old-sha="diffFile.diffRefs.baseSha"
+ :project-path="projectPath"/>
+ </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..a8e8732053b
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -0,0 +1,262 @@
+<script>
+import _ from 'underscore';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import FileIcon from '~/vue_shared/components/file_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,
+ FileIcon,
+ },
+ 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 ||
+ (e.target.classList && e.target.classList.contains('diff-toggle-caret'))
+ ) {
+ 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 append-right-5"
+ @click.stop="handleToggle"
+ />
+ <a
+ ref="titleWrapper"
+ :href="titleLink"
+ class="append-right-4"
+ >
+ <file-icon
+ :file-name="filePath"
+ :size="18"
+ aria-hidden="true"
+ css-classes="js-file-icon append-right-5"
+ />
+ <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-sm-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..7e50a0aed84
--- /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;
+
+ 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..a74ea4bfaaf
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -0,0 +1,202 @@
+<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 { 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,
+ },
+ isMatchLine: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isMetaLine: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isContextLine: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapState({
+ diffViewType: state => state.diffs.diffViewType,
+ diffFiles: state => state.diffs.diffFiles,
+ }),
+ ...mapGetters(['isLoggedIn', 'discussionsByLineCode']),
+ 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', 'showCommentForm']),
+ handleCommentButton() {
+ this.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;
+ 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..6943b462e86
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -0,0 +1,114 @@
+<script>
+import $ from 'jquery';
+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';
+import Autosave from '../../autosave';
+import { DIFF_NOTE_TYPE, NOTE_TYPE } from '../constants';
+
+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(['isLoggedIn', 'noteableType', 'getNoteableData', 'getNotesDataByProp']),
+ },
+ mounted() {
+ if (this.isLoggedIn) {
+ const noteableData = this.getNoteableData;
+ const keys = [
+ NOTE_TYPE,
+ this.noteableType,
+ noteableData.id,
+ noteableData.diff_head_sha,
+ DIFF_NOTE_TYPE,
+ noteableData.source_project_id,
+ this.line.lineCode,
+ ];
+
+ this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys);
+ }
+ },
+ methods: {
+ ...mapActions(['cancelCommentForm', 'saveNote', 'fetchDiscussions']),
+ handleCancelCommentForm() {
+ this.autosave.reset();
+ 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
+ ref="noteForm"
+ :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/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
new file mode 100644
index 00000000000..5b08b161114
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -0,0 +1,145 @@
+<script>
+import { mapGetters } from 'vuex';
+import DiffLineGutterContent from './diff_line_gutter_content.vue';
+import {
+ MATCH_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ EMPTY_CELL_TYPE,
+ OLD_LINE_TYPE,
+ OLD_NO_NEW_LINE_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+ LINE_HOVER_CLASS_NAME,
+ LINE_UNFOLD_CLASS_NAME,
+ INLINE_DIFF_VIEW_TYPE,
+ LINE_POSITION_LEFT,
+ LINE_POSITION_RIGHT,
+} from '../constants';
+
+export default {
+ components: {
+ DiffLineGutterContent,
+ },
+ props: {
+ line: {
+ type: Object,
+ required: true,
+ },
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffViewType: {
+ type: String,
+ required: false,
+ default: INLINE_DIFF_VIEW_TYPE,
+ },
+ showCommentButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ linePosition: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lineType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isContentLine: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isBottom: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isHover: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapGetters(['isLoggedIn']),
+ normalizedLine() {
+ let normalizedLine;
+
+ if (this.diffViewType === INLINE_DIFF_VIEW_TYPE) {
+ normalizedLine = this.line;
+ } else if (this.linePosition === LINE_POSITION_LEFT) {
+ normalizedLine = this.line.left;
+ } else if (this.linePosition === LINE_POSITION_RIGHT) {
+ normalizedLine = this.line.right;
+ }
+
+ return normalizedLine;
+ },
+ isMatchLine() {
+ return this.normalizedLine.type === MATCH_LINE_TYPE;
+ },
+ isContextLine() {
+ return this.normalizedLine.type === CONTEXT_LINE_TYPE;
+ },
+ isMetaLine() {
+ const { type } = this.normalizedLine;
+
+ return (
+ type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
+ );
+ },
+ classNameMap() {
+ const { type } = this.normalizedLine;
+
+ return {
+ [type]: type,
+ [LINE_UNFOLD_CLASS_NAME]: this.isMatchLine,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn &&
+ this.isHover &&
+ !this.isMatchLine &&
+ !this.isContextLine &&
+ !this.isMetaLine,
+ };
+ },
+ lineNumber() {
+ const { lineType, normalizedLine } = this;
+
+ return lineType === OLD_LINE_TYPE ? normalizedLine.oldLine : normalizedLine.newLine;
+ },
+ },
+};
+</script>
+
+<template>
+ <td
+ v-if="isContentLine"
+ :class="lineType"
+ class="line_content"
+ v-html="normalizedLine.richText"
+ >
+ </td>
+ <td
+ v-else
+ :class="classNameMap"
+ >
+ <diff-line-gutter-content
+ :file-hash="diffFile.fileHash"
+ :line-type="normalizedLine.type"
+ :line-code="normalizedLine.lineCode"
+ :line-position="linePosition"
+ :line-number="lineNumber"
+ :meta-data="normalizedLine.metaData"
+ :show-comment-button="showCommentButton"
+ :context-lines-path="diffFile.contextLinesPath"
+ :is-bottom="isBottom"
+ :is-match-line="isMatchLine"
+ :is-context-line="isContentLine"
+ :is-meta-line="isMetaLine"
+ />
+ </td>
+</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_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
new file mode 100644
index 00000000000..0e935f1d68e
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
@@ -0,0 +1,82 @@
+<script>
+import { mapState, mapGetters } from 'vuex';
+import diffDiscussions from './diff_discussions.vue';
+import diffLineNoteForm from './diff_line_note_form.vue';
+
+export default {
+ components: {
+ diffDiscussions,
+ diffLineNoteForm,
+ },
+ props: {
+ line: {
+ type: Object,
+ required: true,
+ },
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ lineIndex: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState({
+ diffLineCommentForms: state => state.diffs.diffLineCommentForms,
+ }),
+ ...mapGetters(['discussionsByLineCode']),
+ isDiscussionExpanded() {
+ if (!this.discussions.length) {
+ return false;
+ }
+
+ return this.discussions.every(discussion => discussion.expanded);
+ },
+ hasCommentForm() {
+ return this.diffLineCommentForms[this.line.lineCode];
+ },
+ discussions() {
+ return this.discussionsByLineCode[this.line.lineCode] || [];
+ },
+ shouldRender() {
+ return this.isDiscussionExpanded || this.hasCommentForm;
+ },
+ className() {
+ return this.discussions.length ? '' : 'js-temp-notes-holder';
+ },
+ },
+};
+</script>
+
+<template>
+ <tr
+ v-if="shouldRender"
+ :class="className"
+ class="notes_holder"
+ >
+ <td
+ class="notes_line"
+ colspan="2"
+ ></td>
+ <td class="notes_content">
+ <div class="content">
+ <diff-discussions
+ :discussions="discussions"
+ />
+ <diff-line-note-form
+ v-if="diffLineCommentForms[line.lineCode]"
+ :diff-file="diffFile"
+ :diff-lines="diffLines"
+ :line="line"
+ :note-target-line="diffLines[lineIndex]"
+ />
+ </div>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
new file mode 100644
index 00000000000..a2470843ca6
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -0,0 +1,104 @@
+<script>
+import { mapGetters } from 'vuex';
+import DiffTableCell from './diff_table_cell.vue';
+import {
+ NEW_LINE_TYPE,
+ OLD_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ CONTEXT_LINE_CLASS_NAME,
+ PARALLEL_DIFF_VIEW_TYPE,
+ LINE_POSITION_LEFT,
+ LINE_POSITION_RIGHT,
+} from '../constants';
+
+export default {
+ components: {
+ DiffTableCell,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ line: {
+ type: Object,
+ required: true,
+ },
+ isBottom: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isHover: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['isInlineView']),
+ isContextLine() {
+ return this.line.type === CONTEXT_LINE_TYPE;
+ },
+ classNameMap() {
+ return {
+ [this.line.type]: this.line.type,
+ [CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
+ [PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView,
+ };
+ },
+ inlineRowId() {
+ const { lineCode, oldLine, newLine } = this.line;
+
+ return lineCode || `${this.diffFile.fileHash}_${oldLine}_${newLine}`;
+ },
+ },
+ created() {
+ this.newLineType = NEW_LINE_TYPE;
+ this.oldLineType = OLD_LINE_TYPE;
+ this.linePositionLeft = LINE_POSITION_LEFT;
+ this.linePositionRight = LINE_POSITION_RIGHT;
+ },
+ methods: {
+ handleMouseMove(e) {
+ // To show the comment icon on the gutter we need to know if we hover the line.
+ // Current table structure doesn't allow us to do this with CSS in both of the diff view types
+ this.isHover = e.type === 'mouseover';
+ },
+ },
+};
+</script>
+
+<template>
+ <tr
+ :id="inlineRowId"
+ :class="classNameMap"
+ class="line_holder"
+ @mouseover="handleMouseMove"
+ @mouseout="handleMouseMove"
+ >
+ <diff-table-cell
+ :diff-file="diffFile"
+ :line="line"
+ :line-type="oldLineType"
+ :is-bottom="isBottom"
+ :is-hover="isHover"
+ :show-comment-button="true"
+ class="diff-line-num old_line"
+ />
+ <diff-table-cell
+ :diff-file="diffFile"
+ :line="line"
+ :line-type="newLineType"
+ :is-bottom="isBottom"
+ :is-hover="isHover"
+ class="diff-line-num new_line"
+ />
+ <diff-table-cell
+ :class="line.type"
+ :diff-file="diffFile"
+ :line="line"
+ :is-content-line="true"
+ />
+ </tr>
+</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..b884230fb63
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -0,0 +1,65 @@
+<script>
+import { mapGetters } from 'vuex';
+import inlineDiffTableRow from './inline_diff_table_row.vue';
+import inlineDiffCommentRow from './inline_diff_comment_row.vue';
+import { trimFirstCharOfLineContent } from '../store/utils';
+
+export default {
+ components: {
+ inlineDiffCommentRow,
+ inlineDiffTableRow,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters(['commit']),
+ normalizedDiffLines() {
+ return this.diffLines.map(line => (line.richText ? trimFirstCharOfLineContent(line) : line));
+ },
+ diffLinesLength() {
+ return this.normalizedDiffLines.length;
+ },
+ commitId() {
+ return this.commit && this.commit.id;
+ },
+ userColorScheme() {
+ return window.gon.user_color_scheme;
+ },
+ },
+};
+</script>
+
+<template>
+ <table
+ :class="userColorScheme"
+ :data-commit-id="commitId"
+ class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view">
+ <tbody>
+ <template
+ v-for="(line, index) in normalizedDiffLines"
+ >
+ <inline-diff-table-row
+ :diff-file="diffFile"
+ :line="line"
+ :is-bottom="index + 1 === diffLinesLength"
+ :key="line.lineCode"
+ />
+ <inline-diff-comment-row
+ :diff-file="diffFile"
+ :diff-lines="normalizedDiffLines"
+ :line="line"
+ :line-index="index"
+ :key="index"
+ />
+ </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_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
new file mode 100644
index 00000000000..5f33ec7a3c2
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -0,0 +1,129 @@
+<script>
+import { mapState, mapGetters } from 'vuex';
+import diffDiscussions from './diff_discussions.vue';
+import diffLineNoteForm from './diff_line_note_form.vue';
+
+export default {
+ components: {
+ diffDiscussions,
+ diffLineNoteForm,
+ },
+ props: {
+ line: {
+ type: Object,
+ required: true,
+ },
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ lineIndex: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState({
+ diffLineCommentForms: state => state.diffs.diffLineCommentForms,
+ }),
+ ...mapGetters(['discussionsByLineCode']),
+ leftLineCode() {
+ return this.line.left.lineCode;
+ },
+ rightLineCode() {
+ return this.line.right.lineCode;
+ },
+ hasDiscussion() {
+ const discussions = this.discussionsByLineCode;
+
+ return discussions[this.leftLineCode] || discussions[this.rightLineCode];
+ },
+ hasExpandedDiscussionOnLeft() {
+ const discussions = this.discussionsByLineCode[this.leftLineCode];
+
+ return discussions ? discussions.every(discussion => discussion.expanded) : false;
+ },
+ hasExpandedDiscussionOnRight() {
+ const discussions = this.discussionsByLineCode[this.rightLineCode];
+
+ return discussions ? discussions.every(discussion => discussion.expanded) : false;
+ },
+ hasAnyExpandedDiscussion() {
+ return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
+ },
+ shouldRenderDiscussionsRow() {
+ const hasDiscussion = this.hasDiscussion && this.hasAnyExpandedDiscussion;
+ const hasCommentFormOnLeft = this.diffLineCommentForms[this.leftLineCode];
+ const hasCommentFormOnRight = this.diffLineCommentForms[this.rightLineCode];
+
+ return hasDiscussion || hasCommentFormOnLeft || hasCommentFormOnRight;
+ },
+ shouldRenderDiscussionsOnLeft() {
+ return this.discussionsByLineCode[this.leftLineCode] && this.hasExpandedDiscussionOnLeft;
+ },
+ shouldRenderDiscussionsOnRight() {
+ return (
+ this.discussionsByLineCode[this.rightLineCode] &&
+ this.hasExpandedDiscussionOnRight &&
+ this.line.right.type
+ );
+ },
+ className() {
+ return this.hasDiscussion ? '' : 'js-temp-notes-holder';
+ },
+ },
+};
+</script>
+
+<template>
+ <tr
+ v-if="shouldRenderDiscussionsRow"
+ :class="className"
+ class="notes_holder"
+ >
+ <td class="notes_line old"></td>
+ <td class="notes_content parallel old">
+ <div
+ v-if="shouldRenderDiscussionsOnLeft"
+ class="content"
+ >
+ <diff-discussions
+ :discussions="discussionsByLineCode[leftLineCode]"
+ />
+ </div>
+ <diff-line-note-form
+ v-if="diffLineCommentForms[leftLineCode] &&
+ diffLineCommentForms[leftLineCode]"
+ :diff-file="diffFile"
+ :diff-lines="diffLines"
+ :line="line.left"
+ :note-target-line="diffLines[lineIndex].left"
+ position="left"
+ />
+ </td>
+ <td class="notes_line new"></td>
+ <td class="notes_content parallel new">
+ <div
+ v-if="shouldRenderDiscussionsOnRight"
+ class="content"
+ >
+ <diff-discussions
+ :discussions="discussionsByLineCode[rightLineCode]"
+ />
+ </div>
+ <diff-line-note-form
+ v-if="diffLineCommentForms[rightLineCode] &&
+ diffLineCommentForms[rightLineCode] && line.right.type"
+ :diff-file="diffFile"
+ :diff-lines="diffLines"
+ :line="line.right"
+ :note-target-line="diffLines[lineIndex].right"
+ position="right"
+ />
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
new file mode 100644
index 00000000000..eb769584d74
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -0,0 +1,150 @@
+<script>
+import $ from 'jquery';
+import { mapGetters } from 'vuex';
+import DiffTableCell from './diff_table_cell.vue';
+import {
+ NEW_LINE_TYPE,
+ OLD_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ CONTEXT_LINE_CLASS_NAME,
+ OLD_NO_NEW_LINE_TYPE,
+ PARALLEL_DIFF_VIEW_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+ LINE_POSITION_LEFT,
+ LINE_POSITION_RIGHT,
+} from '../constants';
+
+export default {
+ components: {
+ DiffTableCell,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ line: {
+ type: Object,
+ required: true,
+ },
+ isBottom: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isLeftHover: false,
+ isRightHover: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['isParallelView']),
+ isContextLine() {
+ return this.line.left.type === CONTEXT_LINE_TYPE;
+ },
+ classNameMap() {
+ return {
+ [CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
+ [PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView,
+ };
+ },
+ parallelViewLeftLineType() {
+ if (this.line.right.type === NEW_NO_NEW_LINE_TYPE) {
+ return OLD_NO_NEW_LINE_TYPE;
+ }
+
+ return this.line.left.type;
+ },
+ },
+ created() {
+ this.newLineType = NEW_LINE_TYPE;
+ this.oldLineType = OLD_LINE_TYPE;
+ this.linePositionLeft = LINE_POSITION_LEFT;
+ this.linePositionRight = LINE_POSITION_RIGHT;
+ this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE;
+ },
+ methods: {
+ handleMouseMove(e) {
+ const isHover = e.type === 'mouseover';
+ const hoveringCell = e.target.closest('td');
+ const allCellsInHoveringRow = Array.from(e.currentTarget.children);
+ const hoverIndex = allCellsInHoveringRow.indexOf(hoveringCell);
+
+ if (hoverIndex >= 2) {
+ this.isRightHover = isHover;
+ } else {
+ this.isLeftHover = isHover;
+ }
+ },
+ // Prevent text selecting on both sides of parallel diff view
+ // Backport of the same code from legacy diff notes.
+ handleParallelLineMouseDown(e) {
+ const line = $(e.currentTarget);
+ const table = line.closest('table');
+
+ table.removeClass('left-side-selected right-side-selected');
+ const [lineClass] = ['left-side', 'right-side'].filter(name => line.hasClass(name));
+
+ if (lineClass) {
+ table.addClass(`${lineClass}-selected`);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <tr
+ :class="classNameMap"
+ class="line_holder"
+ @mouseover="handleMouseMove"
+ @mouseout="handleMouseMove"
+ >
+ <diff-table-cell
+ :diff-file="diffFile"
+ :line="line"
+ :line-type="oldLineType"
+ :line-position="linePositionLeft"
+ :is-bottom="isBottom"
+ :is-hover="isLeftHover"
+ :show-comment-button="true"
+ :diff-view-type="parallelDiffViewType"
+ class="diff-line-num old_line"
+ />
+ <diff-table-cell
+ :id="line.left.lineCode"
+ :diff-file="diffFile"
+ :line="line"
+ :is-content-line="true"
+ :line-position="linePositionLeft"
+ :line-type="parallelViewLeftLineType"
+ :diff-view-type="parallelDiffViewType"
+ class="line_content parallel left-side"
+ @mousedown.native="handleParallelLineMouseDown"
+ />
+ <diff-table-cell
+ :diff-file="diffFile"
+ :line="line"
+ :line-type="newLineType"
+ :line-position="linePositionRight"
+ :is-bottom="isBottom"
+ :is-hover="isRightHover"
+ :show-comment-button="true"
+ :diff-view-type="parallelDiffViewType"
+ class="diff-line-num new_line"
+ />
+ <diff-table-cell
+ :id="line.right.lineCode"
+ :diff-file="diffFile"
+ :line="line"
+ :is-content-line="true"
+ :line-position="linePositionRight"
+ :line-type="line.right.type"
+ :diff-view-type="parallelDiffViewType"
+ class="line_content parallel right-side"
+ @mousedown.native="handleParallelLineMouseDown"
+ />
+ </tr>
+</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..52561e197e6
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -0,0 +1,85 @@
+<script>
+import { mapGetters } from 'vuex';
+import parallelDiffTableRow from './parallel_diff_table_row.vue';
+import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
+import { EMPTY_CELL_TYPE } from '../constants';
+import { trimFirstCharOfLineContent } from '../store/utils';
+
+export default {
+ components: {
+ parallelDiffTableRow,
+ parallelDiffCommentRow,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters(['commit']),
+ parallelDiffLines() {
+ return this.diffLines.map(line => {
+ const parallelLine = Object.assign({}, line);
+
+ if (line.left) {
+ parallelLine.left = trimFirstCharOfLineContent(line.left);
+ } else {
+ parallelLine.left = { type: EMPTY_CELL_TYPE };
+ }
+
+ if (line.right) {
+ parallelLine.right = trimFirstCharOfLineContent(line.right);
+ } else {
+ parallelLine.right = { type: EMPTY_CELL_TYPE };
+ }
+
+ return parallelLine;
+ });
+ },
+ diffLinesLength() {
+ return this.parallelDiffLines.length;
+ },
+ commitId() {
+ return this.commit && this.commit.id;
+ },
+ userColorScheme() {
+ return window.gon.user_color_scheme;
+ },
+ },
+};
+</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"
+ >
+ <parallel-diff-table-row
+ :diff-file="diffFile"
+ :line="line"
+ :is-bottom="index + 1 === diffLinesLength"
+ :key="index"
+ />
+ <parallel-diff-comment-row
+ :key="line.left.lineCode || line.right.lineCode"
+ :line="line"
+ :diff-file="diffFile"
+ :diff-lines="parallelDiffLines"
+ :line-index="index"
+ />
+ </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..2fa8367f528
--- /dev/null
+++ b/app/assets/javascripts/diffs/constants.js
@@ -0,0 +1,27 @@
+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 NOTE_TYPE = 'Note';
+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 LINE_SIDE_LEFT = 'left-side';
+export const LINE_SIDE_RIGHT = 'right-side';
+
+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..aae89109c27
--- /dev/null
+++ b/app/assets/javascripts/diffs/index.js
@@ -0,0 +1,41 @@
+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,
+ projectPath: dataset.projectPath,
+ 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,
+ projectPath: this.projectPath,
+ 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/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
new file mode 100644
index 00000000000..5e0fd5109bb
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -0,0 +1,95 @@
+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 setBaseConfig = ({ commit }, options) => {
+ const { endpoint, projectPath } = options;
+ commit(types.SET_BASE_CONFIG, { endpoint, projectPath });
+};
+
+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 {
+ setBaseConfig,
+ 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..94caa131506
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/modules/index.js
@@ -0,0 +1,26 @@
+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: '',
+ basePath: '',
+ 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..2c8e1a1466f
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -0,0 +1,10 @@
+export const SET_BASE_CONFIG = 'SET_BASE_CONFIG';
+export const SET_LOADING = 'SET_LOADING';
+export const SET_DIFF_DATA = 'SET_DIFF_DATA';
+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..8aa8a114c6f
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -0,0 +1,80 @@
+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_BASE_CONFIG](state, options) {
+ const { endpoint, projectPath } = options;
+ Object.assign(state, { endpoint, projectPath });
+ },
+
+ [types.SET_LOADING](state, isLoading) {
+ Object.assign(state, { isLoading });
+ },
+
+ [types.SET_DIFF_DATA](state, data) {
+ Object.assign(state, {
+ ...convertObjectPropsToCamelCase(data, { 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..d9589baa76e
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -0,0 +1,175 @@
+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);
+ }
+}
+
+/**
+ * Trims the first char of the `richText` property when it's either a space or a diff symbol.
+ * @param {Object} line
+ * @returns {Object}
+ */
+export function trimFirstCharOfLineContent(line = {}) {
+ const parsedLine = Object.assign({}, line);
+
+ if (line.richText) {
+ const firstChar = parsedLine.richText.charAt(0);
+
+ if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
+ parsedLine.richText = line.richText.substring(1);
+ }
+ }
+
+ return parsedLine;
+}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 72f21f13860..a5af37e80b6 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,12 +1,12 @@
-/* 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';
import GfmAutoComplete from './gfm_auto_complete';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
import GlFieldErrors from './gl_field_errors';
import Shortcuts from './shortcuts';
import SearchAutocomplete from './search_autocomplete';
+import performanceBar from './performance_bar';
function initSearch() {
// Only when search form is present
@@ -72,9 +72,7 @@ function initGFMInput() {
function initPerformanceBar() {
if (document.querySelector('#js-peek')) {
- import('./performance_bar')
- .then(m => new m.default({ container: '#js-peek' })) // eslint-disable-line new-cap
- .catch(() => Flash('Error loading performance bar module'));
+ performanceBar({ container: '#js-peek' });
}
}
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 4164149dd06..17ea3bdb179 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -1,7 +1,6 @@
-/* global dateFormat */
-
import $ from 'jquery';
import Pikaday from 'pikaday';
+import dateFormat from 'dateformat';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
@@ -55,7 +54,7 @@ class DueDateSelect {
format: 'yyyy-mm-dd',
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
- onSelect: (dateText) => {
+ onSelect: dateText => {
$dueDateInput.val(calendar.toString(dateText));
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
@@ -73,7 +72,7 @@ class DueDateSelect {
}
initRemoveDueDate() {
- this.$block.on('click', '.js-remove-due-date', (e) => {
+ this.$block.on('click', '.js-remove-due-date', e => {
const calendar = this.$datePicker.data('pikaday');
e.preventDefault();
@@ -124,7 +123,8 @@ class DueDateSelect {
this.$loading.fadeOut();
};
- gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
+ gl.issueBoards.BoardsStore.detail.issue
+ .update(this.$dropdown.attr('data-issue-update'))
.then(fadeOutLoader)
.catch(fadeOutLoader);
}
@@ -147,17 +147,18 @@ class DueDateSelect {
$('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length);
- return axios.put(this.issueUpdateURL, this.datePayload)
- .then(() => {
- const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
- if (isDropdown) {
- this.$dropdown.trigger('loaded.gl.dropdown');
- this.$dropdown.dropdown('toggle');
- }
- this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
+ return axios.put(this.issueUpdateURL, this.datePayload).then(() => {
+ const tooltipText = hasDueDate
+ ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})`
+ : __('Due date');
+ if (isDropdown) {
+ this.$dropdown.trigger('loaded.gl.dropdown');
+ this.$dropdown.dropdown('toggle');
+ }
+ this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
- return this.$loading.fadeOut();
- });
+ return this.$loading.fadeOut();
+ });
}
}
@@ -187,15 +188,19 @@ export default class DueDateSelectors {
$datePicker.data('pikaday', calendar);
});
- $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
+ $('.js-clear-due-date,.js-clear-start-date').on('click', e => {
e.preventDefault();
- const calendar = $(e.target).siblings('.datepicker').data('pikaday');
+ const calendar = $(e.target)
+ .siblings('.datepicker')
+ .data('pikaday');
calendar.setDate(null);
});
}
// eslint-disable-next-line class-methods-use-this
initIssuableSelect() {
- const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
+ const $loading = $('.js-issuable-update .due_date')
+ .find('.block-loading')
+ .hide();
$('.js-due-date-select').each((i, dropdown) => {
const $dropdown = $(dropdown);
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 866e91057ec..5ecdccf63ad 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -292,7 +292,7 @@
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable) {
- const deployable = this.model.last_deployment.deployable;
+ const { deployable } = this.model.last_deployment;
return `${deployable.name} #${deployable.id}`;
}
return '';
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 5f2989ab854..5ce9225a4bb 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -146,7 +146,7 @@ export default class EnvironmentsStore {
* @return {Array}
*/
updateEnvironmentProp(environment, prop, newValue) {
- const environments = this.state.environments;
+ const { environments } = this.state;
const updatedEnvironments = environments.map((env) => {
const updateEnv = Object.assign({}, env);
@@ -161,7 +161,7 @@ export default class EnvironmentsStore {
}
getOpenFolders() {
- const environments = this.state.environments;
+ const { environments } = this.state;
return environments.filter(env => env.isFolder && env.isOpen);
}
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 9bc36c1f9b6..27fff488603 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -35,7 +35,7 @@ export default class DropdownUtils {
// Remove the symbol for filter
if (value[0] === filterSymbol) {
- symbol = value[0];
+ [symbol] = value;
value = value.slice(1);
}
@@ -162,7 +162,7 @@ export default class DropdownUtils {
// Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) {
- const container = FilteredSearchContainer.container;
+ const { container } = FilteredSearchContainer;
const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
const values = [];
@@ -220,7 +220,7 @@ export default class DropdownUtils {
}
static getInputSelectionPosition(input) {
- const selectionStart = input.selectionStart;
+ const { selectionStart } = input;
let inputValue = input.value;
// Replace all spaces inside quote marks with underscores
// (will continue to match entire string until an end quote is found if any)
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index d7e1de18d09..296571606d6 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -159,7 +159,7 @@ export default class FilteredSearchDropdownManager {
load(key, firstLoad = false) {
const mappingKey = this.mapping[key];
const glClass = mappingKey.gl;
- const element = mappingKey.element;
+ const { element } = mappingKey;
let forceShowList = false;
if (!mappingKey.reference) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index cf5ba1e1771..81286c54c4c 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -235,7 +235,7 @@ export default class FilteredSearchManager {
checkForEnter(e) {
if (e.keyCode === 38 || e.keyCode === 40) {
- const selectionStart = this.filteredSearchInput.selectionStart;
+ const { selectionStart } = this.filteredSearchInput;
e.preventDefault();
this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
@@ -496,7 +496,7 @@ export default class FilteredSearchManager {
// Replace underscore with hyphen in the sanitizedkey.
// e.g. 'my_reaction' => 'my-reaction'
sanitizedKey = sanitizedKey.replace('_', '-');
- const symbol = match.symbol;
+ const { symbol } = match;
let quotationsToUse = '';
if (sanitizedValue.indexOf(' ') !== -1) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index 600024c21c3..56fe1ab4e90 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -101,7 +101,7 @@ export default class FilteredSearchVisualTokens {
static updateLabelTokenColor(tokenValueContainer, tokenValue) {
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
- const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
+ const { baseEndpoint } = filteredSearchInput.dataset;
const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
`${baseEndpoint}/labels.json`,
filteredSearchInput.dataset.endpointQueryParams,
@@ -215,7 +215,7 @@ export default class FilteredSearchVisualTokens {
static addFilterVisualToken(tokenName, tokenValue, canEdit) {
const { lastVisualToken, isLastVisualTokenValid }
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
+ const { addVisualTokenElement } = FilteredSearchVisualTokens;
if (isLastVisualTokenValid) {
addVisualTokenElement(tokenName, tokenValue, false, canEdit);
diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js
index f9338b82acf..c1efa9c86f4 100644
--- a/app/assets/javascripts/filtered_search/recent_searches_root.js
+++ b/app/assets/javascripts/filtered_search/recent_searches_root.js
@@ -29,7 +29,7 @@ class RecentSearchesRoot {
}
render() {
- const state = this.store.state;
+ const { state } = this.store;
this.vm = new Vue({
el: this.wrapperElement,
components: {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 9de57db48fd..73b2cd0b2c7 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -7,6 +7,16 @@ function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
}
+export const defaultAutocompleteConfig = {
+ emojis: true,
+ members: true,
+ issues: true,
+ mergeRequests: true,
+ epics: true,
+ milestones: true,
+ labels: true,
+};
+
class GfmAutoComplete {
constructor(dataSources) {
this.dataSources = dataSources || {};
@@ -14,14 +24,7 @@ class GfmAutoComplete {
this.isLoadingData = {};
}
- setup(input, enableMap = {
- emojis: true,
- members: true,
- issues: true,
- milestones: true,
- mergeRequests: true,
- labels: true,
- }) {
+ setup(input, enableMap = defaultAutocompleteConfig) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
this.enableMap = enableMap;
@@ -77,7 +80,7 @@ class GfmAutoComplete {
let tpl = '/${name} ';
let referencePrefix = null;
if (value.params.length > 0) {
- referencePrefix = value.params[0][0];
+ [[referencePrefix]] = value.params;
if (/^[@%~]/.test(referencePrefix)) {
tpl += '<%- referencePrefix %>';
}
@@ -455,7 +458,7 @@ class GfmAutoComplete {
static isLoading(data) {
let dataToInspect = data;
if (data && data.length > 0) {
- dataToInspect = data[0];
+ [dataToInspect] = data;
}
const loadingState = GfmAutoComplete.defaultLoadingData[0];
@@ -490,6 +493,7 @@ GfmAutoComplete.atTypeMap = {
'@': 'members',
'#': 'issues',
'!': 'mergeRequests',
+ '&': 'epics',
'~': 'labels',
'%': 'milestones',
'/': 'commands',
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 7fbba7e27cb..8d231e6c405 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';
@@ -613,7 +613,7 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.renderItem = function(data, group, index) {
- var field, fieldName, html, selected, text, url, value, rowHidden;
+ var field, html, selected, text, url, value, rowHidden;
if (!this.options.renderRow) {
value = this.options.id ? this.options.id(data) : data.id;
@@ -651,7 +651,7 @@ GitLabDropdown = (function() {
html = this.options.renderRow.call(this.options, data, this);
} else {
if (!selected) {
- fieldName = this.options.fieldName;
+ const { fieldName } = this.options;
if (value) {
field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
@@ -705,7 +705,8 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
const occurrences = fuzzaldrinPlus.match(text, term);
- const indexOf = [].indexOf;
+ const { indexOf } = [];
+
return text.split('').map(function(character, i) {
if (indexOf.call(occurrences, i) !== -1) {
return "<b>" + character + "</b>";
@@ -721,9 +722,9 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.rowClicked = function(el) {
- var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
+ var field, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
- fieldName = this.options.fieldName;
+ const { fieldName } = this.options;
isInput = $(this.el).is('input');
if (this.renderedData) {
groupName = el.data('group');
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 9f5eba353d7..c74de7ac34d 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,14 +1,21 @@
import $ from 'jquery';
import autosize from 'autosize';
-import GfmAutoComplete from './gfm_auto_complete';
+import GfmAutoComplete, * as GFMConfig from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
export default class GLForm {
- constructor(form, enableGFM = false) {
+ constructor(form, enableGFM = {}) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
- this.enableGFM = enableGFM;
+ this.enableGFM = Object.assign({}, GFMConfig.defaultAutocompleteConfig, enableGFM);
+ // Disable autocomplete for keywords which do not have dataSources available
+ const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
+ Object.keys(this.enableGFM).forEach(item => {
+ if (item !== 'emojis') {
+ this.enableGFM[item] = !!dataSources[item];
+ }
+ });
// Before we start, we should clean up any previous data for this form
this.destroy();
// Setup the form
@@ -34,14 +41,7 @@ export default class GLForm {
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
- this.autoComplete.setup(this.form.find('.js-gfm-input'), {
- emojis: true,
- members: this.enableGFM,
- issues: this.enableGFM,
- milestones: this.enableGFM,
- mergeRequests: this.enableGFM,
- labels: this.enableGFM,
- });
+ this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
dropzoneInput(this.form);
autosize(this.textarea);
}
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/groups/index.js b/app/assets/javascripts/groups/index.js
index 57eaac72906..83a9008a94b 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -29,7 +29,7 @@ export default () => {
groupsApp,
},
data() {
- const dataset = this.$options.el.dataset;
+ const { dataset } = this.$options.el;
const hideProjects = dataset.hideProjects === 'true';
const store = new GroupsStore(hideProjects);
const service = new GroupsService(dataset.endpoint);
@@ -42,7 +42,7 @@ export default () => {
};
},
beforeMount() {
- const dataset = this.$options.el.dataset;
+ const { dataset } = this.$options.el;
let groupFilterList = null;
const form = document.querySelector(dataset.formSel);
const filter = document.querySelector(dataset.filterSel);
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index b4f3778d946..eb7cb9745ec 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -10,7 +10,7 @@ export default {
},
computed: {
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
- ...mapGetters(['currentProject']),
+ ...mapGetters(['currentProject', 'currentBranch']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
@@ -22,17 +22,30 @@ export default {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
},
},
+ watch: {
+ disableMergeRequestRadio() {
+ this.updateSelectedCommitAction();
+ },
+ },
mounted() {
- if (this.disableMergeRequestRadio) {
- this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
- }
+ this.updateSelectedCommitAction();
},
methods: {
...mapActions('commit', ['updateCommitAction']),
+ updateSelectedCommitAction() {
+ if (this.currentBranch && !this.currentBranch.can_push) {
+ this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
+ } else if (this.disableMergeRequestRadio) {
+ this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
+ }
+ },
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
+ currentBranchPermissionsTooltip: __(
+ "This option is disabled as you don't have write permissions for the current branch",
+ ),
};
</script>
@@ -40,9 +53,11 @@ export default {
<div class="append-bottom-15 ide-commit-radios">
<radio-group
:value="$options.commitToCurrentBranch"
- :checked="true"
+ :disabled="currentBranch && !currentBranch.can_push"
+ :title="$options.currentBranchPermissionsTooltip"
>
<span
+ class="ide-radio-label"
v-html="commitToCurrentBranchText"
>
</span>
@@ -56,6 +71,7 @@ export default {
v-if="currentProject.merge_requests_enabled"
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
+ :title="__('This option is disabled while you still have unstaged changes')"
:show-input="true"
:disabled="disableMergeRequestRadio"
/>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 955d9280728..ee8eb206980 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -24,7 +24,7 @@ export default {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['hasChanges']),
- ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ ...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
overviewText() {
return sprintf(
__(
@@ -36,6 +36,9 @@ export default {
},
);
},
+ commitButtonText() {
+ return this.stagedFiles.length ? __('Commit') : __('Stage & Commit');
+ },
},
watch: {
currentActivityView() {
@@ -117,7 +120,7 @@ export default {
class="btn btn-primary btn-sm btn-block"
@click="toggleIsSmall"
>
- {{ __('Commit') }}
+ {{ __('Commit…') }}
</button>
<p
class="text-center"
@@ -136,14 +139,14 @@ export default {
</transition>
<commit-message-field
:text="commitMessage"
+ :placeholder="preBuiltCommitMessage"
@input="updateCommitMessage"
/>
<div class="clearfix prepend-top-15">
<actions />
<loading-button
:loading="submitCommitLoading"
- :disabled="commitButtonDisabled"
- :label="__('Commit')"
+ :label="commitButtonText"
container-class="btn btn-success btn-sm float-left"
@click="commitChanges"
/>
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 5cda7967130..ee21eeda3cd 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -89,14 +89,14 @@ export default {
<template>
<div class="multi-file-commit-list-item position-relative">
- <button
+ <div
v-tooltip
:title="tooltipTitle"
:class="{
'is-active': isActive
}"
- type="button"
class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0"
+ role="button"
@dblclick="fileAction"
@click="openFileInEditor"
>
@@ -107,7 +107,7 @@ export default {
:css-classes="iconClass"
/>{{ file.name }}
</span>
- </button>
+ </div>
<component
:is="actionComponent"
:path="file.path"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
index 40496c80a46..37ca108fafc 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -16,6 +16,10 @@ export default {
type: String,
required: true,
},
+ placeholder: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -114,7 +118,7 @@ export default {
</div>
<textarea
ref="textarea"
- :placeholder="__('Write a commit message...')"
+ :placeholder="placeholder"
:value="text"
class="note-textarea ide-commit-message-textarea"
name="commit-message"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index 35ab3fd11df..969e2aa61c4 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -1,6 +1,5 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
-import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -32,14 +31,17 @@ export default {
required: false,
default: false,
},
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
tooltipTitle() {
- return this.disabled
- ? __('This option is disabled while you still have unstaged changes')
- : '';
+ return this.disabled ? this.title : '';
},
},
methods: {
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
new file mode 100644
index 00000000000..acbc98b7a7b
--- /dev/null
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -0,0 +1,69 @@
+<script>
+import { mapActions } from 'vuex';
+import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ },
+ props: {
+ message: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ ...mapActions(['setErrorMessage']),
+ clickAction() {
+ if (this.isLoading) return;
+
+ this.isLoading = true;
+
+ this.message
+ .action(this.message.actionPayload)
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ });
+ },
+ clickFlash() {
+ if (!this.message.action) {
+ this.setErrorMessage(null);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="flash-container flash-container-page"
+ @click="clickFlash"
+ >
+ <div class="flash-alert">
+ <span
+ v-html="message.text"
+ >
+ </span>
+ <button
+ v-if="message.action"
+ type="button"
+ class="flash-action text-white p-0 border-top-0 border-right-0 border-left-0 bg-transparent"
+ @click.stop.prevent="clickAction"
+ >
+ {{ message.actionText }}
+ <loading-icon
+ v-show="isLoading"
+ inline
+ />
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue
index a4cf3edb981..f5252ce7706 100644
--- a/app/assets/javascripts/ide/components/file_finder/item.vue
+++ b/app/assets/javascripts/ide/components/file_finder/item.vue
@@ -30,7 +30,7 @@ export default {
},
computed: {
pathWithEllipsis() {
- const path = this.file.path;
+ const { path } = this.file;
return path.length < MAX_PATH_LENGTH
? path
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index f5f7f967a92..9f016e0338f 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -7,6 +7,7 @@ import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
import RightPane from './panes/right.vue';
+import ErrorMessage from './error_message.vue';
const originalStopCallback = Mousetrap.stopCallback;
@@ -18,6 +19,7 @@ export default {
RepoEditor,
FindFile,
RightPane,
+ ErrorMessage,
},
computed: {
...mapState([
@@ -28,6 +30,7 @@ export default {
'fileFindVisible',
'emptyStateSvgPath',
'currentProjectId',
+ 'errorMessage',
]),
...mapGetters(['activeFile', 'hasChanges']),
},
@@ -72,6 +75,10 @@ export default {
<template>
<article class="ide">
+ <error-message
+ v-if="errorMessage"
+ :message="errorMessage"
+ />
<div
class="ide-view"
>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 1814924be39..677b282bd61 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -23,6 +23,7 @@
let { result } = target;
if (!isText) {
+ // eslint-disable-next-line prefer-destructuring
result = result.split('base64,')[1];
}
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index dedc2988618..5cd2c9ce188 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -69,7 +69,7 @@ export default {
>
<icon
:size="16"
- name="pipeline"
+ name="rocket"
/>
</button>
</li>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index c2c678ff0be..50ab242ba2a 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -28,7 +28,7 @@ export default {
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']),
- ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ ...mapGetters('commit', ['discardDraftButtonDisabled']),
showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index c34547fcc60..f490a3a2a39 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -95,24 +95,53 @@ export default {
return this.file.changed || this.file.tempFile || this.file.staged;
},
},
+ mounted() {
+ if (this.hasPathAtCurrentRoute()) {
+ this.scrollIntoView(true);
+ }
+ },
updated() {
if (this.file.type === 'blob' && this.file.active) {
- this.$el.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest',
- });
+ this.scrollIntoView();
}
},
methods: {
...mapActions(['toggleTreeOpen']),
clickFile() {
// Manual Action if a tree is selected/opened
- if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
+ if (this.isTree && this.hasUrlAtCurrentRoute()) {
this.toggleTreeOpen(this.file.path);
}
router.push(`/project${this.file.url}`);
},
+ scrollIntoView(isInit = false) {
+ const block = isInit && this.isTree ? 'center' : 'nearest';
+
+ this.$el.scrollIntoView({
+ behavior: 'smooth',
+ block,
+ });
+ },
+ hasPathAtCurrentRoute() {
+ if (!this.$router || !this.$router.currentRoute) {
+ return false;
+ }
+
+ // - strip route up to "/-/" and ending "/"
+ const routePath = this.$router.currentRoute.path
+ .replace(/^.*?[/]-[/]/g, '')
+ .replace(/[/]$/g, '');
+
+ // - strip ending "/"
+ const filePath = this.file.path
+ .replace(/[/]$/g, '');
+
+ return filePath === routePath;
+ },
+ hasUrlAtCurrentRoute() {
+ return this.$router.currentRoute.path === `/project${this.file.url}`;
+ },
},
};
</script>
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 1ad52c1bd83..03772ae4a4c 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -44,6 +44,8 @@ export default {
methods: {
...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
clickFile(tab) {
+ if (tab.active) return;
+
this.updateDelayViewerUpdated(true);
if (tab.pending) {
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index b52618f4fde..cc8dbb942d8 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -95,14 +95,6 @@ router.beforeEach((to, from, next) => {
}
})
.catch(e => {
- flash(
- 'Error while loading the branch files. Please try again.',
- 'alert',
- document,
- null,
- false,
- true,
- );
throw e;
});
} else if (to.params.mrid) {
diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js
index f09930e8158..78b2eab6399 100644
--- a/app/assets/javascripts/ide/lib/diff/diff_worker.js
+++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js
@@ -2,7 +2,7 @@ import { computeDiff } from './diff';
// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', (e) => {
- const data = e.data;
+ const { data } = e;
// eslint-disable-next-line no-restricted-globals
self.postMessage({
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index da9de25302a..3e939f0c1a3 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -1,15 +1,11 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
+import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
-Vue.use(VueResource);
-
export default {
- getTreeData(endpoint) {
- return Vue.http.get(endpoint, { params: { format: 'json' } });
- },
getFileData(endpoint) {
- return Vue.http.get(endpoint, { params: { format: 'json', viewer: 'none' } });
+ return axios.get(endpoint, {
+ params: { format: 'json', viewer: 'none' },
+ });
},
getRawFileData(file) {
if (file.tempFile) {
@@ -20,7 +16,11 @@ export default {
return Promise.resolve(file.raw);
}
- return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
+ return axios
+ .get(file.rawPath, {
+ params: { format: 'json' },
+ })
+ .then(({ data }) => data);
},
getBaseRawFileData(file, sha) {
if (file.tempFile) {
@@ -31,11 +31,11 @@ export default {
return Promise.resolve(file.baseRaw);
}
- return Vue.http
+ return axios
.get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
params: { format: 'json' },
})
- .then(res => res.text());
+ .then(({ data }) => data);
},
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
@@ -52,28 +52,12 @@ export default {
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
},
- createBranch(projectId, payload) {
- const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
-
- return Vue.http.post(url, payload);
- },
commit(projectId, payload) {
return Api.commitMultiple(projectId, payload);
},
- getTreeLastCommit(endpoint) {
- return Vue.http.get(endpoint, {
- params: {
- format: 'json',
- },
- });
- },
getFiles(projectUrl, branchId) {
const url = `${projectUrl}/files/${branchId}`;
- return Vue.http.get(url, {
- params: {
- format: 'json',
- },
- });
+ return axios.get(url, { params: { format: 'json' } });
},
lastCommitPipelines({ getters }) {
const commitSha = getters.lastCommit.id;
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 3dc365eaead..5e91fa915ff 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -175,6 +175,9 @@ export const setRightPane = ({ commit }, view) => {
export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
+export const setErrorMessage = ({ commit }, errorMessage) =>
+ commit(types.SET_ERROR_MESSAGE, errorMessage);
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 74f9c112f5a..6c0887e11ee 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -1,5 +1,5 @@
-import { normalizeHeaders } from '~/lib/utils/common_utils';
-import flash from '~/flash';
+import { __ } from '../../../locale';
+import { normalizeHeaders } from '../../../lib/utils/common_utils';
import eventHub from '../../eventhub';
import service from '../../services';
import * as types from '../mutation_types';
@@ -8,7 +8,7 @@ import { setPageTitle } from '../utils';
import { viewerTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
- const path = file.path;
+ const { path } = file;
const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
const fileWasActive = file.active;
@@ -66,13 +66,10 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
.getFileData(
`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`,
)
- .then(res => {
- const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
- setPageTitle(pageTitle);
+ .then(({ data, headers }) => {
+ const normalizedHeaders = normalizeHeaders(headers);
+ setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE']));
- return res.json();
- })
- .then(data => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, path);
if (makeFileActive) dispatch('setFileActive', path);
@@ -80,7 +77,13 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
})
.catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
- flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the file.'),
+ action: payload =>
+ dispatch('getFileData', payload).then(() => dispatch('setErrorMessage', null)),
+ actionText: __('Please try again'),
+ actionPayload: { path, makeFileActive },
+ });
});
};
@@ -88,7 +91,7 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => {
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
};
-export const getRawFileData = ({ state, commit }, { path, baseSha }) => {
+export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
const file = state.entries[path];
return new Promise((resolve, reject) => {
service
@@ -113,7 +116,13 @@ export const getRawFileData = ({ state, commit }, { path, baseSha }) => {
}
})
.catch(() => {
- flash('Error loading file content. Please try again.');
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the file content.'),
+ action: payload =>
+ dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)),
+ actionText: __('Please try again'),
+ actionPayload: { path, baseSha },
+ });
reject();
});
});
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index edb20ff96fc..4aa151abcb7 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -1,17 +1,16 @@
-import flash from '~/flash';
+import { __ } from '../../../locale';
import service from '../../services';
import * as types from '../mutation_types';
export const getMergeRequestData = (
- { commit, state },
+ { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
service
.getProjectMergeRequestData(projectId, mergeRequestId)
- .then(res => res.data)
- .then(data => {
+ .then(({ data }) => {
commit(types.SET_MERGE_REQUEST, {
projectPath: projectId,
mergeRequestId,
@@ -21,7 +20,15 @@ export const getMergeRequestData = (
resolve(data);
})
.catch(() => {
- flash('Error loading merge request data. Please try again.');
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the merge request.'),
+ action: payload =>
+ dispatch('getMergeRequestData', payload).then(() =>
+ dispatch('setErrorMessage', null),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, mergeRequestId, force },
+ });
reject(new Error(`Merge Request not loaded ${projectId}`));
});
} else {
@@ -30,15 +37,14 @@ export const getMergeRequestData = (
});
export const getMergeRequestChanges = (
- { commit, state },
+ { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
service
.getProjectMergeRequestChanges(projectId, mergeRequestId)
- .then(res => res.data)
- .then(data => {
+ .then(({ data }) => {
commit(types.SET_MERGE_REQUEST_CHANGES, {
projectPath: projectId,
mergeRequestId,
@@ -47,7 +53,15 @@ export const getMergeRequestChanges = (
resolve(data);
})
.catch(() => {
- flash('Error loading merge request changes. Please try again.');
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the merge request changes.'),
+ action: payload =>
+ dispatch('getMergeRequestChanges', payload).then(() =>
+ dispatch('setErrorMessage', null),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, mergeRequestId, force },
+ });
reject(new Error(`Merge Request Changes not loaded ${projectId}`));
});
} else {
@@ -56,7 +70,7 @@ export const getMergeRequestChanges = (
});
export const getMergeRequestVersions = (
- { commit, state },
+ { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
@@ -73,7 +87,15 @@ export const getMergeRequestVersions = (
resolve(data);
})
.catch(() => {
- flash('Error loading merge request versions. Please try again.');
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the merge request version data.'),
+ action: payload =>
+ dispatch('getMergeRequestVersions', payload).then(() =>
+ dispatch('setErrorMessage', null),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, mergeRequestId, force },
+ });
reject(new Error(`Merge Request Versions not loaded ${projectId}`));
});
} else {
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 0b99bce4a8e..501e25d452b 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,7 +1,10 @@
+import _ from 'underscore';
import flash from '~/flash';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import service from '../../services';
+import api from '../../../api';
import * as types from '../mutation_types';
+import router from '../../ide_router';
export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) =>
new Promise((resolve, reject) => {
@@ -32,7 +35,10 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force
}
});
-export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
+export const getBranchData = (
+ { commit, dispatch, state },
+ { projectId, branchId, force = false } = {},
+) =>
new Promise((resolve, reject) => {
if (
typeof state.projects[`${projectId}`] === 'undefined' ||
@@ -51,15 +57,19 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force =
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
- .catch(() => {
- flash(
- __('Error loading branch data. Please try again.'),
- 'alert',
- document,
- null,
- false,
- true,
- );
+ .catch(e => {
+ if (e.response.status === 404) {
+ dispatch('showBranchNotFoundError', branchId);
+ } else {
+ flash(
+ __('Error loading branch data. Please try again.'),
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ }
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
@@ -80,3 +90,37 @@ export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {})
.catch(() => {
flash(__('Error loading last commit.'), 'alert', document, null, false, true);
});
+
+export const createNewBranchFromDefault = ({ state, dispatch, getters }, branch) =>
+ api
+ .createBranch(state.currentProjectId, {
+ ref: getters.currentProject.default_branch,
+ branch,
+ })
+ .then(() => {
+ dispatch('setErrorMessage', null);
+ router.push(`${router.currentRoute.path}?${Date.now()}`);
+ })
+ .catch(() => {
+ dispatch('setErrorMessage', {
+ text: __('An error occured creating the new branch.'),
+ action: payload => dispatch('createNewBranchFromDefault', payload),
+ actionText: __('Please try again'),
+ actionPayload: branch,
+ });
+ });
+
+export const showBranchNotFoundError = ({ dispatch }, branchId) => {
+ dispatch('setErrorMessage', {
+ text: sprintf(
+ __("Branch %{branchName} was not found in this project's repository."),
+ {
+ branchName: `<strong>${_.escape(branchId)}</strong>`,
+ },
+ false,
+ ),
+ action: payload => dispatch('createNewBranchFromDefault', payload),
+ actionText: __('Create branch'),
+ actionPayload: branchId,
+ });
+};
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index cc5116413f7..ffaaaabff17 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -1,14 +1,23 @@
-import { normalizeHeaders } from '~/lib/utils/common_utils';
-import flash from '~/flash';
+import { __ } from '../../../locale';
import service from '../../services';
import * as types from '../mutation_types';
-import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_TREE_OPEN, path);
};
+export const showTreeEntry = ({ commit, dispatch, state }, path) => {
+ const entry = state.entries[path];
+ const parentPath = entry ? entry.parentPath : '';
+
+ if (parentPath) {
+ commit(types.SET_TREE_OPEN, parentPath);
+
+ dispatch('showTreeEntry', parentPath);
+ }
+};
+
export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') {
dispatch('toggleTreeOpen', row.path);
@@ -21,44 +30,23 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
} else {
dispatch('getFileData', { path: row.path });
}
-};
-export const getLastCommitData = ({ state, commit, dispatch }, tree = state) => {
- if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
-
- service
- .getTreeLastCommit(tree.lastCommitPath)
- .then(res => {
- const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
-
- commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
-
- return res.json();
- })
- .then(data => {
- data.forEach(lastCommit => {
- const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
-
- if (entry) {
- commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
- }
- });
-
- dispatch('getLastCommitData', tree);
- })
- .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
+ dispatch('showTreeEntry', row.path);
};
-export const getFiles = ({ state, commit }, { projectId, branchId } = {}) =>
+export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
new Promise((resolve, reject) => {
- if (!state.trees[`${projectId}/${branchId}`]) {
+ if (
+ !state.trees[`${projectId}/${branchId}`] ||
+ (state.trees[`${projectId}/${branchId}`].tree &&
+ state.trees[`${projectId}/${branchId}`].tree.length === 0)
+ ) {
const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service
.getFiles(selectedProject.web_url, branchId)
- .then(res => res.json())
- .then(data => {
+ .then(({ data }) => {
const worker = new FilesDecoratorWorker();
worker.addEventListener('message', e => {
const { entries, treeList } = e.data;
@@ -86,7 +74,17 @@ export const getFiles = ({ state, commit }, { projectId, branchId } = {}) =>
});
})
.catch(e => {
- flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
+ if (e.response.status === 404) {
+ dispatch('showBranchNotFoundError', branchId);
+ } else {
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading all the files.'),
+ action: payload =>
+ dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, branchId },
+ });
+ }
reject(e);
});
} else {
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index b239a605371..5ce268b0d05 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -82,10 +82,13 @@ export const getStagedFilesCountForPath = state => path =>
getChangesCountForFiles(state.stagedFiles, path);
export const lastCommit = (state, getters) => {
- const branch = getters.currentProject && getters.currentProject.branches[state.currentBranchId];
+ const branch = getters.currentProject && getters.currentBranch;
return branch ? branch.commit : null;
};
+export const currentBranch = (state, getters) =>
+ getters.currentProject && getters.currentProject.branches[state.currentBranchId];
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 7219abc4185..7828c31f20e 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import { sprintf, __ } from '~/locale';
import flash from '~/flash';
-import { stripHtml } from '~/lib/utils/text_utility';
import * as rootTypes from '../../mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
import router from '../../../ide_router';
@@ -103,17 +102,24 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }
export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
- const payload = createCommitPayload({
- branch: getters.branchName,
- newBranch,
- state,
- rootState,
- });
+ const stageFilesPromise = rootState.stagedFiles.length
+ ? Promise.resolve()
+ : dispatch('stageAllChanges', null, { root: true });
commit(types.UPDATE_LOADING, true);
- return service
- .commit(rootState.currentProjectId, payload)
+ return stageFilesPromise
+ .then(() => {
+ const payload = createCommitPayload({
+ branch: getters.branchName,
+ newBranch,
+ getters,
+ state,
+ rootState,
+ });
+
+ return service.commit(rootState.currentProjectId, payload);
+ })
.then(({ data }) => {
commit(types.UPDATE_LOADING, false);
@@ -191,11 +197,18 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
if (err.response.status === 400) {
$('#ide-create-branch-modal').modal('show');
} else {
- let errMsg = __('Error committing changes. Please try again.');
- if (err.response.data && err.response.data.message) {
- errMsg += ` (${stripHtml(err.response.data.message)})`;
- }
- flash(errMsg, 'alert', document, null, false, true);
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('An error accured whilst committing your changes.'),
+ action: () =>
+ dispatch('commitChanges').then(() =>
+ dispatch('setErrorMessage', null, { root: true }),
+ ),
+ actionText: __('Please try again'),
+ },
+ { root: true },
+ );
window.dispatchEvent(new Event('resize'));
}
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index d01060201f2..3db4b2f903e 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -1,3 +1,4 @@
+import { sprintf, n__ } from '../../../../locale';
import * as consts from './constants';
const BRANCH_SUFFIX_COUNT = 5;
@@ -5,9 +6,6 @@ const BRANCH_SUFFIX_COUNT = 5;
export const discardDraftButtonDisabled = state =>
state.commitMessage === '' || state.submitCommitLoading;
-export const commitButtonDisabled = (state, getters, rootState) =>
- getters.discardDraftButtonDisabled || !rootState.stagedFiles.length;
-
export const newBranchName = (state, _, rootState) =>
`${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(
-BRANCH_SUFFIX_COUNT,
@@ -28,5 +26,18 @@ export const branchName = (state, getters, rootState) => {
return rootState.currentBranchId;
};
+export const preBuiltCommitMessage = (state, _, rootState) => {
+ if (state.commitMessage) return state.commitMessage;
+
+ const files = (rootState.stagedFiles.length
+ ? rootState.stagedFiles
+ : rootState.changedFiles
+ ).reduce((acc, val) => acc.concat(val.path), []);
+
+ return sprintf(n__('Update %{files}', 'Update %{files} files', files.length), {
+ files: files.join(', '),
+ });
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
index 551dd322c9b..cdd8076952f 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
@@ -1,6 +1,5 @@
import { __ } from '../../../../locale';
import Api from '../../../../api';
-import flash from '../../../../flash';
import router from '../../../ide_router';
import { scopes } from './constants';
import * as types from './mutation_types';
@@ -8,8 +7,20 @@ import * as rootTypes from '../../mutation_types';
export const requestMergeRequests = ({ commit }, type) =>
commit(types.REQUEST_MERGE_REQUESTS, type);
-export const receiveMergeRequestsError = ({ commit }, type) => {
- flash(__('Error loading merge requests.'));
+export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => {
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('Error loading merge requests.'),
+ action: payload =>
+ dispatch('fetchMergeRequests', payload).then(() =>
+ dispatch('setErrorMessage', null, { root: true }),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { type, search },
+ },
+ { root: true },
+ );
commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type);
};
export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) =>
@@ -22,7 +33,7 @@ export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, searc
Api.mergeRequests({ scope, state, search })
.then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data }))
- .catch(() => dispatch('receiveMergeRequestsError', type));
+ .catch(() => dispatch('receiveMergeRequestsError', { type, search }));
};
export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type);
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index fe1dc9ac8f8..8cb01f25223 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -1,7 +1,7 @@
import Visibility from 'visibilityjs';
import axios from 'axios';
+import httpStatus from '../../../../lib/utils/http_status';
import { __ } from '../../../../locale';
-import flash from '../../../../flash';
import Poll from '../../../../lib/utils/poll';
import service from '../../../services';
import { rightSidebarViews } from '../../../constants';
@@ -18,10 +18,27 @@ export const stopPipelinePolling = () => {
export const restartPipelinePolling = () => {
if (eTagPoll) eTagPoll.restart();
};
+export const forcePipelineRequest = () => {
+ if (eTagPoll) eTagPoll.makeRequest();
+};
export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
-export const receiveLatestPipelineError = ({ commit, dispatch }) => {
- flash(__('There was an error loading latest pipeline'));
+export const receiveLatestPipelineError = ({ commit, dispatch }, err) => {
+ if (err.status !== httpStatus.NOT_FOUND) {
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('An error occured whilst fetching the latest pipline.'),
+ action: () =>
+ dispatch('forcePipelineRequest').then(() =>
+ dispatch('setErrorMessage', null, { root: true }),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: null,
+ },
+ { root: true },
+ );
+ }
commit(types.RECEIVE_LASTEST_PIPELINE_ERROR);
dispatch('stopPipelinePolling');
};
@@ -46,7 +63,7 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
method: 'lastCommitPipelines',
data: { getters: rootGetters },
successCallback: ({ data }) => dispatch('receiveLatestPipelineSuccess', data),
- errorCallback: () => dispatch('receiveLatestPipelineError'),
+ errorCallback: err => dispatch('receiveLatestPipelineError', err),
});
if (!Visibility.hidden()) {
@@ -63,9 +80,21 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
};
export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id);
-export const receiveJobsError = ({ commit }, id) => {
- flash(__('There was an error loading jobs'));
- commit(types.RECEIVE_JOBS_ERROR, id);
+export const receiveJobsError = ({ commit, dispatch }, stage) => {
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('An error occured whilst loading the pipelines jobs.'),
+ action: payload =>
+ dispatch('fetchJobs', payload).then(() =>
+ dispatch('setErrorMessage', null, { root: true }),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: stage,
+ },
+ { root: true },
+ );
+ commit(types.RECEIVE_JOBS_ERROR, stage.id);
};
export const receiveJobsSuccess = ({ commit }, { id, data }) =>
commit(types.RECEIVE_JOBS_SUCCESS, { id, data });
@@ -76,7 +105,7 @@ export const fetchJobs = ({ dispatch }, stage) => {
axios
.get(stage.dropdownPath)
.then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data }))
- .catch(() => dispatch('receiveJobsError', stage.id));
+ .catch(() => dispatch('receiveJobsError', stage));
};
export const toggleStageCollapsed = ({ commit }, stageId) =>
@@ -90,8 +119,18 @@ export const setDetailJob = ({ commit, dispatch }, job) => {
};
export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE);
-export const receiveJobTraceError = ({ commit }) => {
- flash(__('Error fetching job trace'));
+export const receiveJobTraceError = ({ commit, dispatch }) => {
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('An error occured whilst fetching the job trace.'),
+ action: () =>
+ dispatch('fetchJobTrace').then(() => dispatch('setErrorMessage', null, { root: true })),
+ actionText: __('Please try again'),
+ actionPayload: null,
+ },
+ { root: true },
+ );
commit(types.RECEIVE_JOB_TRACE_ERROR);
};
export const receiveJobTraceSuccess = ({ commit }, data) =>
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 99b315ac4db..555802e1811 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -28,6 +28,7 @@ export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
+export const SET_TREE_OPEN = 'SET_TREE_OPEN';
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
export const CREATE_TREE = 'CREATE_TREE';
export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES';
@@ -71,3 +72,5 @@ export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';
export const CLEAR_PROJECTS = 'CLEAR_PROJECTS';
export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
+
+export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 48f1da4eccf..702be2140e2 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -163,6 +163,9 @@ export default {
[types.RESET_OPEN_FILES](state) {
Object.assign(state, { openFiles: [] });
},
+ [types.SET_ERROR_MESSAGE](state, errorMessage) {
+ Object.assign(state, { errorMessage });
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
index 1176c040fb9..2cf34af9274 100644
--- a/app/assets/javascripts/ide/stores/mutations/tree.js
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -6,6 +6,11 @@ export default {
opened: !state.entries[path].opened,
});
},
+ [types.SET_TREE_OPEN](state, path) {
+ Object.assign(state.entries[path], {
+ opened: true,
+ });
+ },
[types.CREATE_TREE](state, { treePath }) {
Object.assign(state, {
trees: Object.assign({}, state.trees, {
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 4aac4696075..be229b2c723 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -25,4 +25,5 @@ export default () => ({
fileFindVisible: false,
rightPane: null,
links: {},
+ errorMessage: null,
});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 10368a4d97c..9e6b86dd844 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -105,9 +105,9 @@ export const setPageTitle = title => {
document.title = title;
};
-export const createCommitPayload = ({ branch, newBranch, state, rootState }) => ({
+export const createCommitPayload = ({ branch, getters, newBranch, state, rootState }) => ({
branch,
- commit_message: state.commitMessage,
+ commit_message: state.commitMessage || getters.preBuiltCommitMessage,
actions: rootState.stagedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js
index 12d56714b34..a319bcccb8f 100644
--- a/app/assets/javascripts/image_diff/helpers/dom_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js
@@ -2,7 +2,8 @@ export function setPositionDataAttribute(el, options) {
// Update position data attribute so that the
// new comment form can use this data for ajax request
const { x, y, width, height } = options;
- const position = el.dataset.position;
+ const { position } = el.dataset;
+
const positionObject = Object.assign({}, JSON.parse(position), {
x,
y,
diff --git a/app/assets/javascripts/image_diff/helpers/utils_helper.js b/app/assets/javascripts/image_diff/helpers/utils_helper.js
index 28d9a969143..beec99e6934 100644
--- a/app/assets/javascripts/image_diff/helpers/utils_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/utils_helper.js
@@ -40,8 +40,7 @@ export function getTargetSelection(event) {
const x = event.offsetX;
const y = event.offsetY;
- const width = imageEl.width;
- const height = imageEl.height;
+ const { width, height } = imageEl;
const actualWidth = imageEl.naturalWidth;
const actualHeight = imageEl.naturalHeight;
diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js
index 882aedfcc76..3c71258e53b 100644
--- a/app/assets/javascripts/init_notes.js
+++ b/app/assets/javascripts/init_notes.js
@@ -7,10 +7,10 @@ export default () => {
notesIds,
now,
diffView,
- autocomplete,
+ enableGFM,
} = JSON.parse(dataEl.innerHTML);
// Create a singleton so that we don't need to assign
// into the window object, we can just access the current isntance with Notes.instance
- Notes.initialize(notesUrl, notesIds, now, diffView, autocomplete);
+ Notes.initialize(notesUrl, notesIds, now, diffView, enableGFM);
};
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/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
index b2c2de9e5de..07cf1eff279 100644
--- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js
+++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
@@ -10,7 +10,7 @@ class AutoWidthDropdownSelect {
}
init() {
- const dropdownClass = this.dropdownClass;
+ const { dropdownClass } = this;
this.$selectElement.select2({
dropdownCssClass: dropdownClass,
...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
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/job.js b/app/assets/javascripts/job.js
index fc13f467675..d4f2a3ef7d3 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -164,7 +164,7 @@ export default class Job extends LogOutputBehaviours {
// eslint-disable-next-line class-methods-use-this
shouldHideSidebarForViewport() {
const bootstrapBreakpoint = bp.getBreakpointSize();
- return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+ return bootstrapBreakpoint === 'xs';
}
toggleSidebar(shouldHide) {
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index f2939ad4dbe..0db7b95636c 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -4,7 +4,7 @@ import jobHeader from './components/header.vue';
import detailsBlock from './components/sidebar_details_block.vue';
export default () => {
- const dataset = document.getElementById('js-job-details-vue').dataset;
+ const { dataset } = document.getElementById('js-job-details-vue');
const mediator = new JobMediator({ endpoint: dataset.endpoint });
mediator.fetchJob();
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..37a45d1d1a2 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 */
@@ -56,7 +56,7 @@ export default class LabelsSelect {
.map(function () {
return this.value;
}).get();
- const handleClick = options.handleClick;
+ const { handleClick } = options;
$sidebarLabelTooltip.tooltip();
@@ -215,7 +215,7 @@ export default class LabelsSelect {
}
else {
if (label.color != null) {
- color = label.color[0];
+ [color] = label.color;
}
}
if (color) {
@@ -243,7 +243,8 @@ export default class LabelsSelect {
var $dropdownParent = $dropdown.parent();
var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
var isSelected = el !== null ? el.hasClass('is-active') : false;
- var title = selected.title;
+
+ var { title } = selected;
var selectedLabels = this.selected;
if ($dropdownInputField.length && $dropdownInputField.val().length) {
@@ -382,7 +383,7 @@ export default class LabelsSelect {
}));
}
else {
- var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
+ var { labels } = gl.issueBoards.BoardsStore.detail.issue;
labels = labels.filter(function (selectedLabel) {
return selectedLabel.id !== label.id;
});
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index d0b0e5e1ba1..6b7550efff8 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;
+ const { top } = $el.offset();
- return $('body, html').animate({
- scrollTop: top - mrTabsHeight - headerHeight,
- }, 200);
+ return $('body, html').animate(
+ {
+ scrollTop: top - contentTop(),
+ },
+ 200,
+ );
};
/**
@@ -170,12 +189,25 @@ export const getParameterByName = (name, urlToParse) => {
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
+const handleSelectedRange = (range) => {
+ const container = range.commonAncestorContainer;
+ // add context to fragment if needed
+ if (container.tagName === 'OL') {
+ const parentContainer = document.createElement(container.tagName);
+ parentContainer.appendChild(range.cloneContents());
+ return parentContainer;
+ }
+ return range.cloneContents();
+};
+
export const getSelectedFragment = () => {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const documentFragment = document.createDocumentFragment();
+
for (let i = 0; i < selection.rangeCount; i += 1) {
- documentFragment.appendChild(selection.getRangeAt(i).cloneContents());
+ const range = selection.getRangeAt(i);
+ documentFragment.appendChild(handleSelectedRange(range));
}
if (documentFragment.textContent.length === 0) return null;
@@ -184,9 +216,7 @@ export const getSelectedFragment = () => {
export const insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
- const selectionStart = target.selectionStart;
- const selectionEnd = target.selectionEnd;
- const value = target.value;
+ const { selectionStart, selectionEnd, value } = target;
const textBefore = value.substring(0, selectionStart);
const textAfter = value.substring(selectionEnd, value.length);
@@ -212,7 +242,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 ||
@@ -225,7 +256,8 @@ export const nodeMatchesSelector = (node, selector) => {
// IE11 doesn't support `node.matches(selector)`
- let parentNode = node.parentNode;
+ let { parentNode } = node;
+
if (!parentNode) {
parentNode = document.createElement('div');
// eslint-disable-next-line no-param-reassign
@@ -241,10 +273,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];
});
@@ -255,12 +287,14 @@ 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(': ');
+
+ // eslint-disable-next-line prefer-destructuring
headersObject[keyValue[0]] = keyValue[1];
});
@@ -295,15 +329,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;
+ }, {});
};
/**
@@ -312,9 +344,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
@@ -322,7 +358,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);
};
@@ -371,7 +407,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) {
@@ -447,7 +483,8 @@ export const resetFavicon = () => {
};
export const setCiStatusFavicon = pageUrl =>
- axios.get(pageUrl)
+ axios
+ .get(pageUrl)
.then(({ data }) => {
if (data && data.favicon) {
return setFaviconOverlay(data.favicon);
@@ -469,28 +506,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/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 7cca32dc6fa..1f66fa811ea 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,11 +1,10 @@
import $ from 'jquery';
import timeago from 'timeago.js';
-import dateFormat from 'vendor/date.format';
+import dateFormat from 'dateformat';
import { pluralize } from './text_utility';
import { languageCode, s__ } from '../../locale';
window.timeago = timeago;
-window.dateFormat = dateFormat;
/**
* Returns i18n month names array.
@@ -143,7 +142,8 @@ export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
if (setTimeago) {
// Recreate with custom template
$(el).tooltip({
- template: '<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',
+ template:
+ '<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',
});
}
@@ -275,10 +275,8 @@ export const totalDaysInMonth = date => {
*
* @param {Array} quarter
*/
-export const totalDaysInQuarter = quarter => quarter.reduce(
- (acc, month) => acc + totalDaysInMonth(month),
- 0,
-);
+export const totalDaysInQuarter = quarter =>
+ quarter.reduce((acc, month) => acc + totalDaysInMonth(month), 0);
/**
* Returns list of Dates referring to Sundays of the month
@@ -333,14 +331,8 @@ export const getTimeframeWindowFrom = (startDate, length) => {
// Iterate and set date for the size of length
// and push date reference to timeframe list
const timeframe = new Array(length)
- .fill()
- .map(
- (val, i) => new Date(
- startDate.getFullYear(),
- startDate.getMonth() + i,
- 1,
- ),
- );
+ .fill()
+ .map((val, i) => new Date(startDate.getFullYear(), startDate.getMonth() + i, 1));
// Change date of last timeframe item to last date of the month
timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1]));
@@ -362,14 +354,15 @@ export const getTimeframeWindowFrom = (startDate, length) => {
* @param {Date} date
* @param {Array} quarter
*/
-export const dayInQuarter = (date, quarter) => quarter.reduce((acc, month) => {
- if (date.getMonth() > month.getMonth()) {
- return acc + totalDaysInMonth(month);
- } else if (date.getMonth() === month.getMonth()) {
- return acc + date.getDate();
- }
- return acc + 0;
-}, 0);
+export const dayInQuarter = (date, quarter) =>
+ quarter.reduce((acc, month) => {
+ if (date.getMonth() > month.getMonth()) {
+ return acc + totalDaysInMonth(month);
+ } else if (date.getMonth() === month.getMonth()) {
+ return acc + date.getDate();
+ }
+ return acc + 0;
+ }, 0);
window.gl = window.gl || {};
window.gl.utils = {
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 f086d962221..afbab59055b 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -13,7 +13,7 @@ export function formatRelevantDigits(number) {
let relevantDigits = 0;
let formattedNumber = '';
if (!Number.isNaN(Number(number))) {
- digitsLeft = number.toString().split('.')[0];
+ [digitsLeft] = number.toString().split('.');
switch (digitsLeft.length) {
case 1:
relevantDigits = 3;
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 303c5d8a894..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';
@@ -142,14 +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 = [];
+ const results = [];
+ const ref = range[0] <= range[1] ? range : range.reverse();
- // eslint-disable-next-line no-multi-assign
- for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
+ for (let lineNumber = range[0]; lineNumber <= ref[1]; lineNumber += 1) {
results.push(this.highlightLine(lineNumber));
}
+
return results;
} else {
return this.highlightLine(range[0]);
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_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index 70f185e3656..1501296ac4f 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -156,7 +156,7 @@ import Cookies from 'js-cookie';
return 0;
}
- const files = this.state.conflictsData.files;
+ const { files } = this.state.conflictsData;
let count = 0;
files.forEach((file) => {
@@ -313,7 +313,7 @@ import Cookies from 'js-cookie';
},
isReadyToCommit() {
- const files = this.state.conflictsData.files;
+ const { files } = this.state.conflictsData;
const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length;
let unresolved = 0;
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 491858c3602..7badd68089c 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -12,7 +12,7 @@ import syntaxHighlight from '../syntax_highlight';
export default function initMergeConflicts() {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const conflictsEl = document.querySelector('#conflicts');
- const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
+ const { mergeConflictsStore } = gl.mergeConflicts;
const mergeConflictsService = new MergeConflictsService({
conflictsPath: conflictsEl.dataset.conflictsPath,
resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath,
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..53d7504de35 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,12 +9,14 @@ 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';
import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight';
import Notes from './notes';
+import { polyfillSticky } from './lib/utils/sticky';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -62,32 +65,45 @@ import Notes from './notes';
/* eslint-enable max-len */
// Store the `location` object, allowing for easier stubbing in tests
-let location = window.location;
+let { location } = window;
export default class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) {
- const mergeRequestTabs = document.querySelector('.js-tabs-affix');
+ this.mergeRequestTabs = document.querySelector('.merge-request-tabs-container');
+ this.mergeRequestTabsAll =
+ this.mergeRequestTabs && this.mergeRequestTabs.querySelectorAll
+ ? this.mergeRequestTabs.querySelectorAll('.merge-request-tabs li')
+ : null;
+ this.mergeRequestTabPanes = document.querySelector('#diff-notes-app');
+ this.mergeRequestTabPanesAll =
+ this.mergeRequestTabPanes && this.mergeRequestTabPanes.querySelectorAll
+ ? this.mergeRequestTabPanes.querySelectorAll('.tab-pane')
+ : null;
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.currentTab = null;
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);
this.tabShown = this.tabShown.bind(this);
- this.showTab = this.showTab.bind(this);
+ this.clickTab = this.clickTab.bind(this);
this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
if (peek) {
this.stickyTop += peek.offsetHeight;
}
- if (mergeRequestTabs) {
- this.stickyTop += mergeRequestTabs.offsetHeight;
+ if (this.mergeRequestTabs) {
+ this.stickyTop += this.mergeRequestTabs.offsetHeight;
}
if (stubLocation) {
@@ -95,25 +111,22 @@ export default class MergeRequestTabs {
}
this.bindEvents();
- this.activateTab(action);
+ if (
+ this.mergeRequestTabs &&
+ this.mergeRequestTabs.querySelector(`a[data-action='${action}']`) &&
+ this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click
+ )
+ this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click();
this.initAffix();
}
bindEvents() {
- $(document)
- .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
- .on('click', '.js-show-tab', this.showTab);
-
- $('.merge-request-tabs a[data-toggle="tab"]').on('click', this.clickTab);
+ $('.merge-request-tabs a[data-toggle="tabvue"]').on('click', this.clickTab);
}
// Used in tests
unbindEvents() {
- $(document)
- .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
- .off('click', '.js-show-tab', this.showTab);
-
- $('.merge-request-tabs a[data-toggle="tab"]').off('click', this.clickTab);
+ $('.merge-request-tabs a[data-toggle="tabvue"]').off('click', this.clickTab);
}
destroyPipelinesView() {
@@ -125,52 +138,86 @@ export default class MergeRequestTabs {
}
}
- showTab(e) {
- e.preventDefault();
- this.activateTab($(e.target).data('action'));
- }
-
clickTab(e) {
- if (e.currentTarget && isMetaClick(e)) {
- const targetLink = e.currentTarget.getAttribute('href');
+ if (e.currentTarget) {
e.stopImmediatePropagation();
e.preventDefault();
- window.open(targetLink, '_blank');
+
+ const { action } = e.currentTarget.dataset;
+
+ if (action) {
+ const href = e.currentTarget.getAttribute('href');
+ this.tabShown(action, href);
+ } else if (isMetaClick(e)) {
+ const targetLink = e.currentTarget.getAttribute('href');
+ window.open(targetLink, '_blank');
+ }
}
}
- tabShown(e) {
- const $target = $(e.target);
- const action = $target.data('action');
-
- if (action === 'commits') {
- this.loadCommits($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
- this.destroyPipelinesView();
- } else if (this.isDiffAction(action)) {
- this.loadDiff($target.attr('href'));
- if (bp.getBreakpointSize() !== 'lg') {
- this.shrinkView();
+ tabShown(action, href) {
+ if (action !== this.currentTab && this.mergeRequestTabs) {
+ this.currentTab = action;
+
+ if (this.mergeRequestTabPanesAll) {
+ this.mergeRequestTabPanesAll.forEach(el => {
+ const tabPane = el;
+ tabPane.style.display = 'none';
+ });
}
- if (this.diffViewType() === 'parallel') {
- this.expandViewContainer();
+
+ if (this.mergeRequestTabsAll) {
+ this.mergeRequestTabsAll.forEach(el => {
+ el.classList.remove('active');
+ });
}
- this.destroyPipelinesView();
- } else if (action === 'pipelines') {
- this.resetViewContainer();
- this.mountPipelinesView();
- } else {
- if (bp.getBreakpointSize() !== 'xs') {
+
+ const tabPane = this.mergeRequestTabPanes.querySelector(`#${action}`);
+ if (tabPane) tabPane.style.display = 'block';
+ const tab = this.mergeRequestTabs.querySelector(`.${action}-tab`);
+ if (tab) tab.classList.add('active');
+
+ if (action === 'commits') {
+ this.loadCommits(href);
+ this.expandView();
+ this.resetViewContainer();
+ this.destroyPipelinesView();
+ } else if (action === 'new') {
this.expandView();
+ this.resetViewContainer();
+ this.destroyPipelinesView();
+ } else if (this.isDiffAction(action)) {
+ if (!isInVueNoteablePage()) {
+ this.loadDiff(href);
+ }
+ if (bp.getBreakpointSize() !== 'lg') {
+ this.shrinkView();
+ }
+ if (this.diffViewType() === 'parallel') {
+ this.expandViewContainer();
+ }
+ this.destroyPipelinesView();
+ this.commitsTab.classList.remove('active');
+ } else if (action === 'pipelines') {
+ this.resetViewContainer();
+ this.mountPipelinesView();
+ } else {
+ this.mergeRequestTabPanes.querySelector('#notes').style.display = 'block';
+ this.mergeRequestTabs.querySelector('.notes-tab').classList.add('active');
+
+ if (bp.getBreakpointSize() !== 'xs') {
+ this.expandView();
+ }
+ this.resetViewContainer();
+ this.destroyPipelinesView();
+
+ initDiscussionTab();
+ }
+ if (this.setUrl) {
+ this.setCurrentAction(action);
}
- this.resetViewContainer();
- this.destroyPipelinesView();
- initDiscussionTab();
- }
- if (this.setUrl) {
- this.setCurrentAction(action);
+ this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction());
}
}
@@ -184,12 +231,6 @@ export default class MergeRequestTabs {
}
}
- // Activate a tab based on the current action
- activateTab(action) {
- // important note: the .tab('show') method triggers 'shown.bs.tab' event itself
- $(`.merge-request-tabs a[data-action='${action}']`).tab('show');
- }
-
// Replaces the current Merge Request-specific action in the URL with a new one
//
// If the action is "notes", the URL is reset to the standard
@@ -270,7 +311,7 @@ export default class MergeRequestTabs {
mountPipelinesView() {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- const CommitPipelinesTable = gl.CommitPipelinesTable;
+ const { CommitPipelinesTable } = gl;
this.commitPipelinesTable = new CommitPipelinesTable({
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
@@ -417,7 +458,6 @@ export default class MergeRequestTabs {
initAffix() {
const $tabs = $('.js-tabs-affix');
- const $fixedNav = $('.navbar-gitlab');
// Screen space on small screens is usually very sparse
// So we dont affix the tabs on these
@@ -430,21 +470,6 @@ export default class MergeRequestTabs {
*/
if ($tabs.css('position') !== 'static') return;
- const $diffTabs = $('#diff-notes-app');
-
- $tabs
- .off('affix.bs.affix affix-top.bs.affix')
- .affix({
- offset: {
- top: () => $diffTabs.offset().top - $tabs.height() - $fixedNav.height(),
- },
- })
- .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
- .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' }));
-
- // Fix bug when reloading the page already scrolling
- if ($tabs.hasClass('affix')) {
- $tabs.trigger('affix.bs.affix');
- }
+ polyfillSticky($tabs);
}
}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 334279137d8..640a4c8260f 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,10 +1,11 @@
-/* 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 */
import $ from 'jquery';
import _ from 'underscore';
import { __ } from '~/locale';
+import '~/gl_dropdown';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
@@ -251,3 +252,5 @@ export default class MilestoneSelect {
});
}
}
+
+window.MilestoneSelect = MilestoneSelect;
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index e1c8b6a6d4a..17a6d5bcd2a 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,5 +1,7 @@
<script>
import _ from 'underscore';
+import { s__ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
@@ -13,6 +15,7 @@ export default {
Graph,
GraphGroup,
EmptyState,
+ Icon,
},
props: {
hasMetrics: {
@@ -80,6 +83,14 @@ export default {
type: String,
required: true,
},
+ environmentsEndpoint: {
+ type: String,
+ required: true,
+ },
+ currentEnvironmentName: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -96,6 +107,7 @@ export default {
this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
+ environmentsEndpoint: this.environmentsEndpoint,
});
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged);
@@ -122,7 +134,11 @@ export default {
this.service
.getDeploymentData()
.then(data => this.store.storeDeploymentData(data))
- .catch(() => new Flash('Error getting deployment information.')),
+ .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))),
+ this.service
+ .getEnvironmentsData()
+ .then((data) => this.store.storeEnvironmentsData(data))
+ .catch(() => Flash(s__('Metrics|There was an error getting environments information.'))),
])
.then(() => {
if (this.store.groups.length < 1) {
@@ -155,8 +171,41 @@ export default {
<template>
<div
v-if="!showEmptyState"
- class="prometheus-graphs"
+ class="prometheus-graphs prepend-top-10"
>
+ <div class="environments d-flex align-items-center">
+ {{ s__('Metrics|Environment') }}
+ <div class="dropdown prepend-left-10">
+ <button
+ class="dropdown-menu-toggle"
+ data-toggle="dropdown"
+ type="button"
+ >
+ <span>
+ {{ currentEnvironmentName }}
+ </span>
+ <icon
+ name="chevron-down"
+ />
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
+ <ul>
+ <li
+ v-for="environment in store.environmentsData"
+ :key="environment.latest.id"
+ >
+ <a
+ :href="environment.latest.metrics_path"
+ :class="{ 'is-active': environment.latest.name == currentEnvironmentName }"
+ class="dropdown-item"
+ >
+ {{ environment.latest.name }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
<graph-group
v-for="(groupData, index) in store.groups"
:key="index"
diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js
index 6fcca36d2fa..260d424378e 100644
--- a/app/assets/javascripts/monitoring/services/monitoring_service.js
+++ b/app/assets/javascripts/monitoring/services/monitoring_service.js
@@ -1,6 +1,7 @@
import axios from '../../lib/utils/axios_utils';
import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils';
+import { s__ } from '../../locale';
const MAX_REQUESTS = 3;
@@ -23,9 +24,10 @@ function backOffRequest(makeRequestCallback) {
}
export default class MonitoringService {
- constructor({ metricsEndpoint, deploymentEndpoint }) {
+ constructor({ metricsEndpoint, deploymentEndpoint, environmentsEndpoint }) {
this.metricsEndpoint = metricsEndpoint;
this.deploymentEndpoint = deploymentEndpoint;
+ this.environmentsEndpoint = environmentsEndpoint;
}
getGraphsData() {
@@ -33,7 +35,7 @@ export default class MonitoringService {
.then(resp => resp.data)
.then((response) => {
if (!response || !response.data) {
- throw new Error('Unexpected metrics data response from prometheus endpoint');
+ throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
}
return response.data;
});
@@ -47,9 +49,20 @@ export default class MonitoringService {
.then(resp => resp.data)
.then((response) => {
if (!response || !response.deployments) {
- throw new Error('Unexpected deployment data response from prometheus endpoint');
+ throw new Error(s__('Metrics|Unexpected deployment data response from prometheus endpoint'));
}
return response.deployments;
});
}
+
+ getEnvironmentsData() {
+ return axios.get(this.environmentsEndpoint)
+ .then(resp => resp.data)
+ .then((response) => {
+ if (!response || !response.environments) {
+ throw new Error(s__('Metrics|There was an error fetching the environments data, please try again'));
+ }
+ return response.environments;
+ });
+ }
}
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 535c415cd6d..748b8cb6e6e 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -24,6 +24,7 @@ export default class MonitoringStore {
constructor() {
this.groups = [];
this.deploymentData = [];
+ this.environmentsData = [];
}
storeMetrics(groups = []) {
@@ -37,6 +38,10 @@ export default class MonitoringStore {
this.deploymentData = deploymentData;
}
+ storeEnvironmentsData(environmentsData = []) {
+ this.environmentsData = environmentsData;
+ }
+
getMetricsCount() {
return this.groups.reduce((count, group) => count + group.metrics.length, 0);
}
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index ed3a27dd68b..cee39fd0559 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -41,10 +41,10 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
} else {
const unusedColors = _.difference(defaultColorOrder, usedColors);
if (unusedColors.length > 0) {
- pick = unusedColors[0];
+ [pick] = unusedColors;
} else {
usedColors = [];
- pick = defaultColorOrder[0];
+ [pick] = defaultColorOrder;
}
}
usedColors.push(pick);
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index e3c5bf06b3d..8aabb840847 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();
+ },
+ },
+ created() {
+ this.setActiveTab(window.mrTabs.getCurrentAction());
+ },
+ mounted() {
+ this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge');
+ $(document).on('visibilitychange', this.updateDiscussionTabCounter);
+ window.mrTabs.eventHub.$on('MergeRequestTabChange', this.setActiveTab);
+ },
+ beforeDestroy() {
+ $(document).off('visibilitychange', this.updateDiscussionTabCounter);
+ window.mrTabs.eventHub.$off('MergeRequestTabChange', this.setActiveTab);
+ },
+ 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 e4096ddb00d..94da1be4066 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';
@@ -101,8 +101,8 @@ export default (function() {
};
BranchGraph.prototype.buildGraph = function() {
- var cuday, cumonth, day, j, len, mm, r, ref;
- r = this.r;
+ var cuday, cumonth, day, j, len, mm, ref;
+ const { r } = this;
cuday = 0;
cumonth = "";
r.rect(0, 0, 40, this.barHeight).attr({
@@ -113,8 +113,7 @@ export default (function() {
});
ref = this.days;
- // eslint-disable-next-line no-multi-assign
- 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
@@ -122,7 +121,7 @@ export default (function() {
font: "12px Monaco, monospace",
fill: "#BBB"
});
- cuday = day[0];
+ [cuday] = day;
}
if (cumonth !== day[1]) {
// Months
@@ -130,6 +129,8 @@ export default (function() {
font: "12px Monaco, monospace",
fill: "#EEE"
});
+
+ // eslint-disable-next-line prefer-destructuring
cumonth = day[1];
}
}
@@ -170,8 +171,8 @@ export default (function() {
};
BranchGraph.prototype.bindEvents = function() {
- var element;
- element = this.element;
+ const { element } = this;
+
return $(element).scroll((function(_this) {
return function(event) {
return _this.renderPartialGraph();
@@ -208,11 +209,13 @@ export default (function() {
};
BranchGraph.prototype.appendLabel = function(x, y, commit) {
- var label, r, rect, shortrefs, text, textbox, triangle;
+ var label, rect, shortrefs, text, textbox, triangle;
+
if (!commit.refs) {
return;
}
- r = this.r;
+
+ const { r } = this;
shortrefs = commit.refs;
// Truncate if longer than 15 chars
if (shortrefs.length > 17) {
@@ -243,11 +246,8 @@ export default (function() {
};
BranchGraph.prototype.appendAnchor = function(x, y, commit) {
- var anchor, options, r, top;
- r = this.r;
- top = this.top;
- options = this.options;
- anchor = r.circle(x, y, 10).attr({
+ const { r, top, options } = this;
+ const anchor = r.circle(x, y, 10).attr({
fill: "#000",
opacity: 0,
cursor: "pointer"
@@ -263,14 +263,15 @@ export default (function() {
};
BranchGraph.prototype.drawDot = function(x, y, commit) {
- var avatar_box_x, avatar_box_y, r;
- r = this.r;
+ const { r } = this;
r.circle(x, y, 3).attr({
fill: this.colors[commit.space],
stroke: "none"
});
- avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10;
- avatar_box_y = y - 10;
+
+ const avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10;
+ const avatar_box_y = y - 10;
+
r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({
stroke: this.colors[commit.space],
"stroke-width": 2
@@ -283,13 +284,12 @@ export default (function() {
};
BranchGraph.prototype.drawLines = function(x, y, commit) {
- var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route;
- r = this.r;
- ref = commit.parents;
- results = [];
+ var arrow, color, i, len, offset, parent, parentCommit, parentX1, parentX2, parentY, route;
+ const { r } = this;
+ const ref = commit.parents;
+ const results = [];
- // eslint-disable-next-line no-multi-assign
- 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;
@@ -333,11 +333,10 @@ export default (function() {
};
BranchGraph.prototype.markCommit = function(commit) {
- var r, x, y;
if (commit.id === this.options.commit_id) {
- r = this.r;
- x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
- y = this.offsetY + this.unitTime * commit.time;
+ const { r } = this;
+ const x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
+ const y = this.offsetY + this.unitTime * commit.time;
r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({
fill: "#000",
"fill-opacity": .5,
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 40c08ee0ace..205d9766656 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';
@@ -52,7 +52,7 @@ export default class NewBranchForm {
validate() {
var errorMessage, errors, formatter, unique, validator;
- const indexOf = [].indexOf;
+ const { indexOf } = [];
this.branchNameError.empty();
unique = function(values, value) {
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 27c5dedcf0b..48cda28a1ae 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 */
@@ -22,6 +20,7 @@ import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_c
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
+import { defaultAutocompleteConfig } from './gfm_auto_complete';
import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
@@ -32,7 +31,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 +46,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) {
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);
}
}
@@ -69,7 +56,7 @@ export default class Notes {
return this.instance;
}
- constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
+ constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = defaultAutocompleteConfig) {
this.updateTargetButtons = this.updateTargetButtons.bind(this);
this.updateComment = this.updateComment.bind(this);
this.visibilityChange = this.visibilityChange.bind(this);
@@ -104,13 +91,11 @@ 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();
- this.setupMainTargetNoteForm();
+ this.setupMainTargetNoteForm(enableGFM);
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
@@ -146,55 +131,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 +162,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 +173,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 +196,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 +239,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) {
@@ -373,7 +311,7 @@ export default class Notes {
},
})
.then(({ data }) => {
- const notes = data.notes;
+ const { notes } = data;
this.last_fetched_at = data.last_fetched_at;
this.setPollingInterval(data.notes.length);
$.each(notes, (i, note) => this.renderNote(note));
@@ -398,8 +336,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 +357,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 +407,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 +418,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 +446,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 +459,6 @@ export default class Notes {
this.setupNewNote($updatedNote);
}
}
-
- Notes.refreshVueNotes();
}
isParallelView() {
@@ -552,13 +476,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 +492,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 +500,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 +518,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);
@@ -703,14 +599,14 @@ export default class Notes {
*
* Sets some hidden fields in the form.
*/
- setupMainTargetNoteForm() {
+ setupMainTargetNoteForm(enableGFM) {
var form;
// find the form
form = $('.js-new-note-form');
// Set a global clone of the form for later cloning
this.formClone = form.clone();
// show the form
- this.setupNoteForm(form);
+ this.setupNoteForm(form, enableGFM);
// fix classes
form.removeClass('js-new-note-form');
form.addClass('js-main-target-form');
@@ -738,9 +634,9 @@ export default class Notes {
* setup GFM auto complete
* show the form
*/
- setupNoteForm(form) {
+ setupNoteForm(form, enableGFM = defaultAutocompleteConfig) {
var textarea, key;
- this.glForm = new GLForm(form, this.enableGFM);
+ this.glForm = new GLForm(form, enableGFM);
textarea = form.find('.js-note-text');
key = [
'Note',
@@ -784,6 +680,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 +836,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 +884,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 +906,6 @@ export default class Notes {
})(this),
);
- Notes.refreshVueNotes();
Notes.checkMergeRequestStatus();
return this.updateNotesCount(-1);
}
@@ -1033,7 +921,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 +995,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 +1005,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 +1038,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 +1107,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 +1272,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 +1282,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 +1359,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 +1392,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 +1463,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 +1621,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 +1688,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 +1709,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 +1754,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 +1789,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 +1832,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..9c2908c477e 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,60 +1,94 @@
<script>
-import $ from 'jquery';
-import syntaxHighlight from '~/syntax_highlight';
-import imageDiffHelper from '~/image_diff/helpers/index';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import DiffFileHeader from './diff_file_header.vue';
+ import { mapState, mapActions } from 'vuex';
+ import imageDiffHelper from '~/image_diff/helpers/index';
+ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+ 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,
- },
- props: {
- discussion: {
- type: Object,
- required: true,
+ export default {
+ components: {
+ DiffFileHeader,
+ SkeletonLoadingContainer,
},
- },
- computed: {
- isImageDiff() {
- return !this.diffFile.text;
+ props: {
+ discussion: {
+ type: Object,
+ required: true,
+ },
},
- diffFileClass() {
- const { text } = this.diffFile;
- return text ? 'text-file' : 'js-image-file';
+ data() {
+ return {
+ error: false,
+ };
},
- diffRows() {
- return $(this.discussion.truncatedDiffLines);
- },
- diffFile() {
- return convertObjectPropsToCamelCase(this.discussion.diffFile);
+ 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;
+ },
+ diffFileClass() {
+ const { text } = this.diffFile;
+ return text ? 'text-file' : 'js-image-file';
+ },
+ 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() {
+ if (this.discussion.truncatedDiffLines) {
+ return this.discussion.truncatedDiffLines.map(line =>
+ trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line)),
+ );
+ }
+
+ return [];
+ },
},
- imageDiffHtml() {
- return this.discussion.imageDiffHtml;
+ mounted() {
+ if (this.isImageDiff) {
+ const canCreateNote = false;
+ const renderCommentBadge = true;
+ imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
+ } else if (!this.hasTruncatedDiffLines) {
+ this.fetchDiff();
+ }
},
- },
- 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);
- });
- }
- },
- methods: {
- rowTag(html) {
- return html.outerHTML ? 'tr' : 'template';
+ 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>
<template>
@@ -63,23 +97,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_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 521b4d16286..225d9f18612 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -200,6 +200,7 @@ export default {
:class="getAwardClassBindings(awardList, awardName)"
:title="awardTitle(awardList)"
class="btn award-control"
+ data-boundary="viewport"
data-placement="bottom"
type="button"
@click="handleAward(awardName)">
@@ -217,6 +218,7 @@ export default {
class="award-control btn js-add-award"
title="Add reaction"
aria-label="Add reaction"
+ data-boundary="viewport"
data-placement="bottom"
type="button">
<span
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..a4e3faa5d75 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,22 +179,22 @@ 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
+ {{ __('Discard draft') }}
</button>
</div>
</form>
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 3f865431155..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 (!window.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 9225a6b1a7c..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,13 +71,14 @@ 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;
},
@@ -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 (!window.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
@@ -174,11 +179,13 @@ export default {
<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"
@@ -195,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..a8995021699 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,10 +1,9 @@
<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 eventHub from '../event_hub';
import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue';
@@ -39,19 +38,23 @@ export default {
required: false,
default: () => ({}),
},
+ shouldShow: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
- store,
data() {
return {
isLoading: true,
};
},
computed: {
- ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
+ ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount']),
noteableType() {
return this.noteableData.noteableType;
},
- allNotes() {
+ allDiscussions() {
if (this.isLoading) {
const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
@@ -59,36 +62,40 @@ export default {
isSkeletonNote: true,
});
}
- return this.notes;
+
+ return this.discussions;
+ },
+ },
+ watch: {
+ shouldShow() {
+ if (!this.isNotesFetched) {
+ this.fetchNotes();
+ }
},
},
created() {
this.setNotesData(this.notesData);
this.setNoteableData(this.noteableData);
this.setUserData(this.userData);
+ this.setTargetNoteHash(getLocationHash());
+ eventHub.$once('fetchNotesData', this.fetchNotes);
},
mounted() {
- this.fetchNotes();
-
- const parentElement = this.$el.parentElement;
+ if (this.shouldShow) {
+ this.fetchNotes();
+ }
- if (
- parentElement &&
- parentElement.classList.contains('js-vue-notes-event')
- ) {
+ const { parentElement } = this.$el;
+ 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,38 +104,42 @@ export default {
setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash',
+ toggleDiscussion: 'toggleDiscussion',
+ setNotesFetchedState: 'setNotesFetchedState',
}),
- 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;
+ this.setNotesFetchedState(true);
})
.then(() => this.$nextTick())
.then(() => this.checkLocationHash())
.catch(() => {
this.isLoading = false;
- Flash(
- 'Something went wrong while fetching comments. Please try again.',
- );
+ this.setNotesFetchedState(true);
+ Flash('Something went wrong while fetching comments. Please try again.');
});
},
initPolling() {
@@ -143,11 +154,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 +174,19 @@ export default {
</script>
<template>
- <div id="notes">
+ <div
+ v-show="shouldShow"
+ id="notes"
+ >
<ul
id="notes-list"
- class="notes main-notes-list timeline">
-
+ 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..f5dce94caad 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,15 +22,13 @@ 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);
},
poll(data = {}) {
const endpoint = data.notesData.notesPath;
- const lastFetchedAt = data.lastFetchedAt;
+ const { lastFetchedAt } = data;
const options = {
headers: {
'X-Last-Fetched-At': lastFetchedAt ? `${lastFetchedAt}` : undefined,
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index b2222476924..671fa4d7d22 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,32 @@ 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 setNotesFetchedState = ({ commit }, state) =>
+ commit(types.SET_NOTES_FETCHED_STATE, state);
+
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 +134,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 +206,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 +214,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 +234,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 +301,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..5c65e1c3bb5 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,60 +1,93 @@
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 isNotesFetched = state => state.isNotesFetched;
+
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.filter(d => d.resolvable).forEach(n => {
if (n.notes) {
- const resolved = n.notes.every(note => note.resolved && !note.system);
+ const resolved = n.notes.filter(note => note.resolvable).every(note => note.resolved);
if (resolved) {
map[n.id] = n;
@@ -71,5 +104,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..b4cb9267e0f
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -0,0 +1,27 @@
+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,
+ isNotesFetched: 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..a25098fbc06 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,8 @@ 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';
+export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
// 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..e5e40ce07fa 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,19 @@ export default {
[types.TOGGLE_STATE_BUTTON_LOADING](state, value) {
Object.assign(state, { isToggleStateButtonLoading: value });
},
+
+ [types.SET_NOTES_FETCHED_STATE](state, value) {
+ Object.assign(state, { isNotesFetched: 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/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index cc2805a1901..d6aa4bb95d2 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -96,7 +96,7 @@
this.enteredUsername = '';
},
onSecondaryAction() {
- const form = this.$refs.form;
+ const { form } = this.$refs;
form.action = this.blockUserUrl;
this.$refs.method.value = 'put';
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index c334eaa90f8..9aa83ce6269 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';
@@ -39,6 +39,7 @@ export default class Todos {
}
initFilters() {
+ this.initFilterDropdown($('.js-group-search'), 'group_id', ['text']);
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-type-search'), 'type');
this.initFilterDropdown($('.js-action-search'), 'action_id');
@@ -53,7 +54,16 @@ export default class Todos {
filterable: searchFields ? true : false,
search: { fields: searchFields },
data: $dropdown.data('data'),
- clicked: () => $dropdown.closest('form.filter-form').submit(),
+ clicked: () => {
+ const $formEl = $dropdown.closest('form.filter-form');
+ const mutexDropdowns = {
+ group_id: 'project_id',
+ project_id: 'group_id',
+ };
+
+ $formEl.find(`input[name="${mutexDropdowns[fieldName]}"]`).remove();
+ $formEl.submit();
+ },
});
}
@@ -61,7 +71,7 @@ export default class Todos {
e.stopPropagation();
e.preventDefault();
- const target = e.target;
+ const { target } = e;
target.setAttribute('disabled', true);
target.classList.add('disabled');
diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js
deleted file mode 100644
index 0c2d7d7c96a..00000000000
--- a/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
-
-gcpSignupOffer();
diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js
deleted file mode 100644
index 0c2d7d7c96a..00000000000
--- a/app/assets/javascripts/pages/projects/clusters/new/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
-
-gcpSignupOffer();
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 37336a8cb69..6c1788dc160 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';
@@ -80,10 +80,11 @@ export default (function() {
};
ContributorsStatGraph.prototype.redraw_authors = function() {
- var author_commits, x_domain;
$("ol").html("");
- x_domain = ContributorsGraph.prototype.x_domain;
- author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
+
+ const { x_domain } = ContributorsGraph.prototype;
+ const author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
+
return _.each(author_commits, (function(_this) {
return function(d) {
_this.redraw_author_commit_info(d);
@@ -102,7 +103,7 @@ export default (function() {
};
ContributorsStatGraph.prototype.change_date_header = function() {
- const x_domain = ContributorsGraph.prototype.x_domain;
+ const { x_domain } = ContributorsGraph.prototype;
const formattedDateRange = sprintf(
s__('ContributorsPage|%{startDate} – %{endDate}'),
{
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 165446a4db6..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 {
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index de1e13de7e9..cc0e6553e83 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,7 +1,21 @@
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation';
document.addEventListener('DOMContentLoaded', () => {
+ const { page } = document.body.dataset;
+ const newClusterViews = [
+ 'projects:clusters:new',
+ 'projects:clusters:create_gcp',
+ 'projects:clusters:create_user',
+ ];
+
+ if (newClusterViews.indexOf(page) > -1) {
+ gcpSignupOffer();
+ initGkeDropdowns();
+ }
+
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
});
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/init_form.js b/app/assets/javascripts/pages/projects/init_form.js
index 0b6c5c1d30b..9f20a3e4e46 100644
--- a/app/assets/javascripts/pages/projects/init_form.js
+++ b/app/assets/javascripts/pages/projects/init_form.js
@@ -3,5 +3,5 @@ import GLForm from '~/gl_form';
export default function ($formEl) {
new ZenMode(); // eslint-disable-line no-new
- new GLForm($formEl, true); // eslint-disable-line no-new
+ new GLForm($formEl); // eslint-disable-line no-new
}
diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js
index 14fddbc9a05..b2b8e5d2300 100644
--- a/app/assets/javascripts/pages/projects/issues/form.js
+++ b/app/assets/javascripts/pages/projects/issues/form.js
@@ -10,7 +10,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
export default () => {
new ShortcutsNavigation();
- new GLForm($('.issue-form'), true);
+ new GLForm($('.issue-form'));
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
diff --git a/app/assets/javascripts/pages/projects/jobs/terminal/index.js b/app/assets/javascripts/pages/projects/jobs/terminal/index.js
new file mode 100644
index 00000000000..7129e24cee1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/jobs/terminal/index.js
@@ -0,0 +1,3 @@
+import initTerminal from '~/terminal/';
+
+document.addEventListener('DOMContentLoaded', initTerminal);
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index 406fc32f9a2..3a3c21f2202 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -12,7 +12,7 @@ import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
export default () => {
new Diff();
new ShortcutsNavigation();
- new GLForm($('.merge-request-form'), true);
+ new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
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/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 37ef77c8e43..1faa59fb45b 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -28,7 +28,7 @@ document.addEventListener('DOMContentLoaded', () => {
const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
autoDevOpsSettings.addEventListener('click', event => {
- const target = event.target;
+ const { target } = event;
if (target.classList.contains('js-toggle-extra-settings')) {
autoDevOpsExtraSettings.classList.toggle(
'hidden',
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/projects/tags/new/index.js b/app/assets/javascripts/pages/projects/tags/new/index.js
index 8d0edf7e06c..b3158f7e939 100644
--- a/app/assets/javascripts/pages/projects/tags/new/index.js
+++ b/app/assets/javascripts/pages/projects/tags/new/index.js
@@ -5,6 +5,6 @@ import GLForm from '../../../../gl_form';
document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
- new GLForm($('.tag-form'), true); // eslint-disable-line no-new
+ new GLForm($('.tag-form')); // eslint-disable-line no-new
new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index 0295653cb29..0a0fe3fc137 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
- new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
+ new GLForm($('.wiki-form')); // eslint-disable-line no-new
const deleteWikiButton = document.getElementById('delete-wiki-button');
diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js
index dcd0b9a76ce..d3e8dbf4000 100644
--- a/app/assets/javascripts/pages/projects/wikis/wikis.js
+++ b/app/assets/javascripts/pages/projects/wikis/wikis.js
@@ -48,7 +48,7 @@ export default class Wikis {
static sidebarCanCollapse() {
const bootstrapBreakpoint = bp.getBreakpointSize();
- return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+ return bootstrapBreakpoint === 'xs';
}
renderSidebar() {
diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js
index 2e1fe78b3fa..e3e0ab91993 100644
--- a/app/assets/javascripts/pages/search/show/search.js
+++ b/app/assets/javascripts/pages/search/show/search.js
@@ -105,7 +105,7 @@ export default class Search {
getProjectsData(term) {
return new Promise((resolve) => {
if (this.groupId) {
- Api.groupProjects(this.groupId, term, resolve);
+ Api.groupProjects(this.groupId, term, {}, resolve);
} else {
Api.projects(term, {
order_by: 'id',
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/snippets/form.js b/app/assets/javascripts/pages/snippets/form.js
index 72d05da1069..f369c7ef9a6 100644
--- a/app/assets/javascripts/pages/snippets/form.js
+++ b/app/assets/javascripts/pages/snippets/form.js
@@ -3,6 +3,14 @@ import GLForm from '~/gl_form';
import ZenMode from '~/zen_mode';
export default () => {
- new GLForm($('.snippet-form'), false); // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new GLForm($('.snippet-form'), {
+ members: false,
+ issues: false,
+ mergeRequests: false,
+ epics: false,
+ milestones: false,
+ labels: false,
+ });
new ZenMode(); // eslint-disable-line no-new
};
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 50d042fef29..9892a039941 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import _ from 'underscore';
import { scaleLinear, scaleThreshold } from 'd3-scale';
import { select } from 'd3-selection';
+import dateFormat from 'dateformat';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
@@ -26,7 +27,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
function formatTooltipText({ date, count }) {
const dateObject = new Date(date);
const dateDayName = getDayName(dateObject);
- const dateText = dateObject.format('mmm d, yyyy');
+ const dateText = dateFormat(dateObject, 'mmm d, yyyy');
let contribText = 'No contributions';
if (count > 0) {
@@ -84,7 +85,7 @@ export default class ActivityCalendar {
date.setDate(date.getDate() + i);
const day = date.getDay();
- const count = timestamps[date.format('yyyy-mm-dd')] || 0;
+ const count = timestamps[dateFormat(date, 'yyyy-mm-dd')] || 0;
// Create a new group array if this is the first day of the week
// or if is first object
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 8ffaa52d9e8..b76965f280b 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -113,7 +113,7 @@ export default {
>
<div
v-if="currentRequest"
- class="container-fluid container-limited"
+ class="d-flex container-fluid container-limited"
>
<div
id="peek-view-host"
@@ -179,6 +179,7 @@ export default {
v-if="currentRequest"
:current-request="currentRequest"
:requests="requests"
+ class="ml-auto"
@change-current-request="changeCurrentRequest"
/>
</div>
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index dd9578a6c7f..ad74f7b38f9 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -35,10 +35,7 @@ export default {
};
</script>
<template>
- <div
- id="peek-request-selector"
- class="float-right"
- >
+ <div id="peek-request-selector">
<select v-model="currentRequestId">
<option
v-for="request in requests"
diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue
index f3219b8291c..34360105176 100644
--- a/app/assets/javascripts/pipelines/components/blank_state.vue
+++ b/app/assets/javascripts/pipelines/components/blank_state.vue
@@ -1,18 +1,18 @@
<script>
- export default {
- name: 'PipelinesSvgState',
- props: {
- svgPath: {
- type: String,
- required: true,
- },
+export default {
+ name: 'PipelinesSvgState',
+ props: {
+ svgPath: {
+ type: String,
+ required: true,
+ },
- message: {
- type: String,
- required: true,
- },
+ message: {
+ type: String,
+ required: true,
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index 50c27bed9fd..c5a45afc634 100644
--- a/app/assets/javascripts/pipelines/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -1,21 +1,21 @@
<script>
- export default {
- name: 'PipelinesEmptyState',
- props: {
- helpPagePath: {
- type: String,
- required: true,
- },
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- canSetCi: {
- type: Boolean,
- required: true,
- },
+export default {
+ name: 'PipelinesEmptyState',
+ props: {
+ helpPagePath: {
+ type: String,
+ required: true,
},
- };
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ canSetCi: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
</script>
<template>
<div class="row empty-state js-empty-state">
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 1f152ed438d..b82e28a0735 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -41,7 +41,6 @@ export default {
type: String,
required: true,
},
-
},
data() {
return {
@@ -67,7 +66,8 @@ export default {
this.isDisabled = true;
- axios.post(`${this.link}.json`)
+ axios
+ .post(`${this.link}.json`)
.then(() => {
this.isDisabled = false;
this.$emit('pipelineActionRequestComplete');
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 e047d10ac93..c32dc83da8e 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -109,6 +109,7 @@ export default {
:key="i"
>
<job-component
+ :dropdown-length="job.size"
:job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 886e62ab1a7..8af984ef91a 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -46,6 +46,11 @@ export default {
required: false,
default: '',
},
+ dropdownLength: {
+ type: Number,
+ required: false,
+ default: Infinity,
+ },
},
computed: {
status() {
@@ -70,6 +75,10 @@ export default {
return textBuilder.join(' ');
},
+ tooltipBoundary() {
+ return this.dropdownLength < 5 ? 'viewport' : null;
+ },
+
/**
* Verifies if the provided job has an action path
*
@@ -94,9 +103,9 @@ export default {
:href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
+ :data-boundary="tooltipBoundary"
data-container="body"
data-html="true"
- data-boundary="viewport"
class="js-pipeline-graph-job-link"
>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
index 14f4964a406..6fdbcc1e049 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
@@ -1,28 +1,28 @@
<script>
- import ciIcon from '../../../vue_shared/components/ci_icon.vue';
+import ciIcon from '../../../vue_shared/components/ci_icon.vue';
- /**
- * Component that renders both the CI icon status and the job name.
- * Used in
- * - Badge component
- * - Dropdown badge components
- */
- export default {
- components: {
- ciIcon,
+/**
+ * Component that renders both the CI icon status and the job name.
+ * Used in
+ * - Badge component
+ * - Dropdown badge components
+ */
+export default {
+ components: {
+ ciIcon,
+ },
+ props: {
+ name: {
+ type: String,
+ required: true,
},
- props: {
- name: {
- type: String,
- required: true,
- },
- status: {
- type: Object,
- required: true,
- },
+ status: {
+ type: Object,
+ required: true,
},
- };
+ },
+};
</script>
<template>
<span class="ci-job-name-component">
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index 5b212ee8931..001eaeaa065 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -1,81 +1,81 @@
<script>
- import ciHeader from '../../vue_shared/components/header_ci_component.vue';
- import eventHub from '../event_hub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import ciHeader from '../../vue_shared/components/header_ci_component.vue';
+import eventHub from '../event_hub';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- export default {
- name: 'PipelineHeaderSection',
- components: {
- ciHeader,
- loadingIcon,
+export default {
+ name: 'PipelineHeaderSection',
+ components: {
+ ciHeader,
+ loadingIcon,
+ },
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
},
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
- },
- data() {
- return {
- actions: this.getActions(),
- };
+ isLoading: {
+ type: Boolean,
+ required: true,
},
+ },
+ data() {
+ return {
+ actions: this.getActions(),
+ };
+ },
- computed: {
- status() {
- return this.pipeline.details && this.pipeline.details.status;
- },
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.pipeline).length;
- },
+ computed: {
+ status() {
+ return this.pipeline.details && this.pipeline.details.status;
+ },
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.pipeline).length;
},
+ },
- watch: {
- pipeline() {
- this.actions = this.getActions();
- },
+ watch: {
+ pipeline() {
+ this.actions = this.getActions();
},
+ },
- methods: {
- postAction(action) {
- const index = this.actions.indexOf(action);
+ methods: {
+ postAction(action) {
+ const index = this.actions.indexOf(action);
- this.$set(this.actions[index], 'isLoading', true);
+ this.$set(this.actions[index], 'isLoading', true);
- eventHub.$emit('headerPostAction', action);
- },
+ eventHub.$emit('headerPostAction', action);
+ },
- getActions() {
- const actions = [];
+ getActions() {
+ const actions = [];
- if (this.pipeline.retry_path) {
- actions.push({
- label: 'Retry',
- path: this.pipeline.retry_path,
- cssClass: 'js-retry-button btn btn-inverted-secondary',
- type: 'button',
- isLoading: false,
- });
- }
+ if (this.pipeline.retry_path) {
+ actions.push({
+ label: 'Retry',
+ path: this.pipeline.retry_path,
+ cssClass: 'js-retry-button btn btn-inverted-secondary',
+ type: 'button',
+ isLoading: false,
+ });
+ }
- if (this.pipeline.cancel_path) {
- actions.push({
- label: 'Cancel running',
- path: this.pipeline.cancel_path,
- cssClass: 'js-btn-cancel-pipeline btn btn-danger',
- type: 'button',
- isLoading: false,
- });
- }
+ if (this.pipeline.cancel_path) {
+ actions.push({
+ label: 'Cancel running',
+ path: this.pipeline.cancel_path,
+ cssClass: 'js-btn-cancel-pipeline btn btn-danger',
+ type: 'button',
+ isLoading: false,
+ });
+ }
- return actions;
- },
+ return actions;
},
- };
+ },
+};
</script>
<template>
<div class="pipeline-header-container">
diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue
index 1fce9f16ee0..9501afb7493 100644
--- a/app/assets/javascripts/pipelines/components/nav_controls.vue
+++ b/app/assets/javascripts/pipelines/components/nav_controls.vue
@@ -1,42 +1,42 @@
<script>
- import LoadingButton from '../../vue_shared/components/loading_button.vue';
+import LoadingButton from '../../vue_shared/components/loading_button.vue';
- export default {
- name: 'PipelineNavControls',
- components: {
- LoadingButton,
+export default {
+ name: 'PipelineNavControls',
+ components: {
+ LoadingButton,
+ },
+ props: {
+ newPipelinePath: {
+ type: String,
+ required: false,
+ default: null,
},
- props: {
- newPipelinePath: {
- type: String,
- required: false,
- default: null,
- },
- resetCachePath: {
- type: String,
- required: false,
- default: null,
- },
+ resetCachePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
- ciLintPath: {
- type: String,
- required: false,
- default: null,
- },
+ ciLintPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
- isResetCacheButtonLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
+ isResetCacheButtonLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- methods: {
- onClickResetCache() {
- this.$emit('resetRunnersCache', this.resetCachePath);
- },
+ },
+ methods: {
+ onClickResetCache() {
+ this.$emit('resetRunnersCache', this.resetCachePath);
},
- };
+ },
+};
</script>
<template>
<div class="nav-controls">
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index a107e579457..75db1e9ae7c 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -1,49 +1,49 @@
<script>
- import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
- import popover from '../../vue_shared/directives/popover';
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import popover from '../../vue_shared/directives/popover';
- export default {
- components: {
- userAvatarLink,
+export default {
+ components: {
+ userAvatarLink,
+ },
+ directives: {
+ tooltip,
+ popover,
+ },
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
- popover,
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
},
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- autoDevopsHelpPath: {
- type: String,
- required: true,
- },
+ },
+ computed: {
+ user() {
+ return this.pipeline.user;
},
- computed: {
- user() {
- return this.pipeline.user;
- },
- popoverOptions() {
- return {
- html: true,
- trigger: 'focus',
- placement: 'top',
- title: `<div class="autodevops-title">
+ popoverOptions() {
+ return {
+ html: true,
+ trigger: 'focus',
+ placement: 'top',
+ title: `<div class="autodevops-title">
This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>
</div>`,
- content: `<a
+ content: `<a
class="autodevops-link"
href="${this.autoDevopsHelpPath}"
target="_blank"
rel="noopener noreferrer nofollow">
Learn more about Auto DevOps
</a>`,
- };
- },
+ };
},
- };
+ },
+};
</script>
<template>
<div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags">
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index b31b4bad7a0..c9d2dc3a3c5 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -1,283 +1,283 @@
<script>
- import _ from 'underscore';
- import { __, sprintf, s__ } from '../../locale';
- import createFlash from '../../flash';
- import PipelinesService from '../services/pipelines_service';
- import pipelinesMixin from '../mixins/pipelines';
- import TablePagination from '../../vue_shared/components/table_pagination.vue';
- import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
- import NavigationControls from './nav_controls.vue';
- import { getParameterByName } from '../../lib/utils/common_utils';
- import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+import _ from 'underscore';
+import { __, sprintf, s__ } from '../../locale';
+import createFlash from '../../flash';
+import PipelinesService from '../services/pipelines_service';
+import pipelinesMixin from '../mixins/pipelines';
+import TablePagination from '../../vue_shared/components/table_pagination.vue';
+import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
+import NavigationControls from './nav_controls.vue';
+import { getParameterByName } from '../../lib/utils/common_utils';
+import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
- export default {
- components: {
- TablePagination,
- NavigationTabs,
- NavigationControls,
+export default {
+ components: {
+ TablePagination,
+ NavigationTabs,
+ NavigationControls,
+ },
+ mixins: [pipelinesMixin, CIPaginationMixin],
+ props: {
+ store: {
+ type: Object,
+ required: true,
},
- mixins: [pipelinesMixin, CIPaginationMixin],
- props: {
- store: {
- type: Object,
- required: true,
- },
- // Can be rendered in 3 different places, with some visual differences
- // Accepts root | child
- // `root` -> main view
- // `child` -> rendered inside MR or Commit View
- viewType: {
- type: String,
- required: false,
- default: 'root',
- },
- endpoint: {
- type: String,
- required: true,
- },
- helpPagePath: {
- type: String,
- required: true,
- },
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- errorStateSvgPath: {
- type: String,
- required: true,
- },
- noPipelinesSvgPath: {
- type: String,
- required: true,
- },
- autoDevopsPath: {
- type: String,
- required: true,
- },
- hasGitlabCi: {
- type: Boolean,
- required: true,
- },
- canCreatePipeline: {
- type: Boolean,
- required: true,
- },
- ciLintPath: {
- type: String,
- required: false,
- default: null,
- },
- resetCachePath: {
- type: String,
- required: false,
- default: null,
- },
- newPipelinePath: {
- type: String,
- required: false,
- default: null,
- },
+ // Can be rendered in 3 different places, with some visual differences
+ // Accepts root | child
+ // `root` -> main view
+ // `child` -> rendered inside MR or Commit View
+ viewType: {
+ type: String,
+ required: false,
+ default: 'root',
},
- data() {
- return {
- // Start with loading state to avoid a glitch when the empty state will be rendered
- isLoading: true,
- state: this.store.state,
- scope: getParameterByName('scope') || 'all',
- page: getParameterByName('page') || '1',
- requestData: {},
- isResetCacheButtonLoading: false,
- };
+ endpoint: {
+ type: String,
+ required: true,
},
- stateMap: {
- // with tabs
- loading: 'loading',
- tableList: 'tableList',
- error: 'error',
- emptyTab: 'emptyTab',
-
- // without tabs
- emptyState: 'emptyState',
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ errorStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ noPipelinesSvgPath: {
+ type: String,
+ required: true,
+ },
+ autoDevopsPath: {
+ type: String,
+ required: true,
+ },
+ hasGitlabCi: {
+ type: Boolean,
+ required: true,
+ },
+ canCreatePipeline: {
+ type: Boolean,
+ required: true,
+ },
+ ciLintPath: {
+ type: String,
+ required: false,
+ default: null,
},
- scopes: {
- all: 'all',
- pending: 'pending',
- running: 'running',
- finished: 'finished',
- branches: 'branches',
- tags: 'tags',
+ resetCachePath: {
+ type: String,
+ required: false,
+ default: null,
},
- computed: {
- /**
- * `hasGitlabCi` handles both internal and external CI.
- * The order on which the checks are made in this method is
- * important to guarantee we handle all the corner cases.
- */
- stateToRender() {
- const { stateMap } = this.$options;
+ newPipelinePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ // Start with loading state to avoid a glitch when the empty state will be rendered
+ isLoading: true,
+ state: this.store.state,
+ scope: getParameterByName('scope') || 'all',
+ page: getParameterByName('page') || '1',
+ requestData: {},
+ isResetCacheButtonLoading: false,
+ };
+ },
+ stateMap: {
+ // with tabs
+ loading: 'loading',
+ tableList: 'tableList',
+ error: 'error',
+ emptyTab: 'emptyTab',
+
+ // without tabs
+ emptyState: 'emptyState',
+ },
+ scopes: {
+ all: 'all',
+ pending: 'pending',
+ running: 'running',
+ finished: 'finished',
+ branches: 'branches',
+ tags: 'tags',
+ },
+ computed: {
+ /**
+ * `hasGitlabCi` handles both internal and external CI.
+ * The order on which the checks are made in this method is
+ * important to guarantee we handle all the corner cases.
+ */
+ stateToRender() {
+ const { stateMap } = this.$options;
- if (this.isLoading) {
- return stateMap.loading;
- }
+ if (this.isLoading) {
+ return stateMap.loading;
+ }
- if (this.hasError) {
- return stateMap.error;
- }
+ if (this.hasError) {
+ return stateMap.error;
+ }
- if (this.state.pipelines.length) {
- return stateMap.tableList;
- }
+ if (this.state.pipelines.length) {
+ return stateMap.tableList;
+ }
- if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) {
- return stateMap.emptyTab;
- }
+ if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) {
+ return stateMap.emptyTab;
+ }
- return stateMap.emptyState;
- },
- /**
- * Tabs are rendered in all states except empty state.
- * They are not rendered before the first request to avoid a flicker on first load.
- */
- shouldRenderTabs() {
- const { stateMap } = this.$options;
- return (
- this.hasMadeRequest &&
- [stateMap.loading, stateMap.tableList, stateMap.error, stateMap.emptyTab].includes(
- this.stateToRender,
- )
- );
- },
+ return stateMap.emptyState;
+ },
+ /**
+ * Tabs are rendered in all states except empty state.
+ * They are not rendered before the first request to avoid a flicker on first load.
+ */
+ shouldRenderTabs() {
+ const { stateMap } = this.$options;
+ return (
+ this.hasMadeRequest &&
+ [stateMap.loading, stateMap.tableList, stateMap.error, stateMap.emptyTab].includes(
+ this.stateToRender,
+ )
+ );
+ },
- shouldRenderButtons() {
- return (
- (this.newPipelinePath || this.resetCachePath || this.ciLintPath) && this.shouldRenderTabs
- );
- },
+ shouldRenderButtons() {
+ return (
+ (this.newPipelinePath || this.resetCachePath || this.ciLintPath) && this.shouldRenderTabs
+ );
+ },
- shouldRenderPagination() {
- return (
- !this.isLoading &&
- this.state.pipelines.length &&
- this.state.pageInfo.total > this.state.pageInfo.perPage
- );
- },
+ shouldRenderPagination() {
+ return (
+ !this.isLoading &&
+ this.state.pipelines.length &&
+ this.state.pageInfo.total > this.state.pageInfo.perPage
+ );
+ },
- emptyTabMessage() {
- const { scopes } = this.$options;
- const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
+ emptyTabMessage() {
+ const { scopes } = this.$options;
+ const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
- if (possibleScopes.includes(this.scope)) {
- return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), {
- scope: this.scope,
- });
- }
+ if (possibleScopes.includes(this.scope)) {
+ return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), {
+ scope: this.scope,
+ });
+ }
- return s__('Pipelines|There are currently no pipelines.');
- },
+ return s__('Pipelines|There are currently no pipelines.');
+ },
- tabs() {
- const { count } = this.state;
- const { scopes } = this.$options;
+ tabs() {
+ const { count } = this.state;
+ const { scopes } = this.$options;
- return [
- {
- name: __('All'),
- scope: scopes.all,
- count: count.all,
- isActive: this.scope === 'all',
- },
- {
- name: __('Pending'),
- scope: scopes.pending,
- count: count.pending,
- isActive: this.scope === 'pending',
- },
- {
- name: __('Running'),
- scope: scopes.running,
- count: count.running,
- isActive: this.scope === 'running',
- },
- {
- name: __('Finished'),
- scope: scopes.finished,
- count: count.finished,
- isActive: this.scope === 'finished',
- },
- {
- name: __('Branches'),
- scope: scopes.branches,
- isActive: this.scope === 'branches',
- },
- {
- name: __('Tags'),
- scope: scopes.tags,
- isActive: this.scope === 'tags',
- },
- ];
- },
+ return [
+ {
+ name: __('All'),
+ scope: scopes.all,
+ count: count.all,
+ isActive: this.scope === 'all',
+ },
+ {
+ name: __('Pending'),
+ scope: scopes.pending,
+ count: count.pending,
+ isActive: this.scope === 'pending',
+ },
+ {
+ name: __('Running'),
+ scope: scopes.running,
+ count: count.running,
+ isActive: this.scope === 'running',
+ },
+ {
+ name: __('Finished'),
+ scope: scopes.finished,
+ count: count.finished,
+ isActive: this.scope === 'finished',
+ },
+ {
+ name: __('Branches'),
+ scope: scopes.branches,
+ isActive: this.scope === 'branches',
+ },
+ {
+ name: __('Tags'),
+ scope: scopes.tags,
+ isActive: this.scope === 'tags',
+ },
+ ];
},
- created() {
- this.service = new PipelinesService(this.endpoint);
- this.requestData = { page: this.page, scope: this.scope };
+ },
+ created() {
+ this.service = new PipelinesService(this.endpoint);
+ this.requestData = { page: this.page, scope: this.scope };
+ },
+ methods: {
+ successCallback(resp) {
+ // Because we are polling & the user is interacting verify if the response received
+ // matches the last request made
+ if (_.isEqual(resp.config.params, this.requestData)) {
+ this.store.storeCount(resp.data.count);
+ this.store.storePagination(resp.headers);
+ this.setCommonData(resp.data.pipelines);
+ }
},
- methods: {
- successCallback(resp) {
- // Because we are polling & the user is interacting verify if the response received
- // matches the last request made
- if (_.isEqual(resp.config.params, this.requestData)) {
- this.store.storeCount(resp.data.count);
- this.store.storePagination(resp.headers);
- this.setCommonData(resp.data.pipelines);
- }
- },
- /**
- * Handles URL and query parameter changes.
- * When the user uses the pagination or the tabs,
- * - update URL
- * - Make API request to the server with new parameters
- * - Update the polling function
- * - Update the internal state
- */
- updateContent(parameters) {
- this.updateInternalState(parameters);
+ /**
+ * Handles URL and query parameter changes.
+ * When the user uses the pagination or the tabs,
+ * - update URL
+ * - Make API request to the server with new parameters
+ * - Update the polling function
+ * - Update the internal state
+ */
+ updateContent(parameters) {
+ this.updateInternalState(parameters);
- // fetch new data
- return this.service
- .getPipelines(this.requestData)
- .then(response => {
- this.isLoading = false;
- this.successCallback(response);
+ // fetch new data
+ return this.service
+ .getPipelines(this.requestData)
+ .then(response => {
+ this.isLoading = false;
+ this.successCallback(response);
- // restart polling
- this.poll.restart({ data: this.requestData });
- })
- .catch(() => {
- this.isLoading = false;
- this.errorCallback();
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ })
+ .catch(() => {
+ this.isLoading = false;
+ this.errorCallback();
- // restart polling
- this.poll.restart({ data: this.requestData });
- });
- },
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ });
+ },
- handleResetRunnersCache(endpoint) {
- this.isResetCacheButtonLoading = true;
+ handleResetRunnersCache(endpoint) {
+ this.isResetCacheButtonLoading = true;
- this.service
- .postAction(endpoint)
- .then(() => {
- this.isResetCacheButtonLoading = false;
- createFlash(s__('Pipelines|Project cache successfully reset.'), 'notice');
- })
- .catch(() => {
- this.isResetCacheButtonLoading = false;
- createFlash(s__('Pipelines|Something went wrong while cleaning runners cache.'));
- });
- },
+ this.service
+ .postAction(endpoint)
+ .then(() => {
+ this.isResetCacheButtonLoading = false;
+ createFlash(s__('Pipelines|Project cache successfully reset.'), 'notice');
+ })
+ .catch(() => {
+ this.isResetCacheButtonLoading = false;
+ createFlash(s__('Pipelines|Something went wrong while cleaning runners cache.'));
+ });
},
- };
+ },
+};
</script>
<template>
<div class="pipelines-container">
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index 5070c253f11..1c8d7303c52 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -1,44 +1,44 @@
<script>
- import eventHub from '../event_hub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import icon from '../../vue_shared/components/icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
+import eventHub from '../event_hub';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import icon from '../../vue_shared/components/icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ loadingIcon,
+ icon,
+ },
+ props: {
+ actions: {
+ type: Array,
+ required: true,
},
- components: {
- loadingIcon,
- icon,
- },
- props: {
- actions: {
- type: Array,
- required: true,
- },
- },
- data() {
- return {
- isLoading: false,
- };
- },
- methods: {
- onClickAction(endpoint) {
- this.isLoading = true;
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ onClickAction(endpoint) {
+ this.isLoading = true;
- eventHub.$emit('postAction', endpoint);
- },
+ eventHub.$emit('postAction', endpoint);
+ },
- isActionDisabled(action) {
- if (action.playable === undefined) {
- return false;
- }
+ isActionDisabled(action) {
+ if (action.playable === undefined) {
+ return false;
+ }
- return !action.playable;
- },
+ return !action.playable;
},
- };
+ },
+};
</script>
<template>
<div class="btn-group">
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index 490df47e154..d40de95e051 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
@@ -1,21 +1,21 @@
<script>
- import tooltip from '../../vue_shared/directives/tooltip';
- import icon from '../../vue_shared/components/icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import icon from '../../vue_shared/components/icon.vue';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ icon,
+ },
+ props: {
+ artifacts: {
+ type: Array,
+ required: true,
},
- components: {
- icon,
- },
- props: {
- artifacts: {
- type: Array,
- required: true,
- },
- },
- };
+ },
+};
</script>
<template>
<div
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index 2e777783636..0d7324f3fb5 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -1,74 +1,82 @@
<script>
- import Modal from '~/vue_shared/components/gl_modal.vue';
- import { s__, sprintf } from '~/locale';
- import PipelinesTableRowComponent from './pipelines_table_row.vue';
- import eventHub from '../event_hub';
+import Modal from '~/vue_shared/components/gl_modal.vue';
+import { s__, sprintf } from '~/locale';
+import PipelinesTableRowComponent from './pipelines_table_row.vue';
+import eventHub from '../event_hub';
- /**
- * Pipelines Table Component.
- *
- * Given an array of objects, renders a table.
- */
- export default {
- components: {
- PipelinesTableRowComponent,
- Modal,
+/**
+ * Pipelines Table Component.
+ *
+ * Given an array of objects, renders a table.
+ */
+export default {
+ components: {
+ PipelinesTableRowComponent,
+ Modal,
+ },
+ props: {
+ pipelines: {
+ type: Array,
+ required: true,
},
- props: {
- pipelines: {
- type: Array,
- required: true,
- },
- updateGraphDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
- autoDevopsHelpPath: {
- type: String,
- required: true,
- },
- viewType: {
- type: String,
- required: true,
- },
+ updateGraphDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- data() {
- return {
- pipelineId: '',
- endpoint: '',
- cancelingPipeline: null,
- };
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
},
- computed: {
- modalTitle() {
- return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
+ viewType: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ pipelineId: '',
+ endpoint: '',
+ cancelingPipeline: null,
+ };
+ },
+ computed: {
+ modalTitle() {
+ return sprintf(
+ s__('Pipeline|Stop pipeline #%{pipelineId}?'),
+ {
pipelineId: `${this.pipelineId}`,
- }, false);
- },
- modalText() {
- return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
- pipelineId: `<strong>#${this.pipelineId}</strong>`,
- }, false);
- },
+ },
+ false,
+ );
},
- created() {
- eventHub.$on('openConfirmationModal', this.setModalData);
+ modalText() {
+ return sprintf(
+ s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'),
+ {
+ pipelineId: `<strong>#${this.pipelineId}</strong>`,
+ },
+ false,
+ );
},
- beforeDestroy() {
- eventHub.$off('openConfirmationModal', this.setModalData);
+ },
+ created() {
+ eventHub.$on('openConfirmationModal', this.setModalData);
+ },
+ beforeDestroy() {
+ eventHub.$off('openConfirmationModal', this.setModalData);
+ },
+ methods: {
+ setModalData(data) {
+ this.pipelineId = data.pipelineId;
+ this.endpoint = data.endpoint;
},
- methods: {
- setModalData(data) {
- this.pipelineId = data.pipelineId;
- this.endpoint = data.endpoint;
- },
- onSubmit() {
- eventHub.$emit('postAction', this.endpoint);
- this.cancelingPipeline = this.pipelineId;
- },
+ onSubmit() {
+ eventHub.$emit('postAction', this.endpoint);
+ this.cancelingPipeline = this.pipelineId;
},
- };
+ },
+};
</script>
<template>
<div class="ci-table">
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index b2744a30c2a..804822a3ea8 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -1,255 +1,253 @@
<script>
- import eventHub from '../event_hub';
- import PipelinesActionsComponent from './pipelines_actions.vue';
- import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
- import CiBadge from '../../vue_shared/components/ci_badge_link.vue';
- import PipelineStage from './stage.vue';
- import PipelineUrl from './pipeline_url.vue';
- import PipelinesTimeago from './time_ago.vue';
- import CommitComponent from '../../vue_shared/components/commit.vue';
- import LoadingButton from '../../vue_shared/components/loading_button.vue';
- import Icon from '../../vue_shared/components/icon.vue';
- import { PIPELINES_TABLE } from '../constants';
+import eventHub from '../event_hub';
+import PipelinesActionsComponent from './pipelines_actions.vue';
+import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
+import CiBadge from '../../vue_shared/components/ci_badge_link.vue';
+import PipelineStage from './stage.vue';
+import PipelineUrl from './pipeline_url.vue';
+import PipelinesTimeago from './time_ago.vue';
+import CommitComponent from '../../vue_shared/components/commit.vue';
+import LoadingButton from '../../vue_shared/components/loading_button.vue';
+import Icon from '../../vue_shared/components/icon.vue';
+import { PIPELINES_TABLE } from '../constants';
- /**
- * Pipeline table row.
- *
- * Given the received object renders a table row in the pipelines' table.
- */
- export default {
- components: {
- PipelinesActionsComponent,
- PipelinesArtifactsComponent,
- CommitComponent,
- PipelineStage,
- PipelineUrl,
- CiBadge,
- PipelinesTimeago,
- LoadingButton,
- Icon,
+/**
+ * Pipeline table row.
+ *
+ * Given the received object renders a table row in the pipelines' table.
+ */
+export default {
+ components: {
+ PipelinesActionsComponent,
+ PipelinesArtifactsComponent,
+ CommitComponent,
+ PipelineStage,
+ PipelineUrl,
+ CiBadge,
+ PipelinesTimeago,
+ LoadingButton,
+ Icon,
+ },
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
},
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- updateGraphDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
- autoDevopsHelpPath: {
- type: String,
- required: true,
- },
- viewType: {
- type: String,
- required: true,
- },
- cancelingPipeline: {
- type: String,
- required: false,
- default: null,
- },
+ updateGraphDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- pipelinesTable: PIPELINES_TABLE,
- data() {
- return {
- isRetrying: false,
- };
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
},
- computed: {
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * This field needs a lot of verification, because of different possible cases:
- *
- * 1. person who is an author of a commit might be a GitLab user
- * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
- * 3. If GitLab user does not have avatar he/she might have a Gravatar
- * 4. If committer is not a GitLab User he/she can have a Gravatar
- * 5. We do not have consistent API object in this case
- * 6. We should improve API and the code
- *
- * @returns {Object|Undefined}
- */
- commitAuthor() {
- let commitAuthorInformation;
+ viewType: {
+ type: String,
+ required: true,
+ },
+ cancelingPipeline: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ pipelinesTable: PIPELINES_TABLE,
+ data() {
+ return {
+ isRetrying: false,
+ };
+ },
+ computed: {
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * This field needs a lot of verification, because of different possible cases:
+ *
+ * 1. person who is an author of a commit might be a GitLab user
+ * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
+ * 3. If GitLab user does not have avatar he/she might have a Gravatar
+ * 4. If committer is not a GitLab User he/she can have a Gravatar
+ * 5. We do not have consistent API object in this case
+ * 6. We should improve API and the code
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ let commitAuthorInformation;
- if (!this.pipeline || !this.pipeline.commit) {
- return null;
- }
+ if (!this.pipeline || !this.pipeline.commit) {
+ return null;
+ }
- // 1. person who is an author of a commit might be a GitLab user
- if (this.pipeline.commit.author) {
- // 2. if person who is an author of a commit is a GitLab user
- // he/she can have a GitLab avatar
- if (this.pipeline.commit.author.avatar_url) {
- commitAuthorInformation = this.pipeline.commit.author;
+ // 1. person who is an author of a commit might be a GitLab user
+ if (this.pipeline.commit.author) {
+ // 2. if person who is an author of a commit is a GitLab user
+ // he/she can have a GitLab avatar
+ if (this.pipeline.commit.author.avatar_url) {
+ commitAuthorInformation = this.pipeline.commit.author;
- // 3. If GitLab user does not have avatar he/she might have a Gravatar
- } else if (this.pipeline.commit.author_gravatar_url) {
- commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
- avatar_url: this.pipeline.commit.author_gravatar_url,
- });
- }
- // 4. If committer is not a GitLab User he/she can have a Gravatar
- } else {
- commitAuthorInformation = {
+ // 3. If GitLab user does not have avatar he/she might have a Gravatar
+ } else if (this.pipeline.commit.author_gravatar_url) {
+ commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
avatar_url: this.pipeline.commit.author_gravatar_url,
- path: `mailto:${this.pipeline.commit.author_email}`,
- username: this.pipeline.commit.author_name,
- };
+ });
}
+ // 4. If committer is not a GitLab User he/she can have a Gravatar
+ } else {
+ commitAuthorInformation = {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ path: `mailto:${this.pipeline.commit.author_email}`,
+ username: this.pipeline.commit.author_name,
+ };
+ }
- return commitAuthorInformation;
- },
+ return commitAuthorInformation;
+ },
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitTag() {
- if (this.pipeline.ref &&
- this.pipeline.ref.tag) {
- return this.pipeline.ref.tag;
- }
- return undefined;
- },
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.pipeline.ref && this.pipeline.ref.tag) {
+ return this.pipeline.ref.tag;
+ }
+ return undefined;
+ },
- /**
- * If provided, returns the commit ref.
- * Needed to render the commit component column.
- *
- * Matches `path` prop sent in the API to `ref_url` prop needed
- * in the commit component.
- *
- * @returns {Object|Undefined}
- */
- commitRef() {
- if (this.pipeline.ref) {
- return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
- if (prop === 'path') {
- // eslint-disable-next-line no-param-reassign
- accumulator.ref_url = this.pipeline.ref[prop];
- } else {
- // eslint-disable-next-line no-param-reassign
- accumulator[prop] = this.pipeline.ref[prop];
- }
- return accumulator;
- }, {});
- }
+ /**
+ * If provided, returns the commit ref.
+ * Needed to render the commit component column.
+ *
+ * Matches `path` prop sent in the API to `ref_url` prop needed
+ * in the commit component.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.pipeline.ref) {
+ return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
+ if (prop === 'path') {
+ // eslint-disable-next-line no-param-reassign
+ accumulator.ref_url = this.pipeline.ref[prop];
+ } else {
+ // eslint-disable-next-line no-param-reassign
+ accumulator[prop] = this.pipeline.ref[prop];
+ }
+ return accumulator;
+ }, {});
+ }
- return undefined;
- },
+ return undefined;
+ },
- /**
- * If provided, returns the commit url.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitUrl() {
- if (this.pipeline.commit &&
- this.pipeline.commit.commit_path) {
- return this.pipeline.commit.commit_path;
- }
- return undefined;
- },
+ /**
+ * If provided, returns the commit url.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.pipeline.commit && this.pipeline.commit.commit_path) {
+ return this.pipeline.commit.commit_path;
+ }
+ return undefined;
+ },
- /**
- * If provided, returns the commit short sha.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitShortSha() {
- if (this.pipeline.commit &&
- this.pipeline.commit.short_id) {
- return this.pipeline.commit.short_id;
- }
- return undefined;
- },
+ /**
+ * If provided, returns the commit short sha.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.pipeline.commit && this.pipeline.commit.short_id) {
+ return this.pipeline.commit.short_id;
+ }
+ return undefined;
+ },
- /**
- * If provided, returns the commit title.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitTitle() {
- if (this.pipeline.commit &&
- this.pipeline.commit.title) {
- return this.pipeline.commit.title;
- }
- return undefined;
- },
+ /**
+ * If provided, returns the commit title.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.pipeline.commit && this.pipeline.commit.title) {
+ return this.pipeline.commit.title;
+ }
+ return undefined;
+ },
- /**
- * Timeago components expects a number
- *
- * @return {type} description
- */
- pipelineDuration() {
- if (this.pipeline.details && this.pipeline.details.duration) {
- return this.pipeline.details.duration;
- }
+ /**
+ * Timeago components expects a number
+ *
+ * @return {type} description
+ */
+ pipelineDuration() {
+ if (this.pipeline.details && this.pipeline.details.duration) {
+ return this.pipeline.details.duration;
+ }
- return 0;
- },
+ return 0;
+ },
- /**
- * Timeago component expects a String.
- *
- * @return {String}
- */
- pipelineFinishedAt() {
- if (this.pipeline.details && this.pipeline.details.finished_at) {
- return this.pipeline.details.finished_at;
- }
+ /**
+ * Timeago component expects a String.
+ *
+ * @return {String}
+ */
+ pipelineFinishedAt() {
+ if (this.pipeline.details && this.pipeline.details.finished_at) {
+ return this.pipeline.details.finished_at;
+ }
- return '';
- },
+ return '';
+ },
- pipelineStatus() {
- if (this.pipeline.details && this.pipeline.details.status) {
- return this.pipeline.details.status;
- }
- return {};
- },
+ pipelineStatus() {
+ if (this.pipeline.details && this.pipeline.details.status) {
+ return this.pipeline.details.status;
+ }
+ return {};
+ },
- displayPipelineActions() {
- return this.pipeline.flags.retryable ||
- this.pipeline.flags.cancelable ||
- this.pipeline.details.manual_actions.length ||
- this.pipeline.details.artifacts.length;
- },
+ displayPipelineActions() {
+ return (
+ this.pipeline.flags.retryable ||
+ this.pipeline.flags.cancelable ||
+ this.pipeline.details.manual_actions.length ||
+ this.pipeline.details.artifacts.length
+ );
+ },
- isChildView() {
- return this.viewType === 'child';
- },
+ isChildView() {
+ return this.viewType === 'child';
+ },
- isCancelling() {
- return this.cancelingPipeline === this.pipeline.id;
- },
+ isCancelling() {
+ return this.cancelingPipeline === this.pipeline.id;
},
+ },
- methods: {
- handleCancelClick() {
- eventHub.$emit('openConfirmationModal', {
- pipelineId: this.pipeline.id,
- endpoint: this.pipeline.cancel_path,
- });
- },
- handleRetryClick() {
- this.isRetrying = true;
- eventHub.$emit('retryPipeline', this.pipeline.retry_path);
- },
+ methods: {
+ handleCancelClick() {
+ eventHub.$emit('openConfirmationModal', {
+ pipelineId: this.pipeline.id,
+ endpoint: this.pipeline.cancel_path,
+ });
+ },
+ handleRetryClick() {
+ this.isRetrying = true;
+ eventHub.$emit('retryPipeline', this.pipeline.retry_path);
},
- };
+ },
+};
</script>
<template>
<div class="commit gl-responsive-table-row">
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index b9231c002fd..56fdb858088 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -186,32 +186,27 @@ export default {
</i>
</button>
- <ul
+ <div
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
aria-labelledby="stageDropdown"
>
-
- <li
+ <loading-icon v-if="isLoading"/>
+ <ul
+ v-else
class="js-builds-dropdown-list scrollable-menu"
>
-
- <loading-icon v-if="isLoading"/>
-
- <ul
- v-else
+ <li
+ v-for="job in dropdownContent"
+ :key="job.id"
>
- <li
- v-for="job in dropdownContent"
- :key="job.id"
- >
- <job-component
- :job="job"
- css-class-job-name="mini-pipeline-graph-dropdown-item"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
- />
- </li>
- </ul>
- </li>
- </ul>
+ <job-component
+ :dropdown-length="dropdownContent.length"
+ :job="job"
+ css-class-job-name="mini-pipeline-graph-dropdown-item"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
+ />
+ </li>
+ </ul>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue
index 0a97df2dc18..cd43d78de40 100644
--- a/app/assets/javascripts/pipelines/components/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/time_ago.vue
@@ -1,60 +1,58 @@
<script>
- import iconTimerSvg from 'icons/_icon_timer.svg';
- import '../../lib/utils/datetime_utility';
- import tooltip from '../../vue_shared/directives/tooltip';
- import timeagoMixin from '../../vue_shared/mixins/timeago';
+import iconTimerSvg from 'icons/_icon_timer.svg';
+import '../../lib/utils/datetime_utility';
+import tooltip from '../../vue_shared/directives/tooltip';
+import timeagoMixin from '../../vue_shared/mixins/timeago';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ finishedTime: {
+ type: String,
+ required: true,
},
- mixins: [
- timeagoMixin,
- ],
- props: {
- finishedTime: {
- type: String,
- required: true,
- },
- duration: {
- type: Number,
- required: true,
- },
+ duration: {
+ type: Number,
+ required: true,
},
- data() {
- return {
- iconTimerSvg,
- };
+ },
+ data() {
+ return {
+ iconTimerSvg,
+ };
+ },
+ computed: {
+ hasDuration() {
+ return this.duration > 0;
},
- computed: {
- hasDuration() {
- return this.duration > 0;
- },
- hasFinishedTime() {
- return this.finishedTime !== '';
- },
- durationFormated() {
- const date = new Date(this.duration * 1000);
+ hasFinishedTime() {
+ return this.finishedTime !== '';
+ },
+ durationFormated() {
+ const date = new Date(this.duration * 1000);
- let hh = date.getUTCHours();
- let mm = date.getUTCMinutes();
- let ss = date.getSeconds();
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
- // left pad
- if (hh < 10) {
- hh = `0${hh}`;
- }
- if (mm < 10) {
- mm = `0${mm}`;
- }
- if (ss < 10) {
- ss = `0${ss}`;
- }
+ // left pad
+ if (hh < 10) {
+ hh = `0${hh}`;
+ }
+ if (mm < 10) {
+ mm = `0${mm}`;
+ }
+ if (ss < 10) {
+ ss = `0${ss}`;
+ }
- return `${hh}:${mm}:${ss}`;
- },
+ return `${hh}:${mm}:${ss}`;
},
- };
+ },
+};
</script>
<template>
<div class="table-section section-15 pipelines-time-ago">
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 30b1eee186d..2cb558b0dec 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -75,8 +75,7 @@ export default {
// Stop polling
this.poll.stop();
// Update the table
- return this.getPipelines()
- .then(() => this.poll.restart());
+ return this.getPipelines().then(() => this.poll.restart());
},
fetchPipelines() {
if (!this.isMakingRequest) {
@@ -86,9 +85,10 @@ export default {
}
},
getPipelines() {
- return this.service.getPipelines(this.requestData)
+ return this.service
+ .getPipelines(this.requestData)
.then(response => this.successCallback(response))
- .catch((error) => this.errorCallback(error));
+ .catch(error => this.errorCallback(error));
},
setCommonData(pipelines) {
this.store.storePipelines(pipelines);
@@ -118,7 +118,8 @@ export default {
}
},
postAction(endpoint) {
- this.service.postAction(endpoint)
+ this.service
+ .postAction(endpoint)
.then(() => this.fetchPipelines())
.catch(() => Flash(__('An error occurred while making the request.')));
},
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index b49a16a87e6..dc9befe6349 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -10,7 +10,7 @@ import eventHub from './event_hub';
Vue.use(Translate);
export default () => {
- const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
+ const { dataset } = document.querySelector('.js-pipeline-details-vue');
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
@@ -31,7 +31,8 @@ export default () => {
requestRefreshPipelineGraph() {
// When an action is clicked
// (wether in the dropdown or in the main nodes, we refresh the big graph)
- this.mediator.refreshPipeline()
+ this.mediator
+ .refreshPipeline()
.catch(() => Flash(__('An error occurred while making the request.')));
},
},
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index 5633e54b28a..bd1e1895660 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -52,7 +52,8 @@ export default class pipelinesMediator {
refreshPipeline() {
this.poll.stop();
- return this.service.getPipeline()
+ return this.service
+ .getPipeline()
.then(response => this.successCallback(response))
.catch(() => this.errorCallback())
.finally(() => this.poll.restart());
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 59c8b9c58e5..8317d3f4510 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -19,7 +19,7 @@ export default class PipelinesService {
getPipelines(data = {}) {
const { scope, page } = data;
- const CancelToken = axios.CancelToken;
+ const { CancelToken } = axios;
this.cancelationSource = CancelToken.source();
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 246a265ef2b..0e973cab4d2 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';
@@ -43,7 +43,7 @@ MarkdownPreview.prototype.showPreview = function ($form) {
this.fetchMarkdownPreview(mdText, url, (function (response) {
var body;
if (response.body.length > 0) {
- body = response.body;
+ ({ body } = response);
} else {
body = this.emptyMessage;
}
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index ba120c4bbdf..f641b23e519 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';
@@ -47,7 +47,8 @@ import _ from 'underscore';
var _this;
_this = this;
this.fileInput.on('change', function(e) {
- return _this.onFileInputChange(e, this);
+ _this.onFileInputChange(e, this);
+ this.value = null;
});
this.pickImageEl.on('click', this.onPickImageClick);
this.modalCrop.on('shown.bs.modal', this.onModalShow);
@@ -85,11 +86,10 @@ import _ from 'underscore';
cropBoxResizable: false,
toggleDragModeOnDblclick: false,
built: function() {
- var $image, container, cropBoxHeight, cropBoxWidth;
- $image = $(this);
- container = $image.cropper('getContainerData');
- cropBoxWidth = _this.cropBoxWidth;
- cropBoxHeight = _this.cropBoxHeight;
+ const $image = $(this);
+ const container = $image.cropper('getContainerData');
+ const { cropBoxWidth, cropBoxHeight } = _this;
+
return $image.cropper('setCropBoxData', {
width: cropBoxWidth,
height: cropBoxHeight,
@@ -136,14 +136,13 @@ import _ from 'underscore';
}
dataURLtoBlob(dataURL) {
- var array, binary, i, k, len, v;
+ var array, binary, i, len, v;
binary = atob(dataURL.split(',')[1]);
array = [];
- // eslint-disable-next-line no-multi-assign
- 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..8cf7f2f23d0 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 {
@@ -64,7 +61,13 @@ export default class Profile {
url: this.form.attr('action'),
data: formData,
})
- .then(({ data }) => flash(data.message, 'notice'))
+ .then(({ data }) => {
+ if (avatarBlob != null) {
+ this.updateHeaderAvatar();
+ }
+
+ flash(data.message, 'notice');
+ })
.then(() => {
window.scrollTo(0, 0);
// Enable submit button after requests ends
@@ -73,6 +76,10 @@ export default class Profile {
.catch(error => flash(error.message));
}
+ updateHeaderAvatar() {
+ $('.header-user-avatar').attr('src', this.avatarGlCrop.dataURL);
+ }
+
setRepoRadio() {
const multiEditRadios = $('input[name="user[multi_file]"]');
if (this.newRepoActivated || this.newRepoActivated === 'true') {
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 17497283695..05485e352dc 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';
@@ -88,12 +88,11 @@ export default class ProjectFindFile {
// render result
renderList(filePaths, searchText) {
- var blobItemUrl, filePath, html, i, j, len, matches, results;
+ var blobItemUrl, filePath, html, i, len, matches, results;
this.element.find(".tree-table > tbody").empty();
results = [];
- // eslint-disable-next-line no-multi-assign
- 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;
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index cb2e6855d1d..bce7556bd40 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';
@@ -47,7 +47,10 @@ export default function projectSelect() {
projectsCallback = finalCallback;
}
if (_this.groupId) {
- return Api.groupProjects(_this.groupId, query.term, projectsCallback);
+ return Api.groupProjects(_this.groupId, query.term, {
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ }, projectsCallback);
} else {
return Api.projects(query.term, {
order_by: _this.orderBy,
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index c772fca14bb..a4c7c143e56 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -47,7 +47,7 @@
},
methods: {
successCallback(res) {
- const pipelines = res.data.pipelines;
+ const { pipelines } = res.data;
if (pipelines.length > 0) {
// The pipeline entity always keeps the latest pipeline info on the `details.status`
this.ciStatus = pipelines[0].details.status;
diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js
index e1ca70c51a6..6056f12aa4f 100644
--- a/app/assets/javascripts/projects_dropdown/index.js
+++ b/app/assets/javascripts/projects_dropdown/index.js
@@ -31,7 +31,7 @@ document.addEventListener('DOMContentLoaded', () => {
projectsDropdownApp,
},
data() {
- const dataset = this.$options.el.dataset;
+ const { dataset } = this.$options.el;
const store = new ProjectsStore();
const service = new ProjectsService(dataset.userName);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 7c61c070a35..b601b19e7be 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -1,11 +1,8 @@
import $ from 'jquery';
-import _ from 'underscore';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
import CreateItemDropdown from '../create_item_dropdown';
import AccessorUtilities from '../lib/utils/accessor';
-const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults';
-
export default class ProtectedBranchCreate {
constructor() {
this.$form = $('.js-new-protected-branch');
@@ -43,8 +40,6 @@ export default class ProtectedBranchCreate {
onSelect: this.onSelectCallback,
getData: ProtectedBranchCreate.getProtectedBranches,
});
-
- this.loadPreviousSelection($allowedToMergeDropdown.data('glDropdown'), $allowedToPushDropdown.data('glDropdown'));
}
// This will run after clicked callback
@@ -59,39 +54,10 @@ export default class ProtectedBranchCreate {
$allowedToPushInput.length
);
- this.savePreviousSelection($allowedToMergeInput.val(), $allowedToPushInput.val());
this.$form.find('input[type="submit"]').prop('disabled', completedForm);
}
static getProtectedBranches(term, callback) {
callback(gon.open_branches);
}
-
- loadPreviousSelection(mergeDropdown, pushDropdown) {
- let mergeIndex = 0;
- let pushIndex = 0;
- if (this.isLocalStorageAvailable) {
- const savedDefaults = JSON.parse(window.localStorage.getItem(PB_LOCAL_STORAGE_KEY));
- if (savedDefaults != null) {
- mergeIndex = _.findLastIndex(mergeDropdown.fullData.roles, {
- id: parseInt(savedDefaults.mergeSelection, 0),
- });
- pushIndex = _.findLastIndex(pushDropdown.fullData.roles, {
- id: parseInt(savedDefaults.pushSelection, 0),
- });
- }
- }
- mergeDropdown.selectRowAtIndex(mergeIndex);
- pushDropdown.selectRowAtIndex(pushIndex);
- }
-
- savePreviousSelection(mergeSelection, pushSelection) {
- if (this.isLocalStorageAvailable) {
- const branchDefaults = {
- mergeSelection,
- pushSelection,
- };
- window.localStorage.setItem(PB_LOCAL_STORAGE_KEY, JSON.stringify(branchDefaults));
- }
- }
}
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js
index 6fb125192b2..e15cd94a915 100644
--- a/app/assets/javascripts/registry/index.js
+++ b/app/assets/javascripts/registry/index.js
@@ -10,7 +10,7 @@ export default () => new Vue({
registryApp,
},
data() {
- const dataset = document.querySelector(this.$options.el).dataset;
+ const { dataset } = document.querySelector(this.$options.el);
return {
endpoint: dataset.endpoint,
};
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index c0de03373d8..a78aa90b7b5 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -20,7 +20,7 @@ export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => {
- const headers = response.headers;
+ const { headers } = response;
return response.json().then(resp => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
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 ef3c71eeafe..5b2e0468784 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';
@@ -289,7 +289,7 @@ export default class SearchAutocomplete {
}
// If the dropdown is closed, we'll open it
- if (!this.dropdown.hasClass('open')) {
+ if (!this.dropdown.hasClass('show')) {
this.loadingSuggestions = false;
this.dropdownToggle.dropdown('toggle');
return this.searchInput.removeClass('disabled');
@@ -424,9 +424,9 @@ export default class SearchAutocomplete {
}
disableAutocomplete() {
- if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
+ if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('disabled');
- this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
+ this.dropdown.removeClass('show').trigger('hidden.bs.dropdown');
this.restoreMenu();
}
}
diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js
index 2f974d6ff9d..8681a1776c6 100644
--- a/app/assets/javascripts/shared/milestones/form.js
+++ b/app/assets/javascripts/shared/milestones/form.js
@@ -6,5 +6,14 @@ import GLForm from '../../gl_form';
export default (initGFM = true) => {
new ZenMode(); // eslint-disable-line no-new
new DueDateSelectors(); // eslint-disable-line no-new
- new GLForm($('.milestone-form'), initGFM); // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new GLForm($('.milestone-form'), {
+ emojis: true,
+ members: initGFM,
+ issues: initGFM,
+ mergeRequests: initGFM,
+ epics: initGFM,
+ milestones: initGFM,
+ labels: initGFM,
+ });
};
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/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 78f7353eb0d..6b595764bc5 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -20,6 +20,7 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
+ Mousetrap.bind('g l', () => findAndFollowLink('.shortcuts-metrics'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project');
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
new file mode 100644
index 00000000000..ffaed9c7193
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -0,0 +1,98 @@
+<script>
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+import Icon from '~/vue_shared/components/icon.vue';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+
+const MARK_TEXT = __('Mark todo as done');
+const TODO_TEXT = __('Add todo');
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ LoadingIcon,
+ },
+ props: {
+ issuableId: {
+ type: Number,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ isTodo: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ isActionActive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ collapsed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ buttonClasses() {
+ return this.collapsed ?
+ 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' :
+ 'btn btn-default btn-todo issuable-header-btn float-right';
+ },
+ buttonLabel() {
+ return this.isTodo ? MARK_TEXT : TODO_TEXT;
+ },
+ collapsedButtonIconClasses() {
+ return this.isTodo ? 'todo-undone' : '';
+ },
+ collapsedButtonIcon() {
+ return this.isTodo ? 'todo-done' : 'todo-add';
+ },
+ },
+ methods: {
+ handleButtonClick() {
+ this.$emit('toggleTodo');
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ v-tooltip
+ :class="buttonClasses"
+ :title="buttonLabel"
+ :aria-label="buttonLabel"
+ :data-issuable-id="issuableId"
+ :data-issuable-type="issuableType"
+ type="button"
+ data-container="body"
+ data-placement="left"
+ data-boundary="viewport"
+ @click="handleButtonClick"
+ >
+ <icon
+ v-show="collapsed"
+ :css-classes="collapsedButtonIconClasses"
+ :name="collapsedButtonIcon"
+ />
+ <span
+ v-show="!collapsed"
+ class="issuable-todo-inner"
+ >
+ {{ buttonLabel }}
+ </span>
+ <loading-icon
+ v-show="isActionActive"
+ :inline="true"
+ />
+ </button>
+</template>
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/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/smart_interval.js b/app/assets/javascripts/smart_interval.js
index 77ab7c964e6..5e385400747 100644
--- a/app/assets/javascripts/smart_interval.js
+++ b/app/assets/javascripts/smart_interval.js
@@ -42,8 +42,7 @@ export default class SmartInterval {
/* public */
start() {
- const cfg = this.cfg;
- const state = this.state;
+ const { cfg, state } = this;
if (cfg.immediateExecution && !this.isLoading) {
cfg.immediateExecution = false;
@@ -100,7 +99,7 @@ export default class SmartInterval {
/* private */
initInterval() {
- const cfg = this.cfg;
+ const { cfg } = this;
if (!cfg.lazyStart) {
this.start();
@@ -151,7 +150,7 @@ export default class SmartInterval {
}
incrementInterval() {
- const cfg = this.cfg;
+ const { cfg } = this;
const currentInterval = this.getCurrentInterval();
if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf;
@@ -166,7 +165,7 @@ export default class SmartInterval {
isPageVisible() { return this.state.pageVisibility === 'visible'; }
stopTimer() {
- const state = this.state;
+ const { state } = this;
state.intervalId = window.clearInterval(state.intervalId);
}
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/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
index e39213cb098..a5c18042ce7 100644
--- a/app/assets/javascripts/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -38,14 +38,14 @@ function simulateEvent(el, type, options = {}) {
function isLast(target) {
const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
- const children = el.children;
+ const { children } = el;
return children.length - 1 === target.index;
}
function getTarget(target) {
const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
- const children = el.children;
+ const { children } = el;
return (
children[target.index] ||
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/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index 96af6d2fcca..78fd7ad441f 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -11,7 +11,6 @@ export default class U2FAuthenticate {
constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
this.u2fUtils = null;
this.container = container;
- this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderAuthenticated = this.renderAuthenticated.bind(this);
this.renderError = this.renderError.bind(this);
this.renderInProgress = this.renderInProgress.bind(this);
@@ -41,7 +40,6 @@ export default class U2FAuthenticate {
this.signRequests = u2fParams.sign_requests.map(request => _(request).omit('challenge'));
this.templates = {
- notSupported: '#js-authenticate-u2f-not-supported',
setup: '#js-authenticate-u2f-setup',
inProgress: '#js-authenticate-u2f-in-progress',
error: '#js-authenticate-u2f-error',
@@ -55,7 +53,7 @@ export default class U2FAuthenticate {
this.u2fUtils = utils;
this.renderInProgress();
})
- .catch(() => this.renderNotSupported());
+ .catch(() => this.switchToFallbackUI());
}
authenticate() {
@@ -96,10 +94,6 @@ export default class U2FAuthenticate {
this.fallbackButton.classList.add('hidden');
}
- renderNotSupported() {
- return this.renderTemplate('notSupported');
- }
-
switchToFallbackUI() {
this.fallbackButton.classList.add('hidden');
this.container[0].classList.add('hidden');
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 349614460e1..e3d7645040d 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 */
@@ -250,7 +250,6 @@ function UsersSelect(currentUser, els, options = {}) {
let anyUser;
let index;
- let j;
let len;
let name;
let obj;
@@ -259,8 +258,7 @@ function UsersSelect(currentUser, els, options = {}) {
showDivider = 0;
if (firstUser) {
// Move current user to the front of the list
- // eslint-disable-next-line no-multi-assign
- 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);
@@ -502,7 +500,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (this.multiSelect) {
selected = getSelected().find(u => user.id === u);
- const fieldName = this.fieldName;
+ const { fieldName } = this;
const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']");
if (field.length) {
@@ -554,7 +552,7 @@ function UsersSelect(currentUser, els, options = {}) {
minimumInputLength: 0,
query: function(query) {
return _this.users(query.term, options, function(users) {
- var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref;
+ var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
data = {
results: users
};
@@ -563,8 +561,7 @@ function UsersSelect(currentUser, els, options = {}) {
// Move current user to the front of the list
ref = data.results;
- // eslint-disable-next-line no-multi-assign
- 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/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index c44419d24e6..5e464f8a0e2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -1,4 +1,5 @@
<script>
+import Icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import tooltip from '../../vue_shared/directives/tooltip';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
@@ -14,6 +15,7 @@ export default {
LoadingButton,
MemoryUsage,
StatusIcon,
+ Icon,
},
directives: {
tooltip,
@@ -110,11 +112,10 @@ export default {
class="deploy-link js-deploy-url"
>
{{ deployment.external_url_formatted }}
- <i
- class="fa fa-external-link"
- aria-hidden="true"
- >
- </i>
+ <icon
+ :size="16"
+ name="external-link"
+ />
</a>
</template>
<span
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index 1fdc3218671..53c4dc8c8f4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -32,7 +32,7 @@
};
</script>
<template>
- <div class="space-children flex-container-block append-right-10">
+ <div class="space-children d-flex append-right-10">
<div
v-if="isLoading"
class="mr-widget-icon"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
index 0d9a560c88e..97f4196b94d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
@@ -82,7 +82,7 @@
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
- <h4 class="flex-container-block">
+ <h4 class="d-flex align-items-start">
<span class="append-right-10">
{{ s__("mrWidget|Set by") }}
<mr-widget-author :author="mr.setToMWPSBy" />
@@ -119,7 +119,7 @@
</p>
<p
v-else
- class="flex-container-block"
+ class="d-flex align-items-start"
>
<span class="append-right-10">
{{ s__("mrWidget|The source branch will not be removed") }}
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..09477da40b5 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() {
@@ -191,7 +191,7 @@ export default {
if (data.ci_status === this.mr.ciStatus) return;
if (!data.pipeline) return;
- const label = data.pipeline.details.status.label;
+ const { label } = data.pipeline.details.status;
const title = `Pipeline ${label}`;
const message = `Pipeline ${label} for "${data.title}"`;
@@ -211,7 +211,7 @@ export default {
// `params` should be an Array contains a Boolean, like `[true]`
// Passing parameter as Boolean didn't work.
eventHub.$on('SetBranchRemoveFlag', (params) => {
- this.mr.isRemovingSourceBranch = params[0];
+ [this.mr.isRemovingSourceBranch] = params;
});
eventHub.$on('FailedToMerge', (mergeError) => {
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index 6851029018a..133bdbb54f7 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -42,7 +42,7 @@ export default {
},
methods: {
onImgLoad() {
- const contentImg = this.$refs.contentImg;
+ const { contentImg } = this.$refs;
if (contentImg) {
this.isZoomable =
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 09e0094054d..a10deb93f0f 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -4,7 +4,7 @@ import { __ } from '~/locale';
import $ from 'jquery';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-const CancelToken = axios.CancelToken;
+const { CancelToken } = axios;
let axiosSource;
export default {
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
index 2c47f5b9b35..d3cbe3c7e74 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -45,11 +45,15 @@ export default {
return DownloadDiffViewer;
}
},
+ basePath() {
+ // We might get the project path from rails with the relative url already setup
+ return this.projectPath.indexOf('/') === 0 ? '' : `${gon.relative_url_root}/`;
+ },
fullOldPath() {
- return `${gon.relative_url_root}/${this.projectPath}/raw/${this.oldSha}/${this.oldPath}`;
+ return `${this.basePath}${this.projectPath}/raw/${this.oldSha}/${this.oldPath}`;
},
fullNewPath() {
- return `${gon.relative_url_root}/${this.projectPath}/raw/${this.newSha}/${this.newPath}`;
+ return `${this.basePath}${this.projectPath}/raw/${this.newSha}/${this.newPath}`;
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 05e8ed2da2c..298971a36b2 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -62,7 +62,15 @@
/*
GLForm class handles all the toolbar buttons
*/
- return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete);
+ return new GLForm($(this.$refs['gl-form']), {
+ emojis: this.enableAutocomplete,
+ members: this.enableAutocomplete,
+ issues: this.enableAutocomplete,
+ mergeRequests: this.enableAutocomplete,
+ epics: this.enableAutocomplete,
+ milestones: this.enableAutocomplete,
+ labels: this.enableAutocomplete,
+ });
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('glForm');
@@ -150,7 +158,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..8c22f3f6536 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -29,8 +29,8 @@
methods: {
isValid(form) {
return !form ||
- form.find('.js-vue-markdown-field').length ||
- $(this.$el).closest('form') === form[0];
+ form.find('.js-vue-markdown-field').length &&
+ $(this.$el).closest('form')[0] === form[0];
},
previewMarkdownTab(event, form) {
@@ -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/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
index ac2e99abe77..80dc7d3557c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -12,6 +12,11 @@ export default {
type: Boolean,
required: true,
},
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
tooltipLabel() {
@@ -30,10 +35,12 @@ export default {
<button
v-tooltip
:title="tooltipLabel"
+ :class="cssClasses"
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
data-container="body"
data-placement="left"
+ data-boundary="viewport"
@click="toggle"
>
<i
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue
index 2370e59d017..8e9621c956f 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue
@@ -55,7 +55,7 @@
},
getItems() {
const total = this.pageInfo.totalPages;
- const page = this.pageInfo.page;
+ const { page } = this.pageInfo;
const items = [];
if (page > 1) {
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.scss b/app/assets/stylesheets/bootstrap.scss
new file mode 100644
index 00000000000..a040c2f8c20
--- /dev/null
+++ b/app/assets/stylesheets/bootstrap.scss
@@ -0,0 +1,37 @@
+/*
+ * Includes specific styles from the bootstrap4 foler in node_modules
+ */
+
+@import "../../../node_modules/bootstrap/scss/functions";
+@import "../../../node_modules/bootstrap/scss/variables";
+@import "../../../node_modules/bootstrap/scss/mixins";
+@import "../../../node_modules/bootstrap/scss/root";
+@import "../../../node_modules/bootstrap/scss/reboot";
+@import "../../../node_modules/bootstrap/scss/type";
+@import "../../../node_modules/bootstrap/scss/images";
+@import "../../../node_modules/bootstrap/scss/code";
+@import "../../../node_modules/bootstrap/scss/grid";
+@import "../../../node_modules/bootstrap/scss/tables";
+@import "../../../node_modules/bootstrap/scss/forms";
+@import "../../../node_modules/bootstrap/scss/buttons";
+@import "../../../node_modules/bootstrap/scss/transitions";
+@import "../../../node_modules/bootstrap/scss/dropdown";
+@import "../../../node_modules/bootstrap/scss/button-group";
+@import "../../../node_modules/bootstrap/scss/input-group";
+@import "../../../node_modules/bootstrap/scss/custom-forms";
+@import "../../../node_modules/bootstrap/scss/nav";
+@import "../../../node_modules/bootstrap/scss/navbar";
+@import "../../../node_modules/bootstrap/scss/card";
+@import "../../../node_modules/bootstrap/scss/breadcrumb";
+@import "../../../node_modules/bootstrap/scss/pagination";
+@import "../../../node_modules/bootstrap/scss/badge";
+@import "../../../node_modules/bootstrap/scss/alert";
+@import "../../../node_modules/bootstrap/scss/progress";
+@import "../../../node_modules/bootstrap/scss/media";
+@import "../../../node_modules/bootstrap/scss/list-group";
+@import "../../../node_modules/bootstrap/scss/close";
+@import "../../../node_modules/bootstrap/scss/modal";
+@import "../../../node_modules/bootstrap/scss/tooltip";
+@import "../../../node_modules/bootstrap/scss/popover";
+@import "../../../node_modules/bootstrap/scss/utilities";
+@import "../../../node_modules/bootstrap/scss/print";
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index e6303ad4642..ded33e8b151 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -89,11 +89,6 @@ a {
color: $gl-link-color;
}
-a:not(.btn):focus,
-a:not(.btn):active {
- text-decoration: underline;
-}
-
hr {
overflow: hidden;
}
@@ -133,6 +128,11 @@ 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,6 +178,12 @@ table {
display: none;
}
+h3.popover-header {
+ // Default bootstrap popovers use <h3>
+ // which we default to having a top margin
+ margin-top: 0;
+}
+
// Add to .label so that old system notes that are saved to the db
// will still receive the correct styling
.badge,
@@ -309,7 +315,7 @@ pre code {
color: $white-light;
h4,
- a,
+ a:not(.btn),
.alert-link {
color: $white-light;
}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 7c28024001f..c46b0b5db09 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -1,6 +1,7 @@
@import 'framework/variables';
@import 'framework/mixins';
-@import '../../../node_modules/bootstrap/scss/bootstrap';
+
+@import 'bootstrap';
@import 'bootstrap_migration';
@import 'framework/layout';
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/awards.scss b/app/assets/stylesheets/framework/awards.scss
index a538b5a2946..8d11b92cf88 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -104,6 +104,10 @@
position: relative;
top: 3px;
}
+
+ > gl-emoji {
+ line-height: 1.5;
+ }
}
.award-menu-holder {
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 0de05548c68..340fddd398b 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;
@@ -349,11 +350,6 @@
}
}
-.flex-container-block {
- display: -webkit-flex;
- display: flex;
-}
-
.flex-right {
margin-left: auto;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 326499125fc..218e37602dd 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -262,12 +262,7 @@ li.note {
}
.milestone {
- &.milestone-closed {
- background: $gray-light;
- }
-
.progress {
- margin-bottom: 0;
margin-top: 4px;
box-shadow: none;
background-color: $border-gray-light;
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index cccd1a6d942..ea4cb9a0b75 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -68,8 +68,7 @@
}
.nav-sidebar {
- transition: width $sidebar-transition-duration,
- left $sidebar-transition-duration;
+ transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
position: fixed;
z-index: 400;
width: $contextual-sidebar-width;
@@ -77,12 +76,12 @@
bottom: 0;
left: 0;
background-color: $gray-light;
- box-shadow: inset -2px 0 0 $border-color;
+ box-shadow: inset -1px 0 0 $border-color;
transform: translate3d(0, 0, 0);
&:not(.sidebar-collapsed-desktop) {
@media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
- box-shadow: inset -2px 0 0 $border-color,
+ box-shadow: inset -1px 0 0 $border-color,
2px 1px 3px $dropdown-shadow-color;
}
}
@@ -193,7 +192,6 @@
&:focus {
background: $link-active-background;
color: $gl-text-color;
- text-decoration: none;
}
}
@@ -215,7 +213,7 @@
> li {
> a {
@include media-breakpoint-up(sm) {
- margin-right: 2px;
+ margin-right: 1px;
}
&:hover {
@@ -225,7 +223,7 @@
&.is-showing-fly-out {
> a {
- margin-right: 2px;
+ margin-right: 1px;
}
.sidebar-sub-level-items {
@@ -318,14 +316,14 @@
.toggle-sidebar-button,
.close-nav-button {
- width: $contextual-sidebar-width - 2px;
+ width: $contextual-sidebar-width - 1px;
transition: width $sidebar-transition-duration;
position: fixed;
bottom: 0;
padding: $gl-padding;
background-color: $gray-light;
border: 0;
- border-top: 2px solid $border-color;
+ border-top: 1px solid $border-color;
color: $gl-text-color-secondary;
display: flex;
align-items: center;
@@ -380,7 +378,7 @@
.toggle-sidebar-button {
padding: 16px;
- width: $contextual-sidebar-collapsed-width - 2px;
+ width: $contextual-sidebar-collapsed-width - 1px;
.collapse-text,
.icon-angle-double-left {
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 527e7d57c5c..3cde0490371 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -4,4 +4,5 @@ gl-emoji {
vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1.5em;
+ line-height: 0.9;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index f060254777c..00eac1688f2 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -322,14 +322,17 @@ span.idiff {
}
.file-title-flex-parent {
- display: flex;
- align-items: center;
- justify-content: space-between;
- background-color: $gray-light;
- border-bottom: 1px solid $border-color;
- padding: 5px $gl-padding;
- margin: 0;
- border-radius: $border-radius-default $border-radius-default 0 0;
+ &,
+ .file-holder & {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: $gray-light;
+ border-bottom: 1px solid $border-color;
+ padding: 5px $gl-padding;
+ margin: 0;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ }
.file-header-content {
white-space: nowrap;
@@ -337,6 +340,17 @@ span.idiff {
text-overflow: ellipsis;
padding-right: 30px;
position: relative;
+ width: auto;
+
+ @media (max-width: map-get($grid-breakpoints, sm)-1) {
+ width: 100%;
+ }
+ }
+
+ .file-holder & {
+ .file-actions {
+ position: static;
+ }
}
.btn-clipboard {
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index a6e324036ae..e4bcb92876d 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -42,7 +42,7 @@
display: inline-block;
}
- a.flash-action {
+ .flash-action {
margin-left: 5px;
text-decoration: none;
font-weight: $gl-font-weight-normal;
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 2b2e6d69e33..282e424fc38 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -243,3 +243,15 @@ label {
}
}
}
+
+.input-icon-wrapper {
+ position: relative;
+
+ .input-icon-right {
+ position: absolute;
+ right: 0.8em;
+ top: 50%;
+ transform: translateY(-50%);
+ color: $theme-gray-600;
+ }
+}
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index b40d02f381a..aaa8bed3df0 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -180,10 +180,6 @@
color: $border-and-box-shadow;
}
- .ide-file-list .file.file-active {
- color: $border-and-box-shadow;
- }
-
.ide-sidebar-link {
&.active {
color: $border-and-box-shadow;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index db59c91e375..8bcaf5eb6ac 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -268,8 +268,6 @@
.navbar-sub-nav,
.navbar-nav {
- align-items: center;
-
> li {
> a:hover,
> a:focus {
@@ -527,7 +525,7 @@
.header-user {
.dropdown-menu {
width: auto;
- min-width: 160px;
+ min-width: unset;
margin-top: 4px;
color: $gl-text-color;
left: auto;
@@ -539,6 +537,10 @@
display: block;
}
}
+
+ svg {
+ vertical-align: text-top;
+ }
}
}
@@ -558,7 +560,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/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 1d247671761..86de88729ee 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -45,4 +45,9 @@
&.status-box-upcoming {
background: $gl-text-color-secondary;
}
+
+ &.status-box-milestone {
+ color: $gl-text-color;
+ background: $gray-darker;
+ }
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index f30f296d41f..7808f6d3a25 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -233,7 +233,7 @@ $md-area-border: #ddd;
/*
* Code
*/
-$code_font_size: 12px;
+$code_font_size: 90%;
$code_line_height: 1.6;
/*
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index 514fac82b1e..161943766d4 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -93,6 +93,10 @@
font-size: 12px;
}
}
+
+ svg {
+ vertical-align: text-top;
+ }
}
.light-well {
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/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
index 8cc5252648d..90a5250c247 100644
--- a/app/assets/stylesheets/highlight/white_base.scss
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -102,7 +102,9 @@ pre.code,
// Diff line
.line_holder {
- &.match .line_content {
+ &.match .line_content,
+ .new-nonewline.line_content,
+ .old-nonewline.line_content {
@include matchLine;
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 7c1d1626f1c..750d2c8b990 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -290,10 +290,6 @@
&.is-active,
&.is-active .board-card-assignee:hover a {
background-color: $row-hover;
-
- &:first-child:not(:only-child) {
- box-shadow: -10px 0 10px 1px $row-hover;
- }
}
.badge {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 49226ae8eac..f75be4e01cd 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -261,12 +261,16 @@
vertical-align: baseline;
}
- a.autodevops-badge {
- color: $white-light;
- }
+ a {
+ color: $gl-text-color;
- a.autodevops-link {
- color: $gl-link-color;
+ &.autodevops-badge {
+ color: $white-light;
+ }
+
+ &.autodevops-link {
+ color: $gl-link-color;
+ }
}
.commit-row-description {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index fbc97ec0c95..7e89f8998fb 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -14,8 +14,8 @@
background-color: $gray-normal;
}
- .diff-toggle-caret {
- padding-right: 6px;
+ svg {
+ vertical-align: middle;
}
}
@@ -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 {
@@ -491,6 +502,10 @@
border-bottom: 0;
}
+.merge-request-details .file-content.image_file img {
+ max-height: 50vh;
+}
+
.diff-stats-summary-toggler {
padding: 0;
background-color: transparent;
@@ -677,21 +692,22 @@
}
@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: 200;
+ margin: $gl-padding 0;
+ padding: 0;
}
&.is-stuck {
padding-top: 0;
padding-bottom: 0;
+ border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
- transform: translateY(16px);
.diff-stats-additions-deletions-expanded,
.inline-parallel-buttons {
@@ -721,6 +737,10 @@
max-width: 560px;
width: 100%;
z-index: 150;
+ min-height: $dropdown-min-height;
+ max-height: $dropdown-max-height;
+ overflow-y: auto;
+ margin-bottom: 0;
@include media-breakpoint-up(sm) {
left: $gl-padding;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 06f08ae2215..199039f38f7 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -222,6 +222,23 @@
}
}
+.prometheus-graphs {
+ .environments {
+ .dropdown-menu-toggle {
+ svg {
+ position: absolute;
+ right: 5%;
+ top: 25%;
+ }
+ }
+
+ .dropdown-menu-toggle,
+ .dropdown-menu {
+ width: 240px;
+ }
+ }
+}
+
.environments-actions {
.external-url,
.monitoring-url,
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index f9fd9f1ab8b..f6617380cc0 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -449,6 +449,7 @@
.todo-undone {
color: $gl-link-color;
+ fill: $gl-link-color;
}
.author {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 79cac7f4ff0..391dfea0703 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -79,6 +79,7 @@
justify-content: space-between;
padding: $gl-padding;
border-radius: $border-radius-default;
+ border: 1px solid $theme-gray-100;
&.sortable-ghost {
opacity: 0.3;
@@ -89,6 +90,7 @@
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
+ border: 0;
&:active {
cursor: -webkit-grabbing;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 596d3aa171c..efd730af558 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -46,7 +46,6 @@
.btn {
font-size: $gl-font-size;
- max-height: 26px;
&[disabled] {
opacity: 0.3;
@@ -600,14 +599,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 {
@@ -740,6 +737,10 @@
> *:not(:last-child) {
margin-right: .3em;
}
+
+ svg {
+ vertical-align: text-top;
+ }
}
.deploy-link {
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index dba83e56d72..46437ce5841 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -3,8 +3,20 @@
}
.milestones {
+ padding: $gl-padding-8;
+ margin-top: $gl-padding-8;
+ border-radius: $border-radius-default;
+ background-color: $theme-gray-100;
+
.milestone {
- padding: 10px 16px;
+ border: 0;
+ padding: $gl-padding-top $gl-padding;
+ border-radius: $border-radius-default;
+ background-color: $white-light;
+
+ &:not(:last-child) {
+ margin-bottom: $gl-padding-4;
+ }
h4 {
font-weight: $gl-font-weight-bold;
@@ -13,6 +25,24 @@
.progress {
width: 100%;
height: 6px;
+ margin-bottom: $gl-padding-4;
+ }
+
+ .milestone-progress {
+ a {
+ color: $gl-link-color;
+ }
+ }
+
+ .status-box {
+ font-size: $tooltip-font-size;
+ margin-top: 0;
+ margin-right: $gl-padding-4;
+
+ @include media-breakpoint-down(xs) {
+ line-height: unset;
+ padding: $gl-padding-4 $gl-input-padding;
+ }
}
}
}
@@ -229,6 +259,10 @@
}
}
+.milestone-range {
+ color: $gl-text-color-tertiary;
+}
+
@include media-breakpoint-down(xs) {
.milestone-banner-text,
.milestone-banner-link {
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..32d14049067 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;
}
@@ -716,7 +721,7 @@ ul.notes {
.line-resolve-all {
vertical-align: middle;
display: inline-block;
- padding: 6px 10px;
+ padding: 5px 10px 6px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
@@ -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/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 0a56153203c..3c24aaa65e8 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -23,6 +23,7 @@
margin-top: 0;
border-top: 1px solid $white-dark;
padding-bottom: $ide-statusbar-height;
+ color: $gl-text-color;
&.is-collapsed {
.ide-file-list {
@@ -45,12 +46,8 @@
.file {
cursor: pointer;
- &.file-open {
- background: $white-normal;
- }
-
&.file-active {
- font-weight: $gl-font-weight-bold;
+ background: $theme-gray-100;
}
.ide-file-name {
@@ -58,7 +55,9 @@
white-space: nowrap;
text-overflow: ellipsis;
max-width: inherit;
- line-height: 22px;
+ line-height: 16px;
+ display: inline-block;
+ height: 18px;
svg {
vertical-align: middle;
@@ -86,12 +85,14 @@
.ide-new-btn {
display: none;
+
+ .btn {
+ padding: 2px 5px;
+ }
}
&:hover,
&:focus {
- background: $white-normal;
-
.ide-new-btn {
display: block;
}
@@ -281,8 +282,8 @@
}
.margin {
- background-color: $gray-light;
- border-right: 1px solid $white-normal;
+ background-color: $white-light;
+ border-right: 1px solid $theme-gray-100;
.line-insert {
border-right: 1px solid $line-added-dark;
@@ -303,6 +304,15 @@
.multi-file-editor-holder {
height: 100%;
min-height: 0;
+
+ &.is-readonly,
+ .editor.original {
+ .monaco-editor,
+ .monaco-editor-background,
+ .monaco-editor .inputarea.ime-input {
+ background-color: $theme-gray-50;
+ }
+ }
}
.preview-container {
@@ -587,11 +597,17 @@
&:hover,
&:focus {
- background: $white-normal;
+ background: $theme-gray-100;
+ }
+
+ &:active {
+ background: $theme-gray-200;
}
}
.multi-file-commit-list-path {
+ cursor: pointer;
+
&.is-active {
background-color: $white-normal;
}
@@ -611,10 +627,6 @@
.multi-file-commit-list-file-path {
@include str-truncated(calc(100% - 30px));
- &:hover {
- text-decoration: underline;
- }
-
&:active {
text-decoration: none;
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 765c926751a..2d66f336076 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -114,7 +114,7 @@ input[type="checkbox"]:hover {
}
.dropdown-content {
- max-height: 302px;
+ max-height: none;
}
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 2f28031b9c8..839ac5ba59b 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -191,6 +191,22 @@
}
}
+.initialize-with-readme-setting {
+ .form-check {
+ margin-bottom: 10px;
+
+ .option-title {
+ font-weight: $gl-font-weight-normal;
+ display: inline-block;
+ color: $gl-text-color;
+ }
+
+ .option-description {
+ color: $project-option-descr-color;
+ }
+ }
+}
+
.prometheus-metrics-monitoring {
.card {
.card-toggle {
@@ -255,25 +271,12 @@
}
}
-.modal-doorkeepr-auth,
-.doorkeeper-app-form {
- .scope-description {
- color: $theme-gray-700;
- }
-}
-
.modal-doorkeepr-auth {
.modal-body {
padding: $gl-padding;
}
}
-.doorkeeper-app-form {
- .scope-description {
- margin: 0 0 5px 17px;
- }
-}
-
.deprecated-service {
cursor: default;
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index e5d7dd13915..010a2c05a1c 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -174,6 +174,18 @@
}
}
+@include media-breakpoint-down(lg) {
+ .todos-filters {
+ .filter-categories {
+ width: 75%;
+
+ .filter-item {
+ margin-bottom: 10px;
+ }
+ }
+ }
+}
+
@include media-breakpoint-down(xs) {
.todo {
.avatar {
@@ -199,6 +211,10 @@
}
.todos-filters {
+ .filter-categories {
+ width: auto;
+ }
+
.dropdown-menu-toggle {
width: 100%;
}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 5127ddfde6e..7a93c4dfa28 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -7,7 +7,6 @@
top: 0;
width: 100%;
z-index: 2000;
- overflow-x: hidden;
height: $performance-bar-height;
background: $black;
@@ -82,7 +81,7 @@
.view {
margin-right: 15px;
- float: left;
+ flex-shrink: 0;
&:last-child {
margin-right: 0;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index cdfe3d6ab1e..9723e400574 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -52,7 +52,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
private
def set_application_setting
- @application_setting = ApplicationSetting.current_without_cache
+ @application_setting = Gitlab::CurrentSettings.current_application_settings
end
def application_setting_params
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 001f6520093..96b7bc65ac9 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -72,10 +72,10 @@ class Admin::GroupsController < Admin::ApplicationController
end
def group_params
- params.require(:group).permit(group_params_ce)
+ params.require(:group).permit(allowed_group_params)
end
- def group_params_ce
+ def allowed_group_params
[
:avatar,
:description,
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index fb788c47ef1..6944857bd33 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -52,8 +52,7 @@ class Admin::HooksController < Admin::ApplicationController
end
def hook_logs
- @hook_logs ||=
- Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
+ @hook_logs ||= hook.web_hook_logs.recent.page(params[:page])
end
def hook_params
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index bfeb5a2d097..653f3dfffc4 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -187,10 +187,10 @@ class Admin::UsersController < Admin::ApplicationController
end
def user_params
- params.require(:user).permit(user_params_ce)
+ params.require(:user).permit(allowed_user_params)
end
- def user_params_ce
+ def allowed_user_params
[
:access_level,
:avatar,
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 56312f801fb..21cc6dfdd16 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -27,7 +27,7 @@ class ApplicationController < ActionController::Base
after_action :set_page_title_header, if: -> { request.format == :json }
- protect_from_forgery with: :exception
+ protect_from_forgery with: :exception, prepend: true
helper_method :can?
helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
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/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/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index 90bb7a87b45..7ac63c914fa 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -10,6 +10,7 @@ module PreviewMarkdown
when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
when 'snippets' then { skip_project_check: true }
when 'groups' then { group: group }
+ when 'projects' then { issuable_state_filter_enabled: true }
else {}
end
diff --git a/app/controllers/concerns/todos_actions.rb b/app/controllers/concerns/todos_actions.rb
new file mode 100644
index 00000000000..c0acdb3498d
--- /dev/null
+++ b/app/controllers/concerns/todos_actions.rb
@@ -0,0 +1,12 @@
+module TodosActions
+ extend ActiveSupport::Concern
+
+ def create
+ todo = TodoService.new.mark_todo(issuable, current_user)
+
+ render json: {
+ count: TodosFinder.new(current_user, state: :pending).execute.count,
+ delete_path: dashboard_todo_path(todo)
+ }
+ end
+end
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index 16374146ae4..434459a225a 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -45,6 +45,16 @@ module UploadsActions
send_upload(uploader, attachment: uploader.filename, disposition: disposition)
end
+ def authorize
+ set_workhorse_internal_api_content_type
+
+ authorized = uploader_class.workhorse_authorize(
+ has_length: false,
+ maximum_size: Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i)
+
+ render json: authorized
+ end
+
private
# Explicitly set the format.
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 4d4ac025f8c..ccfcbbdc776 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -7,7 +7,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
skip_cross_project_access_check :index, :starred
def index
- @projects = load_projects(params.merge(non_public: true)).page(params[:page])
+ @projects = load_projects(params.merge(non_public: true))
respond_to do |format|
format.html
@@ -25,7 +25,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
def starred
@projects = load_projects(params.merge(starred: true))
- .includes(:forked_from_project, :tags).page(params[:page])
+ .includes(:forked_from_project, :tags)
@groups = []
@@ -51,6 +51,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
.new(params: finder_params, current_user: current_user)
.execute
.includes(:route, :creator, namespace: [:route, :owner])
+ .page(finder_params[:page])
prepare_projects_for_rendering(projects)
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index f9e8fe624e8..bd7111e28bc 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -70,7 +70,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def todo_params
- params.permit(:action_id, :author_id, :project_id, :type, :sort, :state)
+ params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
end
def redirect_out_of_range(todos)
diff --git a/app/controllers/groups/uploads_controller.rb b/app/controllers/groups/uploads_controller.rb
index f1578f75e88..74760194a1f 100644
--- a/app/controllers/groups/uploads_controller.rb
+++ b/app/controllers/groups/uploads_controller.rb
@@ -1,9 +1,11 @@
class Groups::UploadsController < Groups::ApplicationController
include UploadsActions
+ include WorkhorseRequest
skip_before_action :group, if: -> { action_name == 'show' && image_or_video? }
- before_action :authorize_upload_file!, only: [:create]
+ before_action :authorize_upload_file!, only: [:create, :authorize]
+ before_action :verify_workhorse_api!, only: [:authorize]
private
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index 16abf7bab7e..3fedd5bfb29 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -1,5 +1,5 @@
class HealthController < ActionController::Base
- protect_from_forgery with: :exception, except: :storage_check
+ protect_from_forgery with: :exception, except: :storage_check, prepend: true
include RequiresWhitelistedMonitoringClient
CHECKS = [
@@ -8,7 +8,6 @@ class HealthController < ActionController::Base
Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck,
Gitlab::HealthChecks::Redis::SharedStateCheck,
- Gitlab::HealthChecks::FsShardsCheck,
Gitlab::HealthChecks::GitalyCheck
].freeze
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index 33b682d2859..0400ffcfee5 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -1,7 +1,7 @@
class MetricsController < ActionController::Base
include RequiresWhitelistedMonitoringClient
- protect_from_forgery with: :exception
+ protect_from_forgery with: :exception, prepend: true
def index
response = if Gitlab::Metrics.prometheus_metrics_enabled?
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 27fd5f7ba37..1547d4b5972 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -2,7 +2,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
- protect_from_forgery except: [:kerberos, :saml, :cas3]
+ protect_from_forgery except: [:kerberos, :saml, :cas3], prepend: true
def handle_omniauth
omniauth_flow(Gitlab::Auth::OAuth)
@@ -119,7 +119,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
set_remember_me(user)
- if user.two_factor_enabled?
+ if user.two_factor_enabled? && !auth_user.bypass_two_factor?
prompt_for_two_factor(user)
else
sign_in_and_redirect(user)
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index a8c0a68fc17..ebc61264b39 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -3,8 +3,8 @@ 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]
@@ -93,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?
@@ -103,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)
diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb
deleted file mode 100644
index c2c5ad61e01..00000000000
--- a/app/controllers/projects/clusters/gcp_controller.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-class Projects::Clusters::GcpController < Projects::ApplicationController
- before_action :authorize_read_cluster!
- before_action :authorize_create_cluster!, only: [:new, :create]
- before_action :authorize_google_api, except: :login
- helper_method :token_in_session
-
- def login
- begin
- state = generate_session_key_redirect(gcp_new_namespace_project_clusters_path.to_s)
-
- @authorize_url = GoogleApi::CloudPlatform::Client.new(
- nil, callback_google_api_auth_url,
- state: state).authorize_url
- rescue GoogleApi::Auth::ConfigMissingError
- # no-op
- end
- end
-
- def new
- @cluster = ::Clusters::Cluster.new.tap do |cluster|
- cluster.build_provider_gcp
- end
- end
-
- def create
- @cluster = ::Clusters::CreateService
- .new(project, current_user, create_params)
- .execute(token_in_session)
-
- if @cluster.persisted?
- redirect_to project_cluster_path(project, @cluster)
- else
- render :new
- end
- end
-
- private
-
- def create_params
- params.require(:cluster).permit(
- :enabled,
- :name,
- :environment_scope,
- provider_gcp_attributes: [
- :gcp_project_id,
- :zone,
- :num_nodes,
- :machine_type
- ]).merge(
- provider_type: :gcp,
- platform_type: :kubernetes
- )
- end
-
- def authorize_google_api
- unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
- .validate_token(expires_at_in_session)
- redirect_to action: 'login'
- end
- end
-
- def token_in_session
- session[GoogleApi::CloudPlatform::Client.session_key_for_token]
- end
-
- def expires_at_in_session
- @expires_at_in_session ||=
- session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
- end
-
- def generate_session_key_redirect(uri)
- GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
- session[key] = uri
- end
- end
-end
diff --git a/app/controllers/projects/clusters/user_controller.rb b/app/controllers/projects/clusters/user_controller.rb
deleted file mode 100644
index d0db64b2fa9..00000000000
--- a/app/controllers/projects/clusters/user_controller.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-class Projects::Clusters::UserController < Projects::ApplicationController
- before_action :authorize_read_cluster!
- before_action :authorize_create_cluster!, only: [:new, :create]
-
- def new
- @cluster = ::Clusters::Cluster.new.tap do |cluster|
- cluster.build_platform_kubernetes
- end
- end
-
- def create
- @cluster = ::Clusters::CreateService
- .new(project, current_user, create_params)
- .execute
-
- if @cluster.persisted?
- redirect_to project_cluster_path(project, @cluster)
- else
- render :new
- end
- end
-
- private
-
- def create_params
- params.require(:cluster).permit(
- :enabled,
- :name,
- :environment_scope,
- platform_kubernetes_attributes: [
- :namespace,
- :api_url,
- :token,
- :ca_cert
- ]).merge(
- provider_type: :user,
- platform_type: :kubernetes
- )
- end
-end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index d58039b7d42..62193257940 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -1,10 +1,15 @@
class Projects::ClustersController < Projects::ApplicationController
- before_action :cluster, except: [:index, :new]
+ before_action :cluster, except: [:index, :new, :create_gcp, :create_user]
before_action :authorize_read_cluster!
+ before_action :generate_gcp_authorize_url, only: [:new]
+ before_action :validate_gcp_token, only: [:new]
+ before_action :gcp_cluster, only: [:new]
+ before_action :user_cluster, only: [:new]
before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:status]
+ helper_method :token_in_session
STATUS_POLLING_INTERVAL = 10_000
@@ -64,6 +69,38 @@ class Projects::ClustersController < Projects::ApplicationController
end
end
+ def create_gcp
+ @gcp_cluster = ::Clusters::CreateService
+ .new(project, current_user, create_gcp_cluster_params)
+ .execute(token_in_session)
+
+ if @gcp_cluster.persisted?
+ redirect_to project_cluster_path(project, @gcp_cluster)
+ else
+ generate_gcp_authorize_url
+ validate_gcp_token
+ user_cluster
+
+ render :new, locals: { active_tab: 'gcp' }
+ end
+ end
+
+ def create_user
+ @user_cluster = ::Clusters::CreateService
+ .new(project, current_user, create_user_cluster_params)
+ .execute(token_in_session)
+
+ if @user_cluster.persisted?
+ redirect_to project_cluster_path(project, @user_cluster)
+ else
+ generate_gcp_authorize_url
+ validate_gcp_token
+ gcp_cluster
+
+ render :new, locals: { active_tab: 'user' }
+ end
+ end
+
private
def cluster
@@ -95,6 +132,80 @@ class Projects::ClustersController < Projects::ApplicationController
end
end
+ def create_gcp_cluster_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :environment_scope,
+ provider_gcp_attributes: [
+ :gcp_project_id,
+ :zone,
+ :num_nodes,
+ :machine_type
+ ]).merge(
+ provider_type: :gcp,
+ platform_type: :kubernetes
+ )
+ end
+
+ def create_user_cluster_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :environment_scope,
+ platform_kubernetes_attributes: [
+ :namespace,
+ :api_url,
+ :token,
+ :ca_cert
+ ]).merge(
+ provider_type: :user,
+ platform_type: :kubernetes
+ )
+ end
+
+ def generate_gcp_authorize_url
+ state = generate_session_key_redirect(new_project_cluster_path(@project).to_s)
+
+ @authorize_url = GoogleApi::CloudPlatform::Client.new(
+ nil, callback_google_api_auth_url,
+ state: state).authorize_url
+ rescue GoogleApi::Auth::ConfigMissingError
+ # no-op
+ end
+
+ def gcp_cluster
+ @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
+ cluster.build_provider_gcp
+ end
+ end
+
+ def user_cluster
+ @user_cluster = ::Clusters::Cluster.new.tap do |cluster|
+ cluster.build_platform_kubernetes
+ end
+ end
+
+ def validate_gcp_token
+ @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
+ .validate_token(expires_at_in_session)
+ end
+
+ def token_in_session
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token]
+ end
+
+ def expires_at_in_session
+ @expires_at_in_session ||=
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
+ end
+
+ def generate_session_key_redirect(uri)
+ GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
+ session[key] = uri
+ end
+ end
+
def authorize_update_cluster!
access_denied! unless can?(current_user, :update_cluster, cluster)
end
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/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 0821362f5df..27b7425b965 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -120,6 +120,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
+ def metrics_redirect
+ environment = project.default_environment
+
+ if environment
+ redirect_to environment_metrics_path(environment)
+ else
+ render :empty
+ end
+ end
+
def metrics
# Currently, this acts as a hint to load the metrics details into the cache
# if they aren't there already
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index dd7aa1a67b9..6800d742b0a 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -58,8 +58,7 @@ class Projects::HooksController < Projects::ApplicationController
end
def hook_logs
- @hook_logs ||=
- Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
+ @hook_logs ||= hook.web_hook_logs.recent.page(params[:page])
end
def hook_params
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index dd12d30a085..e69faae754a 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -2,11 +2,12 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload
before_action :build, except: [:index, :cancel_all]
- before_action :authorize_read_build!,
- only: [:index, :show, :status, :raw, :trace]
+ before_action :authorize_read_build!
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
before_action :authorize_erase_build!, only: [:erase]
+ before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_workhorse_authorize]
+ before_action :verify_api_request!, only: :terminal_websocket_authorize
layout 'project'
@@ -44,12 +45,10 @@ class Projects::JobsController < Projects::ApplicationController
end
def show
- @builds = @project.pipelines
- .find_by_sha(@build.sha)
- .builds
+ @pipeline = @build.pipeline
+ @builds = @pipeline.builds
.order('id DESC')
.present(current_user: current_user)
- @pipeline = @build.pipeline
respond_to do |format|
format.html
@@ -136,6 +135,15 @@ class Projects::JobsController < Projects::ApplicationController
end
end
+ def terminal
+ end
+
+ # GET .../terminal.ws : implemented in gitlab-workhorse
+ def terminal_websocket_authorize
+ set_workhorse_internal_api_content_type
+ render json: Gitlab::Workhorse.terminal_websocket(@build.terminal_specification)
+ end
+
private
def authorize_update_build!
@@ -146,6 +154,14 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :erase_build, build)
end
+ def authorize_use_build_terminal!
+ return access_denied! unless can?(current_user, :create_build_terminal, build)
+ end
+
+ def verify_api_request!
+ Gitlab::Workhorse.verify_api_request!(request.headers)
+ end
+
def raw_send_params
{ type: 'text/plain; charset=utf-8', disposition: 'inline' }
end
@@ -160,7 +176,7 @@ class Projects::JobsController < Projects::ApplicationController
def build
@build ||= project.builds.find(params[:id])
- .present(current_user: current_user)
+ .present(current_user: current_user)
end
def build_path(build)
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index ee4ed674110..3f4962b543d 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -93,7 +93,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
end
def lfs_check_batch_operation!
- if upload_request? && Gitlab::Database.read_only?
+ if batch_operation_disallowed?
render(
json: {
message: lfs_read_only_message
@@ -105,6 +105,11 @@ class Projects::LfsApiController < Projects::GitHttpClientController
end
# Overridden in EE
+ def batch_operation_disallowed?
+ upload_request? && Gitlab::Database.read_only?
+ end
+
+ # Overridden in EE
def lfs_read_only_message
_('You cannot write to this read-only GitLab instance.')
end
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 38918b3cd52..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)
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index f85dcfe6bfc..594563d1f6f 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -77,7 +77,7 @@ class Projects::MilestonesController < Projects::ApplicationController
def promote
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
- flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\">group milestone</a>.".html_safe
+ flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\"><u>group milestone</u></a>.".html_safe
respond_to do |format|
format.html do
redirect_to project_milestones_path(project)
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 768595ceeb4..45cef123c34 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -13,7 +13,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
@pipelines = PipelinesFinder
- .new(project, scope: @scope)
+ .new(project, current_user, scope: @scope)
.execute
.page(params[:page])
.per(30)
@@ -178,7 +178,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def limited_pipelines_count(project, scope = nil)
- finder = PipelinesFinder.new(project, scope: scope)
+ finder = PipelinesFinder.new(project, current_user, scope: scope)
view_context.limited_counter_with_delimiter(finder.execute)
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index fb3f6eec2bd..322ec096ffb 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -74,7 +74,7 @@ module Projects
.ordered
.page(params[:page]).per(20)
- @shared_runners = ::Ci::Runner.shared.active
+ @shared_runners = ::Ci::Runner.instance_type.active
@shared_runners_count = @shared_runners.count(:all)
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index a41fcb85c40..93fb9da6510 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -1,19 +1,13 @@
class Projects::TodosController < Projects::ApplicationController
- before_action :authenticate_user!, only: [:create]
-
- def create
- todo = TodoService.new.mark_todo(issuable, current_user)
+ include Gitlab::Utils::StrongMemoize
+ include TodosActions
- render json: {
- count: TodosFinder.new(current_user, state: :pending).execute.count,
- delete_path: dashboard_todo_path(todo)
- }
- end
+ before_action :authenticate_user!, only: [:create]
private
def issuable
- @issuable ||= begin
+ strong_memoize(:issuable) do
case params[:issuable_type]
when "issue"
IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index f5cf089ad98..7a85046164c 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -1,11 +1,13 @@
class Projects::UploadsController < Projects::ApplicationController
include UploadsActions
+ include WorkhorseRequest
# These will kick you out if you don't have access.
skip_before_action :project, :repository,
if: -> { action_name == 'show' && image_or_video? }
- before_action :authorize_upload_file!, only: [:create]
+ before_action :authorize_upload_file!, only: [:create, :authorize]
+ before_action :verify_workhorse_api!, only: [:authorize]
private
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 242e6491456..aa844e94d89 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -95,6 +95,7 @@ class Projects::WikisController < Projects::ApplicationController
def destroy
@page = @project_wiki.find_page(params[:id])
+
WikiPages::DestroyService.new(@project, current_user).execute(@page)
redirect_to project_wiki_path(@project, :home),
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index efb30ba4715..ec3a5788ba1 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -63,7 +63,7 @@ class ProjectsController < Projects::ApplicationController
redirect_to(edit_project_path(@project))
end
else
- flash[:alert] = result[:message]
+ flash.now[:alert] = result[:message]
format.html { render 'edit' }
end
@@ -347,6 +347,7 @@ class ProjectsController < Projects::ApplicationController
:visibility_level,
:template_name,
:merge_method,
+ :initialize_with_readme,
project_feature_attributes: %i[
builds_access_level
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1a339f76d26..1de6ae24622 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -3,21 +3,27 @@ class SessionsController < Devise::SessionsController
include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
include Recaptcha::ClientHelper
+ include Recaptcha::Verify
skip_before_action :check_two_factor_requirement, only: [:destroy]
prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create]
+ prepend_before_action :check_captcha, only: [:create]
prepend_before_action :store_redirect_uri, only: [:new]
+ prepend_before_action :ldap_servers, only: [:new, :create]
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
after_action :log_failed_login, only: [:new], if: :failed_login?
+ helper_method :captcha_enabled?
+
+ CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha'.freeze
+
def new
set_minimum_password_length
- @ldap_servers = Gitlab::Auth::LDAP::Config.available_servers
super
end
@@ -46,6 +52,43 @@ class SessionsController < Devise::SessionsController
private
+ def captcha_enabled?
+ request.headers[CAPTCHA_HEADER] && Gitlab::Recaptcha.enabled?
+ end
+
+ # From https://github.com/plataformatec/devise/wiki/How-To:-Use-Recaptcha-with-Devise#devisepasswordscontroller
+ def check_captcha
+ return unless user_params[:password].present?
+ return unless captcha_enabled?
+ return unless Gitlab::Recaptcha.load_configurations!
+
+ if verify_recaptcha
+ increment_successful_login_captcha_counter
+ else
+ increment_failed_login_captcha_counter
+
+ self.resource = resource_class.new
+ flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ flash.delete :recaptcha_error
+
+ respond_with_navigational(resource) { render :new }
+ end
+ end
+
+ def increment_failed_login_captcha_counter
+ Gitlab::Metrics.counter(
+ :failed_login_captcha_total,
+ 'Number of failed CAPTCHA attempts for logins'.freeze
+ ).increment
+ end
+
+ def increment_successful_login_captcha_counter
+ Gitlab::Metrics.counter(
+ :successful_login_captcha_total,
+ 'Number of successful CAPTCHA attempts for logins'.freeze
+ ).increment
+ end
+
def log_failed_login
Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}")
end
@@ -152,6 +195,10 @@ class SessionsController < Devise::SessionsController
Gitlab::Recaptcha.load_configurations!
end
+ def ldap_servers
+ @ldap_servers ||= Gitlab::Auth::LDAP::Config.available_servers
+ end
+
def authentication_method
if user_params[:otp_attempt]
"two-factor"
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 5d5f72c4d86..6fdfd964fca 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -7,7 +7,7 @@
# current_user - which user use
# params:
# scope: 'created_by_me' or 'assigned_to_me' or 'all'
-# state: 'opened' or 'closed' or 'all'
+# state: 'opened' or 'closed' or 'locked' or 'all'
# group_id: integer
# project_id: integer
# milestone_title: string
@@ -311,6 +311,8 @@ class IssuableFinder
items.respond_to?(:merged) ? items.merged : items.closed
when 'opened'
items.opened
+ when 'locked'
+ items.where(state: 'locked')
else
items
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 8d84ed4bdfb..40089c082c1 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -6,7 +6,7 @@
# current_user - which user use
# params:
# scope: 'created_by_me' or 'assigned_to_me' or 'all'
-# state: 'open', 'closed', 'merged', or 'all'
+# state: 'open', 'closed', 'merged', 'locked', or 'all'
# group_id: integer
# project_id: integer
# milestone_title: string
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/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index 0a487839aff..a99a889a7e9 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -1,15 +1,20 @@
class PipelinesFinder
- attr_reader :project, :pipelines, :params
+ attr_reader :project, :pipelines, :params, :current_user
ALLOWED_INDEXED_COLUMNS = %w[id status ref user_id].freeze
- def initialize(project, params = {})
+ def initialize(project, current_user, params = {})
@project = project
+ @current_user = current_user
@pipelines = project.pipelines
@params = params
end
def execute
+ unless Ability.allowed?(current_user, :read_pipeline, project)
+ return Ci::Pipeline.none
+ end
+
items = pipelines
items = by_scope(items)
items = by_status(items)
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 09e2c586f2a..2156413fb26 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -15,6 +15,7 @@
class TodosFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
+ include Gitlab::Utils::StrongMemoize
requires_cross_project_access unless: -> { project? }
@@ -34,9 +35,11 @@ class TodosFinder
items = by_author(items)
items = by_state(items)
items = by_type(items)
+ items = by_group(items)
# Filtering by project HAS TO be the last because we use
# the project IDs yielded by the todos query thus far
items = by_project(items)
+ items = visible_to_user(items)
sort(items)
end
@@ -82,6 +85,10 @@ class TodosFinder
params[:project_id].present?
end
+ def group?
+ params[:group_id].present?
+ end
+
def project
return @project if defined?(@project)
@@ -100,18 +107,14 @@ class TodosFinder
@project
end
- def project_ids(items)
- ids = items.except(:order).select(:project_id)
- if Gitlab::Database.mysql?
- # To make UPDATE work on MySQL, wrap it in a SELECT with an alias
- ids = Todo.except(:order).select('*').from("(#{ids.to_sql}) AS t")
+ def group
+ strong_memoize(:group) do
+ Group.find(params[:group_id])
end
-
- ids
end
def type?
- type.present? && %w(Issue MergeRequest).include?(type)
+ type.present? && %w(Issue MergeRequest Epic).include?(type)
end
def type
@@ -148,12 +151,37 @@ class TodosFinder
def by_project(items)
if project?
- items.where(project: project)
- else
- projects = Project.public_or_visible_to_user(current_user)
+ items = items.where(project: project)
+ end
+
+ items
+ end
- items.joins(:project).merge(projects)
+ def by_group(items)
+ if group?
+ groups = group.self_and_descendants
+ items = items.where(
+ 'project_id IN (?) OR group_id IN (?)',
+ Project.where(group: groups).select(:id),
+ groups.select(:id)
+ )
end
+
+ items
+ end
+
+ def visible_to_user(items)
+ projects = Project.public_or_visible_to_user(current_user)
+ groups = Group.public_or_visible_to_user(current_user)
+
+ items
+ .joins('LEFT JOIN namespaces ON namespaces.id = todos.group_id')
+ .joins('LEFT JOIN projects ON projects.id = todos.project_id')
+ .where(
+ 'project_id IN (?) OR group_id IN (?)',
+ projects.select(:id),
+ groups.select(:id)
+ )
end
def by_state(items)
diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb
index 65d6e019746..74776b2ed1f 100644
--- a/app/finders/user_recent_events_finder.rb
+++ b/app/finders/user_recent_events_finder.rb
@@ -56,7 +56,7 @@ class UserRecentEventsFinder
visible = target_user
.project_interactions
- .where(visibility_level: [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC])
+ .where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(current_user))
.select(:id)
Gitlab::SQL::Union.new([authorized, visible]).to_sql
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index de4fc1d8e32..d9f9129d08a 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -2,7 +2,10 @@ class GitlabSchema < GraphQL::Schema
use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Present
+ use Gitlab::Graphql::Connections
query(Types::QueryType)
+
+ default_max_page_size 100
# mutation(Types::MutationType)
end
diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb
new file mode 100644
index 00000000000..9ec45378d8e
--- /dev/null
+++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb
@@ -0,0 +1,23 @@
+module ResolvesPipelines
+ extend ActiveSupport::Concern
+
+ included do
+ type [Types::Ci::PipelineType], null: false
+ argument :status,
+ Types::Ci::PipelineStatusEnum,
+ required: false,
+ description: "Filter pipelines by their status"
+ argument :ref,
+ GraphQL::STRING_TYPE,
+ required: false,
+ description: "Filter pipelines by the ref they are run for"
+ argument :sha,
+ GraphQL::STRING_TYPE,
+ required: false,
+ description: "Filter pipelines by the sha of the commit they are run for"
+ end
+
+ def resolve_pipelines(project, params = {})
+ PipelinesFinder.new(project, context[:current_user], params).execute
+ end
+end
diff --git a/app/graphql/resolvers/merge_request_pipelines_resolver.rb b/app/graphql/resolvers/merge_request_pipelines_resolver.rb
new file mode 100644
index 00000000000..00b51ee1381
--- /dev/null
+++ b/app/graphql/resolvers/merge_request_pipelines_resolver.rb
@@ -0,0 +1,16 @@
+module Resolvers
+ class MergeRequestPipelinesResolver < BaseResolver
+ include ::ResolvesPipelines
+
+ alias_method :merge_request, :object
+
+ def resolve(**args)
+ resolve_pipelines(project, args)
+ .merge(merge_request.all_pipelines)
+ end
+
+ def project
+ merge_request.source_project
+ end
+ end
+end
diff --git a/app/graphql/resolvers/project_pipelines_resolver.rb b/app/graphql/resolvers/project_pipelines_resolver.rb
new file mode 100644
index 00000000000..7f175a3b26c
--- /dev/null
+++ b/app/graphql/resolvers/project_pipelines_resolver.rb
@@ -0,0 +1,11 @@
+module Resolvers
+ class ProjectPipelinesResolver < BaseResolver
+ include ResolvesPipelines
+
+ alias_method :project, :object
+
+ def resolve(**args)
+ resolve_pipelines(project, args)
+ end
+ end
+end
diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb
index e033ef96ce9..754adf4c04d 100644
--- a/app/graphql/types/base_object.rb
+++ b/app/graphql/types/base_object.rb
@@ -1,6 +1,7 @@
module Types
class BaseObject < GraphQL::Schema::Object
prepend Gitlab::Graphql::Present
+ prepend Gitlab::Graphql::ExposePermissions
field_class Types::BaseField
end
diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb
new file mode 100644
index 00000000000..2c12e5001d8
--- /dev/null
+++ b/app/graphql/types/ci/pipeline_status_enum.rb
@@ -0,0 +1,9 @@
+module Types
+ module Ci
+ class PipelineStatusEnum < BaseEnum
+ ::Ci::Pipeline.all_state_names.each do |state_symbol|
+ value state_symbol.to_s.upcase, value: state_symbol.to_s
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
new file mode 100644
index 00000000000..bbb7d9354d0
--- /dev/null
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -0,0 +1,31 @@
+module Types
+ module Ci
+ class PipelineType < BaseObject
+ expose_permissions Types::PermissionTypes::Ci::Pipeline
+
+ graphql_name 'Pipeline'
+
+ field :id, GraphQL::ID_TYPE, null: false
+ field :iid, GraphQL::ID_TYPE, null: false
+
+ field :sha, GraphQL::STRING_TYPE, null: false
+ field :before_sha, GraphQL::STRING_TYPE, null: true
+ field :status, PipelineStatusEnum, null: false
+ field :duration,
+ GraphQL::INT_TYPE,
+ null: true,
+ description: "Duration of the pipeline in seconds"
+ field :coverage,
+ GraphQL::FLOAT_TYPE,
+ null: true,
+ description: "Coverage percentage"
+ field :created_at, Types::TimeType, null: false
+ field :updated_at, Types::TimeType, null: false
+ field :started_at, Types::TimeType, null: true
+ field :finished_at, Types::TimeType, null: true
+ field :committed_at, Types::TimeType, null: true
+
+ # TODO: Add triggering user as a type
+ end
+ end
+end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index d5d24952984..88cd2adc6dc 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -1,5 +1,7 @@
module Types
class MergeRequestType < BaseObject
+ expose_permissions Types::PermissionTypes::MergeRequest
+
present_using MergeRequestPresenter
graphql_name 'MergeRequest'
@@ -43,5 +45,11 @@ module Types
field :upvotes, GraphQL::INT_TYPE, null: false
field :downvotes, GraphQL::INT_TYPE, null: false
field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false
+
+ field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline do
+ authorize :read_pipeline
+ end
+ field :pipelines, Types::Ci::PipelineType.connection_type,
+ resolver: Resolvers::MergeRequestPipelinesResolver
end
end
diff --git a/app/graphql/types/permission_types/base_permission_type.rb b/app/graphql/types/permission_types/base_permission_type.rb
new file mode 100644
index 00000000000..934ed572e56
--- /dev/null
+++ b/app/graphql/types/permission_types/base_permission_type.rb
@@ -0,0 +1,38 @@
+module Types
+ module PermissionTypes
+ class BasePermissionType < BaseObject
+ extend Gitlab::Allowable
+
+ RESOLVING_KEYWORDS = [:resolver, :method, :hash_key, :function].to_set.freeze
+
+ def self.abilities(*abilities)
+ abilities.each { |ability| ability_field(ability) }
+ end
+
+ def self.ability_field(ability, **kword_args)
+ unless resolving_keywords?(kword_args)
+ kword_args[:resolve] ||= -> (object, args, context) do
+ can?(context[:current_user], ability, object, args.to_h)
+ end
+ end
+
+ permission_field(ability, **kword_args)
+ end
+
+ def self.permission_field(name, **kword_args)
+ kword_args = kword_args.reverse_merge(
+ name: name,
+ type: GraphQL::BOOLEAN_TYPE,
+ description: "Whether or not a user can perform `#{name}` on this resource",
+ null: false)
+
+ field(**kword_args)
+ end
+
+ def self.resolving_keywords?(arguments)
+ RESOLVING_KEYWORDS.intersect?(arguments.keys.to_set)
+ end
+ private_class_method :resolving_keywords?
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/ci/pipeline.rb b/app/graphql/types/permission_types/ci/pipeline.rb
new file mode 100644
index 00000000000..942539c7cf7
--- /dev/null
+++ b/app/graphql/types/permission_types/ci/pipeline.rb
@@ -0,0 +1,11 @@
+module Types
+ module PermissionTypes
+ module Ci
+ class Pipeline < BasePermissionType
+ graphql_name 'PipelinePermissions'
+
+ abilities :update_pipeline, :admin_pipeline, :destroy_pipeline
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb
new file mode 100644
index 00000000000..5c21f6ee9c6
--- /dev/null
+++ b/app/graphql/types/permission_types/merge_request.rb
@@ -0,0 +1,17 @@
+module Types
+ module PermissionTypes
+ class MergeRequest < BasePermissionType
+ present_using MergeRequestPresenter
+ description 'Check permissions for the current user on a merge request'
+ graphql_name 'MergeRequestPermissions'
+
+ abilities :read_merge_request, :admin_merge_request,
+ :update_merge_request, :create_note
+
+ permission_field :push_to_source_branch, method: :can_push_to_source_branch?
+ permission_field :remove_source_branch, method: :can_remove_source_branch?
+ permission_field :cherry_pick_on_current_merge_request, method: :can_cherry_pick_on_current_merge_request?
+ permission_field :revert_on_current_merge_request, method: :can_revert_on_current_merge_request?
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb
new file mode 100644
index 00000000000..755699a4415
--- /dev/null
+++ b/app/graphql/types/permission_types/project.rb
@@ -0,0 +1,20 @@
+module Types
+ module PermissionTypes
+ class Project < BasePermissionType
+ graphql_name 'ProjectPermissions'
+
+ abilities :change_namespace, :change_visibility_level, :rename_project,
+ :remove_project, :archive_project, :remove_fork_project,
+ :remove_pages, :read_project, :create_merge_request_in,
+ :read_wiki, :read_project_member, :create_issue, :upload_file,
+ :read_cycle_analytics, :download_code, :download_wiki_code,
+ :fork_project, :create_project_snippet, :read_commit_status,
+ :request_access, :create_pipeline, :create_pipeline_schedule,
+ :create_merge_request_from, :create_wiki, :push_code,
+ :create_deployment, :push_to_delete_protected_branch,
+ :admin_wiki, :admin_project, :update_pages,
+ :admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
+ :create_pages, :destroy_pages
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index d9058ae7431..97707215b4e 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -1,5 +1,7 @@
module Types
class ProjectType < BaseObject
+ expose_permissions Types::PermissionTypes::Project
+
graphql_name 'Project'
field :id, GraphQL::ID_TYPE, null: false
@@ -68,5 +70,10 @@ module Types
resolver: Resolvers::MergeRequestResolver do
authorize :read_merge_request
end
+
+ field :pipelines,
+ Types::Ci::PipelineType.connection_type,
+ null: false,
+ resolver: Resolvers::ProjectPipelinesResolver
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index f5d94ad96a1..0190aa90763 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -270,7 +270,7 @@ module ApplicationHelper
{
members: members_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
issues: issues_project_autocomplete_sources_path(object),
- merge_requests: merge_requests_project_autocomplete_sources_path(object),
+ mergeRequests: merge_requests_project_autocomplete_sources_path(object),
labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
milestones: milestones_project_autocomplete_sources_path(object),
commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id])
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 5fce97164ae..f49b5c7b51a 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -122,7 +122,7 @@ module CiStatusHelper
def no_runners_for_project?(project)
project.runners.blank? &&
- Ci::Runner.shared.blank?
+ Ci::Runner.instance_type.blank?
end
def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body')
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 95fea2f18d1..3c5c8bbd71b 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -128,8 +128,10 @@ module GroupsHelper
def get_group_sidebar_links
links = [:overview, :group_members]
- if can?(current_user, :read_cross_project)
- links += [:activity, :issues, :boards, :labels, :milestones, :merge_requests]
+ resources = [:activity, :issues, :boards, :labels, :milestones,
+ :merge_requests]
+ links += resources.select do |resource|
+ can?(current_user, "read_group_#{resource}".to_sym, @group)
end
if can?(current_user, :admin_group, @group)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 9f501ea55fb..353479776b8 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -131,6 +131,19 @@ module IssuablesHelper
end
end
+ def group_dropdown_label(group_id, default_label)
+ return default_label if group_id.nil?
+ return "Any group" if group_id == "0"
+
+ group = ::Group.find_by(id: group_id)
+
+ if group
+ group.full_name
+ else
+ default_label
+ end
+ end
+
def milestone_dropdown_label(milestone_title, default_label = "Milestone")
title =
case milestone_title
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 5ff06b3e0fc..097be8a0643 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
@@ -106,7 +108,7 @@ module MergeRequestsHelper
data_attrs = {
action: tab.to_s,
target: "##{tab}",
- toggle: options.fetch(:force_link, false) ? '' : 'tab'
+ toggle: options.fetch(:force_link, false) ? '' : 'tabvue'
}
url = case tab
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 7f67574a428..3fa2e5452c8 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -143,7 +143,15 @@ module NotesHelper
notesIds: @notes.map(&:id),
now: Time.now.to_i,
diffView: diff_view,
- autocomplete: autocomplete
+ enableGFM: {
+ emojis: true,
+ members: autocomplete,
+ issues: autocomplete,
+ mergeRequests: autocomplete,
+ epics: autocomplete,
+ milestones: autocomplete,
+ labels: autocomplete
+ }
}
end
@@ -174,11 +182,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 c405e6d117f..b0f381db5ab 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -40,7 +40,8 @@ module ProjectsHelper
name_tag_options[:class] << 'has-tooltip'
end
- content_tag(:span, sanitize(username), name_tag_options)
+ # NOTE: ActionView::Helpers::TagHelper#content_tag HTML escapes username
+ content_tag(:span, username, name_tag_options)
end
def link_to_member(project, author, opts = {}, &block)
@@ -176,6 +177,7 @@ module ProjectsHelper
controller.action_name,
Gitlab::CurrentSettings.cache_key,
"cross-project:#{can?(current_user, :read_cross_project)}",
+ max_project_member_access_cache_key(project),
'v2.6'
]
@@ -350,11 +352,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
@@ -501,4 +507,45 @@ module ProjectsHelper
"list-label"
end
end
+
+ def sidebar_projects_paths
+ %w[
+ projects#show
+ projects#activity
+ cycle_analytics#show
+ ]
+ 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/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index f7620e0b6b8..7cd74358168 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -43,7 +43,7 @@ module TodosHelper
project_commit_path(todo.project,
todo.target, anchor: anchor)
else
- path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
+ path = [todo.parent, todo.target]
path.unshift(:pipelines) if todo.build_failed?
@@ -167,4 +167,12 @@ module TodosHelper
def show_todo_state?(todo)
(todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
end
+
+ def todo_group_options
+ groups = current_user.authorized_groups.map do |group|
+ { id: group.id, text: group.full_name }
+ end
+
+ groups.unshift({ id: '', text: 'Any Group' }).to_json
+ end
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index ce9373f5883..4d17b22a4a1 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -31,6 +31,14 @@ module UsersHelper
current_user_menu_items.include?(item)
end
+ def max_project_member_access(project)
+ current_user&.max_member_access_for_project(project.id) || Gitlab::Access::NO_ACCESS
+ end
+
+ def max_project_member_access_cache_key(project)
+ "access:#{max_project_member_access(project)}"
+ end
+
private
def get_profile_tabs
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/application_setting.rb b/app/models/application_setting.rb
index 3d58a14882f..bddeb8b0352 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -212,14 +212,6 @@ class ApplicationSetting < ActiveRecord::Base
end
end
- validates_each :disabled_oauth_sign_in_sources do |record, attr, value|
- value&.each do |source|
- unless Devise.omniauth_providers.include?(source.to_sym)
- record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
- end
- end
- end
-
validate :terms_exist, if: :enforce_terms?
before_validation :ensure_uuid!
@@ -330,6 +322,11 @@ class ApplicationSetting < ActiveRecord::Base
::Gitlab::Database.cached_column_exists?(:application_settings, :sidekiq_throttling_enabled)
end
+ def disabled_oauth_sign_in_sources=(sources)
+ sources = (sources || []).map(&:to_s) & Devise.omniauth_providers.map(&:to_s)
+ super(sources)
+ end
+
def domain_whitelist_raw
self.domain_whitelist&.join("\n")
end
diff --git a/app/models/board.rb b/app/models/board.rb
index 3cede6fc99a..bb6bb753daf 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -26,4 +26,8 @@ class Board < ActiveRecord::Base
def closed_list
lists.merge(List.closed).take
end
+
+ def scoped?
+ false
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 41446946a5e..19949f83351 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -27,7 +27,13 @@ module Ci
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :metadata, class_name: 'Ci::BuildMetadata'
+ has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
+
+ accepts_nested_attributes_for :runner_session
+
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
+ delegate :url, to: :runner_session, prefix: true, allow_nil: true
+ delegate :terminal_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
##
@@ -174,6 +180,10 @@ module Ci
after_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state
end
+
+ after_transition running: any do |build|
+ Ci::BuildRunnerSession.where(build: build).delete_all
+ end
end
def ensure_metadata
@@ -376,6 +386,10 @@ module Ci
trace.exist?
end
+ def has_old_trace?
+ old_trace.present?
+ end
+
def trace=(data)
raise NotImplementedError
end
@@ -385,6 +399,8 @@ module Ci
end
def erase_old_trace!
+ return unless has_old_trace?
+
update_column(:trace, nil)
end
@@ -584,6 +600,10 @@ module Ci
super(options).merge(when: read_attribute(:when))
end
+ def has_terminal?
+ running? && runner_session_url.present?
+ end
+
private
def update_artifacts_size
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
new file mode 100644
index 00000000000..6f3be31d8e1
--- /dev/null
+++ b/app/models/ci/build_runner_session.rb
@@ -0,0 +1,25 @@
+module Ci
+ # The purpose of this class is to store Build related runner session.
+ # Data will be removed after transitioning from running to any state.
+ class BuildRunnerSession < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+
+ self.table_name = 'ci_builds_runner_session'
+
+ belongs_to :build, class_name: 'Ci::Build', inverse_of: :runner_session
+
+ validates :build, presence: true
+ validates :url, url: { protocols: %w(https) }
+
+ def terminal_specification
+ return {} unless url.present?
+
+ {
+ subprotocols: ['terminal.gitlab.com'].freeze,
+ url: "#{url}/exec".sub("https://", "wss://"),
+ headers: { Authorization: authorization.presence }.compact,
+ ca_pem: certificate.presence
+ }
+ end
+ end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index f430f18ca9a..e5caa3ffa41 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -561,9 +561,9 @@ module Ci
.append(key: 'CI_PIPELINE_IID', value: iid.to_s)
.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
- .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message)
- .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title)
- .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description)
+ .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
+ .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
+ .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
end
def queued_duration
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 8c9aacca8de..bcd0c206bca 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -2,6 +2,7 @@ module Ci
class Runner < ActiveRecord::Base
extend Gitlab::Ci::Model
include Gitlab::SQL::Pattern
+ include IgnorableColumn
include RedisCacheable
include ChronicDurationAttribute
@@ -11,6 +12,8 @@ module Ci
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
+ ignore_column :is_shared
+
has_many :builds
has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects
@@ -21,13 +24,16 @@ module Ci
before_validation :set_default_values
- scope :specific, -> { where(is_shared: false) }
- scope :shared, -> { where(is_shared: true) }
scope :active, -> { where(active: true) }
scope :paused, -> { where(active: false) }
scope :online, -> { where('contacted_at > ?', contact_time_deadline) }
scope :ordered, -> { order(id: :desc) }
+ # BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb`
+ scope :deprecated_shared, -> { instance_type }
+ # this should get replaced with `project_type.or(group_type)` once using Rails5
+ scope :deprecated_specific, -> { where(runner_type: [runner_types[:project_type], runner_types[:group_type]]) }
+
scope :belonging_to_project, -> (project_id) {
joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
}
@@ -39,9 +45,9 @@ module Ci
joins(:groups).where(namespaces: { id: hierarchy_groups })
}
- scope :owned_or_shared, -> (project_id) do
+ scope :owned_or_instance_wide, -> (project_id) do
union = Gitlab::SQL::Union.new(
- [belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), shared],
+ [belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), instance_type],
remove_duplicates: false
)
from("(#{union.to_sql}) ci_runners")
@@ -63,7 +69,6 @@ module Ci
validate :no_groups, unless: :group_type?
validate :any_project, if: :project_type?
validate :exactly_one_group, if: :group_type?
- validate :validate_is_shared
acts_as_taggable
@@ -113,8 +118,7 @@ module Ci
end
def assign_to(project, current_user = nil)
- if shared?
- self.is_shared = false if shared?
+ if instance_type?
self.runner_type = :project_type
elsif group_type?
raise ArgumentError, 'Transitioning a group runner to a project runner is not supported'
@@ -137,10 +141,6 @@ module Ci
description
end
- def shared?
- is_shared
- end
-
def online?
contacted_at && contacted_at > self.class.contact_time_deadline
end
@@ -159,10 +159,6 @@ module Ci
runner_projects.count == 1
end
- def specific?
- !shared?
- end
-
def assigned_to_group?
runner_namespaces.any?
end
@@ -260,7 +256,7 @@ module Ci
end
def assignable_for?(project_id)
- self.class.owned_or_shared(project_id).where(id: self.id).any?
+ self.class.owned_or_instance_wide(project_id).where(id: self.id).any?
end
def no_projects
@@ -287,12 +283,6 @@ module Ci
end
end
- def validate_is_shared
- unless is_shared? == instance_type?
- errors.add(:is_shared, 'is not equal to instance_type?')
- end
- end
-
def accepting_tags?(build)
(run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty?
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 44150b37708..7a459078151 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
@@ -239,6 +243,12 @@ module Issuable
opened?
end
+ def overdue?
+ return false unless respond_to?(:due_date)
+
+ due_date.try(:past?) || false
+ end
+
def user_notes_count
if notes.loaded?
# Use the in-memory association to select and count to avoid hitting the db
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index db7254c27e0..cb76ae971d4 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -12,8 +12,8 @@ module Sortable
scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
scope :order_updated_asc, -> { reorder(updated_at: :asc) }
- scope :order_name_asc, -> { reorder("lower(name) asc") }
- scope :order_name_desc, -> { reorder("lower(name) desc") }
+ scope :order_name_asc, -> { reorder(Arel::Nodes::Ascending.new(arel_table[:name].lower)) }
+ scope :order_name_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:name].lower)) }
end
module ClassMethods
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/group.rb b/app/models/group.rb
index 9c171de7fc3..b0392774379 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -39,6 +39,8 @@ class Group < Namespace
has_many :boards
has_many :badges, class_name: 'GroupBadge'
+ has_many :todos
+
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects
@@ -82,6 +84,12 @@ class Group < Namespace
where(id: user.authorized_groups.select(:id).reorder(nil))
end
+ def public_or_visible_to_user(user)
+ where('id IN (?) OR namespaces.visibility_level IN (?)',
+ user.authorized_groups.select(:id),
+ Gitlab::VisibilityLevel.levels_for_user(user))
+ end
+
def select_for_project_authorization
if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index e72c125fb69..59a1f2aed69 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -7,6 +7,11 @@ class WebHookLog < ActiveRecord::Base
validates :web_hook, presence: true
+ def self.recent
+ where('created_at >= ?', 2.days.ago.beginning_of_day)
+ .order(created_at: :desc)
+ end
+
def success?
response_status =~ /^2/
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d136700836d..983684a5e05 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -48,7 +48,7 @@ class Issue < ActiveRecord::Base
scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)}
- scope :with_due_date, -> { where('due_date IS NOT NULL') }
+ scope :with_due_date, -> { where.not(due_date: nil) }
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
@@ -56,7 +56,7 @@ class Issue < ActiveRecord::Base
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
- scope :order_closest_future_date, -> { reorder('CASE WHEN due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - due_date) ASC') }
+ scope :order_closest_future_date, -> { reorder('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC') }
scope :preload_associations, -> { preload(:labels, project: :namespace) }
@@ -275,10 +275,6 @@ class Issue < ActiveRecord::Base
user ? readable_by?(user) : publicly_visible?
end
- def overdue?
- due_date.try(:past?) || false
- end
-
def check_for_spam?
project.public? && (title_changed? || description_changed?)
end
@@ -308,6 +304,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..b4090fd8baf 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -128,8 +128,10 @@ 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)
+ if merge_request.notify_conflict?
+ NotificationService.new.merge_request_unmergeable(merge_request)
+ TodoService.new.merge_request_became_unmergeable(merge_request)
+ end
end
def check_state?(merge_status)
@@ -369,6 +371,10 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def non_latest_diffs
+ merge_request_diffs.where.not(id: merge_request_diff.id)
+ end
+
def diff_size
# Calling `merge_request_diff.diffs.real_size` will also perform
# highlighting, which we don't need here.
@@ -610,18 +616,7 @@ class MergeRequest < ActiveRecord::Base
def reload_diff(current_user = nil)
return unless open?
- old_diff_refs = self.diff_refs
- new_diff = create_merge_request_diff
-
- MergeRequests::MergeRequestDiffCacheService.new.execute(self, new_diff)
-
- new_diff_refs = self.diff_refs
-
- update_diff_discussion_positions(
- old_diff_refs: old_diff_refs,
- new_diff_refs: new_diff_refs,
- current_user: current_user
- )
+ MergeRequests::ReloadDiffsService.new(self, current_user).execute
end
def check_if_can_be_merged
@@ -706,6 +701,17 @@ class MergeRequest < ActiveRecord::Base
should_remove_source_branch? || force_remove_source_branch?
end
+ def notify_conflict?
+ (opened? || locked?) &&
+ has_commits? &&
+ !branch_missing? &&
+ !project.repository.can_be_merged?(diff_head_sha, target_branch)
+ rescue Gitlab::Git::CommandError
+ # Checking mergeability can trigger exception, e.g. non-utf8
+ # We ignore this type of errors.
+ false
+ end
+
def related_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
@@ -1115,6 +1121,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/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 06aa67c600f..3d72c447b4b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -3,6 +3,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Importable
include ManualInverseAssociation
include IgnorableColumn
+ include EachBatch
# Don't display more than 100 commits at once
COMMITS_SAFE_SIZE = 100
@@ -17,8 +18,14 @@ class MergeRequestDiff < ActiveRecord::Base
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
state_machine :state, initial: :empty do
+ event :clean do
+ transition any => :without_files
+ end
+
state :collected
state :overflow
+ # Diff files have been deleted by the system
+ state :without_files
# Deprecated states: these are no longer used but these values may still occur
# in the database.
state :timeout
@@ -27,6 +34,7 @@ class MergeRequestDiff < ActiveRecord::Base
state :overflow_diff_lines_limit
end
+ scope :with_files, -> { without_states(:without_files, :empty) }
scope :viewable, -> { without_state(:empty) }
scope :by_commit_sha, ->(sha) do
joins(:merge_request_diff_commits).where(merge_request_diff_commits: { sha: sha }).reorder(nil)
@@ -42,6 +50,10 @@ class MergeRequestDiff < ActiveRecord::Base
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
end
+ def viewable?
+ collected? || without_files? || overflow?
+ end
+
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
@@ -170,6 +182,21 @@ class MergeRequestDiff < ActiveRecord::Base
end
def diffs(diff_options = nil)
+ if without_files? && comparison = diff_refs.compare_in(project)
+ # It should fetch the repository when diffs are cleaned by the system.
+ # We don't keep these for storage overload purposes.
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/37639
+ comparison.diffs(diff_options)
+ else
+ diffs_collection(diff_options)
+ end
+ end
+
+ # Should always return the DB persisted diffs collection
+ # (e.g. Gitlab::Diff::FileCollection::MergeRequestDiff.
+ # It's useful when trying to invalidate old caches through
+ # FileCollection::MergeRequestDiff#clear_cache!
+ def diffs_collection(diff_options = nil)
Gitlab::Diff::FileCollection::MergeRequestDiff.new(self, diff_options: diff_options)
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 52fe529c016..7034c633268 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -228,6 +228,10 @@ class Namespace < ActiveRecord::Base
parent.present?
end
+ def root_ancestor
+ ancestors.reorder(nil).find_by(parent_id: nil)
+ end
+
def subgroup?
has_parent?
end
diff --git a/app/models/note.rb b/app/models/note.rb
index b407d3c18ad..bbad9d90cc4 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -229,6 +229,10 @@ class Note < ActiveRecord::Base
!for_personal_snippet?
end
+ def for_issuable?
+ for_issue? || for_merge_request?
+ end
+
def skip_project_check?
!for_project_noteable?
end
@@ -384,6 +388,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 0d777515536..8f40470de82 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1422,7 +1422,7 @@ class Project < ActiveRecord::Base
end
def shared_runners
- @shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
+ @shared_runners ||= shared_runners_available? ? Ci::Runner.instance_type : Ci::Runner.none
end
def group_runners
@@ -1774,6 +1774,15 @@ class Project < ActiveRecord::Base
end
end
+ def default_environment
+ production_first = "(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC"
+
+ environments
+ .with_state(:available)
+ .reorder(production_first)
+ .first
+ end
+
def secret_variables_for(ref:, environment: nil)
# EE would use the environment
if protected_for?(ref)
@@ -2019,6 +2028,10 @@ class Project < ActiveRecord::Base
end
request_cache(:any_lfs_file_locks?) { self.id }
+ def auto_cancel_pending_pipelines?
+ auto_cancel_pending_pipelines == 'enabled'
+ end
+
private
def storage
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index d7d6aaceb27..faa831b1949 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -29,8 +29,8 @@ class ProjectAutoDevops < ActiveRecord::Base
end
if manual?
- variables.append(key: 'STAGING_ENABLED', value: 1)
- variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 1)
+ variables.append(key: 'STAGING_ENABLED', value: '1')
+ variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: '1')
end
end
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 7f4c47a6d14..edc5c00d9c4 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -67,11 +67,11 @@ class BambooService < CiService
def execute(data)
return unless supported_events.include?(data[:object_kind])
- get_path("updateAndBuild.action?buildKey=#{build_key}")
+ get_path("updateAndBuild.action", { buildKey: build_key })
end
def calculate_reactive_cache(sha, ref)
- response = get_path("rest/api/latest/result?label=#{sha}")
+ response = get_path("rest/api/latest/result/byChangeset/#{sha}")
{ build_page: read_build_page(response), commit_status: read_commit_status(response) }
end
@@ -113,18 +113,20 @@ class BambooService < CiService
URI.join("#{bamboo_url}/", path).to_s
end
- def get_path(path)
+ def get_path(path, query_params = {})
url = build_url(path)
if username.blank? && password.blank?
- Gitlab::HTTP.get(url, verify: false)
+ Gitlab::HTTP.get(url, verify: false, query: query_params)
else
- url << '&os_authType=basic'
- Gitlab::HTTP.get(url, verify: false,
- basic_auth: {
- username: username,
- password: password
- })
+ query_params[:os_authType] = 'basic'
+ Gitlab::HTTP.get(url,
+ verify: false,
+ query: query_params,
+ basic_auth: {
+ username: username,
+ password: password
+ })
end
end
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index ddd4026019b..722642f6da7 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -240,7 +240,7 @@ class KubernetesService < DeploymentService
end
def deprecation_validation
- return if active_changed?(from: true, to: false)
+ return if active_changed?(from: true, to: false) || (new_record? && !active?)
if deprecated?
errors[:base] << deprecation_message
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 33280eda0b9..9a38806baab 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -24,7 +24,7 @@ class ProjectTeam
end
def add_role(user, role, current_user: nil)
- send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
+ public_send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
end
def find_member(user_id)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index c2f62badbcb..5f9894f1168 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -21,7 +21,7 @@ class Repository
attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
- delegate :bundle_to_disk, :create_from_bundle, to: :raw_repository
+ delegate :bundle_to_disk, to: :raw_repository
CreateTreeError = Class.new(StandardError)
@@ -99,11 +99,11 @@ class Repository
"#<#{self.class.name}:#{@disk_path}>"
end
- def commit(ref = 'HEAD')
+ def commit(ref = nil)
return nil unless exists?
return ref if ref.is_a?(::Commit)
- find_commit(ref)
+ find_commit(ref || root_ref)
end
# Finding a commit by the passed SHA
@@ -283,6 +283,10 @@ class Repository
)
end
+ def cached_methods
+ CACHED_METHODS
+ end
+
def expire_tags_cache
expire_method_caches(%i(tag_names tag_count))
@tags = nil
@@ -423,7 +427,7 @@ class Repository
# Runs code after the HEAD of a repository is changed.
def after_change_head
- expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
+ expire_all_method_caches
end
# Runs code after a repository has been forked/imported.
@@ -850,7 +854,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/models/service.rb b/app/models/service.rb
index 1d259bcfec7..ad835293b46 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -281,9 +281,9 @@ class Service < ActiveRecord::Base
def self.build_from_template(project_id, template)
service = template.dup
- service.active = false unless service.valid?
service.template = false
service.project_id = project_id
+ service.active = false if service.active? && !service.valid?
service
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index a2ab405fdbe..942cbb754e3 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -22,15 +22,18 @@ class Todo < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :note
belongs_to :project
+ belongs_to :group
belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
delegate :name, :email, to: :author, prefix: true, allow_nil: true
- validates :action, :project, :target_type, :user, presence: true
+ validates :action, :target_type, :user, presence: true
validates :author, presence: true
validates :target_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
+ validates :project, presence: true, unless: :group_id
+ validates :group, presence: true, unless: :project_id
scope :pending, -> { with_state(:pending) }
scope :done, -> { with_state(:done) }
@@ -44,7 +47,7 @@ class Todo < ActiveRecord::Base
state :done
end
- after_save :keep_around_commit
+ after_save :keep_around_commit, if: :commit_id
class << self
# Priority sorting isn't displayed in the dropdown, because we don't show
@@ -79,6 +82,10 @@ class Todo < ActiveRecord::Base
end
end
+ def parent
+ project
+ end
+
def unmergeable?
action == UNMERGEABLE
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 8e0dc91b2a7..27a5d0278b7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -244,7 +244,7 @@ class User < ActiveRecord::Base
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active).non_internal }
- scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
+ scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
@@ -1032,7 +1032,7 @@ class User < ActiveRecord::Base
union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids])
- Ci::Runner.specific.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ Ci::Runner.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 1c0cc7425ec..75c7e529902 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -18,6 +18,10 @@ module Ci
@subject.project.branch_allows_collaboration?(@user, @subject.ref)
end
+ condition(:terminal, scope: :subject) do
+ @subject.has_terminal?
+ end
+
rule { protected_ref }.policy do
prevent :update_build
prevent :erase_build
@@ -29,5 +33,7 @@ module Ci
enable :update_build
enable :update_commit_status
end
+
+ rule { can?(:update_build) & terminal }.enable :create_build_terminal
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 520710b757d..ded9fe30eff 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -72,6 +72,19 @@ class GroupPolicy < BasePolicy
enable :change_visibility_level
end
+ rule { can?(:read_nested_project_resources) }.policy do
+ enable :read_group_activity
+ enable :read_group_issues
+ enable :read_group_boards
+ enable :read_group_labels
+ enable :read_group_milestones
+ enable :read_group_merge_requests
+ end
+
+ rule { can?(:read_cross_project) & can?(:read_group) }.policy do
+ enable :read_nested_project_resources
+ end
+
rule { owner & nested_groups_supported }.enable :create_subgroup
rule { public_group | logged_in_viewable }.enable :view_globally
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 8d466c33510..f77b3541644 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)
@@ -179,6 +168,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
.can_push_to_branch?(source_branch)
end
+ def can_remove_source_branch?
+ source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
+ end
+
def mergeable_discussions_state
# This avoids calling MergeRequest#mergeable_discussions_state without
# considering the state of the MR first. If a MR isn't mergeable, we can
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index ad655a7b3f4..d4d622d84ab 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -27,6 +27,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def statistics_buttons(show_auto_devops_callout:)
[
+ readme_anchor_data,
changelog_anchor_data,
license_anchor_data,
contribution_guide_anchor_data,
@@ -212,11 +213,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def readme_anchor_data
- if current_user && can_current_user_push_to_default_branch? && repository.readme.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.readme.nil?
OpenStruct.new(enabled: false,
label: _('Add Readme'),
link: add_readme_path)
- elsif repository.readme.present?
+ elsif repository.readme
OpenStruct.new(enabled: true,
label: _('Readme'),
link: default_view != 'readme' ? readme_path : '#readme')
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/build_details_entity.rb b/app/serializers/build_details_entity.rb
index ca4480fe2b1..2de9624aed4 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -35,7 +35,7 @@ class BuildDetailsEntity < JobEntity
def build_failed_issue_options
{ title: "Job Failed ##{build.id}",
- description: "Job [##{build.id}](#{project_job_path(project, build)}) failed for #{build.sha}:\n" }
+ description: "Job [##{build.id}](#{project_job_url(project, build)}) failed for #{build.sha}:\n" }
end
def current_user
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index 6e68d275047..61135fba97b 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -1,25 +1,48 @@
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]
+
+ next unless diff_file.blob
+
+ 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 +51,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 +91,67 @@ 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
+ next unless diff_file.content_sha
+
+ 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|
+ next unless diff_file.content_sha
+
+ 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..8a39a4950f5 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? && !d.legacy_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_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..5d72ebdd7fd 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -109,7 +109,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :current_user do
expose :can_remove_source_branch do |merge_request|
- merge_request.source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
+ presenter(merge_request).can_remove_source_branch?
end
expose :can_revert_on_current_merge_request do |merge_request|
@@ -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/serializers/runner_entity.rb b/app/serializers/runner_entity.rb
index e9999a36d8a..db26eadab2d 100644
--- a/app/serializers/runner_entity.rb
+++ b/app/serializers/runner_entity.rb
@@ -4,7 +4,7 @@ class RunnerEntity < Grape::Entity
expose :id, :description
expose :edit_path,
- if: -> (*) { can?(request.current_user, :admin_build, project) && runner.specific? } do |runner|
+ if: -> (*) { can?(request.current_user, :admin_build, project) && runner.project_type? } do |runner|
edit_project_runner_path(project, runner)
end
diff --git a/app/services/base_count_service.rb b/app/services/base_count_service.rb
index f2844854112..975e288301c 100644
--- a/app/services/base_count_service.rb
+++ b/app/services/base_count_service.rb
@@ -17,7 +17,7 @@ class BaseCountService
end
def refresh_cache(&block)
- Rails.cache.write(cache_key, block_given? ? yield : uncached_count, raw: raw?)
+ update_cache_for_key(cache_key, &block)
end
def uncached_count
@@ -41,4 +41,8 @@ class BaseCountService
def cache_options
{ raw: raw? }
end
+
+ def update_cache_for_key(key, &block)
+ Rails.cache.write(key, block_given? ? yield : uncached_count, raw: raw?)
+ end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 9bdbb2c0d99..6eb1c4f52de 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -13,9 +13,9 @@ module Ci
@runner = runner
end
- def execute
+ def execute(params = {})
builds =
- if runner.shared?
+ if runner.instance_type?
builds_for_shared_runner
elsif runner.group_type?
builds_for_group_runner
@@ -41,6 +41,8 @@ module Ci
# with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
begin
build.runner_id = runner.id
+ build.runner_session_attributes = params[:session] if params[:session].present?
+
build.run!
register_success(build)
@@ -99,7 +101,7 @@ module Ci
end
def running_builds_for_shared_runners
- Ci::Build.running.where(runner: Ci::Runner.shared)
+ Ci::Build.running.where(runner: Ci::Runner.instance_type)
.group(:project_id).select(:project_id, 'count(*) AS running_builds')
end
@@ -115,7 +117,7 @@ module Ci
end
def register_success(job)
- labels = { shared_runner: runner.shared?,
+ labels = { shared_runner: runner.instance_type?,
jobs_running_for_project: jobs_running_for_project(job) }
job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) unless job.queued_at.nil?
@@ -123,10 +125,10 @@ module Ci
end
def jobs_running_for_project(job)
- return '+Inf' unless runner.shared?
+ return '+Inf' unless runner.instance_type?
# excluding currently started job
- running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared)
+ running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.instance_type)
.limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
end
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/issues/base_service.rb b/app/services/issues/base_service.rb
index 9f6cfc0f6d3..cbfef175af0 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -32,8 +32,9 @@ module Issues
def filter_assignee(issuable)
return if params[:assignee_ids].blank?
- # The number of assignees is limited by one for GitLab CE
- params[:assignee_ids] = params[:assignee_ids][0, 1]
+ unless issuable.allows_multiple_assignees?
+ params[:assignee_ids] = params[:assignee_ids].take(1)
+ end
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index 78e79344c99..6e5c29a5c40 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -58,7 +58,8 @@ module Issues
def cloneable_label_ids
params = {
project_id: @new_project.id,
- title: @old_issue.labels.pluck(:title)
+ title: @old_issue.labels.pluck(:title),
+ include_ancestor_groups: true
}
LabelsFinder.new(current_user, params).execute.pluck(:id)
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
index 079f611b3f3..a72da3c637f 100644
--- a/app/services/labels/find_or_create_service.rb
+++ b/app/services/labels/find_or_create_service.rb
@@ -20,6 +20,7 @@ module Labels
@available_labels ||= LabelsFinder.new(
current_user,
"#{parent_type}_id".to_sym => parent.id,
+ include_ancestor_groups: include_ancestor_groups?,
only_group_labels: parent_is_group?
).execute(skip_authorization: skip_authorization)
end
@@ -30,7 +31,8 @@ module Labels
new_label = available_labels.find_by(title: title)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
- new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent)
+ create_params = params.except(:include_ancestor_groups)
+ new_label = Labels::CreateService.new(create_params).execute(parent_type.to_sym => parent)
end
new_label
@@ -47,5 +49,9 @@ module Labels
def parent_is_group?
parent_type == "group"
end
+
+ def include_ancestor_groups?
+ params[:include_ancestor_groups] == true
+ end
end
end
diff --git a/app/services/merge_requests/delete_non_latest_diffs_service.rb b/app/services/merge_requests/delete_non_latest_diffs_service.rb
new file mode 100644
index 00000000000..40079b21189
--- /dev/null
+++ b/app/services/merge_requests/delete_non_latest_diffs_service.rb
@@ -0,0 +1,18 @@
+module MergeRequests
+ class DeleteNonLatestDiffsService
+ BATCH_SIZE = 10
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+
+ def execute
+ diffs = @merge_request.non_latest_diffs.with_files
+
+ diffs.each_batch(of: BATCH_SIZE) do |relation, index|
+ ids = relation.pluck(:id).map { |id| [id] }
+ DeleteDiffFilesWorker.bulk_perform_in(index * 5.minutes, ids)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb
deleted file mode 100644
index 10aa9ae609c..00000000000
--- a/app/services/merge_requests/merge_request_diff_cache_service.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module MergeRequests
- class MergeRequestDiffCacheService
- def execute(merge_request, new_diff)
- # Executing the iteration we cache all the highlighted diff information
- merge_request.diffs.diff_files.to_a
-
- # Remove cache for all diffs on this MR. Do not use the association on the
- # model, as that will interfere with other actions happening when
- # reloading the diff.
- MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
- next if merge_request_diff == new_diff
-
- merge_request_diff.diffs.clear_cache!
- end
- end
- end
-end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index c78e78afcd1..7606d68ff29 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -6,15 +6,16 @@ module MergeRequests
#
class PostMergeService < MergeRequests::BaseService
def execute(merge_request)
+ merge_request.mark_as_merged
close_issues(merge_request)
todo_service.merge_merge_request(merge_request, current_user)
- merge_request.mark_as_merged
create_event(merge_request)
create_note(merge_request)
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
+ delete_non_latest_diffs(merge_request)
end
private
@@ -31,6 +32,10 @@ module MergeRequests
end
end
+ def delete_non_latest_diffs(merge_request)
+ DeleteNonLatestDiffsService.new(merge_request).execute
+ end
+
def create_merge_event(merge_request, current_user)
EventCreateService.new.merge_mr(merge_request, current_user)
end
diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb
index c0083cd6afd..5b4bc86b9ba 100644
--- a/app/services/merge_requests/rebase_service.rb
+++ b/app/services/merge_requests/rebase_service.rb
@@ -18,10 +18,18 @@ module MergeRequests
return false
end
+ log_prefix = "#{self.class.name} info (#{merge_request.to_reference(full: true)}):"
+
+ Gitlab::GitLogger.info("#{log_prefix} rebase started")
+
rebase_sha = repository.rebase(current_user, merge_request)
+ Gitlab::GitLogger.info("#{log_prefix} rebased to #{rebase_sha}")
+
merge_request.update_attributes(rebase_commit_sha: rebase_sha)
+ Gitlab::GitLogger.info("#{log_prefix} rebase SHA saved: #{rebase_sha}")
+
true
rescue => e
log_error(REBASE_ERROR, save_message_on_model: true)
diff --git a/app/services/merge_requests/reload_diffs_service.rb b/app/services/merge_requests/reload_diffs_service.rb
new file mode 100644
index 00000000000..2ec7b403903
--- /dev/null
+++ b/app/services/merge_requests/reload_diffs_service.rb
@@ -0,0 +1,43 @@
+module MergeRequests
+ class ReloadDiffsService
+ def initialize(merge_request, current_user)
+ @merge_request = merge_request
+ @current_user = current_user
+ end
+
+ def execute
+ old_diff_refs = merge_request.diff_refs
+ new_diff = merge_request.create_merge_request_diff
+
+ clear_cache(new_diff)
+ update_diff_discussion_positions(old_diff_refs)
+ end
+
+ private
+
+ attr_reader :merge_request, :current_user
+
+ def update_diff_discussion_positions(old_diff_refs)
+ new_diff_refs = merge_request.diff_refs
+
+ merge_request.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs,
+ current_user: current_user)
+ end
+
+ def clear_cache(new_diff)
+ # Executing the iteration we cache highlighted diffs for each diff file of
+ # MergeRequestDiff.
+ new_diff.diffs_collection.diff_files.to_a
+
+ # Remove cache for all diffs on this MR. Do not use the association on the
+ # model, as that will interfere with other actions happening when
+ # reloading the diff.
+ MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
+ next if merge_request_diff == new_diff
+
+ merge_request_diff.diffs_collection.clear_cache!
+ end
+ end
+ end
+end
diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb
index 236e9fe8c44..51ff9eff5e4 100644
--- a/app/services/metrics_service.rb
+++ b/app/services/metrics_service.rb
@@ -6,7 +6,8 @@ class MetricsService
Gitlab::HealthChecks::Redis::RedisCheck,
Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck,
- Gitlab::HealthChecks::Redis::SharedStateCheck
+ Gitlab::HealthChecks::Redis::SharedStateCheck,
+ Gitlab::HealthChecks::GitalyCheck
].freeze
def prometheus_metrics_text
diff --git a/app/services/projects/count_service.rb b/app/services/projects/count_service.rb
index 933829b557b..4c8e000928f 100644
--- a/app/services/projects/count_service.rb
+++ b/app/services/projects/count_service.rb
@@ -22,8 +22,10 @@ module Projects
)
end
- def cache_key
- ['projects', 'count_service', VERSION, @project.id, cache_key_name]
+ def cache_key(key = nil)
+ cache_key = key || cache_key_name
+
+ ['projects', 'count_service', VERSION, @project.id, cache_key]
end
def self.query(project_ids)
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index a02a9052fb2..172497b8e67 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -2,6 +2,8 @@ module Projects
class CreateService < BaseService
def initialize(user, params)
@current_user, @params = user, params.dup
+ @skip_wiki = @params.delete(:skip_wiki)
+ @initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme))
end
def execute
@@ -11,7 +13,6 @@ module Projects
forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data)
- @skip_wiki = params.delete(:skip_wiki)
@project = Project.new(params)
@@ -102,6 +103,8 @@ module Projects
setup_authorizations
current_user.invalidate_personal_projects_count
+
+ create_readme if @initialize_with_readme
end
# Refresh the current user's authorizations inline (so they can access the
@@ -116,6 +119,17 @@ module Projects
end
end
+ def create_readme
+ commit_attrs = {
+ branch_name: 'master',
+ commit_message: 'Initial commit',
+ file_path: 'README.md',
+ file_content: "# #{@project.name}\n\n#{@project.description}"
+ }
+
+ Files::CreateService.new(@project, current_user, commit_attrs).execute
+ end
+
def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki
end
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index 0a004677417..78b1477186a 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -4,6 +4,10 @@ module Projects
class OpenIssuesCountService < Projects::CountService
include Gitlab::Utils::StrongMemoize
+ # Cache keys used to store issues count
+ PUBLIC_COUNT_KEY = 'public_open_issues_count'.freeze
+ TOTAL_COUNT_KEY = 'total_open_issues_count'.freeze
+
def initialize(project, user = nil)
@user = user
@@ -11,7 +15,7 @@ module Projects
end
def cache_key_name
- public_only? ? 'public_open_issues_count' : 'total_open_issues_count'
+ public_only? ? PUBLIC_COUNT_KEY : TOTAL_COUNT_KEY
end
def public_only?
@@ -28,6 +32,32 @@ module Projects
end
end
+ def public_count_cache_key
+ cache_key(PUBLIC_COUNT_KEY)
+ end
+
+ def total_count_cache_key
+ cache_key(TOTAL_COUNT_KEY)
+ end
+
+ def refresh_cache(&block)
+ if block_given?
+ super(&block)
+ else
+ count_grouped_by_confidential = self.class.query(@project, public_only: false).group(:confidential).count
+ public_count = count_grouped_by_confidential[false] || 0
+ total_count = public_count + (count_grouped_by_confidential[true] || 0)
+
+ update_cache_for_key(public_count_cache_key) do
+ public_count
+ end
+
+ update_cache_for_key(total_count_cache_key) do
+ total_count
+ end
+ end
+ end
+
# We only show total issues count for reporters
# which are allowed to view confidential issues
# This will still show a discrepancy on issues number but should be less than before.
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index f91cd03bf5c..46f12086555 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -260,15 +260,15 @@ class TodoService
end
end
- def create_mention_todos(project, target, author, note = nil, skip_users = [])
+ def create_mention_todos(parent, target, author, note = nil, skip_users = [])
# Create Todos for directly addressed users
- directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users)
- attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
+ directly_addressed_users = filter_directly_addressed_users(parent, note || target, author, skip_users)
+ attributes = attributes_for_todo(parent, target, author, Todo::DIRECTLY_ADDRESSED, note)
create_todos(directly_addressed_users, attributes)
# Create Todos for mentioned users
- mentioned_users = filter_mentioned_users(project, note || target, author, skip_users)
- attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
+ mentioned_users = filter_mentioned_users(parent, note || target, author, skip_users)
+ attributes = attributes_for_todo(parent, target, author, Todo::MENTIONED, note)
create_todos(mentioned_users, attributes)
end
@@ -299,36 +299,36 @@ class TodoService
def attributes_for_todo(project, target, author, action, note = nil)
attributes_for_target(target).merge!(
- project_id: project.id,
+ project_id: project&.id,
author_id: author.id,
action: action,
note: note
)
end
- def filter_todo_users(users, project, target)
- reject_users_without_access(users, project, target).uniq
+ def filter_todo_users(users, parent, target)
+ reject_users_without_access(users, parent, target).uniq
end
- def filter_mentioned_users(project, target, author, skip_users = [])
+ def filter_mentioned_users(parent, target, author, skip_users = [])
mentioned_users = target.mentioned_users(author) - skip_users
- filter_todo_users(mentioned_users, project, target)
+ filter_todo_users(mentioned_users, parent, target)
end
- def filter_directly_addressed_users(project, target, author, skip_users = [])
+ def filter_directly_addressed_users(parent, target, author, skip_users = [])
directly_addressed_users = target.directly_addressed_users(author) - skip_users
- filter_todo_users(directly_addressed_users, project, target)
+ filter_todo_users(directly_addressed_users, parent, target)
end
- def reject_users_without_access(users, project, target)
- if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?)
+ def reject_users_without_access(users, parent, target)
+ if target.is_a?(Note) && target.for_issuable?
target = target.noteable
end
if target.is_a?(Issuable)
select_users(users, :"read_#{target.to_ability_name}", target)
else
- select_users(users, :read_project, project)
+ select_users(users, :read_project, parent)
end
end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 7ec52b6ce2b..8a86e47f0ea 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -82,7 +82,7 @@ class WebHookService
post_url = hook.url.gsub("#{parsed_url.userinfo}@", '')
basic_auth = {
username: CGI.unescape(parsed_url.user),
- password: CGI.unescape(parsed_url.password)
+ password: CGI.unescape(parsed_url.password.presence || '')
}
make_request(post_url, basic_auth)
end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index cd819dc9bff..0a166335b4e 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AttachmentUploader < GitlabUploader
include RecordsUploads::Concern
include ObjectStorage::Concern
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 5848e6c6994..b29ef57b071 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AvatarUploader < GitlabUploader
include UploaderHelper
include RecordsUploads::Concern
diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb
index 3639375d474..a0b275b56a9 100644
--- a/app/uploaders/favicon_uploader.rb
+++ b/app/uploaders/favicon_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class FaviconUploader < AttachmentUploader
EXTENSION_WHITELIST = %w[png ico].freeze
diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb
index bd7736ad74e..a7f8615e9ba 100644
--- a/app/uploaders/file_mover.rb
+++ b/app/uploaders/file_mover.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class FileMover
attr_reader :secret, :file_name, :model, :update_field
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 36bc0a4575a..21292ddcf44 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This class breaks the actual CarrierWave concept.
# Every uploader should use a base_dir that is model agnostic so we can build
# back URLs from base_dir-relative paths saved in the `Upload` model.
@@ -81,6 +83,13 @@ class FileUploader < GitlabUploader
apply_context!(uploader_context)
end
+ def initialize_copy(from)
+ super
+
+ @secret = self.class.generate_secret
+ @upload = nil # calling record_upload would delete the old upload if set
+ end
+
# enforce the usage of Hashed storage when storing to
# remote store as the FileMover doesn't support OS
def base_dir(store = nil)
@@ -110,7 +119,7 @@ class FileUploader < GitlabUploader
end
def markdown_link
- markdown = "[#{markdown_name}](#{secure_url})"
+ markdown = +"[#{markdown_name}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous?
markdown
end
@@ -144,6 +153,27 @@ class FileUploader < GitlabUploader
@secret ||= self.class.generate_secret
end
+ # return a new uploader with a file copy on another project
+ def self.copy_to(uploader, to_project)
+ moved = uploader.dup.tap do |u|
+ u.model = to_project
+ end
+
+ moved.copy_file(uploader.file)
+ moved
+ end
+
+ def copy_file(file)
+ to_path = if file_storage?
+ File.join(self.class.root, store_path)
+ else
+ store_path
+ end
+
+ self.file = file.copy_to(to_path)
+ record_upload # after_store is not triggered
+ end
+
private
def apply_context!(uploader_context)
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index f8a237178d9..7919f126075 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GitlabUploader < CarrierWave::Uploader::Base
class_attribute :options
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index 2a5a830ce4f..855cf2fc21c 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class JobArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb
index efb7893d153..b4d0d752016 100644
--- a/app/uploaders/legacy_artifact_uploader.rb
+++ b/app/uploaders/legacy_artifact_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LegacyArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index eb521a22ebc..f3d32e6b39d 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LfsObjectUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
diff --git a/app/uploaders/namespace_file_uploader.rb b/app/uploaders/namespace_file_uploader.rb
index 1085ecb1700..52969762b7d 100644
--- a/app/uploaders/namespace_file_uploader.rb
+++ b/app/uploaders/namespace_file_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NamespaceFileUploader < FileUploader
# Re-Override
def self.root
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index b8ecfc4ee2b..dad6e85fb56 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'fog/aws'
require 'carrierwave/storage/fog'
diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb
index e3898b07730..25474b494ff 100644
--- a/app/uploaders/personal_file_uploader.rb
+++ b/app/uploaders/personal_file_uploader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PersonalFileUploader < FileUploader
# Re-Override
def self.root
diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb
index 301f4681fcd..5795065ae11 100644
--- a/app/uploaders/records_uploads.rb
+++ b/app/uploaders/records_uploads.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RecordsUploads
module Concern
extend ActiveSupport::Concern
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index 207928b61d0..2a2b54a9270 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Extra methods for uploader
module UploaderHelper
IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze
diff --git a/app/uploaders/workhorse.rb b/app/uploaders/workhorse.rb
index 782032cf516..84dc2791b9c 100644
--- a/app/uploaders/workhorse.rb
+++ b/app/uploaders/workhorse.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Workhorse
module UploadPath
def workhorse_upload_path
diff --git a/app/validators/abstract_path_validator.rb b/app/validators/abstract_path_validator.rb
index e43b66cbe3a..45ac695c5ec 100644
--- a/app/validators/abstract_path_validator.rb
+++ b/app/validators/abstract_path_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AbstractPathValidator < ActiveModel::EachValidator
extend Gitlab::EncodingHelper
diff --git a/app/validators/certificate_fingerprint_validator.rb b/app/validators/certificate_fingerprint_validator.rb
index 17df756183a..79d78653ec7 100644
--- a/app/validators/certificate_fingerprint_validator.rb
+++ b/app/validators/certificate_fingerprint_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CertificateFingerprintValidator < ActiveModel::EachValidator
FINGERPRINT_PATTERN = /\A([a-zA-Z0-9]{2}[\s\-:]?){16,}\z/.freeze
diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb
index 8c7bb750339..5b2bbffc066 100644
--- a/app/validators/certificate_key_validator.rb
+++ b/app/validators/certificate_key_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# UrlValidator
#
# Custom validator for private keys.
diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb
index b0c9a1b92a4..de8bb179dfb 100644
--- a/app/validators/certificate_validator.rb
+++ b/app/validators/certificate_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# UrlValidator
#
# Custom validator for private keys.
diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb
index e7d32550176..85fd63f08e5 100644
--- a/app/validators/cluster_name_validator.rb
+++ b/app/validators/cluster_name_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# ClusterNameValidator
#
# Custom validator for ClusterName.
diff --git a/app/validators/color_validator.rb b/app/validators/color_validator.rb
index 571d0007aa2..1932d042e83 100644
--- a/app/validators/color_validator.rb
+++ b/app/validators/color_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# ColorValidator
#
# Custom validator for web color codes. It requires the leading hash symbol and
diff --git a/app/validators/cron_timezone_validator.rb b/app/validators/cron_timezone_validator.rb
index 542c7d006ad..c5f51d65060 100644
--- a/app/validators/cron_timezone_validator.rb
+++ b/app/validators/cron_timezone_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# CronTimezoneValidator
#
# Custom validator for CronTimezone.
diff --git a/app/validators/cron_validator.rb b/app/validators/cron_validator.rb
index 981fade47a6..bd48a7a6efb 100644
--- a/app/validators/cron_validator.rb
+++ b/app/validators/cron_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# CronValidator
#
# Custom validator for Cron.
diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb
index 10ff44031c6..811828169ca 100644
--- a/app/validators/duration_validator.rb
+++ b/app/validators/duration_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# DurationValidator
#
# Validate the format conforms with ChronicDuration
diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb
index aab07a7ece4..9459edb7515 100644
--- a/app/validators/email_validator.rb
+++ b/app/validators/email_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors.add(attribute, :invalid) unless value =~ Devise.email_regexp
diff --git a/app/validators/key_restriction_validator.rb b/app/validators/key_restriction_validator.rb
index 204be827941..891d13b1596 100644
--- a/app/validators/key_restriction_validator.rb
+++ b/app/validators/key_restriction_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class KeyRestrictionValidator < ActiveModel::EachValidator
FORBIDDEN = -1
diff --git a/app/validators/line_code_validator.rb b/app/validators/line_code_validator.rb
index ed29e5aeb67..a351180790e 100644
--- a/app/validators/line_code_validator.rb
+++ b/app/validators/line_code_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# LineCodeValidator
#
# Custom validator for GitLab line codes.
diff --git a/app/validators/namespace_name_validator.rb b/app/validators/namespace_name_validator.rb
index 2e51af2982d..fb1c241037c 100644
--- a/app/validators/namespace_name_validator.rb
+++ b/app/validators/namespace_name_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# NamespaceNameValidator
#
# Custom validator for GitLab namespace name strings.
diff --git a/app/validators/namespace_path_validator.rb b/app/validators/namespace_path_validator.rb
index 7b0ae4db5d4..c078b272b2f 100644
--- a/app/validators/namespace_path_validator.rb
+++ b/app/validators/namespace_path_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NamespacePathValidator < AbstractPathValidator
extend Gitlab::EncodingHelper
diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb
index 424fd77a6a3..aea0a68e7cf 100644
--- a/app/validators/project_path_validator.rb
+++ b/app/validators/project_path_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProjectPathValidator < AbstractPathValidator
extend Gitlab::EncodingHelper
diff --git a/app/validators/public_url_validator.rb b/app/validators/public_url_validator.rb
index 1e8118fccbb..3ff880deedd 100644
--- a/app/validators/public_url_validator.rb
+++ b/app/validators/public_url_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# PublicUrlValidator
#
# Custom validator for URLs. This validator works like UrlValidator but
diff --git a/app/validators/top_level_group_validator.rb b/app/validators/top_level_group_validator.rb
index 7e2e735e0cf..b50c9dca154 100644
--- a/app/validators/top_level_group_validator.rb
+++ b/app/validators/top_level_group_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TopLevelGroupValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value&.subgroup?
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
index 6854fec582e..faaf1283078 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# UrlValidator
#
# Custom validator for URLs.
diff --git a/app/validators/variable_duplicates_validator.rb b/app/validators/variable_duplicates_validator.rb
index 72660be6c43..90193e85f2a 100644
--- a/app/validators/variable_duplicates_validator.rb
+++ b/app/validators/variable_duplicates_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# VariableDuplicatesValidator
#
# This validator is designed for especially the following condition
@@ -22,8 +24,8 @@ class VariableDuplicatesValidator < ActiveModel::EachValidator
def validate_duplicates(record, attribute, values)
duplicates = values.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first)
if duplicates.any?
- error_message = "have duplicate values (#{duplicates.join(", ")})"
- error_message += " for #{values.first.send(options[:scope])} scope" if options[:scope] # rubocop:disable GitlabSecurity/PublicSend
+ error_message = +"have duplicate values (#{duplicates.join(", ")})"
+ error_message << " for #{values.first.send(options[:scope])} scope" if options[:scope] # rubocop:disable GitlabSecurity/PublicSend
record.errors.add(attribute, error_message)
end
end
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index 38607ffca1c..bd43504dd37 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -324,3 +324,6 @@
= _('Configure push mirrors.')
.settings-content
= render partial: 'repository_mirrors_form'
+
+= render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded
+
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 3cdeb103bb8..18f2c1a509f 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -2,7 +2,7 @@
- breadcrumb_title "Dashboard"
%div{ class: container_class }
- = render_if_exists "admin/licenses/breakdown", license: @license
+ = render_if_exists 'admin/licenses/breakdown', license: @license
.admin-dashboard.prepend-top-default
.row
@@ -22,7 +22,7 @@
%h3.text-center
Users:
= approximate_count_with_delimiters(@counts, User)
- = render_if_exists 'users_statistics'
+ = render_if_exists 'admin/dashboard/users_statistics'
%hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new"
.col-sm-4
@@ -101,7 +101,7 @@
%span.light.float-right
= boolean_to_icon Gitlab::IncomingEmail.enabled?
- = render_if_exists 'elastic_and_geo'
+ = render_if_exists 'admin/dashboard/elastic_and_geo'
- container_reg = "Container Registry"
%p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") }
@@ -151,7 +151,7 @@
%span.float-right
= Gitlab::Pages::VERSION
- = render_if_exists 'geo'
+ = render_if_exists 'admin/dashboard/geo'
%p
Ruby
diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml
index 231c0f70882..946d868da01 100644
--- a/app/views/admin/identities/_form.html.haml
+++ b/app/views/admin/identities/_form.html.haml
@@ -7,10 +7,10 @@
- values = Gitlab::Auth::OAuth::Provider.providers.map { |name| ["#{Gitlab::Auth::OAuth::Provider.label_for(name)} (#{name})", name] }
= f.select :provider, values, { allow_blank: false }, class: 'form-control'
.form-group.row
- = f.label :extern_uid, "Identifier", class: 'col-form-label col-sm-2'
+ = f.label :extern_uid, _("Identifier"), class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :extern_uid, class: 'form-control', required: true
.form-actions
- = f.submit 'Save changes', class: "btn btn-save"
+ = f.submit _('Save changes'), class: "btn btn-save"
diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml
index 50fe9478a78..5ed59809db5 100644
--- a/app/views/admin/identities/_identity.html.haml
+++ b/app/views/admin/identities/_identity.html.haml
@@ -5,8 +5,8 @@
= identity.extern_uid
%td
= link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-sm btn-grouped' do
- Edit
+ = _("Edit")
= link_to [:admin, @user, identity], method: :delete,
class: 'btn btn-sm btn-danger',
- data: { confirm: "Are you sure you want to remove this identity?" } do
- Delete
+ data: { confirm: _("Are you sure you want to remove this identity?") } do
+ = _('Delete')
diff --git a/app/views/admin/identities/edit.html.haml b/app/views/admin/identities/edit.html.haml
index 515d46b0f29..1ad6ce969cb 100644
--- a/app/views/admin/identities/edit.html.haml
+++ b/app/views/admin/identities/edit.html.haml
@@ -1,6 +1,6 @@
-- page_title "Edit", @identity.provider, "Identities", @user.name, "Users"
+- page_title _("Edit"), @identity.provider, _("Identities"), @user.name, _("Users")
%h3.page-title
- Edit identity for #{@user.name}
+ = _('Edit identity for %{user_name}') % { user_name: @user.name }
%hr
= render 'form'
diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml
index ee51fb3fda1..59373ee6752 100644
--- a/app/views/admin/identities/index.html.haml
+++ b/app/views/admin/identities/index.html.haml
@@ -1,15 +1,15 @@
-- page_title "Identities", @user.name, "Users"
+- page_title _("Identities"), @user.name, _("Users")
= render 'admin/users/head'
-= link_to 'New identity', new_admin_user_identity_path, class: 'float-right btn btn-new'
+= link_to _('New identity'), new_admin_user_identity_path, class: 'float-right btn btn-new'
- if @identities.present?
.table-holder
%table.table
%thead
%tr
- %th Provider
- %th Identifier
+ %th= _('Provider')
+ %th= _('Identifier')
%th
= render @identities
- else
- %h4 This user has no identities
+ %h4= _('This user has no identities')
diff --git a/app/views/admin/identities/new.html.haml b/app/views/admin/identities/new.html.haml
index e30bf0ef0ee..ee743b0fd3c 100644
--- a/app/views/admin/identities/new.html.haml
+++ b/app/views/admin/identities/new.html.haml
@@ -1,4 +1,4 @@
-- page_title "New Identity"
-%h3.page-title New identity
+- page_title _("New Identity")
+%h3.page-title= _('New identity')
%hr
= render 'form'
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index 7637471f9ae..ee2d4c8430a 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -10,16 +10,16 @@
.col-sm-10
= f.text_field :description, class: "form-control js-quick-submit"
.form-group.row
- = f.label :color, "Background color", class: 'col-form-label col-sm-2'
+ = f.label :color, _("Background color"), class: 'col-form-label col-sm-2'
.col-sm-10
.input-group
.input-group-prepend
.input-group-text.label-color-preview &nbsp;
= f.text_field :color, class: "form-control"
.form-text.text-muted
- Choose any color.
+ = _('Choose any color.')
%br
- Or you can choose one of the suggested colors below
+ = _("Or you can choose one of the suggested colors below")
.suggest-colors
- suggested_colors.each do |color|
@@ -27,5 +27,5 @@
&nbsp;
.form-actions
- = f.submit 'Save', class: 'btn btn-save js-save-button'
- = link_to "Cancel", admin_labels_path, class: 'btn btn-cancel'
+ = f.submit _('Save'), class: 'btn btn-save js-save-button'
+ = link_to _("Cancel"), admin_labels_path, class: 'btn btn-cancel'
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index 009a47dd517..c3ea2352898 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -3,5 +3,5 @@
= render_colored_label(label, tooltip: false)
= markdown_field(label, :description)
.float-right
- = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
- = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
+ = link_to _('Edit'), edit_admin_label_path(label), class: 'btn btn-sm'
+ = link_to _('Delete'), admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
diff --git a/app/views/admin/labels/edit.html.haml b/app/views/admin/labels/edit.html.haml
index 96f0d404ac4..652ed095d00 100644
--- a/app/views/admin/labels/edit.html.haml
+++ b/app/views/admin/labels/edit.html.haml
@@ -1,7 +1,7 @@
-- add_to_breadcrumbs "Labels", admin_labels_path
-- breadcrumb_title "Edit Label"
-- page_title "Edit", @label.name, "Labels"
+- add_to_breadcrumbs _("Labels"), admin_labels_path
+- breadcrumb_title _("Edit Label")
+- page_title _("Edit"), @label.name, _("Labels")
%h3.page-title
- Edit Label
+ = _('Edit Label')
%hr
= render 'form'
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index add38fb333e..d3e5247447a 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -1,10 +1,10 @@
-- page_title "Labels"
+- page_title _("Labels")
%div
= link_to new_admin_label_path, class: "float-right btn btn-nr btn-new" do
- New label
+ = _('New label')
%h3.page-title
- Labels
+ = _('Labels')
%hr
.labels
@@ -14,5 +14,5 @@
= paginate @labels, theme: 'gitlab'
- else
.card.bg-light
- .nothing-here-block There are no labels yet
+ .nothing-here-block= _('There are no labels yet')
diff --git a/app/views/admin/labels/new.html.haml b/app/views/admin/labels/new.html.haml
index 0135ad0723d..20103fb8a29 100644
--- a/app/views/admin/labels/new.html.haml
+++ b/app/views/admin/labels/new.html.haml
@@ -1,5 +1,5 @@
-- page_title "New Label"
+- page_title _("New Label")
%h3.page-title
- New Label
+ = _('New Label')
%hr
= render 'form'
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index a6cd39edcf0..43937b01339 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -1,6 +1,6 @@
%tr{ id: dom_id(runner) }
%td
- - if runner.shared?
+ - if runner.instance_type?
%span.badge.badge-success shared
- elsif runner.group_type?
%span.badge.badge-success group
@@ -21,7 +21,7 @@
%td
= runner.ip_address
%td
- - if runner.shared? || runner.group_type?
+ - if runner.instance_type? || runner.group_type?
n/a
- else
= runner.projects.count(:all)
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 8a0c2bf4c5f..62b7a4cbd07 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -2,7 +2,7 @@
%h3.project-title
Runner ##{@runner.id}
.float-right
- - if @runner.shared?
+ - if @runner.instance_type?
%span.runner-state.runner-state-shared
Shared
- else
@@ -13,7 +13,7 @@
- breadcrumb_title "##{@runner.id}"
- @no_container = true
-- if @runner.shared?
+- if @runner.instance_type?
.bs-callout.bs-callout-success
%h4 This Runner will process jobs from ALL UNASSIGNED projects
%p
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index d5a9cc646a6..8b3974d97f8 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -30,27 +30,33 @@
.todos-filters
.row-content-block.second-block
- = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
- .filter-item.inline
- - if params[:project_id].present?
- = hidden_field_tag(:project_id, params[:project_id])
- = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
- placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project', display: 'static' } })
- .filter-item.inline
- - if params[:author_id].present?
- = hidden_field_tag(:author_id, params[:author_id])
- = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
- placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author', todo_filter: true, todo_state_filter: params[:state] || 'pending' } })
- .filter-item.inline
- - if params[:type].present?
- = hidden_field_tag(:type, params[:type])
- = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
- data: { data: todo_types_options, default_label: 'Type' } })
- .filter-item.inline.actions-filter
- - if params[:action_id].present?
- = hidden_field_tag(:action_id, params[:action_id])
- = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
- data: { data: todo_actions_options, default_label: 'Action' } })
+ = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form d-sm-flex' do
+ .filter-categories.flex-fill
+ .filter-item.inline
+ - if params[:group_id].present?
+ = hidden_field_tag(:group_id, params[:group_id])
+ = dropdown_tag(group_dropdown_label(params[:group_id], 'Group'), options: { toggle_class: 'js-group-search js-filter-submit', title: 'Filter by group', filter: true, filterInput: 'input#group-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit',
+ placeholder: 'Search groups', data: { data: todo_group_options, default_label: 'Group', display: 'static' } })
+ .filter-item.inline
+ - if params[:project_id].present?
+ = hidden_field_tag(:project_id, params[:project_id])
+ = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
+ placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project', display: 'static' } })
+ .filter-item.inline
+ - if params[:author_id].present?
+ = hidden_field_tag(:author_id, params[:author_id])
+ = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
+ placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author', todo_filter: true, todo_state_filter: params[:state] || 'pending' } })
+ .filter-item.inline
+ - if params[:type].present?
+ = hidden_field_tag(:type, params[:type])
+ = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
+ data: { data: todo_types_options, default_label: 'Type' } })
+ .filter-item.inline.actions-filter
+ - if params[:action_id].present?
+ = hidden_field_tag(:action_id, params[:action_id])
+ = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
+ data: { data: todo_actions_options, default_label: 'Action' } })
.filter-item.sort-filter
.dropdown
%button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', 'data-toggle' => 'dropdown' }
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index c45d2214592..0ee563ac066 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -12,5 +12,9 @@
%span Remember me
.float-right.forgot-password
= link_to "Forgot your password?", new_password_path(:user)
+ %div
+ - if captcha_enabled?
+ = recaptcha_tags
+
.submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save"
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/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 6d9c6b5572a..28cdc7607e0 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -35,7 +35,7 @@
- @pre_auth.scopes.each do |scope|
%li
%strong= t scope, scope: [:doorkeeper, :scopes]
- .scope-description= t scope, scope: [:doorkeeper, :scope_desc]
+ .text-secondary= t scope, scope: [:doorkeeper, :scope_desc]
.form-actions.text-right
= form_tag oauth_authorization_path, method: :delete, class: 'inline' do
= hidden_field_tag :client_id, @pre_auth.client.uid
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 8037cf4b69d..5e1ae1dbe38 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -9,7 +9,7 @@
= render 'shared/issuable/nav', type: :issues
.nav-controls
= render 'shared/issuable/feed_buttons'
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues, with_feature_enabled: 'issues'
= render 'shared/issuable/search_bar', type: :issues
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 4ccd16f3e11..e2a317dbf67 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -7,7 +7,7 @@
= render 'shared/issuable/nav', type: :merge_requests
- if current_user
.nav-controls
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests, with_feature_enabled: 'merge_requests'
= render 'shared/issuable/search_bar', type: :merge_requests
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 1e72d88db1e..53f54db1ddf 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -4,27 +4,37 @@
- page_title 'New Group'
- header_title "Groups", dashboard_groups_path
-%h3.page-title
- New Group
-%hr
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = _('New group')
+ %p
+ - group_docs_path = help_page_path('user/group/index')
+ - group_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_docs_path }
+ = s_('%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects.').html_safe % { group_docs_link_start: group_docs_link_start, group_docs_link_end: '</a>'.html_safe }
+ %p
+ - subgroup_docs_path = help_page_path('user/group/subgroups/index')
+ - subgroup_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: subgroup_docs_path }
+ = s_('Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}.').html_safe % { subgroup_docs_link_start: subgroup_docs_link_start, subgroup_docs_link_end: '</a>'.html_safe }
-= form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f|
- = form_errors(@group)
- = render 'shared/group_form', f: f, autofocus: true
+ .col-lg-9
+ = form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f|
+ = form_errors(@group)
+ = render 'shared/group_form', f: f, autofocus: true
- .form-group.row.group-description-holder
- = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2'
- .col-sm-10
- = render 'shared/choose_group_avatar_button', f: f
+ .form-group.row.group-description-holder
+ = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2'
+ .col-sm-10
+ = render 'shared/choose_group_avatar_button', f: f
- = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group
+ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group
- = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
+ = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
- .form-group.row
- .offset-sm-2.col-sm-10
- = render 'shared/group_tips'
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ = render 'shared/group_tips'
- .form-actions
- = f.submit 'Create group', class: "btn btn-create"
- = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
+ .form-actions
+ = f.submit 'Create group', class: "btn btn-create"
+ = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index c23fe0b5c49..37b56f92030 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -182,6 +182,12 @@
%tr
%td.shortcut
%kbd g
+ %kbd l
+ %td
+ Go to metrics
+ %tr
+ %td.shortcut
+ %kbd g
%kbd k
%td
Go to kubernetes
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 24b6c490a5a..9ed05d6e3d0 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -17,6 +17,7 @@
= link_to _("Help"), help_path
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
%li.divider
+ = render 'shared/user_dropdown_contributing_link'
- if current_user_menu?(:sign_out)
%li
= link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 5cec443e969..d8e32651b36 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -21,7 +21,7 @@
- if current_user
= render 'layouts/header/new_dropdown'
- if header_link?(:search)
- %li.nav-item.d-none.d-sm-none.d-md-block
+ %li.nav-item.d-none.d-sm-none.d-md-block.m-auto
= render 'layouts/search' unless current_controller?(:search)
%li.nav-item.d-inline-block.d-sm-none.d-md-none
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index fdb07ce6fc5..00d75b3399b 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -8,7 +8,7 @@
.sidebar-context-title
= @project.name
%ul.sidebar-top-level-items
- = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
+ = nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
= link_to project_path(@project), class: 'shortcuts-project' do
.nav-icon-container
= sprite_icon('project')
@@ -29,13 +29,15 @@
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
%span= _('Activity')
+ = render_if_exists 'projects/sidebar/security_dashboard'
+
- if can?(current_user, :read_cycle_analytics, @project)
= nav_link(path: 'cycle_analytics#show') do
= link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do
%span= _('Cycle Analytics')
- 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
@@ -190,7 +196,7 @@
- if project_nav_tab? :operations
= nav_link(controller: [:environments, :clusters, :user, :gcp]) do
- = link_to project_environments_path(@project), class: 'shortcuts-operations' do
+ = link_to metrics_project_environments_path(@project), class: 'shortcuts-operations' do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
@@ -198,14 +204,19 @@
%ul.sidebar-sub-level-items
= nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
- = link_to project_environments_path(@project) do
+ = link_to metrics_project_environments_path(@project) do
%strong.fly-out-top-item-name
= _('Operations')
%li.divider.fly-out-top-item
- if project_nav_tab? :environments
- = nav_link(controller: :environments) do
- = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
+ = nav_link(controller: :environments, action: [:metrics, :metrics_redirect]) do
+ = link_to metrics_project_environments_path(@project), title: _('Metrics'), class: 'shortcuts-metrics' do
+ %span
+ = _('Metrics')
+
+ = nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
+ = link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments' do
%span
= _('Environments')
@@ -278,7 +289,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 +299,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 +337,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/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 6ea358d9f63..c14700794ce 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -4,10 +4,12 @@
.form-group
= f.label :key, class: 'label-light'
- = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the SSH key. Paste the public part, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'."
+ %p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key.")
+ = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: 'Typically starts with "ssh-rsa …"'
.form-group
= f.label :title, class: 'label-light'
- = f.text_field :title, class: "form-control", required: true
+ = f.text_field :title, class: "form-control", required: true, placeholder: 'e.g. My MacBook key'
+ %p.form-text.text-muted= _('Name your individual key via a title')
.prepend-top-default
= f.submit 'Add key', class: "btn btn-create"
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 1e206def7ee..55ca8d0ebd4 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -11,10 +11,11 @@
%h5.prepend-top-0
Add an SSH key
%p.profile-settings-content
- Before you can add an SSH key you need to
- = link_to "generate one", help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair')
- or use an
- = link_to "existing key.", help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair')
+ - generate_link_url = help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair')
+ - existing_link_url = help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair')
+ - generate_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: generate_link_url }
+ - existing_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: existing_link_url }
+ = _('To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}.').html_safe % { generate_link_start: generate_link_start, existing_link_start: existing_link_start, link_end: '</a>'.html_safe }
= render 'form'
%hr
%h5
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 1e7d9444986..f4d4888bd15 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -3,7 +3,7 @@
- project = local_assigns.fetch(:project)
- expanded = Rails.env.test?
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-export-project{ class: ('expanded' if expanded) }
.settings-header
%h4
Export project
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 6f957533287..f4994f5459b 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -40,5 +40,15 @@
= link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer'
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
+.form-group.row.initialize-with-readme-setting
+ %div{ :class => "col-sm-12" }
+ .form-check
+ = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input'
+ = label_tag 'project[initialize_with_readme]', class: 'form-check-label' do
+ .option-title
+ %strong Initialize repository with a README
+ .option-description
+ Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.
+
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
diff --git a/app/views/projects/clusters/_dropdown.html.haml b/app/views/projects/clusters/_dropdown.html.haml
deleted file mode 100644
index d55a9c60b64..00000000000
--- a/app/views/projects/clusters/_dropdown.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
-
-.dropdown.clusters-dropdown
- %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
- %span.dropdown-toggle-text
- = dropdown_text
- = icon('chevron-down')
- %ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width
- %li
- = link_to(s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
- %li
- = link_to(s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project))
diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
index d0402197821..9298d93663d 100644
--- a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
@@ -6,7 +6,7 @@
= image_tag 'illustrations/logos/google-cloud-platform_logo.svg'
.col-sm-10
%h4= s_('ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform')
- %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
+ %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
%a.btn.btn-info{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' }
Apply for credit
diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml
index db97203a2aa..b46b45fea49 100644
--- a/app/views/projects/clusters/_integration_form.html.haml
+++ b/app/views/projects/clusters/_integration_form.html.haml
@@ -1,6 +1,6 @@
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
- .form-group.append-bottom-20
+ .form-group
%h5= s_('ClusterIntegration|Integration status')
%p
- if @cluster.enabled?
@@ -10,7 +10,7 @@
= s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project.')
- else
= s_('ClusterIntegration|Kubernetes cluster integration is disabled for this project.')
- %label.append-bottom-10.js-cluster-enable-toggle-area
+ %label.append-bottom-0.js-cluster-enable-toggle-area
%button{ type: 'button',
class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
"aria-label": s_("ClusterIntegration|Toggle Kubernetes cluster"),
@@ -20,19 +20,26 @@
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
- .form-group
- %h5= s_('ClusterIntegration|Security')
- %p
- = s_("ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application.")
- = link_to s_("ClusterIntegration|Learn more about security configuration"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
-
- .form-group
- %h5= s_('ClusterIntegration|Environment scope')
- %p
- = s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.")
- = link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
- = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
+ - if has_multiple_clusters?(@project)
+ .form-group
+ %h5= s_('ClusterIntegration|Environment scope')
+ %p
+ = s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.")
+ = link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
+ = field.text_field :environment_scope, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
- if can?(current_user, :update_cluster, @cluster)
.form-group
= field.submit _('Save changes'), class: 'btn btn-success'
+
+ - unless has_multiple_clusters?(@project)
+ %h5= s_('ClusterIntegration|Environment scope')
+ %p
+ %code *
+ is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster.
+ = link_to 'More information', ('https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope')
+
+ %h5= s_('ClusterIntegration|Security')
+ %p
+ = s_("ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application.")
+ = link_to s_("ClusterIntegration|Learn more about security configuration"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications')
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index b8e40b0a38b..0a2e320556d 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -10,8 +10,10 @@
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
-= 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)
+%p= link_to('Select a different Google account', @authorize_url)
+
+= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: create_gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+ = form_errors(@gcp_cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
@@ -19,7 +21,7 @@
= 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|
+ = field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field|
.form-group
= 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' } }
diff --git a/app/views/projects/clusters/gcp/_header.html.haml b/app/views/projects/clusters/gcp/_header.html.haml
index fa989943492..a2ad3cd64df 100644
--- a/app/views/projects/clusters/gcp/_header.html.haml
+++ b/app/views/projects/clusters/gcp/_header.html.haml
@@ -1,4 +1,4 @@
-%h4.prepend-top-20
+%h4
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml
deleted file mode 100644
index 55a42ac4847..00000000000
--- a/app/views/projects/clusters/gcp/login.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-- breadcrumb_title 'Kubernetes'
-- page_title _("Login")
-
-= render_gcp_signup_offer
-
-.row.prepend-top-default
- .col-sm-4
- = render 'projects/clusters/sidebar'
- .col-sm-8
- = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine')
- = render 'header'
-.row
- .col-sm-8.offset-sm-4.signin-with-google
- - if @authorize_url
- = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url)
- = _('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 }
diff --git a/app/views/projects/clusters/gcp/new.html.haml b/app/views/projects/clusters/gcp/new.html.haml
deleted file mode 100644
index ea78d66d883..00000000000
--- a/app/views/projects/clusters/gcp/new.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-- breadcrumb_title 'Kubernetes'
-- page_title _("New Kubernetes Cluster")
-
-.row.prepend-top-default
- .col-sm-4
- = render 'projects/clusters/sidebar'
- .col-sm-8
- = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine')
- = render 'header'
- = render 'form'
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index 828e2a84753..a38003f5750 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -1,15 +1,36 @@
- breadcrumb_title 'Kubernetes'
- page_title _("Kubernetes Cluster")
+- active_tab = local_assigns.fetch(:active_tab, 'gcp')
+= javascript_include_tag 'https://apis.google.com/js/api.js'
= render_gcp_signup_offer
.row.prepend-top-default
- .col-sm-4
+ .col-md-3
= render 'sidebar'
- .col-sm-8
- %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
+ .col-md-9.js-toggle-container
+ %ul.nav-links.nav-tabs.gitlab-tabs.nav{ role: 'tablist' }
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link{ href: '#create-gcp-cluster-pane', id: 'create-gcp-cluster-tab', class: active_when(active_tab == 'gcp'), data: { toggle: 'tab' }, role: 'tab' }
+ %span Create new Cluster on GKE
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link{ href: '#add-user-cluster-pane', id: 'add-user-cluster-tab', class: active_when(active_tab == 'user'), data: { toggle: 'tab' }, role: 'tab' }
+ %span Add existing cluster
- %p= s_('ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab')
- = link_to s_('ClusterIntegration|Create on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
- %p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
- = link_to s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
+ .tab-content.gitlab-tab-content
+ .tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' }
+ = render 'projects/clusters/gcp/header'
+ - if @valid_gcp_token
+ = render 'projects/clusters/gcp/form'
+ - elsif @authorize_url
+ .signin-with-google
+ = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url)
+ = _('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 }
+
+ .tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' }
+ = render 'projects/clusters/user/header'
+ = render 'projects/clusters/user/form'
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
index db57da99ec7..3006bb5073e 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -1,13 +1,14 @@
-= form_for @cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
- = form_errors(@cluster)
+= form_for @user_cluster, url: create_user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+ = form_errors(@user_cluster)
.form-group
= 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'), class: 'label-light'
- = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
+ - if has_multiple_clusters?(@project)
+ .form-group
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light'
+ = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope')
- = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
+ = field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-light'
= platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
diff --git a/app/views/projects/clusters/user/_header.html.haml b/app/views/projects/clusters/user/_header.html.haml
index 37f6a788518..749177fa6c1 100644
--- a/app/views/projects/clusters/user/_header.html.haml
+++ b/app/views/projects/clusters/user/_header.html.haml
@@ -1,4 +1,4 @@
-%h4.prepend-top-20
+%h4
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
- link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index', anchor: 'adding-an-existing-kubernetes-cluster'), target: '_blank', rel: 'noopener noreferrer')
diff --git a/app/views/projects/clusters/user/new.html.haml b/app/views/projects/clusters/user/new.html.haml
deleted file mode 100644
index 7fb75cd9cc7..00000000000
--- a/app/views/projects/clusters/user/new.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- breadcrumb_title 'Kubernetes'
-- page_title _("New Kubernetes cluster")
-
-.row.prepend-top-default
- .col-sm-4
- = render 'projects/clusters/sidebar'
- .col-sm-8
- = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Add an existing Kubernetes cluster')
- = render 'header'
- .prepend-top-20
- = render 'form'
diff --git a/app/views/projects/deploy_tokens/_index.html.haml b/app/views/projects/deploy_tokens/_index.html.haml
index 725720d2222..33faab0c510 100644
--- a/app/views/projects/deploy_tokens/_index.html.haml
+++ b/app/views/projects/deploy_tokens/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = expand_deploy_tokens_section?(@new_deploy_token)
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded) }
.settings-header
%h4= s_('DeployTokens|Deploy Tokens')
%button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
diff --git a/app/views/projects/deploy_tokens/_revoke_modal.html.haml b/app/views/projects/deploy_tokens/_revoke_modal.html.haml
index a67c3a0c841..35eacae2c2e 100644
--- a/app/views/projects/deploy_tokens/_revoke_modal.html.haml
+++ b/app/views/projects/deploy_tokens/_revoke_modal.html.haml
@@ -1,4 +1,4 @@
-.modal{ id: "revoke-modal-#{token.id}" }
+.modal{ id: "revoke-modal-#{token.id}", tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index c7ac687e4a6..282566eeadc 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -14,4 +14,4 @@
= author_avatar(deployment.commit, size: 20)
= link_to_markdown commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message"
- else
- Cant find HEAD commit for this branch
+ = _("Can't find HEAD commit for this branch")
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index 520696b01c6..85bc8ec07e3 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -1,14 +1,14 @@
.gl-responsive-table-row.deployment{ role: 'row' }
.table-section.section-10{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' } ID
+ .table-mobile-header{ role: 'rowheader' }= _("ID")
%strong.table-mobile-content ##{deployment.iid}
.table-section.section-30{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' } Commit
+ .table-mobile-header{ role: 'rowheader' }= _("Commit")
= render 'projects/deployments/commit', deployment: deployment
.table-section.section-25.build-column{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' } Job
+ .table-mobile-header{ role: 'rowheader' }= _("Job")
- if deployment.deployable
.table-mobile-content
.flex-truncate-parent
@@ -21,7 +21,7 @@
= user_avatar(user: deployment.user, size: 20)
.table-section.section-15{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' } Created
+ .table-mobile-header{ role: 'rowheader' }= _("Created")
%span.table-mobile-content= time_ago_with_tooltip(deployment.created_at)
.table-section.section-20.table-button-footer{ role: 'gridcell' }
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
index 5941e01c6f1..95f950948ab 100644
--- a/app/views/projects/deployments/_rollback.haml
+++ b/app/views/projects/deployments/_rollback.haml
@@ -1,6 +1,6 @@
- if can?(current_user, :create_deployment, deployment) && deployment.deployable
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
- if deployment.last?
- Re-deploy
+ = _("Re-deploy")
- else
- Rollback
+ = _("Rollback")
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 9f175d2376f..c2d900cbcf7 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -4,7 +4,7 @@
- expanded = Rails.env.test?
.project-edit-container
- %section.settings.general-settings.no-animate{ class: ('expanded' if expanded) }
+ %section.settings.general-settings.no-animate#js-general-project-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
General project
@@ -65,7 +65,7 @@
= link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted"
= f.submit 'Save changes', class: "btn btn-success js-btn-save-general-project-settings"
- %section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }
+ %section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded) }
.settings-header
%h4
Permissions
@@ -82,7 +82,7 @@
= render_if_exists 'projects/issues_settings'
- %section.qa-merge-request-settings.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
+ %section.qa-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
.settings-header
%h4
Merge request
@@ -101,7 +101,7 @@
= render 'export', project: @project
- %section.qa-advanced-settings.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) }
+ %section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
Advanced
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
index a82ef5ee5bb..a264252e095 100644
--- a/app/views/projects/environments/_external_url.html.haml
+++ b/app/views/projects/environments/_external_url.html.haml
@@ -1,4 +1,4 @@
- if environment.external_url && can?(current_user, :read_environment, environment)
= link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do
- = icon('external-link')
+ = sprite_icon('external-link')
View deployment
diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml
index b4102fcf103..a4b27575095 100644
--- a/app/views/projects/environments/_metrics_button.html.haml
+++ b/app/views/projects/environments/_metrics_button.html.haml
@@ -3,5 +3,5 @@
- return unless can?(current_user, :read_environment, environment)
= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
- = icon('area-chart')
+ = sprite_icon('chart')
Monitoring
diff --git a/app/views/projects/environments/_terminal_button.html.haml b/app/views/projects/environments/_terminal_button.html.haml
index a6201bdbc42..38bc087664b 100644
--- a/app/views/projects/environments/_terminal_button.html.haml
+++ b/app/views/projects/environments/_terminal_button.html.haml
@@ -1,3 +1,3 @@
- if environment.has_terminals? && can?(current_user, :admin_environment, @project)
= link_to terminal_project_environment_path(@project, environment), class: 'btn terminal-button' do
- = icon('terminal')
+ = sprite_icon('terminal')
diff --git a/app/views/projects/environments/empty.html.haml b/app/views/projects/environments/empty.html.haml
new file mode 100644
index 00000000000..1413930ebdb
--- /dev/null
+++ b/app/views/projects/environments/empty.html.haml
@@ -0,0 +1,14 @@
+- page_title _("Metrics")
+
+.row
+ .col-sm-12
+ .svg-content
+ = image_tag 'illustrations/operations_metrics_empty.svg'
+.row.empty-environments
+ .col-sm-12.text-center
+ %h4
+ = s_('Metrics|No deployed environments')
+ .state-description
+ = s_('Metrics|Check out the CI/CD documentation on deploying to an environment')
+ .prepend-top-10
+ = link_to s_("Metrics|Learn about environments"), help_page_path('ci/environments'), class: 'btn btn-success'
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index d6f0b230b58..290970a1045 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -2,15 +2,9 @@
- page_title "Metrics for environment", @environment.name
.prometheus-container{ class: container_class }
- .top-area
- .row
- .col-sm-6
- %h3
- Environment:
- = link_to @environment.name, environment_path(@environment)
-
#prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'),
"clusters-path": project_clusters_path(@project),
+ "current-environment-name": @environment.name,
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
@@ -18,6 +12,7 @@
"empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json),
"deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json),
+ "environments-endpoint": project_environments_path(@project, format: :json),
"project-path": project_path(@project),
"tags-path": project_tags_path(@project),
"has-metrics": "#{@environment.has_metrics?}" } }
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index 6ec4ff56552..5b680189bc8 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -16,7 +16,7 @@
.nav-controls
- if @environment.external_url.present?
= link_to @environment.external_url, class: 'btn btn-default', target: '_blank', rel: 'noopener noreferrer nofollow' do
- = icon('external-link')
+ = sprite_icon('external-link')
= render 'projects/deployments/actions', deployment: @environment.last_deployment
.terminal-container{ class: container_class }
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index 983cb187c2f..3f1974d05f4 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -30,7 +30,7 @@
#{@commits_graph.start_date.strftime('%b %d')}
- end_time = capture do
#{@commits_graph.end_date.strftime('%b %d')}
- = (_("Commit statistics for %{ref} %{start_time} - %{end_time}") % { ref: "<strong>#{@ref}</strong>", start_time: start_time, end_time: end_time }).html_safe
+ = (_("Commit statistics for %{ref} %{start_time} - %{end_time}") % { ref: "<strong>#{h @ref}</strong>", start_time: start_time, end_time: end_time }).html_safe
.col-md-6
.tree-ref-container
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/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 459150c1067..b88fe47726d 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -1,6 +1,10 @@
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container
.blocks-container
+ - if can?(current_user, :create_build_terminal, @build)
+ .block
+ = link_to terminal_project_job_path(@project, @build), class: 'terminal-button pull-right btn visible-md-block visible-lg-block', title: 'Terminal' do
+ Terminal
#js-details-block-vue{ data: { can_user_retry: can?(current_user, :update_build, @build) && @build.retryable? } }
@@ -14,8 +18,8 @@
#{time_ago_with_tooltip(@build.artifacts_expire_at)}
- elsif @build.has_expiring_artifacts?
%p.build-detail-row
- The artifacts will be removed in
- %span= time_ago_with_tooltip @build.artifacts_expire_at
+ The artifacts will be removed
+ #{time_ago_with_tooltip(@build.artifacts_expire_at)}
- if @build.artifacts?
.btn-group.d-flex{ role: :group }
diff --git a/app/views/projects/jobs/terminal.html.haml b/app/views/projects/jobs/terminal.html.haml
new file mode 100644
index 00000000000..efea666a4d9
--- /dev/null
+++ b/app/views/projects/jobs/terminal.html.haml
@@ -0,0 +1,11 @@
+- @no_container = true
+- add_to_breadcrumbs 'Jobs', project_jobs_path(@project)
+- add_to_breadcrumbs "##{@build.id}", project_job_path(@project, @build)
+- breadcrumb_title 'Terminal'
+- page_title 'Terminal', "#{@build.name} (##{@build.id})", 'Jobs'
+
+- content_for :page_specific_javascripts do
+ = stylesheet_link_tag "xterm/xterm"
+
+.terminal-container{ class: container_class }
+ #terminal{ data: { project_path: terminal_project_job_path(@project, @build, format: :ws) } }
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index b2eacabc21a..f7a5d85500f 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -24,23 +24,28 @@
There are no commits yet.
= custom_icon ('illustration_no_commits')
- else
- %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom
- %li.commits-tab
- = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
- Commits
- %span.badge.badge-pill= @commits.size
- - if @pipelines.any?
- %li.builds-tab
- = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
- Pipelines
- %span.badge.badge-pill= @pipelines.size
- %li.diffs-tab
- = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
- Changes
- %span.badge.badge-pill= @merge_request.diff_size
+ .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
+ .merge-request-tabs-container
+ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.js-tabs-affix
+ %li.commits-tab.new-tab
+ = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do
+ Commits
+ %span.badge.badge-pill= @commits.size
+ - if @pipelines.any?
+ %li.builds-tab
+ = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tabvue'} do
+ Pipelines
+ %span.badge.badge-pill= @pipelines.size
+ %li.diffs-tab
+ = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue'} do
+ Changes
+ %span.badge.badge-pill= @merge_request.diff_size
- .tab-content
- #commits.commits.tab-pane.active
+ #diff-notes-app.tab-content
+ #new.commits.tab-pane.active
= render "projects/merge_requests/commits"
#diffs.diffs.tab-pane
-# This tab is always loaded via AJAX
diff --git a/app/views/projects/merge_requests/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml
index 19659fe5140..bf3df0abf86 100644
--- a/app/views/projects/merge_requests/diffs/_diffs.html.haml
+++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml
@@ -16,6 +16,6 @@
%span.ref-name= @merge_request.target_branch
.text-center= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save'
- else
- - diff_viewable = @merge_request_diff ? @merge_request_diff.collected? || @merge_request_diff.overflow? : true
+ - diff_viewable = @merge_request_diff ? @merge_request_diff.viewable? : true
- if diff_viewable
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 2f1877a15c2..b23baa22d8b 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,21 @@
%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,
+ project_path: project_path(@merge_request.project)} }
.mr-loading-status
= spinner
diff --git a/app/views/projects/mirrors/_push.html.haml b/app/views/projects/mirrors/_push.html.haml
index c3dcd9617a6..2b2871a81e5 100644
--- a/app/views/projects/mirrors/_push.html.haml
+++ b/app/views/projects/mirrors/_push.html.haml
@@ -1,5 +1,5 @@
- expanded = Rails.env.test?
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
Push to a remote repository
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index fe2903b456f..9a50a51e4be 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = Rails.env.test?
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-protected-tags-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
Protected Tags
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index a23f5d6f0c3..6ee83fae25e 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -26,7 +26,7 @@
- else
- runner_project = @project.runner_projects.find_by(runner_id: runner)
= link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
- - elsif !(runner.is_shared? || runner.group_type?) # We can simplify this to `runner.project_type?` when migrating #runner_type is complete
+ - elsif runner.project_type?
= form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: runner.id
= f.submit _('Enable for this project'), class: 'btn btn-sm'
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 209b9c71390..9314804c5dd 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -6,13 +6,13 @@
1.
= link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noopener noreferrer nofollow' do
Enable custom slash commands
- = icon('external-link')
+ = sprite_icon('external-link', size: 16)
on your Mattermost installation
%li
2.
= link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noopener noreferrer nofollow' do
Add a slash command
- = icon('external-link')
+ = sprite_icon('external-link', size: 16)
in your Mattermost team with these options:
%hr
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index b20614dc88f..f51dd581d29 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -7,7 +7,7 @@
project by entering slash commands in Mattermost.
= link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
View documentation
- = icon('external-link')
+ = sprite_icon('external-link', size: 16)
%p.inline
See list of available commands in Mattermost after setting up this service,
by entering
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 9d045d84b52..f25d2ecdfb1 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -8,7 +8,7 @@
project by entering slash commands in Slack.
= link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
View documentation
- = icon('external-link')
+ = sprite_icon('external-link', size: 16)
%p.inline
See list of available commands in Slack after setting up this service,
by entering
@@ -20,7 +20,7 @@
1.
= link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
Add a slash command
- = icon('external-link')
+ = sprite_icon('external-link', size: 16)
in your Slack team with these options:
%hr
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 4359362bb05..31c2616d283 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -63,4 +63,4 @@
.form-text.text-muted
= s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
- = f.submit 'Save changes', class: "btn btn-success prepend-top-15"
+ = f.submit _('Save changes'), class: "btn btn-success prepend-top-15"
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 7142a9d635e..fb113aa7639 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -4,20 +4,20 @@
= form_errors(@project)
%fieldset.builds-feature
.form-group.append-bottom-default.js-secret-runner-token
- = f.label :runners_token, "Runner token", class: 'label-light'
+ = f.label :runners_token, _("Runner token"), class: 'label-light'
.form-control.js-secret-value-placeholder
= '*' * 20
= f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89'
- %p.form-text.text-muted The secure token used by the Runner to checkout the project
+ %p.form-text.text-muted= _("The secure token used by the Runner to checkout the project")
%button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } }
= _('Reveal value')
%hr
.form-group
%h5.prepend-top-0
- Git strategy for pipelines
+ = _("Git strategy for pipelines")
%p
- Choose between <code>clone</code> or <code>fetch</code> to get the recent application code
+ = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank'
.form-check
= f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' }
@@ -25,29 +25,29 @@
%strong git clone
%br
%span.descr
- Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job
+ = _("Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job")
.form-check
= f.radio_button :build_allow_git_fetch, 'true', { class: 'form-check-input' }
= f.label :build_allow_git_fetch_true, class: 'form-check-label' do
%strong git fetch
%br
%span.descr
- Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)
+ = _("Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)")
%hr
.form-group
- = f.label :build_timeout_human_readable, 'Timeout', class: 'label-light'
+ = f.label :build_timeout_human_readable, _('Timeout'), class: 'label-light'
= f.text_field :build_timeout_human_readable, class: 'form-control'
%p.form-text.text-muted
- Per job. If a job passes this threshold, it will be marked as failed
+ = _("Per job. If a job passes this threshold, it will be marked as failed")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr
.form-group
- = f.label :ci_config_path, 'Custom CI config path', class: 'label-light'
+ = f.label :ci_config_path, _('Custom CI config path'), class: 'label-light'
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
%p.form-text.text-muted
- The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>
+ = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank'
%hr
@@ -55,36 +55,35 @@
.form-check
= f.check_box :public_builds, { class: 'form-check-input' }
= f.label :public_builds, class: 'form-check-label' do
- %strong Public pipelines
+ %strong= _("Public pipelines")
.form-text.text-muted
- Allow public access to pipelines and job details, including output logs and artifacts
+ = _("Allow public access to pipelines and job details, including output logs and artifacts")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank'
.bs-callout.bs-callout-info
- %p If enabled:
+ %p #{_("If enabled")}:
%ul
%li
- For public projects, anyone can view pipelines and access job details (output logs and artifacts)
+ = _("For public projects, anyone can view pipelines and access job details (output logs and artifacts)")
%li
- For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)
+ = _("For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)")
%li
- For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)
+ = _("For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)")
%p
- If disabled, the access level will depend on the user's
- permissions in the project.
+ = _("If disabled, the access level will depend on the user's permissions in the project.")
%hr
.form-group
.form-check
= f.check_box :auto_cancel_pending_pipelines, { class: 'form-check-input' }, 'enabled', 'disabled'
= f.label :auto_cancel_pending_pipelines, class: 'form-check-label' do
- %strong Auto-cancel redundant, pending pipelines
+ %strong= _("Auto-cancel redundant, pending pipelines")
.form-text.text-muted
- New pipelines will cancel older, pending pipelines on the same branch
+ = _("New pipelines will cancel older, pending pipelines on the same branch")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'auto-cancel-pending-pipelines'), target: '_blank'
%hr
.form-group
- = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
+ = f.label :build_coverage_regex, _("Test coverage parsing"), class: 'label-light'
.input-group
%span.input-group-prepend
.input-group-text /
@@ -92,11 +91,10 @@
%span.input-group-append
.input-group-text /
%p.form-text.text-muted
- A regular expression that will be used to find the test coverage
- output in the job trace. Leave blank to disable
+ = _("A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank'
.bs-callout.bs-callout-info
- %p Below are examples of regex for existing tools:
+ %p= _("Below are examples of regex for existing tools:")
%ul
%li
Simplecov (Ruby) -
@@ -119,8 +117,11 @@
%li
JaCoCo (Java/Kotlin)
%code Total.*?([0-9]{1,3})%
+ %li
+ go test -cover (Go)
+ %code coverage: \d+.\d+% of statements
- = f.submit 'Save changes', class: "btn btn-save"
+ = f.submit _('Save changes'), class: "btn btn-save"
%hr
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 56c175f5649..be22bbd7a9b 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -1,6 +1,6 @@
- @content_class = "limit-container-width" unless fluid_layout
-- page_title "CI / CD Settings"
-- page_title "CI / CD"
+- page_title _("CI / CD Settings")
+- page_title _("CI / CD")
- expanded = Rails.env.test?
- general_expanded = @project.errors.empty? ? expanded : true
@@ -8,11 +8,11 @@
%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
.settings-header
%h4
- General pipelines
+ = _("General pipelines")
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
- Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.
+ = _("Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.")
.settings-content
= render 'form'
@@ -31,11 +31,11 @@
%section.qa-runners-settings.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
- Runners
+ = _("Runners")
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
- Register and see your runners for this project.
+ = _("Register and see your runners for this project.")
.settings-content
= render 'projects/runners/index'
@@ -45,21 +45,19 @@
= _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p.append-bottom-0
= render "ci/variables/content"
.settings-content
= render 'ci/variables/index', save_endpoint: project_variables_path(@project)
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-pipeline-triggers{ class: ('expanded' if expanded) }
.settings-header
%h4
- Pipeline triggers
+ = _("Pipeline triggers")
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
- Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
- impersonate their associated user including their access to projects and their project
- permissions.
+ = _("Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will impersonate their associated user including their access to projects and their project permissions.")
.settings-content
= render 'projects/triggers/index'
diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml
index 77d88aed883..ef445f2e139 100644
--- a/app/views/projects/settings/integrations/_project_hook.html.haml
+++ b/app/views/projects/settings/integrations/_project_hook.html.haml
@@ -8,9 +8,9 @@
%span.badge.badge-gray.deploy-project-label= event.to_s.titleize
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
%span.append-right-10.inline
- SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
- = link_to 'Edit', edit_project_hook_path(@project, hook), class: 'btn btn-sm'
+ #{_("SSL Verification")}: #{hook.enable_ssl_verification ? _('enabled') : _('disabled')}
+ = link_to _('Edit'), edit_project_hook_path(@project, hook), class: 'btn btn-sm'
= render 'shared/web_hooks/test_button', triggers: ProjectHook.triggers, hook: hook, button_class: 'btn-sm'
- = link_to project_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-transparent' do
- %span.sr-only Remove
+ = link_to project_hook_path(@project, hook), data: { confirm: _('Are you sure?') }, method: :delete, class: 'btn btn-transparent' do
+ %span.sr-only= _("Remove")
= icon('trash')
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
index 2f1a548e119..76770290f36 100644
--- a/app/views/projects/settings/integrations/show.html.haml
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -1,5 +1,5 @@
- @content_class = "limit-container-width" unless fluid_layout
-- breadcrumb_title "Integrations Settings"
-- page_title 'Integrations'
+- breadcrumb_title _("Integrations Settings")
+- page_title _('Integrations')
= render 'projects/hooks/index'
= render 'projects/services/index'
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
index ea2cd36b212..5fca734222b 100644
--- a/app/views/projects/settings/members/show.html.haml
+++ b/app/views/projects/settings/members/show.html.haml
@@ -1,5 +1,5 @@
- @content_class = "limit-container-width" unless fluid_layout
-- page_title "Members"
+- page_title _("Members")
= render "projects/project_members/index"
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 5dda2ec28b4..98c609d7bd4 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title "Repository Settings"
-- page_title "Repository"
+- breadcrumb_title _("Repository Settings")
+- page_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
= render "projects/mirrors/show"
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index b3a9fa9dd91..4a3aa3dc626 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -3,34 +3,34 @@
.d-none.d-sm-block
- if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_project_snippet_path(@project, @snippet), class: "btn btn-grouped" do
- Edit
+ = _('Edit')
- if can?(current_user, :update_project_snippet, @snippet)
- = link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
- Delete
+ = link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: _("Are you sure?") }, class: "btn btn-grouped btn-inverted btn-remove", title: _('Delete Snippet') do
+ = _('Delete')
- if can?(current_user, :create_project_snippet, @project)
- = link_to new_project_snippet_path(@project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
- New snippet
+ = link_to new_project_snippet_path(@project), class: 'btn btn-grouped btn-inverted btn-create', title: _("New snippet") do
+ = _('New snippet')
- if @snippet.submittable_as_spam_by?(current_user)
- = link_to 'Submit as spam', mark_as_spam_project_snippet_path(@project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
+ = link_to _('Submit as spam'), mark_as_spam_project_snippet_path(@project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: _('Submit as spam')
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.d-block.d-sm-none.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
- Options
+ = _('Options')
= icon('caret-down')
.dropdown-menu.dropdown-menu-full-width
%ul
- if can?(current_user, :create_project_snippet, @project)
%li
- = link_to new_project_snippet_path(@project), title: "New snippet" do
- New snippet
+ = link_to new_project_snippet_path(@project), title: _("New snippet") do
+ = _('New snippet')
- if can?(current_user, :update_project_snippet, @snippet)
%li
- = link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
- Delete
+ = link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: _("Are you sure?") }, title: _('Delete Snippet') do
+ = _('Delete')
- if can?(current_user, :update_project_snippet, @snippet)
%li
= link_to edit_project_snippet_path(@project, @snippet) do
- Edit
+ = _('Edit')
- if @snippet.submittable_as_spam_by?(current_user)
%li
- = link_to 'Submit as spam', mark_as_spam_project_snippet_path(@project, @snippet), method: :post
+ = link_to _('Submit as spam'), mark_as_spam_project_snippet_path(@project, @snippet), method: :post
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index 32844f5204a..6dbd67df886 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -1,8 +1,8 @@
-- add_to_breadcrumbs "Snippets", project_snippets_path(@project)
+- add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
- breadcrumb_title @snippet.to_reference
-- page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
+- page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
%h3.page-title
- Edit Snippet
+ = _("Edit Snippet")
%hr
= render "shared/snippets/form", url: project_snippet_path(@project, @snippet)
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 65efc083fdd..1c4c73dc776 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,4 +1,4 @@
-- page_title "Snippets"
+- page_title _("Snippets")
- if current_user
.top-area
@@ -7,6 +7,6 @@
.nav-controls
- if can?(current_user, :create_project_snippet, @project)
- = link_to "New snippet", new_project_snippet_path(@project), class: "btn btn-new", title: "New snippet"
+ = link_to _("New snippet"), new_project_snippet_path(@project), class: "btn btn-new", title: _("New snippet")
= render 'snippets/snippets'
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index 1359a815429..26b333d4ecf 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -1,8 +1,8 @@
-- add_to_breadcrumbs "Snippets", project_snippets_path(@project)
-- breadcrumb_title "New"
-- page_title "New Snippets"
+- add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
+- breadcrumb_title _("New")
+- page_title _("New Snippets")
%h3.page-title
- New Snippet
+ = _('New Snippet')
%hr
= render "shared/snippets/form", url: project_snippets_path(@project, @snippet)
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 7062c5b765e..f495b4eaf30 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,7 +1,7 @@
- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout
-- add_to_breadcrumbs "Snippets", project_snippets_path(@project)
+- add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
- breadcrumb_title @snippet.to_reference
-- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
+- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
= render 'shared/snippets/header'
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 5eec7b02b54..e93925b5ef9 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -21,8 +21,9 @@
= sprite_icon('star-o')
%button.label-action.remove-priority.btn.btn-transparent.has-tooltip{ title: _('Remove priority'), type: 'button', data: { placement: 'top' }, aria_label: _('Deprioritize label') }
= sprite_icon('star')
+ - if can?(current_user, :admin_label, label)
%li.inline
- = link_to edit_label_path(label), class: 'btn btn-transparent label-action', aria_label: 'Edit label' do
+ = link_to edit_label_path(label), class: 'btn btn-transparent label-action edit', aria_label: 'Edit label' do
= sprite_icon('pencil')
%li.inline
.dropdown
@@ -42,9 +43,10 @@
container: 'body',
toggle: 'modal' } }
= _('Promote to group label')
- %li
- %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
- %button.text-danger.remove-row{ type: 'button' }= _('Delete')
+ - if can?(current_user, :admin_label, label)
+ %li
+ %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
+ %button.text-danger.remove-row{ type: 'button' }= _('Delete')
- if current_user
%li.inline.label-subscription
- if can_subscribe_to_label_in_different_levels?(label)
diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml
index 5e9007aaaac..099e3ac8462 100644
--- a/app/views/shared/_milestone_expired.html.haml
+++ b/app/views/shared/_milestone_expired.html.haml
@@ -1,7 +1,6 @@
- if milestone.expired? and not milestone.closed?
- %span.cred (Expired)
+ .status-box.status-box-expired.append-bottom-5 Expired
- if milestone.upcoming?
- %span.clgray (Upcoming)
-- if milestone.due_date || milestone.start_date
- %span
- = milestone_date_range(milestone)
+ .status-box.status-box-mr-merged.append-bottom-5 Upcoming
+- if milestone.closed?
+ .status-box.status-box-closed.append-bottom-5 Closed
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index b8b1f4ca42f..28407b543b9 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -9,13 +9,17 @@
= form_errors(token)
- .form-group
- = f.label :name, class: 'label-light'
- = f.text_field :name, class: "form-control", required: true
+ .row
+ .form-group.col-md-6
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: "form-control", required: true
- .form-group
- = f.label :expires_at, class: 'label-light'
- = f.text_field :expires_at, class: "datepicker form-control"
+ .row
+ .form-group.col-md-6
+ = f.label :expires_at, class: 'label-light'
+ .input-icon-wrapper
+ = f.text_field :expires_at, class: "datepicker form-control", placeholder: 'YYYY-MM-DD'
+ = icon('calendar', { class: 'input-icon-right' })
.form-group
= f.label :scopes, class: 'label-light'
diff --git a/app/views/shared/_user_dropdown_contributing_link.html.haml b/app/views/shared/_user_dropdown_contributing_link.html.haml
new file mode 100644
index 00000000000..333d6fa3489
--- /dev/null
+++ b/app/views/shared/_user_dropdown_contributing_link.html.haml
@@ -0,0 +1,5 @@
+%li
+ = link_to "https://about.gitlab.com/contributing", target: '_blank', class: 'text-nowrap' do
+ = _("Contribute to GitLab")
+ = sprite_icon('external-link', size: 16)
+%li.divider
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 496b94ec953..28e6fe1b16d 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -2,19 +2,20 @@
- group = local_assigns.fetch(:group, false)
- @no_breadcrumb_container = true
- @no_container = true
-- @content_class = "issue-boards-content"
-- breadcrumb_title "Issue Board"
-- page_title "Boards"
+- @content_class = "issue-boards-content js-focus-mode-board"
+- breadcrumb_title _("Issue Boards")
+- page_title _("Boards")
- content_for :page_specific_javascripts do
-# haml-lint:disable InlineJavaScript
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
+ %script#js-board-promotion{ type: "text/x-template" }= render_if_exists "shared/promotions/promote_issue_board"
#board-app.boards-app{ "v-cloak" => true, data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
.d-none.d-sm-none.d-md-block
- = render 'shared/issuable/search_bar', type: :boards
+ = render 'shared/issuable/search_bar', type: :boards, board: board
.boards-list
.boards-app-loading.text-center{ "v-if" => "loading" }
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index 76843ce7cc0..03e008f5fa0 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -30,21 +30,20 @@
%board-delete{ "inline-template" => true,
":list" => "list",
"v-if" => "!list.preset && list.id" }
- %button.board-delete.has-tooltip.float-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
+ %button.board-delete.has-tooltip.float-right{ type: "button", title: _("Delete list"), "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
- .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' }
+ .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"' }
%span.issue-count-badge-count.float-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_list, current_board_parent)
%button.issue-count-badge-add-button.btn.btn-sm.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"',
- "aria-label" => "New issue",
- "title" => "New issue",
+ "aria-label" => _("New issue"),
+ "title" => _("New issue"),
data: { placement: "top", container: "body" } }
= icon("plus", class: "js-no-trigger-collapse")
-
- %board-list{ "v-if" => 'list.type !== "blank"',
+ %board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"',
":list" => "list",
":issues" => "list.issues",
":loading" => "list.loading",
@@ -55,3 +54,4 @@
"ref" => "board-list" }
- if can?(current_user, :admin_list, current_board_parent)
%board-blank-state{ "v-if" => 'list.id == "blank"' }
+ = render_if_exists 'shared/boards/board_promotion_state'
diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml
index 774dafe5f2c..1ff956649ed 100644
--- a/app/views/shared/boards/components/_sidebar.html.haml
+++ b/app/views/shared/boards/components/_sidebar.html.haml
@@ -8,6 +8,7 @@
{{ issue.title }}
%br/
%span
+ = render_if_exists "shared/boards/components/sidebar/issue_project_path"
= precede "#" do
{{ issue.iid }}
%a.gutter-toggle.float-right{ role: "button",
@@ -17,9 +18,11 @@
= custom_icon("icon_close", size: 15)
.js-issuable-update
= render "shared/boards/components/sidebar/assignee"
+ = render_if_exists "shared/boards/components/sidebar/epic"
= render "shared/boards/components/sidebar/milestone"
= render "shared/boards/components/sidebar/due_date"
= render "shared/boards/components/sidebar/labels"
+ = render_if_exists "shared/boards/components/sidebar/weight"
= render "shared/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue",
":issue-update" => "issue.sidebarInfoEndpoint",
diff --git a/app/views/shared/boards/components/sidebar/_due_date.html.haml b/app/views/shared/boards/components/sidebar/_due_date.html.haml
index 10217b6cbf0..5630375f428 100644
--- a/app/views/shared/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/shared/boards/components/sidebar/_due_date.html.haml
@@ -1,20 +1,20 @@
.block.due_date
.title
- Due date
+ = _("Due date")
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
+ = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value
.value-content
%span.no-value{ "v-if" => "!issue.dueDate" }
- No due date
+ = _("No due date")
%span.bold{ "v-if" => "issue.dueDate" }
{{ issue.dueDate | due-date }}
- if can_admin_issue?
%span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
\-
%a.js-remove-due-date{ href: "#", role: "button" }
- remove due date
+ = _('remove due date')
- if can_admin_issue?
.selectbox
%input{ type: "hidden",
@@ -23,9 +23,9 @@
.dropdown
%button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" } }
- %span.dropdown-toggle-text Due date
+ %span.dropdown-toggle-text= _("Due date")
= icon('chevron-down')
.dropdown-menu.dropdown-menu-due-date
- = dropdown_title('Due date')
+ = dropdown_title(_('Due date'))
= dropdown_content do
.js-due-date-calendar
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index daee691e358..607e7f471c9 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -1,12 +1,12 @@
.block.labels
.title
- Labels
+ = _("Labels")
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
+ = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value.issuable-show-labels.dont-hide
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
- None
+ = _("None")
%a{ href: "#",
"v-for" => "label in issue.labels" }
.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
@@ -28,7 +28,7 @@
namespace_path: @namespace_path,
project_path: @project.try(:path) } }
%span.dropdown-toggle-text
- Label
+ = _("Label")
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default"
diff --git a/app/views/shared/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml
index f2bedd5e3c9..b15d60002fc 100644
--- a/app/views/shared/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml
@@ -1,12 +1,12 @@
.block.milestone
.title
- Milestone
+ = _("Milestone")
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
+ = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value
%span.no-value{ "v-if" => "!issue.milestone" }
- None
+ = _("None")
%span.bold.has-tooltip{ "v-if" => "issue.milestone" }
{{ issue.milestone.title }}
- if can_admin_issue?
@@ -19,10 +19,10 @@
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
":data-selected" => "milestoneTitle",
":data-issuable-id" => "issue.iid" }
- Milestone
+ = _("Milestone")
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable
- = dropdown_title("Assign milestone")
- = dropdown_filter("Search milestones")
+ = dropdown_title(_("Assign milestone"))
+ = dropdown_filter(_("Search milestones"))
= dropdown_content
= dropdown_loading
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index c7c33288e9d..2e26fe63d3e 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -16,7 +16,7 @@
- if has_button
.text-center
- if project_select_button
- = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues
+ = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues, with_feature_enabled: 'issues'
- else
= link_to 'New issue', button_path, class: 'btn btn-success', title: 'New issue', id: 'new_issue_link'
- else
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index 014220761a9..186139f3526 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -15,7 +15,7 @@
= _("Interested parties can even contribute by pushing commits if they want to.")
.text-center
- if project_select_button
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('New merge request'), type: :merge_requests
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('New merge request'), type: :merge_requests, with_feature_enabled: 'merge_requests'
- else
= link_to _('New merge request'), button_path, class: 'btn btn-new', title: _('New merge request'), id: 'new_merge_request_link'
- else
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index ef9ea2194ee..9ce7f6fe269 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -1,9 +1,14 @@
- type = local_assigns.fetch(:type)
+- board = local_assigns.fetch(:board, nil)
- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
- full_path = @project.present? ? @project.full_path : @group.full_path
+- user_can_admin_list = board && can?(current_user, :admin_list, board.parent)
.issues-filters
.issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
+ - if type == :boards
+ #js-multiple-boards-switcher.inline.boards-switcher{ "v-cloak" => true }
+ = render_if_exists "shared/boards/switcher", board: board
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
@@ -99,13 +104,18 @@
%gl-emoji
%span.js-data-value.prepend-left-10
{{name}}
+
+ = render_if_exists 'shared/issuable/filter_weight', type: type
+
%button.clear-search.hidden{ type: 'button' }
= icon('times')
.filter-dropdown-container
- if type == :boards
- - if can?(current_user, :admin_list, board.parent)
- = render_if_exists 'shared/issuable/board_create_list_dropdown', board: board
+ .js-board-config{ data: { can_admin_list: user_can_admin_list, has_scope: board.scoped? } }
+ - if user_can_admin_list
+ = render 'shared/issuable/board_create_list_dropdown', board: board
- if @project
#js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
+ #js-toggle-focus-btn
- elsif type != :boards_modal
= render 'shared/sort_dropdown'
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index c35d0b3751f..e49bdec386a 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -6,7 +6,7 @@
%div{ class: div_class }
= form.text_field :title, required: true, maxlength: 255, autofocus: true,
- autocomplete: 'off', class: 'form-control pad qa-issuable-form-title'
+ autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title')
- if issuable.respond_to?(:work_in_progress?)
%p.form-text.text-muted
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/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 09bbd04c2bf..c559945a9c9 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -1,76 +1,59 @@
- dashboard = local_assigns[:dashboard]
- custom_dom_id = dom_id(milestone.try(:milestones) ? milestone.milestones.first : milestone)
+- milestone_type = milestone.group_milestone? ? 'Group Milestone' : 'Project Milestone'
%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
.row
.col-sm-6
- %strong= link_to truncate(milestone.title, length: 100), milestone_path
- - if milestone.group_milestone?
- %span - Group Milestone
- - else
- %span - Project Milestone
+ .append-bottom-5
+ %strong= link_to truncate(milestone.title, length: 100), milestone_path
+ - if @group
+ = " - #{milestone_type}"
- .col-sm-6
- .float-right.light #{milestone.percent_complete(current_user)}% complete
- .row
- .col-sm-6
+ - if @project || milestone.is_a?(GlobalMilestone) || milestone.group_milestone?
+ - if milestone.due_date || milestone.start_date
+ .milestone-range.append-bottom-5
+ = milestone_date_range(milestone)
+ %div
+ = render('shared/milestone_expired', milestone: milestone)
+ - if milestone.legacy_group_milestone?
+ .projects
+ - milestone.milestones.each do |milestone|
+ = link_to milestone_path(milestone) do
+ %span.label-badge.label-badge-blue.d-inline-block.append-bottom-5
+ = dashboard ? milestone.project.full_name : milestone.project.name
+
+ .col-sm-4.milestone-progress
+ = milestone_progress_bar(milestone)
= link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
- .col-sm-6= milestone_progress_bar(milestone)
- - if milestone.is_a?(GlobalMilestone) || milestone.group_milestone?
- .row
- .col-sm-6
- - if milestone.legacy_group_milestone?
- .expiration= render('shared/milestone_expired', milestone: milestone)
- .projects
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.badge.badge-gray
- = dashboard ? milestone.project.full_name : milestone.project.name
- - if @group
- .col-sm-6.milestone-actions
+ .float-lg-right.light #{milestone.percent_complete(current_user)}% complete
+ .col-sm-2
+ .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
+ - if @project
+ - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
+ - if @project.group
+ %button.js-promote-project-milestone-button.btn.btn-blank.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
+ disabled: true,
+ type: 'button',
+ data: { url: promote_project_milestone_path(milestone.project, milestone),
+ milestone_title: milestone.title,
+ group_name: @project.group.name,
+ target: '#promote-milestone-modal',
+ container: 'body',
+ toggle: 'modal' } }
+ = sprite_icon('level-up', size: 14)
+
+ = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close btn-grouped"
+ - unless milestone.active?
+ = link_to 'Reopen Milestone', project_milestone_path(@project, milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
+ - if @group
- if can?(current_user, :admin_milestones, @group)
- - if milestone.group_milestone?
- = link_to edit_group_milestone_path(@group, milestone), class: "btn btn-sm btn-grouped" do
- Edit
- \
- if milestone.closed?
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- else
= link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
-
- - if @project
- .row
- .col-sm-6
- = render('shared/milestone_expired', milestone: milestone)
- .col-sm-6.milestone-actions
- - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
- = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-sm btn-grouped" do
- Edit
- \
-
- - if @project.group
- %button.js-promote-project-milestone-button.btn.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
- disabled: true,
- type: 'button',
- data: { url: promote_project_milestone_path(milestone.project, milestone),
- milestone_title: milestone.title,
- group_name: @project.group.name,
- target: '#promote-milestone-modal',
- container: 'body',
- toggle: 'modal' } }
- = _('Promote')
-
- = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close btn-grouped"
-
- %button.js-delete-milestone-button.btn.btn-sm.btn-grouped.btn-danger{ data: { toggle: 'modal',
- target: '#delete-milestone-modal',
- milestone_id: milestone.id,
- milestone_title: markdown_field(milestone, :title),
- milestone_url: project_milestone_path(milestone.project, milestone),
- milestone_issue_count: milestone.issues.count,
- milestone_merge_request_count: milestone.merge_requests.count },
- disabled: true }
- = _('Delete')
- = icon('spin spinner', class: 'js-loading-icon hidden' )
+ - if dashboard
+ .status-box.status-box-milestone
+ = milestone_type
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index c360f1ffe2a..6b2715b47a7 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -40,5 +40,5 @@
= yield(:note_actions)
- %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
+ %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Discard draft" } }
Discard draft
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 526330f4e50..d4e8f30e458 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -59,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/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 88f0675f795..6be1fb485a4 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -4,7 +4,7 @@
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- user = local_assigns[:user]
-- access = user&.max_member_access_for_project(project.id) unless user.nil?
+- access = max_project_member_access(project)
- css_class = '' unless local_assigns[:css_class]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml
index e50b7fa68dd..362569bfbaf 100644
--- a/app/views/shared/runners/show.html.haml
+++ b/app/views/shared/runners/show.html.haml
@@ -3,7 +3,7 @@
%h3.page-title
Runner ##{@runner.id}
.float-right
- - if @runner.shared?
+ - if @runner.instance_type?
%span.runner-state.runner-state-shared
Shared
- elsif @runner.group_type?
@@ -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/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index 2d0bb722189..dcb3fca23f2 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -3,8 +3,7 @@
- token = local_assigns.fetch(:token)
- scopes.each do |scope|
- %fieldset
- = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
- = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: "label-light"
- %span= t(scope, scope: [:doorkeeper, :scopes])
- .scope-description= t scope, scope: [:doorkeeper, :scope_desc]
+ %fieldset.form-group.form-check
+ = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: 'form-check-input'
+ = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: 'label-light form-check-label'
+ .text-secondary= t scope, scope: [:doorkeeper, :scope_desc]
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index 7eb221620ad..1c788b9a737 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -2,9 +2,6 @@
%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code
-# haml-lint:disable InlineJavaScript
-%script#js-authenticate-u2f-not-supported{ type: "text/template" }
- %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
-
%script#js-authenticate-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index 044e470141e..06324575ffc 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AdminEmailWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 30b6796a7d6..b8b854853b7 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -11,7 +11,7 @@
- cronjob:remove_old_web_hook_logs
- cronjob:remove_unreferenced_lfs_objects
- cronjob:repository_archive_cache
-- cronjob:repository_check_batch
+- cronjob:repository_check_dispatch
- cronjob:requests_profiles
- cronjob:schedule_update_user_activity
- cronjob:stuck_ci_jobs
@@ -20,6 +20,7 @@
- cronjob:ci_archive_traces_cron
- cronjob:trending_projects
- cronjob:issue_due_scheduler
+- cronjob:prune_web_hook_logs
- gcp_cluster:cluster_install_app
- gcp_cluster:cluster_provision
@@ -71,6 +72,7 @@
- pipeline_processing:update_head_pipeline_for_merge_request
- repository_check:repository_check_clear
+- repository_check:repository_check_batch
- repository_check:repository_check_single_repository
- default
@@ -118,3 +120,4 @@
- web_hook
- repository_update_remote_mirror
- create_note_diff_file
+- delete_diff_files
diff --git a/app/workers/archive_trace_worker.rb b/app/workers/archive_trace_worker.rb
index dea7425ad88..c6f89a17729 100644
--- a/app/workers/archive_trace_worker.rb
+++ b/app/workers/archive_trace_worker.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
class ArchiveTraceWorker
include ApplicationWorker
include PipelineBackgroundQueue
def perform(job_id)
- Ci::Build.find_by(id: job_id).try do |job|
+ Ci::Build.without_archived_trace.find_by(id: job_id).try do |job|
job.trace.archive!
end
end
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 8fe3619f6ee..dd62bb0f33d 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AuthorizedProjectsWorker
include ApplicationWorker
prepend WaitableWorker
diff --git a/app/workers/background_migration_worker.rb b/app/workers/background_migration_worker.rb
index 376703f6319..eaec7d48f35 100644
--- a/app/workers/background_migration_worker.rb
+++ b/app/workers/background_migration_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BackgroundMigrationWorker
include ApplicationWorker
diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb
index 62b212c79be..53d77dc4524 100644
--- a/app/workers/build_coverage_worker.rb
+++ b/app/workers/build_coverage_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BuildCoverageWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index 46f1ac09915..9dc2c7f3601 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BuildFinishedWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
index cbfca8c342c..f1f71dc589c 100644
--- a/app/workers/build_hooks_worker.rb
+++ b/app/workers/build_hooks_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BuildHooksWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
index e4f4e6c1d9e..1b3f1fd3c2a 100644
--- a/app/workers/build_queue_worker.rb
+++ b/app/workers/build_queue_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BuildQueueWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index 4b9097bc5e4..e1c1cc24a94 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BuildSuccessWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/build_trace_sections_worker.rb b/app/workers/build_trace_sections_worker.rb
index c0f5c144e10..f4114b3353c 100644
--- a/app/workers/build_trace_sections_worker.rb
+++ b/app/workers/build_trace_sections_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BuildTraceSectionsWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb
index 2ac65f41f4e..7d4e9660a4e 100644
--- a/app/workers/ci/archive_traces_cron_worker.rb
+++ b/app/workers/ci/archive_traces_cron_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Ci
class ArchiveTracesCronWorker
include ApplicationWorker
@@ -10,6 +12,7 @@ module Ci
Ci::Build.finished.with_live_trace.find_each(batch_size: 100) do |build|
begin
build.trace.archive!
+ rescue ::Gitlab::Ci::Trace::AlreadyArchivedError
rescue => e
failed_archive_counter.increment
Rails.logger.error "Failed to archive stale live trace. id: #{build.id} message: #{e.message}"
diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb
index 218d6688bd9..6376c6d32cf 100644
--- a/app/workers/ci/build_trace_chunk_flush_worker.rb
+++ b/app/workers/ci/build_trace_chunk_flush_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Ci
class BuildTraceChunkFlushWorker
include ApplicationWorker
diff --git a/app/workers/cluster_install_app_worker.rb b/app/workers/cluster_install_app_worker.rb
index f771cb4939f..32e2ea7996c 100644
--- a/app/workers/cluster_install_app_worker.rb
+++ b/app/workers/cluster_install_app_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClusterInstallAppWorker
include ApplicationWorker
include ClusterQueue
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
index 1ab4de3b647..59de7903c1c 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClusterProvisionWorker
include ApplicationWorker
include ClusterQueue
diff --git a/app/workers/cluster_wait_for_app_installation_worker.rb b/app/workers/cluster_wait_for_app_installation_worker.rb
index d564d5e48bf..e8d7e52f70f 100644
--- a/app/workers/cluster_wait_for_app_installation_worker.rb
+++ b/app/workers/cluster_wait_for_app_installation_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClusterWaitForAppInstallationWorker
include ApplicationWorker
include ClusterQueue
diff --git a/app/workers/cluster_wait_for_ingress_ip_address_worker.rb b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
index 8ba5951750c..6865384df44 100644
--- a/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
+++ b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ClusterWaitForIngressIpAddressWorker
include ApplicationWorker
include ClusterQueue
diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb
index 37586e161c9..bb06e31641d 100644
--- a/app/workers/concerns/application_worker.rb
+++ b/app/workers/concerns/application_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
Sidekiq::Worker.extend ActiveSupport::Concern
module ApplicationWorker
diff --git a/app/workers/concerns/cluster_applications.rb b/app/workers/concerns/cluster_applications.rb
index 24ecaa0b52f..9758a1ceb0e 100644
--- a/app/workers/concerns/cluster_applications.rb
+++ b/app/workers/concerns/cluster_applications.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ClusterApplications
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/cluster_queue.rb b/app/workers/concerns/cluster_queue.rb
index 24b9f145220..e44b40c36c9 100644
--- a/app/workers/concerns/cluster_queue.rb
+++ b/app/workers/concerns/cluster_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
##
# Concern for setting Sidekiq settings for the various Gcp clusters workers.
#
diff --git a/app/workers/concerns/cronjob_queue.rb b/app/workers/concerns/cronjob_queue.rb
index b6581779f6a..0683b229381 100644
--- a/app/workers/concerns/cronjob_queue.rb
+++ b/app/workers/concerns/cronjob_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Concern that sets various Sidekiq settings for workers executed using a
# cronjob.
module CronjobQueue
diff --git a/app/workers/concerns/each_shard_worker.rb b/app/workers/concerns/each_shard_worker.rb
new file mode 100644
index 00000000000..d0a728fb495
--- /dev/null
+++ b/app/workers/concerns/each_shard_worker.rb
@@ -0,0 +1,31 @@
+module EachShardWorker
+ extend ActiveSupport::Concern
+ include ::Gitlab::Utils::StrongMemoize
+
+ def each_eligible_shard
+ Gitlab::ShardHealthCache.update(eligible_shard_names)
+
+ eligible_shard_names.each do |shard_name|
+ yield shard_name
+ end
+ end
+
+ # override when you want to filter out some shards
+ def eligible_shard_names
+ healthy_shard_names
+ end
+
+ def healthy_shard_names
+ strong_memoize(:healthy_shard_names) do
+ healthy_ready_shards.map { |result| result.labels[:shard] }
+ end
+ end
+
+ def healthy_ready_shards
+ ready_shards.select(&:success)
+ end
+
+ def ready_shards
+ Gitlab::HealthChecks::GitalyCheck.readiness
+ end
+end
diff --git a/app/workers/concerns/exception_backtrace.rb b/app/workers/concerns/exception_backtrace.rb
index ea0f1f8d19b..37c9eaba0d7 100644
--- a/app/workers/concerns/exception_backtrace.rb
+++ b/app/workers/concerns/exception_backtrace.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Concern for enabling a few lines of exception backtraces in Sidekiq
module ExceptionBacktrace
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/gitlab/github_import/queue.rb b/app/workers/concerns/gitlab/github_import/queue.rb
index 22c2ce458e8..59b621f16ab 100644
--- a/app/workers/concerns/gitlab/github_import/queue.rb
+++ b/app/workers/concerns/gitlab/github_import/queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module GithubImport
module Queue
diff --git a/app/workers/concerns/mail_scheduler_queue.rb b/app/workers/concerns/mail_scheduler_queue.rb
index f3e9680d756..c051151e973 100644
--- a/app/workers/concerns/mail_scheduler_queue.rb
+++ b/app/workers/concerns/mail_scheduler_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MailSchedulerQueue
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/new_issuable.rb b/app/workers/concerns/new_issuable.rb
index 526ed0bad07..7735dec5e6b 100644
--- a/app/workers/concerns/new_issuable.rb
+++ b/app/workers/concerns/new_issuable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module NewIssuable
attr_reader :issuable, :user
diff --git a/app/workers/concerns/object_storage_queue.rb b/app/workers/concerns/object_storage_queue.rb
index a80f473a6d4..8650eed213a 100644
--- a/app/workers/concerns/object_storage_queue.rb
+++ b/app/workers/concerns/object_storage_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Concern for setting Sidekiq settings for the various GitLab ObjectStorage workers.
module ObjectStorageQueue
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/pipeline_background_queue.rb b/app/workers/concerns/pipeline_background_queue.rb
index 8bf43de6b26..bbb8ad0c982 100644
--- a/app/workers/concerns/pipeline_background_queue.rb
+++ b/app/workers/concerns/pipeline_background_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
##
# Concern for setting Sidekiq settings for the low priority CI pipeline workers.
#
diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb
index e77093a6902..3aaed4669e5 100644
--- a/app/workers/concerns/pipeline_queue.rb
+++ b/app/workers/concerns/pipeline_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
##
# Concern for setting Sidekiq settings for the various CI pipeline workers.
#
diff --git a/app/workers/concerns/project_import_options.rb b/app/workers/concerns/project_import_options.rb
index ef23990ad97..22bdf441d6b 100644
--- a/app/workers/concerns/project_import_options.rb
+++ b/app/workers/concerns/project_import_options.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ProjectImportOptions
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/project_start_import.rb b/app/workers/concerns/project_start_import.rb
index 4e55a1ee3d6..46a133db2a1 100644
--- a/app/workers/concerns/project_start_import.rb
+++ b/app/workers/concerns/project_start_import.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Used in EE by mirroring
module ProjectStartImport
def start(project)
diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb
index 43fb66c31b0..216d67e5dbc 100644
--- a/app/workers/concerns/repository_check_queue.rb
+++ b/app/workers/concerns/repository_check_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Concern for setting Sidekiq settings for the various repository check workers.
module RepositoryCheckQueue
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/waitable_worker.rb b/app/workers/concerns/waitable_worker.rb
index 48ebe862248..d85bc7d1660 100644
--- a/app/workers/concerns/waitable_worker.rb
+++ b/app/workers/concerns/waitable_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module WaitableWorker
extend ActiveSupport::Concern
diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb
index f371731f68c..a2da1bda11f 100644
--- a/app/workers/create_gpg_signature_worker.rb
+++ b/app/workers/create_gpg_signature_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateGpgSignatureWorker
include ApplicationWorker
diff --git a/app/workers/create_note_diff_file_worker.rb b/app/workers/create_note_diff_file_worker.rb
index 624b638a24e..0850250f7e3 100644
--- a/app/workers/create_note_diff_file_worker.rb
+++ b/app/workers/create_note_diff_file_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateNoteDiffFileWorker
include ApplicationWorker
diff --git a/app/workers/create_pipeline_worker.rb b/app/workers/create_pipeline_worker.rb
index c3ac35e54f5..037b4a57d4b 100644
--- a/app/workers/create_pipeline_worker.rb
+++ b/app/workers/create_pipeline_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreatePipelineWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/delete_diff_files_worker.rb b/app/workers/delete_diff_files_worker.rb
new file mode 100644
index 00000000000..bb8fbb9c373
--- /dev/null
+++ b/app/workers/delete_diff_files_worker.rb
@@ -0,0 +1,17 @@
+class DeleteDiffFilesWorker
+ include ApplicationWorker
+
+ def perform(merge_request_diff_id)
+ merge_request_diff = MergeRequestDiff.find(merge_request_diff_id)
+
+ return if merge_request_diff.without_files?
+
+ MergeRequestDiff.transaction do
+ merge_request_diff.clean!
+
+ MergeRequestDiffFile
+ .where(merge_request_diff_id: merge_request_diff.id)
+ .delete_all
+ end
+ end
+end
diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb
index 07cd1f02fb5..017d7fd1cb0 100644
--- a/app/workers/delete_merged_branches_worker.rb
+++ b/app/workers/delete_merged_branches_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DeleteMergedBranchesWorker
include ApplicationWorker
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index 6c431b02979..4d0295f8d2e 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DeleteUserWorker
include ApplicationWorker
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index dd8a6cbbef1..f9f0efb302a 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmailReceiverWorker
include ApplicationWorker
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 2a4d65b5cb3..8d0cfc73ccd 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmailsOnPushWorker
include ApplicationWorker
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index 87e5dca01fd..5d3a9a39b93 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ExpireBuildArtifactsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
index 234b4357cf7..3b57ecb36e3 100644
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ExpireBuildInstanceArtifactsWorker
include ApplicationWorker
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
index 7217364a9f2..14a57b90114 100644
--- a/app/workers/expire_job_cache_worker.rb
+++ b/app/workers/expire_job_cache_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ExpireJobCacheWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
index db73d37868a..992fc63c451 100644
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ExpirePipelineCacheWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index ae5c5fac834..fd49bc18161 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GitGarbageCollectWorker
include ApplicationWorker
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
index a0028e41332..0e4d40acc5c 100644
--- a/app/workers/gitlab_shell_worker.rb
+++ b/app/workers/gitlab_shell_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GitlabShellWorker
include ApplicationWorker
include Gitlab::ShellAdapter
diff --git a/app/workers/gitlab_usage_ping_worker.rb b/app/workers/gitlab_usage_ping_worker.rb
index 6dd281b1147..b75e724ca98 100644
--- a/app/workers/gitlab_usage_ping_worker.rb
+++ b/app/workers/gitlab_usage_ping_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GitlabUsagePingWorker
LEASE_TIMEOUT = 86400
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index 509bd09dc2e..b4a3ddcae51 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GroupDestroyWorker
include ApplicationWorker
include ExceptionBacktrace
diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb
index 9788c8df3a3..da3debdeede 100644
--- a/app/workers/import_export_project_cleanup_worker.rb
+++ b/app/workers/import_export_project_cleanup_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ImportExportProjectCleanupWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb
index 6774ab307c6..4724ab7ad98 100644
--- a/app/workers/invalid_gpg_signature_update_worker.rb
+++ b/app/workers/invalid_gpg_signature_update_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class InvalidGpgSignatureUpdateWorker
include ApplicationWorker
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 9ae5456be4c..29631c6b7ac 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'json'
require 'socket'
@@ -69,8 +71,8 @@ class IrkerWorker
newbranch = "#{Gitlab.config.gitlab.url}/#{repo_path}/branches"
newbranch = "\x0302\x1f#{newbranch}\x0f" if @colors
- privmsg = "[#{repo_name}] #{committer} has created a new branch "
- privmsg += "#{branch}: #{newbranch}"
+ privmsg = "[#{repo_name}] #{committer} has created a new branch " \
+ "#{branch}: #{newbranch}"
sendtoirker privmsg
end
@@ -112,9 +114,7 @@ class IrkerWorker
url = compare_url data, project.full_path
commits = colorize_commits data['total_commits_count']
- new_commits = 'new commit'
- new_commits += 's' if data['total_commits_count'] > 1
-
+ new_commits = 'new commit'.pluralize(data['total_commits_count'])
sendtoirker "[#{repo}] #{committer} pushed #{commits} #{new_commits} " \
"to #{branch}: #{url}"
end
@@ -122,8 +122,8 @@ class IrkerWorker
def compare_url(data, repo_path)
sha1 = Commit.truncate_sha(data['before'])
sha2 = Commit.truncate_sha(data['after'])
- compare_url = "#{Gitlab.config.gitlab.url}/#{repo_path}/compare"
- compare_url += "/#{sha1}...#{sha2}"
+ compare_url = "#{Gitlab.config.gitlab.url}/#{repo_path}/compare" \
+ "/#{sha1}...#{sha2}"
colorize_url compare_url
end
@@ -144,8 +144,7 @@ class IrkerWorker
def files_count(commit)
diff_size = commit.raw_deltas.size
- files = "#{diff_size} file"
- files += 's' if diff_size > 1
+ files = "#{diff_size} file".pluralize(diff_size)
files
end
diff --git a/app/workers/issue_due_scheduler_worker.rb b/app/workers/issue_due_scheduler_worker.rb
index 16ab5d069e0..c04a2d75e0b 100644
--- a/app/workers/issue_due_scheduler_worker.rb
+++ b/app/workers/issue_due_scheduler_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class IssueDueSchedulerWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb
index 54285884a52..8794ad7a82c 100644
--- a/app/workers/mail_scheduler/issue_due_worker.rb
+++ b/app/workers/mail_scheduler/issue_due_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module MailScheduler
class IssueDueWorker
include ApplicationWorker
diff --git a/app/workers/mail_scheduler/notification_service_worker.rb b/app/workers/mail_scheduler/notification_service_worker.rb
index 7cfe0aa0df1..4726e416182 100644
--- a/app/workers/mail_scheduler/notification_service_worker.rb
+++ b/app/workers/mail_scheduler/notification_service_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'active_job/arguments'
module MailScheduler
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index ba832fe30c6..ee864b733cd 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class MergeWorker
include ApplicationWorker
diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb
index adb25c2a170..d9df42c9e17 100644
--- a/app/workers/namespaceless_project_destroy_worker.rb
+++ b/app/workers/namespaceless_project_destroy_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Worker to destroy projects that do not have a namespace
#
# It destroys everything it can without having the info about the namespace it
diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb
index 3bc030f9c62..85b53973f56 100644
--- a/app/workers/new_issue_worker.rb
+++ b/app/workers/new_issue_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NewIssueWorker
include ApplicationWorker
include NewIssuable
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
index bda2a0ab59d..5d8b8904502 100644
--- a/app/workers/new_merge_request_worker.rb
+++ b/app/workers/new_merge_request_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NewMergeRequestWorker
include ApplicationWorker
include NewIssuable
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 67c54fbf10e..74f34dcf9aa 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NewNoteWorker
include ApplicationWorker
diff --git a/app/workers/object_storage/background_move_worker.rb b/app/workers/object_storage/background_move_worker.rb
index 9c4d72e0ecf..8dff65e46e3 100644
--- a/app/workers/object_storage/background_move_worker.rb
+++ b/app/workers/object_storage/background_move_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ObjectStorage
class BackgroundMoveWorker
include ApplicationWorker
diff --git a/app/workers/object_storage_upload_worker.rb b/app/workers/object_storage_upload_worker.rb
index 5c80f34069c..f17980a83d8 100644
--- a/app/workers/object_storage_upload_worker.rb
+++ b/app/workers/object_storage_upload_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# @Deprecated - remove once the `object_storage_upload` queue is empty
# The queue has been renamed `object_storage:object_storage_background_upload`
#
diff --git a/app/workers/pages_domain_verification_cron_worker.rb b/app/workers/pages_domain_verification_cron_worker.rb
index a3ff4bd2101..92d62a15aee 100644
--- a/app/workers/pages_domain_verification_cron_worker.rb
+++ b/app/workers/pages_domain_verification_cron_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PagesDomainVerificationCronWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/pages_domain_verification_worker.rb b/app/workers/pages_domain_verification_worker.rb
index 2e93489113c..4610b688189 100644
--- a/app/workers/pages_domain_verification_worker.rb
+++ b/app/workers/pages_domain_verification_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PagesDomainVerificationWorker
include ApplicationWorker
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index 66a0ff83bef..13a6576a301 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PagesWorker
include ApplicationWorker
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index c94918ff4ee..58023e0af1b 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineHooksWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index d46d1f122fc..a97019b100a 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineMetricsWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
index a9a1168a6e3..3a8846b3747 100644
--- a/app/workers/pipeline_notification_worker.rb
+++ b/app/workers/pipeline_notification_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineNotificationWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index 24424b3f472..83744c5338a 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineProcessWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index c49758878a4..a1815757735 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineScheduleWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index 2ab0739a17f..68e9af6a619 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineSuccessWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
index fc9da2d45b1..c33468c1f14 100644
--- a/app/workers/pipeline_update_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PipelineUpdateWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/plugin_worker.rb b/app/workers/plugin_worker.rb
index bfcc683d99a..c293e28be4a 100644
--- a/app/workers/plugin_worker.rb
+++ b/app/workers/plugin_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PluginWorker
include ApplicationWorker
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index f88b3fdbfb1..09a594cdb4e 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PostReceive
include ApplicationWorker
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 201e7f332b4..ed39b4a1ea8 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Worker for processing individiual commit messages pushed to a repository.
#
# Jobs for this worker are scheduled for every commit that is being pushed. As a
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index a993b4b2680..b0e1d8837d9 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Worker for updating any project specific caches.
class ProjectCacheWorker
include ApplicationWorker
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index 1ba854ca4cb..4447e867240 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProjectDestroyWorker
include ApplicationWorker
include ExceptionBacktrace
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index c3d84bb0b93..ed9da39c7c3 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProjectExportWorker
include ApplicationWorker
include ExceptionBacktrace
diff --git a/app/workers/project_migrate_hashed_storage_worker.rb b/app/workers/project_migrate_hashed_storage_worker.rb
index d01eb744e5d..9e4d66250a4 100644
--- a/app/workers/project_migrate_hashed_storage_worker.rb
+++ b/app/workers/project_migrate_hashed_storage_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProjectMigrateHashedStorageWorker
include ApplicationWorker
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index 75c4b8b3663..a0bc9288cf0 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ProjectServiceWorker
include ApplicationWorker
diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb
index 635a97c99af..c9da1cae255 100644
--- a/app/workers/propagate_service_template_worker.rb
+++ b/app/workers/propagate_service_template_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Worker for updating any project specific caches.
class PropagateServiceTemplateWorker
include ApplicationWorker
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
index 5ff62ab1369..c1d05ebbcfd 100644
--- a/app/workers/prune_old_events_worker.rb
+++ b/app/workers/prune_old_events_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PruneOldEventsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/prune_web_hook_logs_worker.rb b/app/workers/prune_web_hook_logs_worker.rb
new file mode 100644
index 00000000000..45c7d32f7eb
--- /dev/null
+++ b/app/workers/prune_web_hook_logs_worker.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# Worker that deletes a fixed number of outdated rows from the "web_hook_logs"
+# table.
+class PruneWebHookLogsWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ # The maximum number of rows to remove in a single job.
+ DELETE_LIMIT = 50_000
+
+ def perform
+ # MySQL doesn't allow "DELETE FROM ... WHERE id IN ( ... )" if the inner
+ # query refers to the same table. To work around this we wrap the IN body in
+ # another sub query.
+ WebHookLog
+ .where(
+ 'id IN (SELECT id FROM (?) ids_to_remove)',
+ WebHookLog
+ .select(:id)
+ .where('created_at < ?', 90.days.ago.beginning_of_day)
+ .limit(DELETE_LIMIT)
+ )
+ .delete_all
+ end
+end
diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
index ef3ddb9024b..9b331f15dc5 100644
--- a/app/workers/reactive_caching_worker.rb
+++ b/app/workers/reactive_caching_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ReactiveCachingWorker
include ApplicationWorker
diff --git a/app/workers/rebase_worker.rb b/app/workers/rebase_worker.rb
index 090987778a2..a6baebc1443 100644
--- a/app/workers/rebase_worker.rb
+++ b/app/workers/rebase_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RebaseWorker
include ApplicationWorker
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index 7e64c3070a8..6b8b972a440 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RemoveExpiredGroupLinksWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index 68960f72bf6..41913900571 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RemoveExpiredMembersWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/remove_old_web_hook_logs_worker.rb b/app/workers/remove_old_web_hook_logs_worker.rb
index 87fed42d7ce..17140ac4450 100644
--- a/app/workers/remove_old_web_hook_logs_worker.rb
+++ b/app/workers/remove_old_web_hook_logs_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RemoveOldWebHookLogsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/remove_unreferenced_lfs_objects_worker.rb b/app/workers/remove_unreferenced_lfs_objects_worker.rb
index 8daf079fc31..95e7a9f537f 100644
--- a/app/workers/remove_unreferenced_lfs_objects_worker.rb
+++ b/app/workers/remove_unreferenced_lfs_objects_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RemoveUnreferencedLfsObjectsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb
index 86a258cf94f..c1dff8ced90 100644
--- a/app/workers/repository_archive_cache_worker.rb
+++ b/app/workers/repository_archive_cache_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RepositoryArchiveCacheWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index 72f0a9b0619..051382a08a9 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -1,13 +1,20 @@
+# frozen_string_literal: true
+
module RepositoryCheck
class BatchWorker
include ApplicationWorker
- include CronjobQueue
+ include RepositoryCheckQueue
RUN_TIME = 3600
BATCH_SIZE = 10_000
- def perform
+ attr_reader :shard_name
+
+ def perform(shard_name)
+ @shard_name = shard_name
+
return unless Gitlab::CurrentSettings.repository_checks_enabled
+ return unless Gitlab::ShardHealthCache.healthy_shard?(shard_name)
start = Time.now
@@ -37,18 +44,22 @@ module RepositoryCheck
end
def never_checked_project_ids(batch_size)
- Project.where(last_repository_check_at: nil)
+ projects_on_shard.where(last_repository_check_at: nil)
.where('created_at < ?', 24.hours.ago)
.limit(batch_size).pluck(:id)
end
def old_checked_project_ids(batch_size)
- Project.where.not(last_repository_check_at: nil)
+ projects_on_shard.where.not(last_repository_check_at: nil)
.where('last_repository_check_at < ?', 1.month.ago)
.reorder(last_repository_check_at: :asc)
.limit(batch_size).pluck(:id)
end
+ def projects_on_shard
+ Project.where(repository_storage: shard_name)
+ end
+
def try_obtain_lease(id)
# Use a 24-hour timeout because on servers/projects where 'git fsck' is
# super slow we definitely do not want to run it twice in parallel.
diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb
index 97b89dc3db5..81e1a4b63bb 100644
--- a/app/workers/repository_check/clear_worker.rb
+++ b/app/workers/repository_check/clear_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RepositoryCheck
class ClearWorker
include ApplicationWorker
diff --git a/app/workers/repository_check/dispatch_worker.rb b/app/workers/repository_check/dispatch_worker.rb
new file mode 100644
index 00000000000..891a273afd7
--- /dev/null
+++ b/app/workers/repository_check/dispatch_worker.rb
@@ -0,0 +1,15 @@
+module RepositoryCheck
+ class DispatchWorker
+ include ApplicationWorker
+ include CronjobQueue
+ include ::EachShardWorker
+
+ def perform
+ return unless Gitlab::CurrentSettings.repository_checks_enabled
+
+ each_eligible_shard do |shard_name|
+ RepositoryCheck::BatchWorker.perform_async(shard_name)
+ end
+ end
+ end
+end
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index 3cffb8b14e4..f44e5693b25 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module RepositoryCheck
class SingleRepositoryWorker
include ApplicationWorker
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index dbb215f1964..5ef9b744db3 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RepositoryForkWorker
include ApplicationWorker
include Gitlab::ShellAdapter
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index d79b5ee5346..25fec542ac7 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RepositoryImportWorker
include ApplicationWorker
include ExceptionBacktrace
diff --git a/app/workers/repository_remove_remote_worker.rb b/app/workers/repository_remove_remote_worker.rb
index 1c19b604b77..a85e9fa9394 100644
--- a/app/workers/repository_remove_remote_worker.rb
+++ b/app/workers/repository_remove_remote_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RepositoryRemoveRemoteWorker
include ApplicationWorker
include ExclusiveLeaseGuard
diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb
index bb963979e88..9d4e67deb9c 100644
--- a/app/workers/repository_update_remote_mirror_worker.rb
+++ b/app/workers/repository_update_remote_mirror_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RepositoryUpdateRemoteMirrorWorker
UpdateAlreadyInProgressError = Class.new(StandardError)
UpdateError = Class.new(StandardError)
diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb
index 55c236e9e9d..ae022d43e29 100644
--- a/app/workers/requests_profiles_worker.rb
+++ b/app/workers/requests_profiles_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RequestsProfilesWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index 8f5138fc873..1f6cb18c812 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RunPipelineScheduleWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/schedule_update_user_activity_worker.rb b/app/workers/schedule_update_user_activity_worker.rb
index d9376577597..ff42fb8f0e5 100644
--- a/app/workers/schedule_update_user_activity_worker.rb
+++ b/app/workers/schedule_update_user_activity_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ScheduleUpdateUserActivityWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb
index e4b683fca33..ec8c8e3689f 100644
--- a/app/workers/stage_update_worker.rb
+++ b/app/workers/stage_update_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StageUpdateWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/storage_migrator_worker.rb b/app/workers/storage_migrator_worker.rb
index 0aff0c4c7c6..fa76fbac55c 100644
--- a/app/workers/storage_migrator_worker.rb
+++ b/app/workers/storage_migrator_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StorageMigratorWorker
include ApplicationWorker
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index 7ebf69bdc39..c78b7fac589 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StuckCiJobsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index 6fdd7592e74..79ce06dd66e 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StuckImportJobsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index 16394293c79..b0a62f76e94 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class StuckMergeJobsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/system_hook_push_worker.rb b/app/workers/system_hook_push_worker.rb
index ceeaaf8d189..15e369ebcfb 100644
--- a/app/workers/system_hook_push_worker.rb
+++ b/app/workers/system_hook_push_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class SystemHookPushWorker
include ApplicationWorker
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
index 7eb65452a7d..3297a1fe3d0 100644
--- a/app/workers/trending_projects_worker.rb
+++ b/app/workers/trending_projects_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TrendingProjectsWorker
include ApplicationWorker
include CronjobQueue
diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb
index 76f84ff920f..0487a393566 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UpdateHeadPipelineForMergeRequestWorker
include ApplicationWorker
include PipelineQueue
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 74bb9993275..742841219b3 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UpdateMergeRequestsWorker
include ApplicationWorker
diff --git a/app/workers/update_user_activity_worker.rb b/app/workers/update_user_activity_worker.rb
index 27ec5cd33fb..15f01a70337 100644
--- a/app/workers/update_user_activity_worker.rb
+++ b/app/workers/update_user_activity_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UpdateUserActivityWorker
include ApplicationWorker
diff --git a/app/workers/upload_checksum_worker.rb b/app/workers/upload_checksum_worker.rb
index 65d40336f18..2a0536106d7 100644
--- a/app/workers/upload_checksum_worker.rb
+++ b/app/workers/upload_checksum_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UploadChecksumWorker
include ApplicationWorker
diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb
index 19cdb279aaa..8aa1d9290fd 100644
--- a/app/workers/wait_for_cluster_creation_worker.rb
+++ b/app/workers/wait_for_cluster_creation_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class WaitForClusterCreationWorker
include ApplicationWorker
include ClusterQueue
diff --git a/app/workers/web_hook_worker.rb b/app/workers/web_hook_worker.rb
index dfc3f33ad9d..09219a24a16 100644
--- a/app/workers/web_hook_worker.rb
+++ b/app/workers/web_hook_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class WebHookWorker
include ApplicationWorker