summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorAndreas Kämmerle <andreas.kaemmerle@gmail.com>2018-07-03 15:30:36 +0200
committerAndreas Kämmerle <andreas.kaemmerle@gmail.com>2018-07-03 15:30:36 +0200
commite4a310113a3a5784be863151e5bcecacb23aa244 (patch)
tree79f9019b2e001a192eae3569b5746ba9c4ec9476 /app
parentd505b48806c0880ac810374973c4b9ba802c26e8 (diff)
parentc489d53b2e2eecb22f8dc7034da142221220e89f (diff)
downloadgitlab-ce-e4a310113a3a5784be863151e5bcecacb23aa244.tar.gz
Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into update-template-name-via-sentence-case
# Conflicts: # .gitlab/issue_templates/Feature proposal.md
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_canceled.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_created.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_failed.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_manual.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_not_found.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_pending.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_running.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_skipped.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_success.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_warning.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_canceled.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_canceled.pngbin0 -> 864 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_created.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_created.pngbin0 -> 889 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_failed.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_failed.pngbin0 -> 1015 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_manual.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_manual.pngbin0 -> 1067 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_not_found.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_not_found.pngbin0 -> 945 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_pending.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_pending.pngbin0 -> 919 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_running.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_running.pngbin0 -> 1077 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_skipped.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_skipped.pngbin0 -> 923 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_success.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_success.pngbin0 -> 1044 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_warning.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_warning.pngbin0 -> 830 bytes
-rw-r--r--app/assets/images/favicon-blue.icobin5430 -> 0 bytes
-rw-r--r--app/assets/images/favicon-blue.pngbin0 -> 1522 bytes
-rw-r--r--app/assets/images/favicon-yellow.icobin5430 -> 0 bytes
-rw-r--r--app/assets/images/favicon-yellow.pngbin0 -> 1667 bytes
-rw-r--r--app/assets/images/favicon.icobin5430 -> 0 bytes
-rw-r--r--app/assets/images/favicon.pngbin0 -> 1611 bytes
-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/badges/components/badge.vue12
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue16
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue10
-rw-r--r--app/assets/javascripts/badges/components/badge_settings.vue2
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js42
-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/viewer/index.js2
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js3
-rw-r--r--app/assets/javascripts/boards/components/board.js87
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue130
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js9
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue53
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue18
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js67
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js195
-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)68
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue (renamed from app/assets/javascripts/boards/components/modal/footer.js)60
-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.vue162
-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.js3
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js73
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue72
-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.js12
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js1
-rw-r--r--app/assets/javascripts/boards/models/assignee.js12
-rw-r--r--app/assets/javascripts/boards/models/issue.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js92
-rw-r--r--app/assets/javascripts/boards/models/milestone.js2
-rw-r--r--app/assets/javascripts/boards/services/board_service.js10
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js28
-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/clusters/components/application_row.vue10
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue18
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js2
-rw-r--r--app/assets/javascripts/commit/image_file.js8
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue4
-rw-r--r--app/assets/javascripts/commits.js2
-rw-r--r--app/assets/javascripts/compare_autocomplete.js2
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js61
-rw-r--r--app/assets/javascripts/cycle_analytics/components/banner.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_component.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.vue4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.vue2
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue2
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue8
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue30
-rw-r--r--app/assets/javascripts/deploy_keys/components/keys_panel.vue2
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js5
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js151
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js18
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js139
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js10
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js27
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js31
-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.vue258
-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.vue131
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_row.vue191
-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_view.vue38
-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_view.vue55
-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/mixins/diff_content.js57
-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.js11
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js86
-rw-r--r--app/assets/javascripts/diffs/store/utils.js172
-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/container.vue6
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue14
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue8
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue18
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue8
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue8
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue6
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue4
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue4
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js4
-rw-r--r--app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue2
-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.js19
-rw-r--r--app/assets/javascripts/gl_form.js22
-rw-r--r--app/assets/javascripts/group_label_subscription.js18
-rw-r--r--app/assets/javascripts/groups/components/app.vue7
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue12
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue4
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue22
-rw-r--r--app/assets/javascripts/groups/components/item_stats_value.vue2
-rw-r--r--app/assets/javascripts/groups/index.js4
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue44
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue26
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue19
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue75
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue9
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue40
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue16
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue18
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue34
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue8
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue8
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue69
-rw-r--r--app/assets/javascripts/ide/components/external_link.vue4
-rw-r--r--app/assets/javascripts/ide/components/file_finder/index.vue18
-rw-r--r--app/assets/javascripts/ide/components/file_finder/item.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide.vue9
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue19
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue95
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue34
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue4
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue137
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue47
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue66
-rw-r--r--app/assets/javascripts/ide/components/jobs/item.vue40
-rw-r--r--app/assets/javascripts/ide/components/jobs/list.vue3
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue10
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/dropdown.vue63
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/item.vue63
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue132
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue4
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue8
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue6
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue3
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue30
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue4
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue31
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue65
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue47
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_loading_file.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue10
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue2
-rw-r--r--app/assets/javascripts/ide/components/resizable_panel.vue4
-rw-r--r--app/assets/javascripts/ide/constants.js13
-rw-r--r--app/assets/javascripts/ide/ide_router.js8
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js16
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js5
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js4
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff_worker.js4
-rw-r--r--app/assets/javascripts/ide/lib/editor.js36
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js1
-rw-r--r--app/assets/javascripts/ide/monaco_loader.js16
-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.js48
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js69
-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.js69
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js17
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/actions.js45
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/constants.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/getters.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js18
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/state.js13
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js42
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js13
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/utils.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js6
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js9
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js15
-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.js8
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js2
-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/importer_status.js14
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js2
-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/issue_show/components/app.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue10
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue18
-rw-r--r--app/assets/javascripts/issue_show/components/edited.vue4
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue8
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description_template.vue10
-rw-r--r--app/assets/javascripts/issue_show/components/fields/title.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue4
-rw-r--r--app/assets/javascripts/issue_show/components/locked_warning.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue4
-rw-r--r--app/assets/javascripts/job.js99
-rw-r--r--app/assets/javascripts/jobs/components/header.vue7
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue44
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js2
-rw-r--r--app/assets/javascripts/label_manager.js17
-rw-r--r--app/assets/javascripts/labels_select.js13
-rw-r--r--app/assets/javascripts/lazy_loader.js4
-rw-r--r--app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js221
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js112
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js7
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js1
-rw-r--r--app/assets/javascripts/lib/utils/logoutput_behaviours.js46
-rw-r--r--app/assets/javascripts/lib/utils/notify.js2
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js4
-rw-r--r--app/assets/javascripts/lib/utils/scroll_utils.js29
-rw-r--r--app/assets/javascripts/lib/utils/sticky.js23
-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/lib/utils/url_utility.js4
-rw-r--r--app/assets/javascripts/line_highlighter.js14
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js17
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js13
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js11
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_service.js29
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js4
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js41
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js20
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js32
-rw-r--r--app/assets/javascripts/merge_request.js3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js177
-rw-r--r--app/assets/javascripts/milestone.js4
-rw-r--r--app/assets/javascripts/milestone_select.js9
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue27
-rw-r--r--app/assets/javascripts/monitoring/components/graph/axis.vue20
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue14
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_line.vue2
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js6
-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/notebook/cells/code.vue4
-rw-r--r--app/assets/javascripts/notebook/cells/code/index.vue4
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue2
-rw-r--r--app/assets/javascripts/notes.js293
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue77
-rw-r--r--app/assets/javascripts/notes/components/diff_file_header.vue12
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue127
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue23
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue48
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue18
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue17
-rw-r--r--app/assets/javascripts/notes/components/note_edited_text.vue13
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue75
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue19
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue217
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue58
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue106
-rw-r--r--app/assets/javascripts/notes/constants.js3
-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/collapse_utils.js108
-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/jobs/index/components/stop_jobs_modal.vue2
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue6
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue12
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js4
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue2
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js15
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js9
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js8
-rw-r--r--app/assets/javascripts/pages/projects/init_form.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/form.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue2
-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/pipeline_schedules/shared/components/interval_pattern_input.vue22
-rw-r--r--app/assets/javascripts/pages/projects/project.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js14
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.js4
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue8
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue32
-rw-r--r--app/assets/javascripts/pages/projects/tags/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue4
-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.js11
-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/pages/users/user_tabs.js4
-rw-r--r--app/assets/javascripts/pdf/index.vue4
-rw-r--r--app/assets/javascripts/pdf/page/index.vue2
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue3
-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.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue15
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue40
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue126
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.vue66
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue82
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue514
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue74
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.vue32
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue132
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue462
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue48
-rw-r--r--app/assets/javascripts/pipelines/components/time_ago.vue102
-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/account/components/delete_account_modal.vue12
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue8
-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.js9
-rw-r--r--app/assets/javascripts/project_import.js2
-rw-r--r--app/assets/javascripts/project_label_subscription.js28
-rw-r--r--app/assets/javascripts/project_select.js7
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue10
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue10
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue10
-rw-r--r--app/assets/javascripts/projects/project_new.js4
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue6
-rw-r--r--app/assets/javascripts/projects_dropdown/components/app.vue6
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue4
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_item.vue10
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_search.vue2
-rw-r--r--app/assets/javascripts/projects_dropdown/components/search.vue4
-rw-r--r--app/assets/javascripts/projects_dropdown/index.js2
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js2
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js34
-rw-r--r--app/assets/javascripts/registry/components/app.vue2
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue10
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue6
-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.js10
-rw-r--r--app/assets/javascripts/settings_panels.js4
-rw-r--r--app/assets/javascripts/shared/milestones/form.js11
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js4
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js24
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue30
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue12
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue12
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue14
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue6
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js1
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js2
-rw-r--r--app/assets/javascripts/single_file_diff.js4
-rw-r--r--app/assets/javascripts/smart_interval.js9
-rw-r--r--app/assets/javascripts/snippet/snippet_embed.js2
-rw-r--r--app/assets/javascripts/syntax_highlight.js2
-rw-r--r--app/assets/javascripts/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.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue4
-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/source_branch_removal_status.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/deprecated_modal.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/constants.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue74
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue69
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue160
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue158
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue41
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/identicon.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_icon.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/memory_graph.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue141
-rw-r--r--app/assets/javascripts/vue_shared/components/panel_resizer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/project_avatar/image.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/tabs/tab.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/tabs/tabs.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue6
-rw-r--r--app/assets/javascripts/vue_shared/models/assignee.js13
-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.scss175
-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.scss7
-rw-r--r--app/assets/stylesheets/framework/buttons.scss12
-rw-r--r--app/assets/stylesheets/framework/common.scss13
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss35
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss6
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1
-rw-r--r--app/assets/stylesheets/framework/files.scss78
-rw-r--r--app/assets/stylesheets/framework/filters.scss1
-rw-r--r--app/assets/stylesheets/framework/flash.scss16
-rw-r--r--app/assets/stylesheets/framework/forms.scss18
-rw-r--r--app/assets/stylesheets/framework/gfm.scss1
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss168
-rw-r--r--app/assets/stylesheets/framework/header.scss34
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss5
-rw-r--r--app/assets/stylesheets/framework/layout.scss10
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss1
-rw-r--r--app/assets/stylesheets/framework/modal.scss4
-rw-r--r--app/assets/stylesheets/framework/pagination.scss91
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss6
-rw-r--r--app/assets/stylesheets/framework/tables.scss90
-rw-r--r--app/assets/stylesheets/framework/terms.scss8
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/framework/toggle.scss4
-rw-r--r--app/assets/stylesheets/framework/typography.scss23
-rw-r--r--app/assets/stylesheets/framework/variables.scss85
-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/mailers/highlighted_diff_email.scss1
-rw-r--r--app/assets/stylesheets/pages/boards.scss18
-rw-r--r--app/assets/stylesheets/pages/builds.scss17
-rw-r--r--app/assets/stylesheets/pages/clusters.scss4
-rw-r--r--app/assets/stylesheets/pages/commits.scss28
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss3
-rw-r--r--app/assets/stylesheets/pages/diff.scss166
-rw-r--r--app/assets/stylesheets/pages/environments.scss13
-rw-r--r--app/assets/stylesheets/pages/groups.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss11
-rw-r--r--app/assets/stylesheets/pages/labels.scss231
-rw-r--r--app/assets/stylesheets/pages/members.scss4
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss15
-rw-r--r--app/assets/stylesheets/pages/milestone.scss36
-rw-r--r--app/assets/stylesheets/pages/note_form.scss18
-rw-r--r--app/assets/stylesheets/pages/notes.scss72
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss89
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss79
-rw-r--r--app/assets/stylesheets/pages/projects.scss42
-rw-r--r--app/assets/stylesheets/pages/repo.scss207
-rw-r--r--app/assets/stylesheets/pages/search.scss7
-rw-r--r--app/assets/stylesheets/pages/settings.scss31
-rw-r--r--app/assets/stylesheets/pages/settings_ci_cd.scss9
-rw-r--r--app/assets/stylesheets/performance_bar.scss12
-rw-r--r--app/assets/stylesheets/print.scss5
-rw-r--r--app/controllers/admin/appearances_controller.rb9
-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.rb2
-rw-r--r--app/controllers/admin/users_controller.rb4
-rw-r--r--app/controllers/application_controller.rb21
-rw-r--r--app/controllers/boards/lists_controller.rb14
-rw-r--r--app/controllers/concerns/internal_redirect.rb4
-rw-r--r--app/controllers/concerns/issuable_actions.rb32
-rw-r--r--app/controllers/concerns/issuable_collections.rb7
-rw-r--r--app/controllers/concerns/issues_action.rb12
-rw-r--r--app/controllers/concerns/issues_calendar.rb24
-rw-r--r--app/controllers/concerns/notes_actions.rb6
-rw-r--r--app/controllers/concerns/uploads_actions.rb20
-rw-r--r--app/controllers/dashboard_controller.rb2
-rw-r--r--app/controllers/graphql_controller.rb45
-rw-r--r--app/controllers/groups/group_members_controller.rb6
-rw-r--r--app/controllers/groups/labels_controller.rb23
-rw-r--r--app/controllers/groups/milestones_controller.rb5
-rw-r--r--app/controllers/groups/shared_projects_controller.rb4
-rw-r--r--app/controllers/health_controller.rb3
-rw-r--r--app/controllers/import/base_controller.rb4
-rw-r--r--app/controllers/import/bitbucket_controller.rb2
-rw-r--r--app/controllers/import/fogbugz_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb2
-rw-r--r--app/controllers/import/gitlab_controller.rb2
-rw-r--r--app/controllers/import/google_code_controller.rb2
-rw-r--r--app/controllers/metrics_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb4
-rw-r--r--app/controllers/projects/artifacts_controller.rb9
-rw-r--r--app/controllers/projects/blob_controller.rb70
-rw-r--r--app/controllers/projects/branches_controller.rb5
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb2
-rw-r--r--app/controllers/projects/commits_controller.rb14
-rw-r--r--app/controllers/projects/discussions_controller.rb9
-rw-r--r--app/controllers/projects/issues_controller.rb10
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/controllers/projects/lfs_api_controller.rb7
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb25
-rw-r--r--app/controllers/projects/merge_requests_controller.rb38
-rw-r--r--app/controllers/projects/milestones_controller.rb11
-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/wikis_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb6
-rw-r--r--app/controllers/registrations_controller.rb27
-rw-r--r--app/controllers/sessions_controller.rb31
-rw-r--r--app/controllers/users/terms_controller.rb5
-rw-r--r--app/finders/group_members_finder.rb26
-rw-r--r--app/finders/issuable_finder.rb43
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/members_finder.rb4
-rw-r--r--app/finders/merge_requests_finder.rb6
-rw-r--r--app/finders/notes_finder.rb2
-rw-r--r--app/finders/snippets_finder.rb15
-rw-r--r--app/finders/user_recent_events_finder.rb2
-rw-r--r--app/graphql/functions/base_function.rb4
-rw-r--r--app/graphql/functions/echo.rb13
-rw-r--r--app/graphql/gitlab_schema.rb8
-rw-r--r--app/graphql/mutations/.keep0
-rw-r--r--app/graphql/resolvers/base_resolver.rb4
-rw-r--r--app/graphql/resolvers/full_path_resolver.rb19
-rw-r--r--app/graphql/resolvers/merge_request_resolver.rb20
-rw-r--r--app/graphql/resolvers/project_resolver.rb11
-rw-r--r--app/graphql/types/base_enum.rb4
-rw-r--r--app/graphql/types/base_field.rb5
-rw-r--r--app/graphql/types/base_input_object.rb4
-rw-r--r--app/graphql/types/base_interface.rb5
-rw-r--r--app/graphql/types/base_object.rb8
-rw-r--r--app/graphql/types/base_scalar.rb4
-rw-r--r--app/graphql/types/base_union.rb4
-rw-r--r--app/graphql/types/merge_request_type.rb49
-rw-r--r--app/graphql/types/mutation_type.rb7
-rw-r--r--app/graphql/types/permission_types/base_permission_type.rb38
-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.rb74
-rw-r--r--app/graphql/types/query_type.rb14
-rw-r--r--app/graphql/types/time_type.rb14
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/application_settings_helper.rb6
-rw-r--r--app/helpers/favicon_helper.rb7
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/helpers/labels_helper.rb8
-rw-r--r--app/helpers/merge_requests_helper.rb8
-rw-r--r--app/helpers/notes_helper.rb16
-rw-r--r--app/helpers/page_layout_helper.rb5
-rw-r--r--app/helpers/projects_helper.rb81
-rw-r--r--app/helpers/search_helper.rb12
-rw-r--r--app/helpers/workhorse_helper.rb2
-rw-r--r--app/mailers/emails/merge_requests.rb2
-rw-r--r--app/models/appearance.rb1
-rw-r--r--app/models/application_setting.rb13
-rw-r--r--app/models/application_setting/term.rb6
-rw-r--r--app/models/ci/build.rb15
-rw-r--r--app/models/ci/group.rb8
-rw-r--r--app/models/ci/legacy_stage.rb6
-rw-r--r--app/models/ci/pipeline.rb57
-rw-r--r--app/models/ci/runner.rb6
-rw-r--r--app/models/ci/stage.rb32
-rw-r--r--app/models/clusters/applications/prometheus.rb3
-rw-r--r--app/models/clusters/platforms/kubernetes.rb4
-rw-r--r--app/models/clusters/providers/gcp.rb2
-rw-r--r--app/models/commit_status.rb10
-rw-r--r--app/models/concerns/atomic_internal_id.rb16
-rw-r--r--app/models/concerns/avatarable.rb47
-rw-r--r--app/models/concerns/cache_markdown_field.rb2
-rw-r--r--app/models/concerns/enum_with_nil.rb33
-rw-r--r--app/models/concerns/has_status.rb2
-rw-r--r--app/models/concerns/iid_routes.rb9
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/concerns/protected_ref_access.rb4
-rw-r--r--app/models/concerns/redis_cacheable.rb2
-rw-r--r--app/models/concerns/sortable.rb4
-rw-r--r--app/models/concerns/with_uploads.rb4
-rw-r--r--app/models/deployment.rb1
-rw-r--r--app/models/discussion.rb4
-rw-r--r--app/models/group.rb29
-rw-r--r--app/models/internal_id.rb2
-rw-r--r--app/models/issue.rb9
-rw-r--r--app/models/label.rb19
-rw-r--r--app/models/list.rb18
-rw-r--r--app/models/merge_request.rb54
-rw-r--r--app/models/merge_request_diff.rb27
-rw-r--r--app/models/milestone.rb1
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/note.rb5
-rw-r--r--app/models/notification_recipient.rb31
-rw-r--r--app/models/personal_snippet.rb1
-rw-r--r--app/models/project.rb59
-rw-r--r--app/models/project_auto_devops.rb31
-rw-r--r--app/models/project_services/bamboo_service.rb22
-rw-r--r--app/models/project_services/chat_message/base_message.rb7
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb4
-rw-r--r--app/models/project_services/chat_notification_service.rb1
-rw-r--r--app/models/project_services/gemnasium_service.rb7
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/microsoft_teams_service.rb2
-rw-r--r--app/models/project_team.rb2
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/protected_branch.rb2
-rw-r--r--app/models/remote_mirror.rb2
-rw-r--r--app/models/repository.rb33
-rw-r--r--app/models/term_agreement.rb2
-rw-r--r--app/models/timelog.rb5
-rw-r--r--app/models/user.rb7
-rw-r--r--app/policies/ci/build_policy.rb6
-rw-r--r--app/policies/ci/pipeline_policy.rb6
-rw-r--r--app/policies/project_policy.rb3
-rw-r--r--app/presenters/merge_request_presenter.rb34
-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.rb123
-rw-r--r--app/serializers/diffs_entity.rb65
-rw-r--r--app/serializers/diffs_serializer.rb3
-rw-r--r--app/serializers/discussion_entity.rb49
-rw-r--r--app/serializers/merge_request_basic_entity.rb4
-rw-r--r--app/serializers/merge_request_diff_entity.rb46
-rw-r--r--app/serializers/merge_request_user_entity.rb24
-rw-r--r--app/serializers/merge_request_widget_entity.rb16
-rw-r--r--app/serializers/note_entity.rb28
-rw-r--r--app/serializers/pipeline_details_entity.rb2
-rw-r--r--app/serializers/pipeline_serializer.rb17
-rw-r--r--app/serializers/status_entity.rb11
-rw-r--r--app/services/application_settings/update_service.rb2
-rw-r--r--app/services/applications/create_service.rb5
-rw-r--r--app/services/base_count_service.rb6
-rw-r--r--app/services/base_service.rb2
-rw-r--r--app/services/boards/issues/list_service.rb21
-rw-r--r--app/services/boards/issues/move_service.rb6
-rw-r--r--app/services/boards/lists/create_service.rb20
-rw-r--r--app/services/ci/register_job_service.rb12
-rw-r--r--app/services/commits/create_service.rb2
-rw-r--r--app/services/concerns/exclusive_lease_guard.rb2
-rw-r--r--app/services/concerns/issues/resolve_discussions.rb1
-rw-r--r--app/services/create_branch_service.rb2
-rw-r--r--app/services/delete_branch_service.rb2
-rw-r--r--app/services/issues/move_service.rb3
-rw-r--r--app/services/lfs/unlock_file_service.rb2
-rw-r--r--app/services/merge_requests/base_service.rb4
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb4
-rw-r--r--app/services/merge_requests/delete_non_latest_diffs_service.rb18
-rw-r--r--app/services/merge_requests/ff_merge_service.rb2
-rw-r--r--app/services/merge_requests/merge_request_diff_cache_service.rb17
-rw-r--r--app/services/merge_requests/merge_service.rb2
-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/pages_service.rb15
-rw-r--r--app/services/projects/autocomplete_service.rb2
-rw-r--r--app/services/projects/count_service.rb6
-rw-r--r--app/services/projects/create_service.rb5
-rw-r--r--app/services/projects/destroy_service.rb5
-rw-r--r--app/services/projects/import_service.rb29
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_link_list_service.rb93
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb58
-rw-r--r--app/services/projects/lfs_pointers/lfs_import_service.rb92
-rw-r--r--app/services/projects/lfs_pointers/lfs_link_service.rb29
-rw-r--r--app/services/projects/lfs_pointers/lfs_list_service.rb19
-rw-r--r--app/services/projects/open_issues_count_service.rb32
-rw-r--r--app/services/projects/update_service.rb23
-rw-r--r--app/services/quick_actions/interpret_service.rb11
-rw-r--r--app/services/tags/create_service.rb2
-rw-r--r--app/services/tags/destroy_service.rb2
-rw-r--r--app/services/test_hooks/project_service.rb4
-rw-r--r--app/services/validate_new_branch_service.rb2
-rw-r--r--app/services/web_hook_service.rb2
-rw-r--r--app/uploaders/favicon_uploader.rb13
-rw-r--r--app/uploaders/file_uploader.rb6
-rw-r--r--app/uploaders/object_storage.rb75
-rw-r--r--app/uploaders/records_uploads.rb2
-rw-r--r--app/uploaders/uploader_helper.rb2
-rw-r--r--app/validators/url_validator.rb14
-rw-r--r--app/views/admin/appearances/_form.html.haml23
-rw-r--r--app/views/admin/application_settings/_abuse.html.haml11
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml60
-rw-r--r--app/views/admin/application_settings/_background_jobs.html.haml35
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml76
-rw-r--r--app/views/admin/application_settings/_email.html.haml38
-rw-r--r--app/views/admin/application_settings/_gitaly.html.haml39
-rw-r--r--app/views/admin/application_settings/_help_page.html.haml29
-rw-r--r--app/views/admin/application_settings/_influx.html.haml104
-rw-r--r--app/views/admin/application_settings/_ip_limits.html.haml87
-rw-r--r--app/views/admin/application_settings/_koding.html.haml34
-rw-r--r--app/views/admin/application_settings/_logging.html.haml50
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml11
-rw-r--r--app/views/admin/application_settings/_pages.html.haml30
-rw-r--r--app/views/admin/application_settings/_performance.html.haml25
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml18
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml26
-rw-r--r--app/views/admin/application_settings/_prometheus.html.haml25
-rw-r--r--app/views/admin/application_settings/_realtime.html.haml23
-rw-r--r--app/views/admin/application_settings/_registry.html.haml7
-rw-r--r--app/views/admin/application_settings/_repository_check.html.haml90
-rw-r--r--app/views/admin/application_settings/_repository_mirrors_form.html.haml19
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml89
-rw-r--r--app/views/admin/application_settings/_signin.html.haml96
-rw-r--r--app/views/admin/application_settings/_signup.html.haml94
-rw-r--r--app/views/admin/application_settings/_spam.html.haml110
-rw-r--r--app/views/admin/application_settings/_terminal.html.haml13
-rw-r--r--app/views/admin/application_settings/_terms.html.haml29
-rw-r--r--app/views/admin/application_settings/_usage.html.haml58
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml103
-rw-r--r--app/views/admin/application_settings/show.html.haml13
-rw-r--r--app/views/admin/dashboard/index.html.haml8
-rw-r--r--app/views/admin/gitaly_servers/index.html.haml4
-rw-r--r--app/views/admin/groups/_form.html.haml7
-rw-r--r--app/views/admin/groups/_group.html.haml3
-rw-r--r--app/views/admin/groups/show.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/projects/show.html.haml6
-rw-r--r--app/views/admin/runners/index.html.haml2
-rw-r--r--app/views/admin/services/_form.html.haml3
-rw-r--r--app/views/admin/users/_access_levels.html.haml20
-rw-r--r--app/views/admin/users/_form.html.haml2
-rw-r--r--app/views/admin/users/_user.html.haml4
-rw-r--r--app/views/award_emoji/_awards_block.html.haml4
-rw-r--r--app/views/ci/variables/_variable_row.html.haml1
-rw-r--r--app/views/devise/sessions/_new_base.html.haml4
-rw-r--r--app/views/devise/shared/_signup_box.html.haml7
-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/email_rejection_mailer/rejection.html.haml1
-rw-r--r--app/views/email_rejection_mailer/rejection.text.haml1
-rw-r--r--app/views/errors/access_denied.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml2
-rw-r--r--app/views/groups/_activities.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml7
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/groups/labels/index.html.haml45
-rw-r--r--app/views/groups/merge_requests.html.haml2
-rw-r--r--app/views/groups/new.html.haml46
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml4
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml164
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/import/gitea/new.html.haml4
-rw-r--r--app/views/import/github/new.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_first_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_gap.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_last_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_next_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_paginator.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_prev_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_without_count.html.haml4
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/devise.html.haml2
-rw-r--r--app/views/layouts/devise_empty.html.haml2
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml5
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/header/_new_dropdown.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml20
-rw-r--r--app/views/layouts/terms.html.haml6
-rw-r--r--app/views/notify/merge_request_unmergeable_email.html.haml6
-rw-r--r--app/views/notify/merge_request_unmergeable_email.text.haml5
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/new_merge_request_email.text.erb2
-rw-r--r--app/views/profiles/keys/_form.html.haml6
-rw-r--r--app/views/profiles/keys/index.html.haml9
-rw-r--r--app/views/profiles/preferences/show.html.haml10
-rw-r--r--app/views/projects/_export.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml4
-rw-r--r--app/views/projects/_new_project_fields.html.haml2
-rw-r--r--app/views/projects/_project_templates.html.haml24
-rw-r--r--app/views/projects/_stat_anchor_list.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml4
-rw-r--r--app/views/projects/blob/_header.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_download.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml2
-rw-r--r--app/views/projects/buttons/_xcode_link.html.haml2
-rw-r--r--app/views/projects/clusters/_advanced_settings.html.haml4
-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/_sidebar.html.haml6
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml23
-rw-r--r--app/views/projects/clusters/gcp/login.html.haml5
-rw-r--r--app/views/projects/clusters/user/_form.html.haml17
-rw-r--r--app/views/projects/clusters/user/_show.html.haml10
-rw-r--r--app/views/projects/commit/_change.html.haml20
-rw-r--r--app/views/projects/commit/branches.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml3
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/deploy_tokens/_index.html.haml11
-rw-r--r--app/views/projects/deploy_tokens/_new_deploy_token.html.haml28
-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/diffs/_collapsed.html.haml2
-rw-r--r--app/views/projects/edit.html.haml14
-rw-r--r--app/views/projects/empty.html.haml13
-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/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/issues/_new_branch.html.haml4
-rw-r--r--app/views/projects/jobs/show.html.haml4
-rw-r--r--app/views/projects/labels/index.html.haml34
-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/diffs/_version_controls.html.haml4
-rw-r--r--app/views/projects/merge_requests/show.html.haml75
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/mirrors/_push.html.haml2
-rw-r--r--app/views/projects/new.html.haml12
-rw-r--r--app/views/projects/pages/_destroy.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml44
-rw-r--r--app/views/projects/project_members/_groups.html.haml2
-rw-r--r--app/views/projects/project_members/_team.html.haml7
-rw-r--r--app/views/projects/project_members/index.html.haml10
-rw-r--r--app/views/projects/protected_branches/shared/_branches_list.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml6
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml4
-rw-r--r--app/views/projects/refs/logs_tree.js.haml2
-rw-r--r--app/views/projects/releases/edit.html.haml2
-rw-r--r--app/views/projects/runners/_group_runners.html.haml4
-rw-r--r--app/views/projects/services/_index.html.haml2
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml4
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml27
-rw-r--r--app/views/projects/services/prometheus/_configuration_banner.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_help.html.haml2
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml169
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml83
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml50
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml36
-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/tags/new.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml2
-rw-r--r--app/views/projects/wikis/git_access.html.haml2
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/search/results/_blob.html.haml18
-rw-r--r--app/views/search/results/_blob_data.html.haml10
-rw-r--r--app/views/search/results/_commit.html.haml2
-rw-r--r--app/views/search/results/_wiki_blob.html.haml15
-rw-r--r--app/views/shared/_clone_panel.html.haml2
-rw-r--r--app/views/shared/_field.html.haml6
-rw-r--r--app/views/shared/_label.html.haml133
-rw-r--r--app/views/shared/_label_row.html.haml35
-rw-r--r--app/views/shared/_milestone_expired.html.haml9
-rw-r--r--app/views/shared/_new_merge_request_checkbox.html.haml2
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml16
-rw-r--r--app/views/shared/_service_settings.html.haml24
-rw-r--r--app/views/shared/_visibility_level.html.haml4
-rw-r--r--app/views/shared/boards/_show.html.haml4
-rw-r--r--app/views/shared/boards/components/_board.html.haml24
-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.haml10
-rw-r--r--app/views/shared/boards/components/sidebar/_milestone.html.haml12
-rw-r--r--app/views/shared/builds/_build_output.html.haml3
-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/empty_states/_wikis.html.haml2
-rw-r--r--app/views/shared/form_elements/_description.html.haml2
-rw-r--r--app/views/shared/issuable/_board_create_list_dropdown.html.haml8
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml3
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml7
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml9
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml6
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml4
-rw-r--r--app/views/shared/issuable/form/_contribution.html.haml16
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml13
-rw-r--r--app/views/shared/issuable/form/_title.html.haml2
-rw-r--r--app/views/shared/members/_group.html.haml18
-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/_edit_form.html.haml2
-rw-r--r--app/views/shared/notes/_form.html.haml4
-rw-r--r--app/views/shared/notes/_note.html.haml4
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml7
-rw-r--r--app/views/shared/notifications/_button.html.haml4
-rw-r--r--app/views/shared/projects/_edit_information.html.haml2
-rw-r--r--app/views/shared/runners/show.html.haml14
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml9
-rw-r--r--app/views/u2f/_authenticate.html.haml3
-rw-r--r--app/views/users/terms/index.html.haml25
-rw-r--r--app/workers/admin_email_worker.rb2
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/archive_trace_worker.rb2
-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.rb28
-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.rb60
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/import_lfs_object_worker.rb25
-rw-r--r--app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb32
-rw-r--r--app/workers/gitlab/github_import/stage/import_notes_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.rb35
-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.rb27
-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.rb27
-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
1190 files changed, 16691 insertions, 7961 deletions
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico
deleted file mode 100644
index 4af3582b60d..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_created.ico b/app/assets/images/ci_favicons/dev/favicon_status_created.ico
deleted file mode 100644
index 13639da2e8a..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_created.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico
deleted file mode 100644
index 5f0e711b104..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico
deleted file mode 100644
index 8b1168a1267..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico
deleted file mode 100644
index ed19b69e1c5..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico
deleted file mode 100644
index 5dfefd4cc5a..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_running.ico b/app/assets/images/ci_favicons/dev/favicon_status_running.ico
deleted file mode 100644
index a41539c0e3e..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_running.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico
deleted file mode 100644
index 2c1ae552b93..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_success.ico b/app/assets/images/ci_favicons/dev/favicon_status_success.ico
deleted file mode 100644
index 70f0ca61eca..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_success.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico
deleted file mode 100644
index db289e03eb1..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico
deleted file mode 100644
index 23adcffff50..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_canceled.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.png b/app/assets/images/ci_favicons/favicon_status_canceled.png
new file mode 100644
index 00000000000..8adaa9c600b
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_canceled.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico
deleted file mode 100644
index f9d93b390d8..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_created.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_created.png b/app/assets/images/ci_favicons/favicon_status_created.png
new file mode 100644
index 00000000000..ca788dd0034
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_created.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico
deleted file mode 100644
index 28a22ebf724..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_failed.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_failed.png b/app/assets/images/ci_favicons/favicon_status_failed.png
new file mode 100644
index 00000000000..93f1e2772fd
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_failed.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico
deleted file mode 100644
index dbbf1abf30c..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_manual.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_manual.png b/app/assets/images/ci_favicons/favicon_status_manual.png
new file mode 100644
index 00000000000..c926062c806
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_manual.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico
deleted file mode 100644
index 49b9b232dd1..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_not_found.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.png b/app/assets/images/ci_favicons/favicon_status_not_found.png
new file mode 100644
index 00000000000..df3049315a9
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_not_found.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico
deleted file mode 100644
index 05962f3f148..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_pending.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_pending.png b/app/assets/images/ci_favicons/favicon_status_pending.png
new file mode 100644
index 00000000000..f7d67d4a230
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_pending.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico
deleted file mode 100644
index 7fa3d4d48d4..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_running.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_running.png b/app/assets/images/ci_favicons/favicon_status_running.png
new file mode 100644
index 00000000000..ff4167c4b20
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_running.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico
deleted file mode 100644
index b0c26b62068..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_skipped.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.png b/app/assets/images/ci_favicons/favicon_status_skipped.png
new file mode 100644
index 00000000000..a9c36464b69
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_skipped.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico
deleted file mode 100644
index b150960b5be..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_success.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_success.png b/app/assets/images/ci_favicons/favicon_status_success.png
new file mode 100644
index 00000000000..bcc30c73f5f
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_success.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico
deleted file mode 100644
index 7e71d71684d..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_warning.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_warning.png b/app/assets/images/ci_favicons/favicon_status_warning.png
new file mode 100644
index 00000000000..6db3b0280f5
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_warning.png
Binary files differ
diff --git a/app/assets/images/favicon-blue.ico b/app/assets/images/favicon-blue.ico
deleted file mode 100644
index 156fcf07588..00000000000
--- a/app/assets/images/favicon-blue.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/favicon-blue.png b/app/assets/images/favicon-blue.png
new file mode 100644
index 00000000000..2229fe79462
--- /dev/null
+++ b/app/assets/images/favicon-blue.png
Binary files differ
diff --git a/app/assets/images/favicon-yellow.ico b/app/assets/images/favicon-yellow.ico
deleted file mode 100644
index b650f277fb6..00000000000
--- a/app/assets/images/favicon-yellow.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/favicon-yellow.png b/app/assets/images/favicon-yellow.png
new file mode 100644
index 00000000000..2d5289818b4
--- /dev/null
+++ b/app/assets/images/favicon-yellow.png
Binary files differ
diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico
deleted file mode 100644
index 3479cbbb46f..00000000000
--- a/app/assets/images/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png
new file mode 100644
index 00000000000..845e0ec34a5
--- /dev/null
+++ b/app/assets/images/favicon.png
Binary files differ
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/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index d0f60e1d4cb..b4bfaee1d85 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -72,11 +72,11 @@ export default {
rel="noopener noreferrer"
>
<img
- class="project-badge"
:src="imageUrlWithRetries"
+ class="project-badge"
+ aria-hidden="true"
@load="onLoad"
@error="onError"
- aria-hidden="true"
/>
</a>
@@ -91,9 +91,9 @@ export default {
>
<div class="btn btn-default btn-sm disabled">
<icon
+ :size="16"
class="prepend-left-8 append-right-8"
name="doc_image"
- :size="16"
aria-hidden="true"
/>
</div>
@@ -105,16 +105,16 @@ export default {
</div>
<button
+ v-tooltip
v-show="hasError"
+ :title="s__('Badges|Reload badge image')"
class="btn btn-transparent btn-sm text-primary"
type="button"
- v-tooltip
- :title="s__('Badges|Reload badge image')"
@click="reloadImage"
>
<icon
- name="retry"
:size="16"
+ name="retry"
/>
</button>
</div>
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 5975cb9669e..7a13f74c570 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -153,10 +153,10 @@ export default {
<label for="badge-link-url">{{ s__('Badges|Link') }}</label>
<input
id="badge-link-url"
- type="text"
- class="form-control"
v-model="linkUrl"
:placeholder="$options.badgeLinkUrlPlaceholder"
+ type="text"
+ class="form-control"
@input="debouncedPreview"
/>
<span
@@ -169,10 +169,10 @@ export default {
<label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label>
<input
id="badge-image-url"
- type="text"
- class="form-control"
v-model="imageUrl"
:placeholder="$options.badgeImageUrlPlaceholder"
+ type="text"
+ class="form-control"
@input="debouncedPreview"
/>
<span
@@ -184,8 +184,8 @@ export default {
<div class="form-group">
<label for="badge-preview">{{ s__('Badges|Badge image preview') }}</label>
<badge
- id="badge-preview"
v-show="renderedBadge && !isRendering"
+ id="badge-preview"
:image-url="renderedImageUrl"
:link-url="renderedLinkUrl"
/>
@@ -202,16 +202,16 @@ export default {
<div class="row-content-block">
<loading-button
- type="submit"
- container-class="btn btn-success"
:disabled="!canSubmit"
:loading="isSaving"
:label="submitButtonLabel"
+ type="submit"
+ container-class="btn btn-success"
/>
<button
+ v-if="isEditing"
class="btn btn-cancel"
type="button"
- v-if="isEditing"
@click="onCancel"
>{{ __('Cancel') }}</button>
</div>
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
index af062bdf8c6..98aa00af0d7 100644
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -41,9 +41,9 @@ export default {
<template>
<div class="gl-responsive-table-row-layout gl-responsive-table-row">
<badge
- class="table-section section-30"
:image-url="badge.renderedImageUrl"
:link-url="badge.renderedLinkUrl"
+ class="table-section section-30"
/>
<span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-10">
@@ -54,29 +54,29 @@ export default {
v-if="canEditBadge"
class="table-action-buttons">
<button
+ :disabled="badge.isDeleting"
class="btn btn-default append-right-8"
type="button"
- :disabled="badge.isDeleting"
@click="editBadge(badge)"
>
<icon
- name="pencil"
:size="16"
:aria-label="__('Edit')"
+ name="pencil"
/>
</button>
<button
+ :disabled="badge.isDeleting"
class="btn btn-danger"
type="button"
data-toggle="modal"
data-target="#delete-badge-modal"
- :disabled="badge.isDeleting"
@click="updateBadgeInModal(badge)"
>
<icon
- name="remove"
:size="16"
:aria-label="__('Delete')"
+ name="remove"
/>
</button>
<loading-icon
diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue
index 83f78394238..cc47e56dd1e 100644
--- a/app/assets/javascripts/badges/components/badge_settings.vue
+++ b/app/assets/javascripts/badges/components/badge_settings.vue
@@ -44,8 +44,8 @@ export default {
<gl-modal
id="delete-badge-modal"
:header-title-text="s__('Badges|Delete badge?')"
- footer-primary-button-variant="danger"
:footer-primary-button-text="s__('Badges|Delete badge')"
+ footer-primary-button-variant="danger"
@submit="onSubmitModal">
<div class="well">
<badge
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 75cf90de0b5..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';
@@ -119,7 +119,7 @@ const gfmRules = {
return el.outerHTML;
},
'dl'(el, text) {
- let lines = text.trim().split('\n');
+ let lines = text.replace(/\n\n/g, '\n').trim().split('\n');
// Add two spaces to the front of subsequent list items lines,
// or leave the line entirely blank.
lines = lines.map((l) => {
@@ -129,9 +129,13 @@ const gfmRules = {
return ` ${line}`;
});
- return `<dl>\n${lines.join('\n')}\n</dl>`;
+ return `<dl>\n${lines.join('\n')}\n</dl>\n`;
},
- 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) {
+ 'dt, dd, summary, details'(el, text) {
+ const tag = el.nodeName.toLowerCase();
+ return `<${tag}>${text}</${tag}>\n`;
+ },
+ 'sup, sub, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) {
const tag = el.nodeName.toLowerCase();
return `<${tag}>${text}</${tag}>`;
},
@@ -215,22 +219,22 @@ const gfmRules = {
return text.replace(/^- /mg, '1. ');
},
'h1'(el, text) {
- return `# ${text.trim()}`;
+ return `# ${text.trim()}\n`;
},
'h2'(el, text) {
- return `## ${text.trim()}`;
+ return `## ${text.trim()}\n`;
},
'h3'(el, text) {
- return `### ${text.trim()}`;
+ return `### ${text.trim()}\n`;
},
'h4'(el, text) {
- return `#### ${text.trim()}`;
+ return `#### ${text.trim()}\n`;
},
'h5'(el, text) {
- return `##### ${text.trim()}`;
+ return `##### ${text.trim()}\n`;
},
'h6'(el, text) {
- return `###### ${text.trim()}`;
+ return `###### ${text.trim()}\n`;
},
'strong'(el, text) {
return `**${text}**`;
@@ -241,11 +245,13 @@ const gfmRules = {
'del'(el, text) {
return `~~${text}~~`;
},
- 'sup'(el, text) {
- return `^${text}`;
- },
'hr'(el) {
- return '-----';
+ // extra leading \n is to ensure that there is a blank line between
+ // a list followed by an hr, otherwise this breaks old redcarpet rendering
+ return '\n-----\n';
+ },
+ 'p'(el, text) {
+ return `${text.trim()}\n`;
},
'table'(el) {
const theadEl = el.querySelector('thead');
@@ -263,7 +269,9 @@ const gfmRules = {
let before = '';
let after = '';
- switch (cell.style.textAlign) {
+ const alignment = cell.align || cell.style.textAlign;
+
+ switch (alignment) {
case 'center':
before = ':';
after = ':';
@@ -313,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();
@@ -330,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/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index f61c0be9230..5485248cfaf 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -70,7 +70,7 @@ export default class BlobViewer {
const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)');
let initialViewerName = initialViewer.getAttribute('data-type');
- if (this.switcher && location.hash.indexOf('#L') === 0) {
+ if (this.switcher && window.location.hash.indexOf('#L') === 0) {
initialViewerName = 'simple';
}
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 4424232f642..a603d89b84a 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -1,5 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
-/* global EditBlob */
+/* eslint-disable no-new */
import $ from 'jquery';
import NewCommitForm from '../new_commit_form';
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index ac06d79fb60..a2355d7fd5c 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -1,6 +1,5 @@
-/* eslint-disable comma-dangle, space-before-function-paren, one-var */
+/* eslint-disable comma-dangle */
-import $ from 'jquery';
import Sortable from 'sortablejs';
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
@@ -14,17 +13,28 @@ window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.Board = Vue.extend({
- template: '#js-board-template',
components: {
boardList,
'board-delete': gl.issueBoards.BoardDelete,
BoardBlankState,
},
props: {
- list: Object,
- disabled: Boolean,
- issueLinkBase: String,
- rootPath: String,
+ list: {
+ type: Object,
+ default: () => ({}),
+ },
+ disabled: {
+ type: Boolean,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
boardId: {
type: String,
required: true,
@@ -46,56 +56,8 @@ gl.issueBoards.Board = Vue.extend({
});
},
deep: true,
- },
- detailIssue: {
- handler () {
- if (!Object.keys(this.detailIssue.issue).length) return;
-
- const issue = this.list.findIssue(this.detailIssue.issue.id);
-
- if (issue) {
- const offsetLeft = this.$el.offsetLeft;
- const boardsList = document.querySelectorAll('.boards-list')[0];
- const left = boardsList.scrollLeft - offsetLeft;
- let right = (offsetLeft + this.$el.offsetWidth);
-
- if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
- // -290 here because width of boardsList is animating so therefore
- // getting the width here is incorrect
- // 290 is the width of the sidebar
- right -= (boardsList.offsetWidth - 290);
- } else {
- right -= boardsList.offsetWidth;
- }
-
- if (right - boardsList.scrollLeft > 0) {
- $(boardsList).animate({
- scrollLeft: right
- }, this.sortableOptions.animation);
- } else if (left > 0) {
- $(boardsList).animate({
- scrollLeft: offsetLeft
- }, this.sortableOptions.animation);
- }
- }
- },
- deep: true
}
},
- methods: {
- showNewIssueForm() {
- this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
- },
- toggleExpanded(e) {
- if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
- this.list.isExpanded = !this.list.isExpanded;
-
- if (AccessorUtilities.isLocalStorageAccessSafe()) {
- localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
- }
- }
- },
- },
mounted () {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
@@ -125,4 +87,19 @@ gl.issueBoards.Board = Vue.extend({
this.list.isExpanded = !isCollapsed;
}
},
+ methods: {
+ showNewIssueForm() {
+ this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
+ },
+ toggleExpanded(e) {
+ if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
+ this.list.isExpanded = !this.list.isExpanded;
+
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
+ }
+ }
+ },
+ },
+ template: '#js-board-template',
});
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue
index 2049eeb9c30..286529b4d13 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.vue
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -72,8 +72,8 @@ export default {
:key="index"
>
<span
- class="label-color"
- :style="{ backgroundColor: label.color }">
+ :style="{ backgroundColor: label.color }"
+ class="label-color">
</span>
{{ label.title }}
</li>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 33e3369b971..0398102ad02 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,83 +1,82 @@
<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>
<li
- class="board-card"
:class="{
'user-can-drag': !disabled && issue.id,
'is-disabled': disabled || !issue.id,
@@ -85,6 +84,7 @@ export default {
}"
:index="index"
:data-issue-id="issue.id"
+ class="board-card"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)">
diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js
index 7be98825fda..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';
@@ -8,13 +8,16 @@ window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardDelete = Vue.extend({
props: {
- list: Object
+ list: {
+ type: Object,
+ default: () => ({}),
+ },
},
methods: {
deleteBoard () {
$(this.$el).tooltip('hide');
- if (confirm('Are you sure you want to delete this list?')) {
+ if (window.confirm('Are you sure you want to delete this list?')) {
this.list.destroy();
}
}
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 84a7f277227..5c7565234d8 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -87,10 +87,46 @@ export default {
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: true,
- group: 'issues',
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
dataIdAttr: 'data-issue-id',
+ group: {
+ name: 'issues',
+ /**
+ * Dynamically determine between which containers
+ * items can be moved or copied as
+ * Assignee lists (EE feature) require this behavior
+ */
+ pull: (to, from, dragEl, e) => {
+ // As per Sortable's docs, `to` should provide
+ // reference to exact sortable container on which
+ // we're trying to drag element, but either it is
+ // a library's bug or our markup structure is too complex
+ // that `to` never points to correct container
+ // See https://github.com/RubaXa/Sortable/issues/1037
+ //
+ // So we use `e.target` which is always accurate about
+ // which element we're currently dragging our card upon
+ // So from there, we can get reference to actual container
+ // and thus the container type to enable Copy or Move
+ if (e.target) {
+ const containerEl = e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list');
+ const toBoardType = containerEl.dataset.boardType;
+
+ if (toBoardType) {
+ const fromBoardType = this.list.type;
+
+ if ((fromBoardType === 'assignee' && toBoardType === 'label') ||
+ (fromBoardType === 'label' && toBoardType === 'assignee')) {
+ return 'clone';
+ }
+ }
+ }
+
+ return true;
+ },
+ revertClone: true,
+ },
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
@@ -169,21 +205,22 @@ export default {
<template>
<div class="board-list-component">
<div
+ v-if="loading"
class="board-list-loading text-center"
- aria-label="Loading issues"
- v-if="loading">
+ aria-label="Loading issues">
<loading-icon />
</div>
<board-new-issue
+ v-if="list.type !== 'closed' && showIssueForm"
:group-id="groupId"
- :list="list"
- v-if="list.type !== 'closed' && showIssueForm"/>
+ :list="list"/>
<ul
- class="board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
- :class="{ 'is-smaller': showIssueForm }">
+ :data-board-type="list.type"
+ :class="{ 'is-smaller': showIssueForm }"
+ class="board-list js-board-list">
<board-card
v-for="(issue, index) in issues"
ref="issue"
@@ -196,8 +233,8 @@ export default {
:disabled="disabled"
:key="issue.id" />
<li
- class="board-list-count text-center"
v-if="showCount"
+ class="board-list-count text-center"
data-issue-id="-1">
<loading-icon
v-show="list.loadingMore"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index e8dfd95f7ae..ec23b1e7c11 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -49,11 +49,12 @@ export default {
this.error = false;
const labels = this.list.label ? [this.list.label] : [];
+ const assignees = this.list.assignee ? [this.list.assignee] : [];
const issue = new ListIssue({
title: this.title,
labels,
subscribed: true,
- assignees: [],
+ assignees,
project_id: this.selectedProject.id,
});
@@ -95,26 +96,26 @@ export default {
<div class="board-card">
<form @submit="submit($event)">
<div
- class="flash-container"
v-if="error"
+ class="flash-container"
>
<div class="flash-alert">
An error occurred. Please try again.
</div>
</div>
<label
- class="label-light"
:for="list.id + '-title'"
+ class="label-light"
>
Title
</label>
<input
+ ref="input"
+ v-model="title"
+ :id="list.id + '-title'"
class="form-control"
type="text"
- v-model="title"
- ref="input"
autocomplete="off"
- :id="list.id + '-title'"
/>
<project-select
v-if="groupId"
@@ -122,10 +123,10 @@ export default {
/>
<div class="clearfix prepend-top-10">
<button
+ ref="submit-button"
+ :disabled="disabled"
class="btn btn-success float-left"
type="submit"
- :disabled="disabled"
- ref="submit-button"
>
Submit issue
</button>
@@ -141,4 +142,3 @@ export default {
</div>
</div>
</template>
-
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index c4ee4f6c855..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;
@@ -21,8 +21,17 @@ window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardSidebar = Vue.extend({
+ components: {
+ AssigneeTitle,
+ Assignees,
+ RemoveBtn,
+ Subscriptions,
+ },
props: {
- currentUser: Object
+ currentUser: {
+ type: Object,
+ default: () => ({}),
+ },
},
data() {
return {
@@ -64,6 +73,26 @@ gl.issueBoards.BoardSidebar = Vue.extend({
deep: true
},
},
+ created () {
+ // Get events from glDropdown
+ eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
+ eventHub.$on('sidebar.addAssignee', this.addAssignee);
+ eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
+ eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
+ },
+ beforeDestroy() {
+ eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
+ eventHub.$off('sidebar.addAssignee', this.addAssignee);
+ eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
+ eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
+ },
+ mounted () {
+ new IssuableContext(this.currentUser);
+ new MilestoneSelect();
+ new DueDateSelectors();
+ new LabelsSelect();
+ new Sidebar();
+ },
methods: {
closeSidebar () {
this.detail.issue = {};
@@ -97,30 +126,4 @@ gl.issueBoards.BoardSidebar = Vue.extend({
});
},
},
- created () {
- // Get events from glDropdown
- eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
- eventHub.$on('sidebar.addAssignee', this.addAssignee);
- eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
- eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
- },
- beforeDestroy() {
- eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
- eventHub.$off('sidebar.addAssignee', this.addAssignee);
- eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
- eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
- },
- mounted () {
- new IssuableContext(this.currentUser);
- new MilestoneSelect();
- new DueDateSelectors();
- new LabelsSelect();
- new Sidebar();
- },
- components: {
- assigneeTitle,
- assignees,
- removeBtn: gl.issueBoards.RemoveIssueBtn,
- subscriptions,
- },
});
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 dcc07810d01..00000000000
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ /dev/null
@@ -1,195 +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({
- 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,
- },
- },
- data() {
- return {
- limitBeforeCounter: 3,
- maxRender: 4,
- maxCounter: 99,
- };
- },
- components: {
- UserAvatarLink,
- },
- 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 1e5f2383223..dbd69f84526 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -1,12 +1,9 @@
-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],
- data() {
- return ModalStore.store;
- },
props: {
newIssuePath: {
type: String,
@@ -17,6 +14,9 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
required: true,
},
},
+ data() {
+ return ModalStore.store;
+ },
computed: {
contents() {
const obj = {
@@ -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 11bb3e98334..e0dac6003f1 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -1,12 +1,15 @@
-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: {
+ ListsDropdown,
+ },
mixins: [modalMixin],
data() {
return {
@@ -52,31 +55,32 @@ gl.issueBoards.ModalFooter = Vue.extend({
this.toggleModal(false);
},
},
- components: {
- 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
- },
- 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 67c29ebca72..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({
- 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();
- },
- },
- components: {
- 'modal-tabs': gl.issueBoards.ModalTabs,
- modalFilters,
- },
- 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 3083b3e4405..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({
- 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;
- },
- 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,
- },
- },
- 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
- });
- },
- },
- 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;
- },
- },
- created() {
- this.page = 1;
- },
- components: {
- 'modal-header': gl.issueBoards.ModalHeader,
- 'modal-list': gl.issueBoards.ModalList,
- 'modal-footer': gl.issueBoards.ModalFooter,
- 'empty-state': gl.issueBoards.ModalEmptyState,
- loadingIcon,
- },
- 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 f86896d2178..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({
- props: {
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- watch: {
- activeTab() {
- if (this.activeTab === 'all') {
- ModalStore.purgeUnselectedIssues();
- }
- },
- },
- 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;
- },
- },
- 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;
- }
- },
- },
- 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);
- },
- components: {
- 'issue-card-inner': gl.issueBoards.IssueCardInner,
- },
- 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..02ac36d7367
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -0,0 +1,162 @@
+<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 71f49319c36..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';
@@ -56,6 +56,7 @@ gl.issueBoards.newListDropdownInit = () => {
filterable: true,
selectable: true,
multiSelect: true,
+ containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
clicked (options) {
const { e } = options;
const label = options.selectedObj;
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..55278626ffc
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
@@ -0,0 +1,72 @@
+<script>
+ import Vue from 'vue';
+ import Flash from '../../../flash';
+ import { __ } from '../../../locale';
+
+ const Store = gl.issueBoards.BoardsStore;
+
+ export default {
+ 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 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 = {};
+ },
+ },
+ };
+</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 29ab13b8e0b..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';
@@ -7,6 +7,7 @@ import Vue from 'vue';
import Flash from '~/flash';
import { __ } from '~/locale';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
@@ -15,7 +16,6 @@ import './models/issue';
import './models/list';
import './models/milestone';
import './models/project';
-import './models/assignee';
import './stores/boards_store';
import ModalStore from './stores/modal_store';
import BoardService from './services/board_service';
@@ -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/assignee.js b/app/assets/javascripts/boards/models/assignee.js
deleted file mode 100644
index 05dd449e4fd..00000000000
--- a/app/assets/javascripts/boards/models/assignee.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/* eslint-disable no-unused-vars */
-
-class ListAssignee {
- constructor(user, defaultAvatar) {
- this.id = user.id;
- this.name = user.name;
- this.username = user.username;
- this.avatar = user.avatar_url || defaultAvatar;
- }
-}
-
-window.ListAssignee = ListAssignee;
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index b381d48d625..b85266b6bc3 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
+/* eslint-disable no-unused-vars, comma-dangle */
/* global ListLabel */
/* global ListMilestone */
/* global ListAssignee */
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 7144f4190e7..e35f277a865 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,12 +1,14 @@
-/* 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 */
-/* global ListLabel */
+
+import ListLabel from '~/vue_shared/models/label';
+import ListAssignee from '~/vue_shared/models/assignee';
import queryData from '../utils/query_data';
const PER_PAGE = 20;
class List {
- constructor (obj, defaultAvatar) {
+ constructor(obj, defaultAvatar) {
this.id = obj.id;
this._uid = this.guid();
this.position = obj.position;
@@ -24,6 +26,9 @@ class List {
if (obj.label) {
this.label = new ListLabel(obj.label);
+ } else if (obj.user) {
+ this.assignee = new ListAssignee(obj.user);
+ this.title = this.assignee.name;
}
if (this.type !== 'blank' && this.id) {
@@ -34,14 +39,26 @@ class List {
}
guid() {
- const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+ const s4 = () =>
+ Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
}
- save () {
- return gl.boardService.createList(this.label.id)
+ save() {
+ const entity = this.label || this.assignee;
+ let entityType = '';
+ if (this.label) {
+ entityType = 'label_id';
+ } else {
+ entityType = 'assignee_id';
+ }
+
+ return gl.boardService
+ .createList(entity.id, entityType)
.then(res => res.data)
- .then((data) => {
+ .then(data => {
this.id = data.id;
this.type = data.list_type;
this.position = data.position;
@@ -50,25 +67,23 @@ class List {
});
}
- destroy () {
+ destroy() {
const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this);
gl.issueBoards.BoardsStore.state.lists.splice(index, 1);
gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
- gl.boardService.destroyList(this.id)
- .catch(() => {
- // TODO: handle request error
- });
+ gl.boardService.destroyList(this.id).catch(() => {
+ // TODO: handle request error
+ });
}
- update () {
- gl.boardService.updateList(this.id, this.position)
- .catch(() => {
- // TODO: handle request error
- });
+ update() {
+ gl.boardService.updateList(this.id, this.position).catch(() => {
+ // TODO: handle request error
+ });
}
- nextPage () {
+ nextPage() {
if (this.issuesSize > this.issues.length) {
if (this.issues.length / PER_PAGE >= 1) {
this.page += 1;
@@ -78,7 +93,7 @@ class List {
}
}
- getIssues (emptyIssues = true) {
+ getIssues(emptyIssues = true) {
const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page });
if (this.label && data.label_name) {
@@ -89,7 +104,8 @@ class List {
this.loading = true;
}
- return gl.boardService.getIssuesForList(this.id, data)
+ return gl.boardService
+ .getIssuesForList(this.id, data)
.then(res => res.data)
.then((data) => {
this.loading = false;
@@ -103,11 +119,12 @@ class List {
});
}
- newIssue (issue) {
+ newIssue(issue) {
this.addIssue(issue, null, 0);
this.issuesSize += 1;
- return gl.boardService.newIssue(this.id, issue)
+ return gl.boardService
+ .newIssue(this.id, issue)
.then(res => res.data)
.then((data) => {
issue.id = data.id;
@@ -123,13 +140,13 @@ class List {
});
}
- createIssues (data) {
- data.forEach((issueObj) => {
+ createIssues(data) {
+ data.forEach(issueObj => {
this.addIssue(new ListIssue(issueObj, this.defaultAvatar));
});
}
- addIssue (issue, listFrom, newIndex) {
+ addIssue(issue, listFrom, newIndex) {
let moveBeforeId = null;
let moveAfterId = null;
@@ -152,6 +169,13 @@ class List {
issue.addLabel(this.label);
}
+ if (this.assignee) {
+ if (listFrom && listFrom.type === 'assignee') {
+ issue.removeAssignee(listFrom.assignee);
+ }
+ issue.addAssignee(this.assignee);
+ }
+
if (listFrom) {
this.issuesSize += 1;
@@ -160,29 +184,29 @@ class List {
}
}
- moveIssue (issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
+ moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
this.issues.splice(oldIndex, 1);
this.issues.splice(newIndex, 0, issue);
- gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId)
- .catch(() => {
- // TODO: handle request error
- });
+ gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => {
+ // TODO: handle request error
+ });
}
updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
- gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
+ gl.boardService
+ .moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
.catch(() => {
// TODO: handle request error
});
}
- findIssue (id) {
+ findIssue(id) {
return this.issues.find(issue => issue.id === id);
}
- removeIssue (removeIssue) {
- this.issues = this.issues.filter((issue) => {
+ removeIssue(removeIssue) {
+ this.issues = this.issues.filter(issue => {
const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) {
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/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 7c90597f77c..029b0971f2c 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -30,11 +30,13 @@ export default class BoardService {
return axios.post(this.listsEndpointGenerate, {});
}
- createList(labelId) {
+ createList(entityId, entityType) {
+ const list = {
+ [entityType]: entityId,
+ };
+
return axios.post(this.listsEndpoint, {
- list: {
- label_id: labelId,
- },
+ list,
});
}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 20e78edf2a2..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';
@@ -103,8 +103,15 @@ gl.issueBoards.BoardsStore = {
const listLabels = issueLists.map(listIssue => listIssue.label);
if (!issueTo) {
- // Add to new lists issues if it doesn't already exist
- listTo.addIssue(issue, listFrom, newIndex);
+ // Check if target list assignee is already present in this issue
+ if ((listTo.type === 'assignee' && listFrom.type === 'assignee') &&
+ issue.findAssignee(listTo.assignee)) {
+ const targetIssue = listTo.findIssue(issue.id);
+ targetIssue.removeAssignee(listFrom.assignee);
+ } else {
+ // Add to new lists issues if it doesn't already exist
+ listTo.addIssue(issue, listFrom, newIndex);
+ }
} else {
listTo.updateIssueLabel(issue, listFrom);
issueTo.removeLabel(listFrom.label);
@@ -115,7 +122,11 @@ gl.issueBoards.BoardsStore = {
list.removeIssue(issue);
});
issue.removeLabels(listLabels);
- } else {
+ } else if (listTo.type === 'backlog' && listFrom.type === 'assignee') {
+ issue.removeAssignee(listFrom.assignee);
+ listFrom.removeIssue(issue);
+ } else if ((listTo.type !== 'label' && listFrom.type === 'assignee') ||
+ (listTo.type !== 'assignee' && listFrom.type === 'label')) {
listFrom.removeIssue(issue);
}
},
@@ -126,13 +137,14 @@ gl.issueBoards.BoardsStore = {
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
},
findList (key, val, type = 'label') {
- return this.state.lists.filter((list) => {
- const byType = type ? list['type'] === type : true;
+ const filteredList = this.state.lists.filter((list) => {
+ const byType = type ? (list.type === type) || (list.type === 'assignee') : true;
return list[key] === val && byType;
- })[0];
+ });
+ return filteredList[0];
},
updateFiltersUrl () {
- history.pushState(null, null, `?${this.filter.path}`);
+ window.history.pushState(null, null, `?${this.filter.path}`);
}
};
diff --git a/app/assets/javascripts/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/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index 30567993322..ec52fdfdf32 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -125,8 +125,8 @@
<template>
<div
- class="gl-responsive-table-row gl-responsive-table-row-col-span"
:class="rowJsClass"
+ class="gl-responsive-table-row gl-responsive-table-row-col-span"
>
<div
class="gl-responsive-table-row-layout"
@@ -155,8 +155,8 @@
<slot name="description"></slot>
</div>
<div
- class="table-section table-button-footer section-align-top"
:class="{ 'section-20': showManageButton, 'section-15': !showManageButton }"
+ class="table-section table-button-footer section-align-top"
role="gridcell"
>
<div
@@ -164,18 +164,18 @@
class="btn-group table-action-buttons"
>
<a
- class="btn"
:href="manageLink"
+ class="btn"
>
{{ manageButtonLabel }}
</a>
</div>
<div class="btn-group table-action-buttons">
<loading-button
- class="js-cluster-application-install-button"
:loading="installButtonLoading"
:disabled="installButtonDisabled"
:label="installButtonLabel"
+ class="js-cluster-application-install-button"
@click="installClicked"
/>
</div>
@@ -187,7 +187,7 @@
role="row"
>
<div
- class="alert alert-danger alert-block append-bottom-0"
+ class="alert alert-danger alert-block append-bottom-0 clusters-error-alert"
role="gridcell"
>
<div>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 9d6be555a2c..8ee7279e544 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -152,11 +152,11 @@ export default {
<application-row
id="helm"
:title="applications.helm.title"
- title-link="https://docs.helm.sh/"
:status="applications.helm.status"
:status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
+ title-link="https://docs.helm.sh/"
>
<div slot="description">
{{ s__(`ClusterIntegration|Helm streamlines installing
@@ -168,11 +168,11 @@ export default {
<application-row
:id="ingressId"
:title="applications.ingress.title"
- title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
:status="applications.ingress.status"
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
+ title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
>
<div slot="description">
<p>
@@ -191,10 +191,10 @@ export default {
class="input-group"
>
<input
- type="text"
id="ingress-ip-address"
- class="form-control js-ip-address"
:value="ingressExternalIp"
+ type="text"
+ class="form-control js-ip-address"
readonly
/>
<span class="input-group-append">
@@ -255,12 +255,12 @@ export default {
<application-row
id="prometheus"
:title="applications.prometheus.title"
- title-link="https://prometheus.io/docs/introduction/overview/"
:manage-link="managePrometheusPath"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
+ title-link="https://prometheus.io/docs/introduction/overview/"
>
<div
slot="description"
@@ -271,11 +271,11 @@ export default {
<application-row
id="runner"
:title="applications.runner.title"
- title-link="https://docs.gitlab.com/runner/"
:status="applications.runner.status"
:status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason"
+ title-link="https://docs.gitlab.com/runner/"
>
<div slot="description">
{{ s__(`ClusterIntegration|GitLab Runner connects to this
@@ -287,12 +287,12 @@ export default {
<application-row
id="jupyter"
:title="applications.jupyter.title"
- title-link="https://jupyterhub.readthedocs.io/en/stable/"
:status="applications.jupyter.status"
:status-reason="applications.jupyter.statusReason"
:request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
+ title-link="https://jupyterhub.readthedocs.io/en/stable/"
>
<div slot="description">
<p>
@@ -311,10 +311,10 @@ export default {
<div class="input-group">
<input
- type="text"
- class="form-control js-hostname"
v-model="applications.jupyter.hostname"
:readonly="jupyterInstalled"
+ type="text"
+ class="form-control js-hostname"
/>
<span
class="input-group-btn"
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 3a4ac09f67c..d90db7b103c 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -95,7 +95,7 @@ export default class ClusterStore {
this.state.applications.jupyter.hostname =
serverAppEntry.hostname ||
(this.state.applications.ingress.externalIp
- ? `jupyter.${this.state.applications.ingress.externalIp}.xip.io`
+ ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
: '');
}
});
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/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 24d63b99a29..95c4be64d35 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -77,9 +77,9 @@
<div class="content-list pipelines">
<loading-icon
+ v-if="isLoading"
:label="s__('Pipelines|Loading Pipelines')"
size="3"
- v-if="isLoading"
class="prepend-top-20"
/>
@@ -91,8 +91,8 @@
/>
<div
- class="table-holder"
v-else-if="shouldRenderTable"
+ class="table-holder"
>
<pipelines-table-component
:pipelines="state.pipelines"
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 7e2a3573f81..9a3ea7a55b6 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -45,7 +45,7 @@ export default class CommitsList {
this.content.fadeTo('fast', 1.0);
// Change url so if user reload a page - search results are saved
- history.replaceState({
+ window.history.replaceState({
page: commitsUrl,
}, document.title, commitsUrl);
})
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index ffe15f02f2e..a252036d657 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
+/* eslint-disable func-names, one-var, no-var, one-var-declaration-per-line, object-shorthand, no-else-return, max-len */
import $ from 'jquery';
import { __ } from './locale';
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index 09d490106df..02aa507ba03 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -66,8 +66,14 @@ export default class CreateMergeRequestDropdown {
}
bindEvents() {
- this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
- this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
+ this.createMergeRequestButton.addEventListener(
+ 'click',
+ this.onClickCreateMergeRequestButton.bind(this),
+ );
+ this.createTargetButton.addEventListener(
+ 'click',
+ this.onClickCreateMergeRequestButton.bind(this),
+ );
this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
@@ -77,7 +83,8 @@ export default class CreateMergeRequestDropdown {
checkAbilityToCreateBranch() {
this.setUnavailableButtonState();
- axios.get(this.canCreatePath)
+ axios
+ .get(this.canCreatePath)
.then(({ data }) => {
this.setUnavailableButtonState(false);
@@ -105,7 +112,8 @@ export default class CreateMergeRequestDropdown {
createBranch() {
this.isCreatingBranch = true;
- return axios.post(this.createBranchPath)
+ return axios
+ .post(this.createBranchPath)
.then(({ data }) => {
this.branchCreated = true;
window.location.href = data.url;
@@ -116,7 +124,8 @@ export default class CreateMergeRequestDropdown {
createMergeRequest() {
this.isCreatingMergeRequest = true;
- return axios.post(this.createMrPath)
+ return axios
+ .post(this.createMrPath)
.then(({ data }) => {
this.mergeRequestCreated = true;
window.location.href = data.url;
@@ -195,7 +204,8 @@ export default class CreateMergeRequestDropdown {
getRef(ref, target = 'all') {
if (!ref) return false;
- return axios.get(this.refsPath + ref)
+ return axios
+ .get(`${this.refsPath}${encodeURIComponent(ref)}`)
.then(({ data }) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
@@ -204,7 +214,8 @@ export default class CreateMergeRequestDropdown {
if (target === 'branch') {
result = CreateMergeRequestDropdown.findByValue(branches, ref);
} else {
- result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
+ result =
+ CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
CreateMergeRequestDropdown.findByValue(tags, ref, true);
this.suggestedRef = result;
}
@@ -255,11 +266,13 @@ export default class CreateMergeRequestDropdown {
}
isBusy() {
- return this.isCreatingMergeRequest ||
+ return (
+ this.isCreatingMergeRequest ||
this.mergeRequestCreated ||
this.isCreatingBranch ||
this.branchCreated ||
- this.isGettingRef;
+ this.isGettingRef
+ );
}
onChangeInput(event) {
@@ -268,10 +281,11 @@ 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 = event.target.value.slice(0, event.target.selectionStart) +
+ value =
+ event.target.value.slice(0, event.target.selectionStart) +
event.target.value.slice(event.target.selectionEnd);
} else {
return false;
@@ -352,7 +366,7 @@ export default class CreateMergeRequestDropdown {
removeMessage(target) {
const { input, message } = this.getTargetData(target);
const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline'];
- const messageClasses = ['gl-field-hint', 'gl-field-error-message', 'gl-field-success-message'];
+ const messageClasses = ['text-muted', 'text-danger', 'text-success'];
inputClasses.forEach(cssClass => input.classList.remove(cssClass));
messageClasses.forEach(cssClass => message.classList.remove(cssClass));
@@ -379,7 +393,7 @@ export default class CreateMergeRequestDropdown {
this.removeMessage(target);
input.classList.add('gl-field-success-outline');
- message.classList.add('gl-field-success-message');
+ message.classList.add('text-success');
message.textContent = sprintf(__('%{text} is available'), { text });
message.style.display = 'inline-block';
}
@@ -389,18 +403,19 @@ export default class CreateMergeRequestDropdown {
const text = target === 'branch' ? __('branch name') : __('source');
this.removeMessage(target);
- message.classList.add('gl-field-hint');
+ message.classList.add('text-muted');
message.textContent = sprintf(__('Checking %{text} availability…'), { text });
message.style.display = 'inline-block';
}
showNotAvailableMessage(target) {
const { input, message } = this.getTargetData(target);
- const text = target === 'branch' ? __('Branch is already taken') : __('Source is not available');
+ const text =
+ target === 'branch' ? __('Branch is already taken') : __('Source is not available');
this.removeMessage(target);
input.classList.add('gl-field-error-outline');
- message.classList.add('gl-field-error-message');
+ message.classList.add('text-danger');
message.textContent = text;
message.style.display = 'inline-block';
}
@@ -459,11 +474,15 @@ export default class CreateMergeRequestDropdown {
// target - 'branch' or 'ref'
// ref - string - the new value to use as branch or ref
updateCreatePaths(target, ref) {
- const pathReplacement = `$1${ref}`;
+ const pathReplacement = `$1${encodeURIComponent(ref)}`;
- this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
- pathReplacement);
- this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
- pathReplacement);
+ this.createBranchPath = this.createBranchPath.replace(
+ this.regexps[target].createBranchPath,
+ pathReplacement,
+ );
+ this.createMrPath = this.createMrPath.replace(
+ this.regexps[target].createMrPath,
+ pathReplacement,
+ );
}
}
diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue
index 3204b8dd8e7..410d4873e55 100644
--- a/app/assets/javascripts/cycle_analytics/components/banner.vue
+++ b/app/assets/javascripts/cycle_analytics/components/banner.vue
@@ -23,9 +23,9 @@
<template>
<div class="landing content-block">
<button
+ :aria-label="__('Dismiss Cycle Analytics introduction box')"
class="js-ca-dismiss-button dismiss-button"
type="button"
- :aria-label="__('Dismiss Cycle Analytics introduction box')"
@click="dismissOverviewDialog"
>
<i
diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
index 5be17081b58..b626b187651 100644
--- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
@@ -19,14 +19,14 @@
class="events-info float-right"
>
<i
- class="fa fa-warning"
v-tooltip
- aria-hidden="true"
:title="n__(
'Limited to showing %d event at most',
'Limited to showing %d events at most',
50
)"
+ class="fa fa-warning"
+ aria-hidden="true"
data-placement="top"
>
</i>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_component.vue
index 907638d798a..312fe75dde4 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_component.vue
@@ -38,8 +38,8 @@
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a
- class="issue-title"
:href="issue.url"
+ class="issue-title"
>
{{ issue.title }}
</a>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
index 34aa04083e6..d4735d030fc 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
@@ -74,12 +74,12 @@
</template>
<template v-else>
<span
- class="merge-request-branch"
v-if="mergeRequest.branch"
+ class="merge-request-branch"
>
<icon
- name="fork"
:size="16"
+ name="fork"
/>
<a :href="mergeRequest.branch.url">
{{ mergeRequest.branch.name }}
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
index 92f2a95a66a..22637485c01 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
@@ -38,8 +38,8 @@
<ul class="stage-event-list">
<li
v-for="(build, i) in items"
- class="stage-event-item item-build-component"
:key="i"
+ class="stage-event-item item-build-component"
>
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
@@ -52,8 +52,8 @@
#{{ build.id }}
</a>
<icon
- name="fork"
:size="16"
+ name="fork"
/>
<a
:href="build.branch.url"
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
index b84bb6ed792..a0796f299e7 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
@@ -64,8 +64,8 @@
#{{ build.id }}
</a>
<icon
- name="fork"
:size="16"
+ name="fork"
/>
<a
:href="build.branch.url"
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index 67dda0e29cb..7399fc97d45 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -40,9 +40,9 @@ export default {
<template>
<button
- class="btn"
:class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading"
+ class="btn"
@click="doAction">
<slot></slot>
<loading-icon
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index c41fe55db63..d91e4809126 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -98,7 +98,7 @@ export default {
},
disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert
- if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
+ if (window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
this.service
.disableKey(deployKey.id)
.then(this.fetchKeys)
@@ -116,8 +116,8 @@ export default {
<div class="append-bottom-default deploy-keys">
<loading-icon
v-if="isLoading && !hasKeys"
- size="2"
:label="s__('DeployKeys|Loading deploy keys')"
+ size="2"
/>
<template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
@@ -138,16 +138,16 @@ export default {
<navigation-tabs
:tabs="tabs"
- @onChangeTab="onChangeTab"
scope="deployKeys"
+ @onChangeTab="onChangeTab"
/>
</div>
<keys-panel
- class="qa-project-deploy-keys"
:project-id="projectId"
:keys="keys[currentTab]"
:store="store"
:endpoint="endpoint"
+ class="qa-project-deploy-keys"
/>
</template>
</div>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 6c2af7fa768..f66ca070445 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -135,9 +135,9 @@ export default {
<div class="table-mobile-content deploy-project-list">
<template v-if="projects.length > 0">
<a
- class="label deploy-project-label"
- :title="projectTooltipTitle(firstProject)"
v-tooltip
+ :title="projectTooltipTitle(firstProject)"
+ class="label deploy-project-label"
>
<span>
{{ firstProject.project.full_name }}
@@ -145,22 +145,22 @@ export default {
<icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/>
</a>
<a
+ v-tooltip
v-if="isExpandable"
+ :title="restProjectsTooltip"
class="label deploy-project-label"
@click="toggleExpanded"
- :title="restProjectsTooltip"
- v-tooltip
>
<span>{{ restProjectsLabel }}</span>
</a>
<a
- v-else-if="isExpanded"
+ v-tooltip
v-for="deployKeysProject in restProjects"
+ v-else-if="isExpanded"
:key="deployKeysProject.project.full_path"
- class="label deploy-project-label"
:href="deployKeysProject.project.full_path"
:title="projectTooltipTitle(deployKeysProject)"
- v-tooltip
+ class="label deploy-project-label"
>
<span>
{{ deployKeysProject.project.full_name }}
@@ -181,8 +181,8 @@ export default {
</div>
<div class="table-mobile-content text-secondary key-created-at">
<span
- :title="tooltipTitle(deployKey.created_at)"
- v-tooltip>
+ v-tooltip
+ :title="tooltipTitle(deployKey.created_at)">
<icon name="calendar"/>
<span>{{ timeFormated(deployKey.created_at) }}</span>
</span>
@@ -198,34 +198,34 @@ export default {
{{ __('Enable') }}
</action-btn>
<a
+ v-tooltip
v-if="deployKey.can_edit"
- class="btn btn-default text-secondary"
:href="editDeployKeyPath"
:title="__('Edit')"
+ class="btn btn-default text-secondary"
data-container="body"
- v-tooltip
>
<icon name="pencil"/>
</a>
<action-btn
+ v-tooltip
v-if="isRemovable"
:deploy-key="deployKey"
+ :title="__('Remove')"
btn-css-class="btn-danger"
type="remove"
- :title="__('Remove')"
data-container="body"
- v-tooltip
>
<icon name="remove"/>
</action-btn>
<action-btn
+ v-tooltip
v-else-if="isEnabled"
:deploy-key="deployKey"
+ :title="__('Disable')"
btn-css-class="btn-warning"
type="disable"
- :title="__('Disable')"
data-container="body"
- v-tooltip
>
<icon name="cancel"/>
</action-btn>
diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
index 3b146c7389a..2f057ca29f6 100644
--- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue
+++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
@@ -59,8 +59,8 @@ export default {
/>
</template>
<div
- class="settings-message text-center"
v-else
+ class="settings-message text-center"
>
{{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
</div>
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
index d1260ff5373..ed24d1775f4 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
@@ -6,7 +6,10 @@ import Vue from 'vue';
const CommentAndResolveBtn = Vue.extend({
props: {
- discussionId: String,
+ discussionId: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
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 fe9b0795609..5528d2a542b 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -7,7 +7,15 @@ import Notes from '../../notes';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const DiffNoteAvatars = Vue.extend({
- props: ['discussionId'],
+ components: {
+ userAvatarImage,
+ },
+ props: {
+ discussionId: {
+ type: String,
+ required: true,
+ },
+ },
data() {
return {
isVisible: false,
@@ -17,77 +25,6 @@ const DiffNoteAvatars = Vue.extend({
collapseIcon,
};
},
- components: {
- userAvatarImage,
- },
- template: `
- <div class="diff-comment-avatar-holders"
- :class="discussionClassName"
- v-show="notesCount !== 0">
- <div v-if="!isVisible">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image
- v-for="note in notesSubset"
- :key="note.id"
- class="diff-comment-avatar js-diff-comment-avatar"
- @click.native="clickedAvatar($event)"
- :img-src="note.authorAvatar"
- :tooltip-text="getTooltipText(note)"
- :data-line-type="lineType"
- :size="19"
- data-html="true"
- />
- <span v-if="notesCount > shownAvatars"
- class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
- data-container="body"
- data-placement="top"
- ref="extraComments"
- role="button"
- :data-line-type="lineType"
- :title="extraNotesTitle"
- @click="clickedAvatar($event)">{{ moreText }}</span>
- </div>
- <button class="diff-notes-collapse js-diff-comment-avatar"
- type="button"
- aria-label="Show comments"
- :data-line-type="lineType"
- @click="clickedAvatar($event)"
- v-if="isVisible"
- v-html="collapseIcon">
- </button>
- </div>
- `,
- mounted() {
- this.$nextTick(() => {
- this.addNoCommentClass();
- this.setDiscussionVisible();
-
- this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
- });
-
- $(document).on('toggle.comments', () => {
- this.$nextTick(() => {
- this.setDiscussionVisible();
- });
- });
- },
- beforeDestroy() {
- this.addNoCommentClass();
- $(document).off('toggle.comments');
- },
- watch: {
- storeState: {
- handler() {
- this.$nextTick(() => {
- $('.has-tooltip', this.$el).tooltip('_fixTitle');
-
- // We need to add/remove a class to an element that is outside the Vue instance
- this.addNoCommentClass();
- });
- },
- deep: true,
- },
- },
computed: {
discussionClassName() {
return `js-diff-avatars-${this.discussionId}`;
@@ -128,6 +65,37 @@ const DiffNoteAvatars = Vue.extend({
return `${plusSign}${this.notesCount - this.shownAvatars}`;
},
},
+ watch: {
+ storeState: {
+ handler() {
+ this.$nextTick(() => {
+ $('.has-tooltip', this.$el).tooltip('_fixTitle');
+
+ // We need to add/remove a class to an element that is outside the Vue instance
+ this.addNoCommentClass();
+ });
+ },
+ deep: true,
+ },
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.addNoCommentClass();
+ this.setDiscussionVisible();
+
+ this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
+ });
+
+ $(document).on('toggle.comments', () => {
+ this.$nextTick(() => {
+ this.setDiscussionVisible();
+ });
+ });
+ },
+ beforeDestroy() {
+ this.addNoCommentClass();
+ $(document).off('toggle.comments');
+ },
methods: {
clickedAvatar(e) {
Notes.instance.onAddDiffNote(e);
@@ -143,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)
@@ -164,6 +132,43 @@ const DiffNoteAvatars = Vue.extend({
return `${note.authorName}: ${note.noteTruncated}`;
},
},
+ template: `
+ <div class="diff-comment-avatar-holders"
+ :class="discussionClassName"
+ v-show="notesCount !== 0">
+ <div v-if="!isVisible">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image
+ v-for="note in notesSubset"
+ :key="note.id"
+ class="diff-comment-avatar js-diff-comment-avatar"
+ @click.native="clickedAvatar($event)"
+ :img-src="note.authorAvatar"
+ :tooltip-text="getTooltipText(note)"
+ :data-line-type="lineType"
+ :size="19"
+ data-html="true"
+ />
+ <span v-if="notesCount > shownAvatars"
+ class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
+ data-container="body"
+ data-placement="top"
+ ref="extraComments"
+ role="button"
+ :data-line-type="lineType"
+ :title="extraNotesTitle"
+ @click="clickedAvatar($event)">{{ moreText }}</span>
+ </div>
+ <button class="diff-notes-collapse js-diff-comment-avatar"
+ type="button"
+ aria-label="Show comments"
+ :data-line-type="lineType"
+ @click="clickedAvatar($event)"
+ v-if="isVisible"
+ v-html="collapseIcon">
+ </button>
+ </div>
+ `,
});
Vue.component('diff-note-avatars', DiffNoteAvatars);
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 8f9186dfb9a..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,16 +1,18 @@
-/* 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],
props: {
- discussionId: String
+ discussionId: {
+ type: String,
+ required: true,
+ },
},
data: function () {
return {
@@ -52,6 +54,9 @@ const JumpToDiscussion = Vue.extend({
return lastId;
}
},
+ created() {
+ this.discussion = this.discussions[this.discussionId];
+ },
methods: {
jumpToNextUnresolvedDiscussion: function () {
let discussionsSelector;
@@ -68,7 +73,7 @@ const JumpToDiscussion = Vue.extend({
}).toArray();
};
- const discussions = this.discussions;
+ const { discussions } = this;
if (activeTab === 'diffs') {
discussionsSelector = '.diffs .notes[data-discussion-id]';
@@ -202,9 +207,6 @@ const JumpToDiscussion = Vue.extend({
});
}
},
- created() {
- this.discussion = this.discussions[this.discussionId];
- },
});
Vue.component('jump-to-discussion', JumpToDiscussion);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index 8d66417abac..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 */
@@ -8,113 +7,135 @@ import Flash from '../../flash';
const ResolveBtn = Vue.extend({
props: {
- noteId: Number,
- discussionId: String,
- resolved: Boolean,
- canResolve: Boolean,
- resolvedBy: String,
- authorName: String,
- authorAvatar: String,
- noteTruncated: String,
+ noteId: {
+ type: Number,
+ required: true,
+ },
+ discussionId: {
+ type: String,
+ required: true,
+ },
+ resolved: {
+ type: Boolean,
+ required: true,
+ },
+ canResolve: {
+ type: Boolean,
+ required: true,
+ },
+ resolvedBy: {
+ type: String,
+ required: true,
+ },
+ authorName: {
+ type: String,
+ required: true,
+ },
+ authorAvatar: {
+ type: String,
+ required: true,
+ },
+ noteTruncated: {
+ type: String,
+ required: true,
+ },
},
- data: function () {
+ data() {
return {
discussions: CommentsStore.state,
- loading: false
+ loading: false,
};
},
- watch: {
- 'discussions': {
- handler: 'updateTooltip',
- deep: true
- }
- },
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: {
+ handler: 'updateTooltip',
+ deep: true,
+ },
+ },
+ mounted() {
+ $(this.$refs.button).tooltip({
+ container: 'body',
+ });
+ },
+ beforeDestroy() {
+ CommentsStore.delete(this.discussionId, this.noteId);
+ },
+ created() {
+ CommentsStore.create({
+ discussionId: this.discussionId,
+ noteId: this.noteId,
+ canResolve: this.canResolve,
+ resolved: this.resolved,
+ resolvedBy: this.resolvedBy,
+ authorName: this.authorName,
+ authorAvatar: this.authorAvatar,
+ noteTruncated: this.noteTruncated,
+ });
+ },
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.'));
- }
- },
- mounted: function () {
- $(this.$refs.button).tooltip({
- container: 'body'
- });
- },
- beforeDestroy: function () {
- CommentsStore.delete(this.discussionId, this.noteId);
+ .catch(
+ () => new Flash('An error occurred when trying to resolve a comment. Please try again.'),
+ );
+ },
},
- created: function () {
- CommentsStore.create({
- discussionId: this.discussionId,
- noteId: this.noteId,
- canResolve: this.canResolve,
- resolved: this.resolved,
- resolvedBy: this.resolvedBy,
- authorName: this.authorName,
- authorAvatar: this.authorAvatar,
- noteTruncated: this.noteTruncated,
- });
- }
});
Vue.component('resolve-btn', ResolveBtn);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js
index fe7cf8f5fc1..e2683e09f40 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js
@@ -1,15 +1,17 @@
-/* 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],
props: {
- loggedOut: Boolean
+ loggedOut: {
+ type: Boolean,
+ required: true,
+ },
},
data: function () {
return {
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 6a036e96171..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 */
@@ -6,9 +6,18 @@ import Vue from 'vue';
const ResolveDiscussionBtn = Vue.extend({
props: {
- discussionId: String,
- mergeRequestId: Number,
- canResolve: Boolean,
+ discussionId: {
+ type: String,
+ required: true,
+ },
+ mergeRequestId: {
+ type: Number,
+ required: true,
+ },
+ canResolve: {
+ type: Boolean,
+ required: true,
+ },
},
data: function() {
return {
@@ -45,16 +54,16 @@ const ResolveDiscussionBtn = Vue.extend({
}
}
},
+ created: function () {
+ CommentsStore.createDiscussion(this.discussionId, this.canResolve);
+
+ this.discussion = CommentsStore.state[this.discussionId];
+ },
methods: {
resolve: function () {
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
}
},
- created: function () {
- CommentsStore.createDiscussion(this.discussionId, this.canResolve);
-
- this.discussion = CommentsStore.state[this.discussionId];
- }
});
Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index e17daec6a92..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,14 +69,5 @@ export default () => {
gl.diffNotesCompileComponents();
- if (!hasVueMRDiscussionsCookie()) {
- new Vue({
- el: '#resolve-count-app',
- 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..48ba967285f
--- /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-if="isInlineView"
+ :diff-file="diffFile"
+ :diff-lines="diffFile.highlightedDiffLines || []"
+ />
+ <parallel-diff-view
+ v-else-if="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..fba1d1af7cd
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -0,0 +1,258 @@
+<script>
+import _ from 'underscore';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import Tooltip from '~/vue_shared/directives/tooltip';
+import { truncateSha } from '~/lib/utils/text_utility';
+import { __, s__, sprintf } from '~/locale';
+import EditButton from './edit_button.vue';
+
+export default {
+ components: {
+ ClipboardButton,
+ EditButton,
+ Icon,
+ },
+ directives: {
+ Tooltip,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ collapsible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ addMergeRequestButtons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ expanded: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ discussionsExpanded: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ blobForkSuggestion: null,
+ };
+ },
+ computed: {
+ icon() {
+ if (this.diffFile.submodule) {
+ return 'archive';
+ }
+
+ return this.diffFile.blob.icon;
+ },
+ titleLink() {
+ if (this.diffFile.submodule) {
+ return this.diffFile.submoduleTreeUrl || this.diffFile.submoduleLink;
+ }
+
+ return `#${this.diffFile.fileHash}`;
+ },
+ filePath() {
+ if (this.diffFile.submodule) {
+ return `${this.diffFile.filePath} @ ${truncateSha(this.diffFile.blob.id)}`;
+ }
+
+ if (this.diffFile.deletedFile) {
+ return sprintf(__('%{filePath} deleted'), { filePath: this.diffFile.filePath }, false);
+ }
+
+ return this.diffFile.filePath;
+ },
+ titleTag() {
+ return this.diffFile.fileHash ? 'a' : 'span';
+ },
+ isUsingLfs() {
+ return this.diffFile.storedExternally && this.diffFile.externalStorage === 'lfs';
+ },
+ collapseIcon() {
+ return this.expanded ? 'chevron-down' : 'chevron-right';
+ },
+ isDiscussionsExpanded() {
+ return this.discussionsExpanded && this.expanded;
+ },
+ viewFileButtonText() {
+ const truncatedContentSha = _.escape(truncateSha(this.diffFile.contentSha));
+ return sprintf(
+ s__('MergeRequests|View file @ %{commitId}'),
+ {
+ commitId: `<span class="commit-sha">${truncatedContentSha}</span>`,
+ },
+ false,
+ );
+ },
+ viewReplacedFileButtonText() {
+ const truncatedBaseSha = _.escape(truncateSha(this.diffFile.diffRefs.baseSha));
+ return sprintf(
+ s__('MergeRequests|View replaced file @ %{commitId}'),
+ {
+ commitId: `<span class="commit-sha">${truncatedBaseSha}</span>`,
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ handleToggle(e, checkTarget) {
+ if (
+ !checkTarget ||
+ e.target === this.$refs.header ||
+ (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"
+ @click.stop="handleToggle"
+ />
+ <a
+ ref="titleWrapper"
+ :href="titleLink"
+ >
+ <i
+ :class="`fa-${icon}`"
+ class="fa fa-fw"
+ aria-hidden="true"
+ ></i>
+ <span v-if="diffFile.renamedFile">
+ <strong
+ v-tooltip
+ :title="diffFile.oldPath"
+ class="file-title-name"
+ data-container="body"
+ >
+ {{ diffFile.oldPath }}
+ </strong>
+ →
+ <strong
+ v-tooltip
+ :title="diffFile.newPath"
+ class="file-title-name"
+ data-container="body"
+ >
+ {{ diffFile.newPath }}
+ </strong>
+ </span>
+
+ <strong
+ v-tooltip
+ v-else
+ :title="filePath"
+ class="file-title-name"
+ data-container="body"
+ >
+ {{ filePath }}
+ </strong>
+ </a>
+
+ <clipboard-button
+ :title="__('Copy file path to clipboard')"
+ :text="diffFile.filePath"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+
+ <small
+ v-if="diffFile.modeChanged"
+ ref="fileMode"
+ >
+ {{ diffFile.aMode }} → {{ diffFile.bMode }}
+ </small>
+
+ <span
+ v-if="isUsingLfs"
+ class="label label-lfs append-right-5"
+ >
+ {{ __('LFS') }}
+ </span>
+ </div>
+
+ <div
+ v-if="!diffFile.submodule && addMergeRequestButtons"
+ class="file-actions d-none d-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..68fe6787f9b
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -0,0 +1,131 @@
+<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,
+} from '../constants';
+
+export default {
+ components: {
+ DiffLineGutterContent,
+ },
+ props: {
+ line: {
+ type: Object,
+ required: true,
+ },
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ 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', 'isInlineView']),
+ normalizedLine() {
+ if (this.isInlineView) {
+ return this.line;
+ }
+
+ return this.lineType === OLD_LINE_TYPE ? this.line.left : this.line.right;
+ },
+ isMatchLine() {
+ return this.normalizedLine.type === MATCH_LINE_TYPE;
+ },
+ isContextLine() {
+ return this.normalizedLine.type === CONTEXT_LINE_TYPE;
+ },
+ isMetaLine() {
+ return (
+ this.normalizedLine.type === OLD_NO_NEW_LINE_TYPE ||
+ this.normalizedLine.type === NEW_NO_NEW_LINE_TYPE ||
+ this.normalizedLine.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/diff_table_row.vue b/app/assets/javascripts/diffs/components/diff_table_row.vue
new file mode 100644
index 00000000000..8716fdaf44d
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_table_row.vue
@@ -0,0 +1,191 @@
+<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 {
+ isHover: false,
+ isLeftHover: false,
+ isRightHover: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['isInlineView', 'isParallelView']),
+ isContextLine() {
+ return this.line.left
+ ? this.line.left.type === CONTEXT_LINE_TYPE
+ : 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}`;
+ },
+ 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;
+ },
+ methods: {
+ handleMouseMove(e) {
+ const isHover = e.type === 'mouseover';
+
+ if (this.isInlineView) {
+ this.isHover = isHover;
+ } else {
+ 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
+ v-if="isInlineView"
+ :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>
+
+ <tr
+ v-else
+ :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"
+ class="diff-line-num old_line"
+ />
+ <diff-table-cell
+ :id="line.left.lineCode"
+ :diff-file="diffFile"
+ :line="line"
+ :is-content-line="true"
+ :line-type="parallelViewLeftLineType"
+ 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"
+ class="diff-line-num new_line"
+ />
+ <diff-table-cell
+ :id="line.right.lineCode"
+ :diff-file="diffFile"
+ :line="line"
+ :is-content-line="true"
+ :line-type="line.right.type"
+ class="line_content parallel right-side"
+ @mousedown.native="handleParallelLineMouseDown"
+ />
+ </tr>
+</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_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
new file mode 100644
index 00000000000..e72f85df77a
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -0,0 +1,38 @@
+<script>
+import diffContentMixin from '../mixins/diff_content';
+import inlineDiffCommentRow from './inline_diff_comment_row.vue';
+
+export default {
+ components: {
+ inlineDiffCommentRow,
+ },
+ mixins: [diffContentMixin],
+};
+</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"
+ >
+ <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_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
new file mode 100644
index 00000000000..ed92b4ee249
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -0,0 +1,55 @@
+<script>
+import diffContentMixin from '../mixins/diff_content';
+import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
+import { EMPTY_CELL_TYPE } from '../constants';
+
+export default {
+ components: {
+ parallelDiffCommentRow,
+ },
+ mixins: [diffContentMixin],
+ computed: {
+ parallelDiffLines() {
+ return this.normalizedDiffLines.map(line => {
+ if (!line.left) {
+ Object.assign(line, { left: { type: EMPTY_CELL_TYPE } });
+ } else if (!line.right) {
+ Object.assign(line, { right: { type: EMPTY_CELL_TYPE } });
+ }
+
+ return line;
+ });
+ },
+ },
+};
+</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"
+ >
+ <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/mixins/diff_content.js b/app/assets/javascripts/diffs/mixins/diff_content.js
new file mode 100644
index 00000000000..ebb511d3a7e
--- /dev/null
+++ b/app/assets/javascripts/diffs/mixins/diff_content.js
@@ -0,0 +1,57 @@
+import { mapGetters } from 'vuex';
+import diffDiscussions from '../components/diff_discussions.vue';
+import diffLineGutterContent from '../components/diff_line_gutter_content.vue';
+import diffLineNoteForm from '../components/diff_line_note_form.vue';
+import diffTableRow from '../components/diff_table_row.vue';
+import { trimFirstCharOfLineContent } from '../store/utils';
+
+export default {
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ diffLines: {
+ type: Array,
+ required: true,
+ },
+ },
+ components: {
+ diffDiscussions,
+ diffTableRow,
+ diffLineNoteForm,
+ diffLineGutterContent,
+ },
+ computed: {
+ ...mapGetters(['commit']),
+ commitId() {
+ return this.commit && this.commit.id;
+ },
+ userColorScheme() {
+ return window.gon.user_color_scheme;
+ },
+ normalizedDiffLines() {
+ return this.diffLines.map(line => {
+ if (line.richText) {
+ return trimFirstCharOfLineContent(line);
+ }
+
+ if (line.left) {
+ Object.assign(line, { left: trimFirstCharOfLineContent(line.left) });
+ }
+
+ if (line.right) {
+ Object.assign(line, { right: trimFirstCharOfLineContent(line.right) });
+ }
+
+ return line;
+ });
+ },
+ diffLinesLength() {
+ return this.normalizedDiffLines.length;
+ },
+ fileHash() {
+ return this.diffFile.fileHash;
+ },
+ },
+};
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..63e9239dce4
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -0,0 +1,11 @@
+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_FILES = 'SET_DIFF_FILES';
+export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE';
+export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS';
+export const ADD_COMMENT_FORM_LINE = 'ADD_COMMENT_FORM_LINE';
+export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE';
+export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
+export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
+export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
new file mode 100644
index 00000000000..339a33f8b71
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -0,0 +1,86 @@
+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_DIFF_FILES](state, diffFiles) {
+ Object.assign(state, {
+ diffFiles: convertObjectPropsToCamelCase(diffFiles, { deep: true }),
+ });
+ },
+
+ [types.SET_MERGE_REQUEST_DIFFS](state, mergeRequestDiffs) {
+ Object.assign(state, {
+ mergeRequestDiffs: convertObjectPropsToCamelCase(mergeRequestDiffs, { deep: true }),
+ });
+ },
+
+ [types.SET_DIFF_VIEW_TYPE](state, diffViewType) {
+ Object.assign(state, { diffViewType });
+ },
+
+ [types.ADD_COMMENT_FORM_LINE](state, { lineCode }) {
+ Vue.set(state.diffLineCommentForms, lineCode, true);
+ },
+
+ [types.REMOVE_COMMENT_FORM_LINE](state, { lineCode }) {
+ Vue.delete(state.diffLineCommentForms, lineCode);
+ },
+
+ [types.ADD_CONTEXT_LINES](state, options) {
+ const { lineNumbers, contextLines, fileHash } = options;
+ const { bottom } = options.params;
+ const diffFile = findDiffFile(state.diffFiles, fileHash);
+ const { highlightedDiffLines, parallelDiffLines } = diffFile;
+
+ removeMatchLine(diffFile, lineNumbers, bottom);
+ const lines = addLineReferences(contextLines, lineNumbers, bottom);
+ addContextLines({
+ inlineLines: highlightedDiffLines,
+ parallelLines: parallelDiffLines,
+ contextLines: lines,
+ bottom,
+ lineNumbers,
+ });
+ },
+
+ [types.ADD_COLLAPSED_DIFFS](state, { file, data }) {
+ const normalizedData = convertObjectPropsToCamelCase(data, { deep: true });
+ const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash);
+
+ if (newFileData) {
+ const index = _.findIndex(state.diffFiles, f => f.fileHash === file.fileHash);
+ state.diffFiles.splice(index, 1, newFileData);
+ }
+ },
+
+ [types.EXPAND_ALL_FILES](state) {
+ const diffFiles = [];
+
+ state.diffFiles.forEach(file => {
+ diffFiles.push({
+ ...file,
+ collapsed: false,
+ });
+ });
+
+ Object.assign(state, { diffFiles });
+ },
+};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
new file mode 100644
index 00000000000..da7ae16aaf1
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -0,0 +1,172 @@
+import _ from 'underscore';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import {
+ LINE_POSITION_LEFT,
+ LINE_POSITION_RIGHT,
+ TEXT_DIFF_POSITION_TYPE,
+ DIFF_NOTE_TYPE,
+ NEW_LINE_TYPE,
+ OLD_LINE_TYPE,
+ MATCH_LINE_TYPE,
+} from '../constants';
+
+export function findDiffFile(files, hash) {
+ return files.filter(file => file.fileHash === hash)[0];
+}
+
+export const getReversePosition = linePosition => {
+ if (linePosition === LINE_POSITION_RIGHT) {
+ return LINE_POSITION_LEFT;
+ }
+
+ return LINE_POSITION_RIGHT;
+};
+
+export function getNoteFormData(params) {
+ const {
+ note,
+ noteableType,
+ noteableData,
+ diffFile,
+ noteTargetLine,
+ diffViewType,
+ linePosition,
+ } = params;
+
+ const position = JSON.stringify({
+ base_sha: diffFile.diffRefs.baseSha,
+ start_sha: diffFile.diffRefs.startSha,
+ head_sha: diffFile.diffRefs.headSha,
+ old_path: diffFile.oldPath,
+ new_path: diffFile.newPath,
+ position_type: TEXT_DIFF_POSITION_TYPE,
+ old_line: noteTargetLine.oldLine,
+ new_line: noteTargetLine.newLine,
+ });
+
+ const postData = {
+ view: diffViewType,
+ line_type: linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE,
+ merge_request_diff_head_sha: diffFile.diffRefs.headSha,
+ in_reply_to_discussion_id: '',
+ note_project_id: '',
+ target_type: noteableData.targetType,
+ target_id: noteableData.id,
+ note: {
+ note,
+ position,
+ noteable_type: noteableType,
+ noteable_id: noteableData.id,
+ commit_id: '',
+ type: DIFF_NOTE_TYPE,
+ line_code: noteTargetLine.lineCode,
+ },
+ };
+
+ return {
+ endpoint: noteableData.create_note_path,
+ data: postData,
+ };
+}
+
+export const findIndexInInlineLines = (lines, lineNumbers) => {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+
+ return _.findIndex(
+ lines,
+ line => line.oldLine === oldLineNumber && line.newLine === newLineNumber,
+ );
+};
+
+export const findIndexInParallelLines = (lines, lineNumbers) => {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+
+ return _.findIndex(
+ lines,
+ line =>
+ line.left &&
+ line.right &&
+ line.left.oldLine === oldLineNumber &&
+ line.right.newLine === newLineNumber,
+ );
+};
+
+export function removeMatchLine(diffFile, lineNumbers, bottom) {
+ const indexForInline = findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers);
+ const indexForParallel = findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers);
+ const factor = bottom ? 1 : -1;
+
+ diffFile.highlightedDiffLines.splice(indexForInline + factor, 1);
+ diffFile.parallelDiffLines.splice(indexForParallel + factor, 1);
+}
+
+export function addLineReferences(lines, lineNumbers, bottom) {
+ const { oldLineNumber, newLineNumber } = lineNumbers;
+ const lineCount = lines.length;
+ let matchLineIndex = -1;
+
+ const linesWithNumbers = lines.map((l, index) => {
+ const line = convertObjectPropsToCamelCase(l);
+
+ if (line.type === MATCH_LINE_TYPE) {
+ matchLineIndex = index;
+ } else {
+ Object.assign(line, {
+ oldLine: bottom ? oldLineNumber + index + 1 : oldLineNumber + index - lineCount,
+ newLine: bottom ? newLineNumber + index + 1 : newLineNumber + index - lineCount,
+ });
+ }
+
+ return line;
+ });
+
+ if (matchLineIndex > -1) {
+ const line = linesWithNumbers[matchLineIndex];
+ const targetLine = bottom
+ ? linesWithNumbers[matchLineIndex - 1]
+ : linesWithNumbers[matchLineIndex + 1];
+
+ Object.assign(line, {
+ metaData: {
+ oldPos: targetLine.oldLine,
+ newPos: targetLine.newLine,
+ },
+ });
+ }
+
+ return linesWithNumbers;
+}
+
+export function addContextLines(options) {
+ const { inlineLines, parallelLines, contextLines, lineNumbers } = options;
+ const normalizedParallelLines = contextLines.map(line => ({
+ left: line,
+ right: line,
+ }));
+
+ if (options.bottom) {
+ inlineLines.push(...contextLines);
+ parallelLines.push(...normalizedParallelLines);
+ } else {
+ const inlineIndex = findIndexInInlineLines(inlineLines, lineNumbers);
+ const parallelIndex = findIndexInParallelLines(parallelLines, lineNumbers);
+ inlineLines.splice(inlineIndex, 0, ...contextLines);
+ parallelLines.splice(parallelIndex, 0, ...normalizedParallelLines);
+ }
+}
+
+export function trimFirstCharOfLineContent(line) {
+ if (!line.richText) {
+ return line;
+ }
+
+ const firstChar = line.richText.charAt(0);
+
+ if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
+ Object.assign(line, {
+ richText: line.richText.substring(1),
+ });
+ }
+
+ return line;
+}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 72f21f13860..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/container.vue b/app/assets/javascripts/environments/components/container.vue
index 6bd7c6b49cb..9aa224fa407 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -43,17 +43,17 @@
<div class="environments-container">
<loading-icon
+ v-if="isLoading"
class="prepend-top-default"
label="Loading environments"
- v-if="isLoading"
size="3"
/>
<slot name="emptyState"></slot>
<div
- class="table-holder"
- v-if="!isLoading && environments.length > 0">
+ v-if="!isLoading && environments.length > 0"
+ class="table-holder">
<environment-table
:environments="environments"
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 0b3fef9fcca..e3652fe739e 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -52,18 +52,18 @@
role="group">
<button
v-tooltip
+ :title="title"
+ :aria-label="title"
+ :disabled="isLoading"
type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
data-container="body"
data-toggle="dropdown"
- :title="title"
- :aria-label="title"
- :disabled="isLoading"
>
<span>
<icon
- name="play"
:size="12"
+ name="play"
/>
<i
class="fa fa-caret-down"
@@ -79,15 +79,15 @@
v-for="(action, i) in actions"
:key="i">
<button
+ :class="{ disabled: isActionDisabled(action) }"
+ :disabled="isActionDisabled(action)"
type="button"
class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)"
- :class="{ disabled: isActionDisabled(action) }"
- :disabled="isActionDisabled(action)"
>
<icon
- name="play"
:size="12"
+ name="play"
/>
<span>
{{ action.name }}
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index ea6f1168c68..68195225d50 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -29,17 +29,17 @@
<template>
<a
v-tooltip
+ :title="title"
+ :aria-label="title"
+ :href="externalUrl"
class="btn external-url"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
- :title="title"
- :aria-label="title"
- :href="externalUrl"
>
<icon
- name="external-link"
:size="12"
+ name="external-link"
/>
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 23aaab2c441..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 '';
@@ -427,11 +427,11 @@
</script>
<template>
<div
- class="gl-responsive-table-row"
:class="{
'js-child-row environment-child-row': model.isChildren,
'folder-row': model.isFolder,
}"
+ class="gl-responsive-table-row"
role="row">
<div
class="table-section section-10"
@@ -446,19 +446,19 @@
</div>
<a
v-if="!model.isFolder"
- class="environment-name flex-truncate-parent table-mobile-content"
- :href="environmentPath">
+ :href="environmentPath"
+ class="environment-name flex-truncate-parent table-mobile-content">
<span
- class="flex-truncate-child"
v-tooltip
:title="model.name"
+ class="flex-truncate-child"
>{{ model.name }}</span>
</a>
<span
v-else
class="folder-name"
- @click="onClickFolder"
- role="button">
+ role="button"
+ @click="onClickFolder">
<span class="folder-icon">
<i
@@ -503,11 +503,11 @@
<span v-if="!model.isFolder && deploymentHasUser">
by
<user-avatar-link
- class="js-deploy-user-container"
:link-href="deploymentUser.web_url"
:img-src="deploymentUser.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="deploymentUser.username"
+ class="js-deploy-user-container"
/>
</span>
</div>
@@ -518,8 +518,8 @@
>
<a
v-if="shouldRenderBuildName"
- class="build-link flex-truncate-parent"
:href="buildPath"
+ class="build-link flex-truncate-parent"
>
<span class="flex-truncate-child">{{ buildName }}</span>
</a>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 8df1b6317e3..947e8c901e9 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -28,16 +28,16 @@
<template>
<a
v-tooltip
- class="btn monitoring-url d-none d-sm-none d-md-block"
- data-container="body"
- rel="noopener noreferrer nofollow"
:href="monitoringUrl"
:title="title"
:aria-label="title"
+ class="btn monitoring-url d-none d-sm-none d-md-block"
+ data-container="body"
+ rel="noopener noreferrer nofollow"
>
<icon
- name="chart"
:size="12"
+ name="chart"
/>
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 7515d711c50..310835c5ea9 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -39,10 +39,10 @@
</script>
<template>
<button
+ :disabled="isLoading"
type="button"
class="btn d-none d-sm-none d-md-block"
@click="onClick"
- :disabled="isLoading"
>
<span v-if="isLastDeployment">
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index 7055f208451..eba58bedd6d 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -40,7 +40,7 @@
methods: {
onClick() {
// eslint-disable-next-line no-alert
- if (confirm('Are you sure you want to stop this environment?')) {
+ if (window.confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
$(this.$el).tooltip('dispose');
@@ -54,13 +54,13 @@
<template>
<button
v-tooltip
+ :disabled="isLoading"
+ :title="title"
+ :aria-label="title"
type="button"
class="btn stop-env-link d-none d-sm-none d-md-block"
data-container="body"
@click="onClick"
- :disabled="isLoading"
- :title="title"
- :aria-label="title"
>
<i
class="fa fa-stop stop-env-icon"
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 0dbbbb75e07..f8e3165f8cd 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -30,15 +30,15 @@
<template>
<a
v-tooltip
- class="btn terminal-button d-none d-sm-none d-md-block"
- data-container="body"
:title="title"
:aria-label="title"
:href="terminalPath"
+ class="btn terminal-button d-none d-sm-none d-md-block"
+ data-container="body"
>
<icon
- name="terminal"
:size="12"
+ name="terminal"
/>
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 3da762446c9..b18f02343d6 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -93,8 +93,8 @@
<div class="top-area">
<tabs
:tabs="tabs"
- @onChangeTab="onChangeTab"
scope="environments"
+ @onChangeTab="onChangeTab"
/>
<div
@@ -119,8 +119,8 @@
@onChangePage="onChangePage"
>
<empty-state
- slot="emptyState"
v-if="!isLoading && state.environments.length === 0"
+ slot="emptyState"
:new-path="newEnvironmentPath"
:help-path="helpPagePath"
:can-create-environment="canCreateEnvironment"
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index 5ef5e347387..5f72a39c5cb 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -39,8 +39,8 @@
<template>
<div :class="cssContainerClass">
<div
- class="top-area"
v-if="!isLoading"
+ class="top-area"
>
<h4 class="js-folder-name environments-folder-name">
@@ -49,8 +49,8 @@
<tabs
:tabs="tabs"
- @onChangeTab="onChangeTab"
scope="environments"
+ @onChangeTab="onChangeTab"
/>
</div>
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/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
index 26618af9515..a8eb8d94be3 100644
--- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
+++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
@@ -72,9 +72,9 @@ export default {
@click="onItemActivated(item.text)">
<span>
<span
- class="filtered-search-history-dropdown-token"
v-for="(token, index) in item.tokens"
:key="`dropdown-token-${index}`"
+ class="filtered-search-history-dropdown-token"
>
<span class="name">{{ token.prefix }}</span>
<span class="value">{{ token.suffix }}</span>
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 746a06b7c4f..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';
@@ -602,14 +602,18 @@ GitLabDropdown = (function() {
var selector;
selector = '.dropdown-content';
if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one .dropdown-content";
+ if (this.options.containerSelector) {
+ selector = this.options.containerSelector;
+ } else {
+ selector = '.dropdown-page-one .dropdown-content';
+ }
}
return $(selector, this.dropdown).empty();
};
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;
@@ -647,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}']`);
@@ -701,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>";
@@ -717,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/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
index 5648cb9a888..d33e3a37580 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -1,7 +1,12 @@
import $ from 'jquery';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
-import { __ } from './locale';
+
+const tooltipTitles = {
+ group: __('Unsubscribe at group level'),
+ project: __('Unsubscribe at project level'),
+};
export default class GroupLabelSubscription {
constructor(container) {
@@ -35,6 +40,7 @@ export default class GroupLabelSubscription {
this.$unsubscribeButtons.attr('data-url', url);
axios.post(url)
+ .then(() => GroupLabelSubscription.setNewTooltip($btn))
.then(() => this.toggleSubscriptionButtons())
.catch(() => flash(__('There was an error when subscribing to this label.')));
}
@@ -44,4 +50,14 @@ export default class GroupLabelSubscription {
this.$subscribeButtons.toggleClass('hidden');
this.$unsubscribeButtons.toggleClass('hidden');
}
+
+ static setNewTooltip($button) {
+ if (!$button.hasClass('js-subscribe-button')) return;
+
+ const type = $button.hasClass('js-group-level') ? 'group' : 'project';
+ const newTitle = tooltipTitles[type];
+
+ $('.js-unsubscribe-button', $button.closest('.label-actions-list'))
+ .tooltip('hide').attr('title', newTitle).tooltip('_fixTitle');
+ }
}
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 22eb7bd44c5..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,
})
@@ -216,10 +215,10 @@ export default {
<template>
<div>
<loading-icon
- class="loading-animation prepend-top-20"
- size="2"
v-if="isLoading"
:label="s__('GroupsTree|Loading groups')"
+ class="loading-animation prepend-top-20"
+ size="2"
/>
<groups-component
v-if="!isLoading"
@@ -230,10 +229,10 @@ export default {
/>
<deprecated-modal
v-show="showModal"
- kind="warning"
:primary-button-label="__('Leave')"
:title="__('Are you sure?')"
:text="groupLeaveConfirmationMessage"
+ kind="warning"
@cancel="hideLeaveGroupModal"
@submit="leaveGroup"
/>
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 7f64a9bd741..efbf2e3a295 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -71,14 +71,14 @@ export default {
<template>
<li
- @click.stop="onClickRowGroup"
:id="groupDomId"
:class="rowClass"
class="group-row"
+ @click.stop="onClickRowGroup"
>
<div
- class="group-row-contents"
- :class="{ 'project-row-contents': !isGroup }">
+ :class="{ 'project-row-contents': !isGroup }"
+ class="group-row-contents">
<item-actions
v-if="isGroup"
:group="group"
@@ -99,8 +99,8 @@ export default {
/>
</div>
<div
- class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block"
:class="{ 'content-loading': group.isChildrenLoading }"
+ class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block"
>
<a
:href="group.relativePath"
@@ -108,14 +108,14 @@ export default {
>
<img
v-if="hasAvatar"
- class="avatar s24"
:src="group.avatarUrl"
+ class="avatar s24"
/>
<identicon
v-else
- size-class="s24"
:entity-id="group.id"
:entity-name="group.name"
+ size-class="s24"
/>
</a>
</div>
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index 87065b3d6e3..24eec4901ec 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -54,13 +54,13 @@ export default {
<a
v-tooltip
v-if="group.canLeave"
- @click.prevent="onLeaveGroup"
:href="group.leavePath"
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
data-container="body"
data-placement="bottom"
- class="leave-group btn no-expand">
+ class="leave-group btn no-expand"
+ @click.prevent="onLeaveGroup">
<icon name="leave"/>
</a>
</div>
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index 168b4e4af2c..87ab5480c15 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -45,44 +45,44 @@
<div class="stats">
<item-stats-value
v-if="isGroup"
- css-class="number-subgroups"
- icon-name="folder"
:title="__('Subgroups')"
:value="item.subgroupCount"
+ css-class="number-subgroups"
+ icon-name="folder"
/>
<item-stats-value
v-if="isGroup"
- css-class="number-projects"
- icon-name="bookmark"
:title="__('Projects')"
:value="item.projectCount"
+ css-class="number-projects"
+ icon-name="bookmark"
/>
<item-stats-value
v-if="isGroup"
- css-class="number-users"
- icon-name="users"
:title="__('Members')"
:value="item.memberCount"
+ css-class="number-users"
+ icon-name="users"
/>
<item-stats-value
v-if="isProject"
+ :value="item.starCount"
css-class="project-stars"
icon-name="star"
- :value="item.starCount"
/>
<item-stats-value
- css-class="item-visibility"
- tooltip-placement="left"
:icon-name="visibilityIcon"
:title="visibilityTooltip"
+ css-class="item-visibility"
+ tooltip-placement="left"
/>
<div
- class="last-updated"
v-if="isProject"
+ class="last-updated"
>
<time-ago-tooltip
- tooltip-placement="bottom"
:time="item.updatedAt"
+ tooltip-placement="bottom"
/>
</div>
</div>
diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue
index 4d86ac8023c..ef9f2bca76c 100644
--- a/app/assets/javascripts/groups/components/item_stats_value.vue
+++ b/app/assets/javascripts/groups/components/item_stats_value.vue
@@ -52,10 +52,10 @@
<template>
<span
v-tooltip
- data-container="body"
:data-placement="tooltipPlacement"
:class="cssClass"
:title="title"
+ data-container="body"
>
<icon :name="iconName" />
<span
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/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 05dbc1410de..62697e0ecc3 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -1,4 +1,5 @@
<script>
+import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
@@ -20,6 +21,13 @@ export default {
},
methods: {
...mapActions(['updateActivityBarView']),
+ changedActivityView(e, view) {
+ e.currentTarget.blur();
+
+ this.updateActivityBarView(view);
+
+ $(e.currentTarget).tooltip('hide');
+ },
},
activityBarViews,
};
@@ -31,12 +39,12 @@ export default {
<li v-once>
<a
v-tooltip
- data-container="body"
- data-placement="right"
:href="goBackUrl"
- class="ide-sidebar-link"
:title="s__('IDE|Go back')"
:aria-label="s__('IDE|Go back')"
+ data-container="body"
+ data-placement="right"
+ class="ide-sidebar-link"
>
<icon
:size="16"
@@ -47,16 +55,16 @@ export default {
<li>
<button
v-tooltip
- data-container="body"
- data-placement="right"
- type="button"
- class="ide-sidebar-link js-ide-edit-mode"
:class="{
active: currentActivityView === $options.activityBarViews.edit
}"
- @click.prevent="updateActivityBarView($options.activityBarViews.edit)"
:title="s__('IDE|Edit')"
:aria-label="s__('IDE|Edit')"
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-edit-mode"
+ @click.prevent="changedActivityView($event, $options.activityBarViews.edit)"
>
<icon
name="code"
@@ -66,16 +74,16 @@ export default {
<li>
<button
v-tooltip
- data-container="body"
- data-placement="right"
- type="button"
- class="ide-sidebar-link js-ide-review-mode"
:class="{
active: currentActivityView === $options.activityBarViews.review
}"
- @click.prevent="updateActivityBarView($options.activityBarViews.review)"
:title="s__('IDE|Review')"
:aria-label="s__('IDE|Review')"
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-review-mode"
+ @click.prevent="changedActivityView($event, $options.activityBarViews.review)"
>
<icon
name="file-modified"
@@ -85,16 +93,16 @@ export default {
<li v-show="hasChanges">
<button
v-tooltip
- data-container="body"
- data-placement="right"
- type="button"
- class="ide-sidebar-link js-ide-commit-mode"
:class="{
active: currentActivityView === $options.activityBarViews.commit
}"
- @click.prevent="updateActivityBarView($options.activityBarViews.commit)"
:title="s__('IDE|Commit')"
:aria-label="s__('IDE|Commit')"
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-commit-mode"
+ @click.prevent="changedActivityView($event, $options.activityBarViews.commit)"
>
<icon
name="commit"
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 e2b42ab2642..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() {
@@ -91,7 +94,6 @@ export default {
<template>
<div
- class="multi-file-commit-form"
:class="{
'is-compact': isCompact,
'is-full': !isCompact
@@ -99,6 +101,7 @@ export default {
:style="{
height: componentHeight ? `${componentHeight}px` : null,
}"
+ class="multi-file-commit-form"
>
<transition
name="commit-form-slide-up"
@@ -108,16 +111,16 @@ export default {
>
<div
v-if="isCompact"
- class="commit-form-compact"
ref="compactEl"
+ class="commit-form-compact"
>
<button
- type="button"
:disabled="!hasChanges"
+ type="button"
class="btn btn-primary btn-sm btn-block"
@click="toggleIsSmall"
>
- {{ __('Commit') }}
+ {{ __('Commit…') }}
</button>
<p
class="text-center"
@@ -126,8 +129,8 @@ export default {
</div>
<form
v-if="!isCompact"
- @submit.prevent.stop="commitChanges"
ref="formEl"
+ @submit.prevent.stop="commitChanges"
>
<transition name="fade">
<success-message
@@ -136,15 +139,15 @@ 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="commitButtonText"
container-class="btn btn-success btn-sm float-left"
- :label="__('Commit')"
@click="commitChanges"
/>
<button
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 1325fc993b2..d0fb0e3d99e 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -34,6 +34,10 @@ export default {
type: String,
required: true,
},
+ actionBtnIcon: {
+ type: String,
+ required: true,
+ },
itemActionComponent: {
type: String,
required: true,
@@ -43,11 +47,15 @@ export default {
required: false,
default: false,
},
- },
- data() {
- return {
- showActionButton: false,
- };
+ activeFileKey: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ keyPrefix: {
+ type: String,
+ required: true,
+ },
},
computed: {
titleText() {
@@ -55,15 +63,15 @@ export default {
title: this.title,
});
},
+ filesLength() {
+ return this.fileList.length;
+ },
},
methods: {
...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() {
this[this.action]();
},
- setShowActionButton(show) {
- this.showActionButton = show;
- },
},
};
</script>
@@ -74,8 +82,6 @@ export default {
>
<header
class="multi-file-commit-panel-header"
- @mouseenter="setShowActionButton(true)"
- @mouseleave="setShowActionButton(false)"
>
<div
class="multi-file-commit-panel-header-title"
@@ -86,24 +92,40 @@ export default {
:size="18"
/>
{{ titleText }}
- <span
- v-show="!showActionButton"
- class="ide-commit-file-count"
- >
- {{ fileList.length }}
- </span>
- <button
- v-show="showActionButton"
- type="button"
- class="btn btn-blank btn-link ide-staged-action-btn"
- @click="actionBtnClicked"
- >
- {{ actionBtnText }}
- </button>
+ <div class="d-flex ml-auto">
+ <button
+ v-tooltip
+ v-show="filesLength"
+ :class="{
+ 'd-flex': filesLength
+ }"
+ :title="actionBtnText"
+ type="button"
+ class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center"
+ data-placement="bottom"
+ data-container="body"
+ data-boundary="viewport"
+ @click="actionBtnClicked"
+ >
+ <icon
+ :name="actionBtnIcon"
+ :size="12"
+ class="ml-auto mr-auto"
+ />
+ </button>
+ <span
+ :class="{
+ 'rounded-right': !filesLength
+ }"
+ class="ide-commit-file-count order-0 rounded-left text-center"
+ >
+ {{ filesLength }}
+ </span>
+ </div>
</div>
</header>
<ul
- v-if="fileList.length"
+ v-if="filesLength"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
@@ -113,8 +135,9 @@ export default {
<list-item
:file="file"
:action-component="itemActionComponent"
- :key-prefix="title"
+ :key-prefix="keyPrefix"
:staged-list="stagedList"
+ :active-file-key="activeFileKey"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 2254271c679..d376a004e84 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -38,14 +38,17 @@ export default {
return this.modifiedFilesLength ? 'multi-file-modified' : '';
},
additionsTooltip() {
- return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
+ return sprintf(n__('1 %{type} addition', '%{count} %{type} additions', this.addedFilesLength), {
type: this.title.toLowerCase(),
+ count: this.addedFilesLength,
});
},
modifiedTooltip() {
return sprintf(
- n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
- { type: this.title.toLowerCase() },
+ n__('1 %{type} modification', '%{count} %{type} modifications', this.modifiedFilesLength), {
+ type: this.title.toLowerCase(),
+ count: this.modifiedFilesLength,
+ },
);
},
titleTooltip() {
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 03f3e4de83c..ee21eeda3cd 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions } from 'vuex';
+import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
@@ -11,6 +12,9 @@ export default {
StageButton,
UnstageButton,
},
+ directives: {
+ tooltip,
+ },
props: {
file: {
type: Object,
@@ -30,6 +34,11 @@ export default {
required: false,
default: false,
},
+ activeFileKey: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
iconName() {
@@ -39,6 +48,15 @@ export default {
iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
},
+ fullKey() {
+ return `${this.keyPrefix}-${this.file.key}`;
+ },
+ isActive() {
+ return this.activeFileKey === this.fullKey;
+ },
+ tooltipTitle() {
+ return this.file.path === this.file.name ? '' : this.file.path;
+ },
},
methods: {
...mapActions([
@@ -51,7 +69,7 @@ export default {
openFileInEditor() {
return this.openPendingTab({
file: this.file,
- keyPrefix: this.keyPrefix.toLowerCase(),
+ keyPrefix: this.keyPrefix,
}).then(changeViewer => {
if (changeViewer) {
this.updateViewer(viewerTypes.diff);
@@ -70,24 +88,30 @@ export default {
</script>
<template>
- <div class="multi-file-commit-list-item">
- <button
- type="button"
- class="multi-file-commit-list-path"
+ <div class="multi-file-commit-list-item position-relative">
+ <div
+ v-tooltip
+ :title="tooltipTitle"
+ :class="{
+ 'is-active': isActive
+ }"
+ class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0"
+ role="button"
@dblclick="fileAction"
@click="openFileInEditor"
>
- <span class="multi-file-commit-list-file-path">
+ <span class="multi-file-commit-list-file-path d-flex align-items-center">
<icon
:name="iconName"
:size="16"
:css-classes="iconClass"
- />{{ file.path }}
+ />{{ file.name }}
</span>
- </button>
+ </div>
<component
:is="actionComponent"
:path="file.path"
+ class="d-flex position-absolute"
/>
</div>
</template>
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 f14fcdc88ed..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 {
@@ -54,7 +58,7 @@ export default {
placement: 'top',
content: sprintf(
__(`
- The character highligher helps you keep the subject line to %{titleLength} characters
+ The character highlighter helps you keep the subject line to %{titleLength} characters
and wrap the body at %{bodyLength} so they are readable in git.
`),
{ titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
@@ -66,10 +70,10 @@ export default {
<template>
<fieldset class="common-note-form ide-commit-message-field">
<div
- class="md-area"
:class="{
'is-focused': isFocused
}"
+ class="md-area"
>
<div
v-once
@@ -92,10 +96,10 @@ export default {
<div class="ide-commit-message-textarea-container">
<div class="ide-commit-message-highlights-container">
<div
- class="note-textarea highlights monospace"
:style="{
transform: `translate3d(0, ${-scrollTop}px, 0)`
}"
+ class="note-textarea highlights monospace"
>
<div
v-for="(line, index) in allLines"
@@ -113,15 +117,15 @@ export default {
</div>
</div>
<textarea
+ ref="textarea"
+ :placeholder="placeholder"
+ :value="text"
class="note-textarea ide-commit-message-textarea"
name="commit-message"
- :placeholder="__('Write a commit message...')"
- :value="text"
@scroll="handleScroll"
@input="onInput"
@focus="updateIsFocused(true)"
@blur="updateIsFocused(false)"
- ref="textarea"
>
</textarea>
</div>
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 00f2312ae51..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: {
@@ -58,12 +60,12 @@ export default {
}"
>
<input
- type="radio"
- name="commit-action"
:value="value"
- @change="updateCommitAction($event.target.value)"
:checked="commitAction === value"
:disabled="disabled"
+ type="radio"
+ name="commit-action"
+ @change="updateCommitAction($event.target.value)"
/>
<span class="prepend-left-10">
<span
@@ -80,9 +82,9 @@ export default {
class="ide-commit-new-branch"
>
<input
+ :placeholder="newBranchName"
type="text"
class="form-control monospace"
- :placeholder="newBranchName"
@input="updateBranchName($event.target.value)"
/>
</div>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
index 52dce8412ab..7014b9f605e 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -25,35 +25,51 @@ export default {
<template>
<div
v-once
- class="multi-file-discard-btn"
+ class="multi-file-discard-btn dropdown"
>
<button
v-tooltip
- type="button"
- class="btn btn-blank append-right-5"
:aria-label="__('Stage changes')"
:title="__('Stage changes')"
+ type="button"
+ class="btn btn-blank append-right-5 d-flex align-items-center"
data-container="body"
+ data-boundary="viewport"
+ data-placement="bottom"
@click.stop="stageChange(path)"
>
<icon
- name="mobile-issue-close"
:size="12"
+ name="mobile-issue-close"
/>
</button>
<button
v-tooltip
+ :title="__('More actions')"
type="button"
- class="btn btn-blank"
- :aria-label="__('Discard changes')"
- :title="__('Discard changes')"
+ class="btn btn-blank d-flex align-items-center"
data-container="body"
- @click.stop="discardFileChanges(path)"
+ data-boundary="viewport"
+ data-placement="bottom"
+ data-toggle="dropdown"
+ data-display="static"
>
<icon
- name="remove"
:size="12"
+ name="more"
/>
</button>
+ <div class="dropdown-menu dropdown-menu-right">
+ <ul>
+ <li>
+ <button
+ type="button"
+ @click.stop="discardFileChanges(path)"
+ >
+ {{ __('Discard changes') }}
+ </button>
+ </li>
+ </ul>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
index 123d60da47e..9cec73ec00e 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
@@ -29,16 +29,18 @@ export default {
>
<button
v-tooltip
- type="button"
- class="btn btn-blank"
:aria-label="__('Unstage changes')"
:title="__('Unstage changes')"
+ type="button"
+ class="btn btn-blank d-flex align-items-center"
data-container="body"
+ data-boundary="viewport"
+ data-placement="bottom"
@click="unstageChange(path)"
>
<icon
- name="history"
:size="12"
+ name="history"
/>
</button>
</div>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index b9af4d27145..95598c9aca6 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -44,11 +44,11 @@ export default {
<ul>
<li>
<a
- href="#"
- @click.prevent="changeMode($options.viewerTypes.mr)"
:class="{
'is-active': viewer === $options.viewerTypes.mr,
}"
+ href="#"
+ @click.prevent="changeMode($options.viewerTypes.mr)"
>
<strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
@@ -60,11 +60,11 @@ export default {
</li>
<li>
<a
- href="#"
- @click.prevent="changeMode($options.viewerTypes.diff)"
:class="{
'is-active': viewer === $options.viewerTypes.diff,
}"
+ href="#"
+ @click.prevent="changeMode($options.viewerTypes.diff)"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
<span class="dropdown-menu-inner-content">
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/external_link.vue b/app/assets/javascripts/ide/components/external_link.vue
index cf3316a8179..e24fe5bbccb 100644
--- a/app/assets/javascripts/ide/components/external_link.vue
+++ b/app/assets/javascripts/ide/components/external_link.vue
@@ -26,15 +26,15 @@ export default {
>
<a
:href="file.permalink"
- target="_blank"
:title="s__('IDE|Open in file view')"
+ target="_blank"
rel="noopener noreferrer"
>
<span class="vertical-align-middle">Open in file view</span>
<icon
+ :size="16"
name="external-link"
css-classes="vertical-align-middle space-right"
- :size="16"
/>
</a>
</div>
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue
index cabb3f59b17..0ba33053717 100644
--- a/app/assets/javascripts/ide/components/file_finder/index.vue
+++ b/app/assets/javascripts/ide/components/file_finder/index.vue
@@ -173,38 +173,38 @@ export default {
>
<div class="dropdown-input">
<input
+ ref="searchInput"
+ :placeholder="__('Search files')"
+ v-model="searchText"
type="search"
class="dropdown-input-field"
- :placeholder="__('Search files')"
autocomplete="off"
- v-model="searchText"
- ref="searchInput"
@keydown="onKeydown($event)"
@keyup="onKeyup($event)"
/>
<i
- aria-hidden="true"
- class="fa fa-search dropdown-input-search"
:class="{
hidden: showClearInputButton
}"
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
></i>
<i
- role="button"
:aria-label="__('Clear search input')"
- class="fa fa-times dropdown-input-clear"
:class="{
show: showClearInputButton
}"
+ role="button"
+ class="fa fa-times dropdown-input-clear"
@click="clearSearchInput"
></i>
</div>
<div>
<virtual-list
+ ref="virtualScrollList"
:size="listHeight"
:remain="listShowCount"
wtag="ul"
- ref="virtualScrollList"
>
<template v-if="filteredBlobsLength">
<li
@@ -212,11 +212,11 @@ export default {
:key="file.key"
>
<item
- class="disable-hover"
:file="file"
:search-text="searchText"
:focused="index === focusedIndex"
:index="index"
+ class="disable-hover"
@click="openFile"
@mouseover="onMouseOver"
@mousemove="onMouseMove"
diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue
index d4427420207..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
@@ -59,11 +59,11 @@ export default {
<template>
<button
- type="button"
- class="diff-changed-file"
:class="{
'is-focused': focused,
}"
+ type="button"
+ class="diff-changed-file"
@click.prevent="clickRow"
@mouseover="mouseOverRow"
@mousemove="mouseMove"
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index f5f832521c5..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"
>
@@ -93,8 +100,8 @@ export default {
:merge-request-id="currentMergeRequestId"
/>
<repo-editor
- class="multi-file-edit-pane-content"
:file="activeFile"
+ class="multi-file-edit-pane-content"
/>
</template>
<template
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
index 0c9ec3b00f0..f9978762c45 100644
--- a/app/assets/javascripts/ide/components/ide_review.vue
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -11,17 +11,20 @@ export default {
},
computed: {
...mapGetters(['currentMergeRequest']),
- ...mapState(['viewer']),
+ ...mapState(['viewer', 'currentMergeRequestId']),
showLatestChangesText() {
- return !this.currentMergeRequest || this.viewer === viewerTypes.diff;
+ return !this.currentMergeRequestId || this.viewer === viewerTypes.diff;
},
showMergeRequestText() {
- return this.currentMergeRequest && this.viewer === viewerTypes.mr;
+ return this.currentMergeRequestId && this.viewer === viewerTypes.mr;
+ },
+ mergeRequestId() {
+ return `!${this.currentMergeRequest.iid}`;
},
},
mounted() {
this.$nextTick(() => {
- this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff);
+ this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff);
});
},
methods: {
@@ -33,8 +36,8 @@ export default {
<template>
<ide-tree-list
:viewer-type="viewer"
- header-class="ide-review-header"
:disable-action-dropdown="true"
+ header-class="ide-review-header"
>
<template
slot="header"
@@ -54,7 +57,11 @@ export default {
</template>
<template v-else-if="showMergeRequestText">
{{ __('Merge request') }}
- (<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>)
+ (<a
+ v-if="currentMergeRequest"
+ :href="currentMergeRequest.web_url"
+ v-text="mergeRequestId"
+ ></a>)
</template>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 3f980203911..21906674c4b 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,4 +1,5 @@
<script>
+import $ from 'jquery';
import { mapState, mapGetters } from 'vuex';
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import Icon from '~/vue_shared/components/icon.vue';
@@ -13,6 +14,7 @@ import CommitSection from './repo_commit_section.vue';
import CommitForm from './commit_sidebar/form.vue';
import IdeReview from './ide_review.vue';
import SuccessMessage from './commit_sidebar/success_message.vue';
+import MergeRequestDropdown from './merge_requests/dropdown.vue';
import { activityBarViews } from '../constants';
export default {
@@ -32,10 +34,12 @@ export default {
CommitForm,
IdeReview,
SuccessMessage,
+ MergeRequestDropdown,
},
data() {
return {
showTooltip: false,
+ showMergeRequestsDropdown: false,
};
},
computed: {
@@ -46,6 +50,7 @@ export default {
'changedFiles',
'stagedFiles',
'lastCommitMsg',
+ 'currentMergeRequestId',
]),
...mapGetters(['currentProject', 'someUncommitedChanges']),
showSuccessMessage() {
@@ -61,9 +66,39 @@ export default {
watch: {
currentBranchId() {
this.$nextTick(() => {
+ if (!this.$refs.branchId) return;
+
this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
});
},
+ loading() {
+ this.$nextTick(() => {
+ this.addDropdownListeners();
+ });
+ },
+ },
+ mounted() {
+ this.addDropdownListeners();
+ },
+ beforeDestroy() {
+ $(this.$refs.mergeRequestDropdown)
+ .off('show.bs.dropdown')
+ .off('hide.bs.dropdown');
+ },
+ methods: {
+ addDropdownListeners() {
+ if (!this.$refs.mergeRequestDropdown) return;
+
+ $(this.$refs.mergeRequestDropdown)
+ .on('show.bs.dropdown', () => {
+ this.toggleMergeRequestDropdown();
+ }).on('hide.bs.dropdown', () => {
+ this.toggleMergeRequestDropdown();
+ });
+ },
+ toggleMergeRequestDropdown() {
+ this.showMergeRequestsDropdown = !this.showMergeRequestsDropdown;
+ },
},
};
</script>
@@ -80,53 +115,79 @@ export default {
<div class="multi-file-commit-panel-inner">
<template v-if="loading">
<div
- class="multi-file-loading-container"
v-for="n in 3"
:key="n"
+ class="multi-file-loading-container"
>
<skeleton-loading-container />
</div>
</template>
<template v-else>
- <div class="context-header ide-context-header">
- <a
- :href="currentProject.web_url"
+ <div
+ ref="mergeRequestDropdown"
+ class="context-header ide-context-header dropdown"
+ >
+ <button
+ type="button"
+ data-toggle="dropdown"
>
<div
v-if="currentProject.avatar_url"
class="avatar-container s40 project-avatar"
>
<project-avatar-image
- class="avatar-container project-avatar"
:link-href="currentProject.path"
:img-src="currentProject.avatar_url"
:img-alt="currentProject.name"
:img-size="40"
+ class="avatar-container project-avatar"
/>
</div>
<identicon
v-else
- size-class="s40"
:entity-id="currentProject.id"
:entity-name="currentProject.name"
+ size-class="s40"
/>
<div class="ide-sidebar-project-title">
<div class="sidebar-context-title">
{{ currentProject.name }}
</div>
- <div
- class="sidebar-context-title ide-sidebar-branch-title"
- ref="branchId"
- v-tooltip
- :title="branchTooltipTitle"
- >
- <icon
- name="branch"
- css-classes="append-right-5"
- />{{ currentBranchId }}
+ <div class="d-flex">
+ <div
+ v-tooltip
+ v-if="currentBranchId"
+ ref="branchId"
+ :title="branchTooltipTitle"
+ class="sidebar-context-title ide-sidebar-branch-title"
+ >
+ <icon
+ name="branch"
+ css-classes="append-right-5"
+ />{{ currentBranchId }}
+ </div>
+ <div
+ v-if="currentMergeRequestId"
+ :class="{
+ 'prepend-left-8': currentBranchId
+ }"
+ class="sidebar-context-title ide-sidebar-branch-title"
+ >
+ <icon
+ name="git-merge"
+ css-classes="append-right-5"
+ />!{{ currentMergeRequestId }}
+ </div>
</div>
</div>
- </a>
+ <icon
+ class="ml-auto"
+ name="chevron-down"
+ />
+ </button>
+ <merge-request-dropdown
+ :show="showMergeRequestsDropdown"
+ />
</div>
<div class="multi-file-commit-panel-inner-scroll">
<component
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 368a2995ed9..0582ad32e92 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -35,9 +35,7 @@ export default {
},
watch: {
lastCommit() {
- if (!this.isPollingInitialized) {
- this.initPipelinePolling();
- }
+ this.initPipelinePolling();
},
},
mounted() {
@@ -47,9 +45,8 @@ export default {
if (this.intervalId) {
clearInterval(this.intervalId);
}
- if (this.isPollingInitialized) {
- this.stopPipelinePolling();
- }
+
+ this.stopPipelinePolling();
},
methods: {
...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
@@ -59,8 +56,9 @@ export default {
}, 1000);
},
initPipelinePolling() {
- this.fetchLatestPipeline();
- this.isPollingInitialized = true;
+ if (this.lastCommit) {
+ this.fetchLatestPipeline();
+ }
},
commitAgeUpdate() {
if (this.lastCommit) {
@@ -77,22 +75,22 @@ export default {
<template>
<footer class="ide-status-bar">
<div
- class="ide-status-branch"
v-if="lastCommit && lastCommitFormatedAge"
+ class="ide-status-branch"
>
<span
- class="ide-status-pipeline"
v-if="latestPipeline && latestPipeline.details"
+ class="ide-status-pipeline"
>
<ci-icon
- :status="latestPipeline.details.status"
v-tooltip
+ :status="latestPipeline.details.status"
:title="latestPipeline.details.status.text"
/>
Pipeline
<a
- class="monospace"
- :href="latestPipeline.details.status.details_path">#{{ latestPipeline.id }}</a>
+ :href="latestPipeline.details.status.details_path"
+ class="monospace">#{{ latestPipeline.id }}</a>
{{ latestPipeline.details.status.text }}
for
</span>
@@ -102,18 +100,18 @@ export default {
/>
<a
v-tooltip
- class="commit-sha"
:title="lastCommit.message"
:href="getCommitPath(lastCommit.short_id)"
+ class="commit-sha"
>{{ lastCommit.short_id }}</a>
by
{{ lastCommit.author_name }}
<time
v-tooltip
- data-placement="top"
- data-container="body"
:datetime="lastCommit.committed_date"
:title="tooltipTitle(lastCommit.committed_date)"
+ data-placement="top"
+ data-container="body"
>
{{ lastCommitFormatedAge }}
</time>
@@ -131,8 +129,8 @@ export default {
{{ file.eol }}
</div>
<div
- class="ide-status-file"
- v-if="file && !file.binary">
+ v-if="file && !file.binary"
+ class="ide-status-file">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index e64a09fcc90..0df99798d21 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -50,17 +50,17 @@ export default {
>
<template v-if="showLoading">
<div
- class="multi-file-loading-container"
v-for="n in 3"
:key="n"
+ class="multi-file-loading-container"
>
<skeleton-loading-container />
</div>
</template>
<template v-else>
<header
- class="ide-tree-header"
:class="headerClass"
+ class="ide-tree-header"
>
<slot name="header"></slot>
</header>
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
new file mode 100644
index 00000000000..f39ce545656
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -0,0 +1,137 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import _ from 'underscore';
+import { __ } from '../../../locale';
+import tooltip from '../../../vue_shared/directives/tooltip';
+import Icon from '../../../vue_shared/components/icon.vue';
+import ScrollButton from './detail/scroll_button.vue';
+import JobDescription from './detail/description.vue';
+
+const scrollPositions = {
+ top: 0,
+ bottom: 1,
+};
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ ScrollButton,
+ JobDescription,
+ },
+ data() {
+ return {
+ scrollPos: scrollPositions.top,
+ };
+ },
+ computed: {
+ ...mapState('pipelines', ['detailJob']),
+ isScrolledToBottom() {
+ return this.scrollPos === scrollPositions.bottom;
+ },
+ isScrolledToTop() {
+ return this.scrollPos === scrollPositions.top;
+ },
+ jobOutput() {
+ return this.detailJob.output || __('No messages were logged');
+ },
+ },
+ mounted() {
+ this.getTrace();
+ },
+ methods: {
+ ...mapActions('pipelines', ['fetchJobTrace', 'setDetailJob']),
+ scrollDown() {
+ if (this.$refs.buildTrace) {
+ this.$refs.buildTrace.scrollTo(0, this.$refs.buildTrace.scrollHeight);
+ }
+ },
+ scrollUp() {
+ if (this.$refs.buildTrace) {
+ this.$refs.buildTrace.scrollTo(0, 0);
+ }
+ },
+ scrollBuildLog: _.throttle(function buildLogScrollDebounce() {
+ const { scrollTop } = this.$refs.buildTrace;
+ const { offsetHeight, scrollHeight } = this.$refs.buildTrace;
+
+ if (scrollTop + offsetHeight === scrollHeight) {
+ this.scrollPos = scrollPositions.bottom;
+ } else if (scrollTop === 0) {
+ this.scrollPos = scrollPositions.top;
+ } else {
+ this.scrollPos = '';
+ }
+ }),
+ getTrace() {
+ return this.fetchJobTrace().then(() => this.scrollDown());
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="ide-pipeline build-page d-flex flex-column flex-fill">
+ <header class="ide-job-header d-flex align-items-center">
+ <button
+ class="btn btn-default btn-sm d-flex"
+ @click="setDetailJob(null)"
+ >
+ <icon
+ name="chevron-left"
+ />
+ {{ __('View jobs') }}
+ </button>
+ </header>
+ <div class="top-bar d-flex border-left-0">
+ <job-description
+ :job="detailJob"
+ />
+ <div class="controllers ml-auto">
+ <a
+ v-tooltip
+ :title="__('Show complete raw log')"
+ :href="detailJob.rawPath"
+ data-placement="top"
+ data-container="body"
+ class="controllers-buttons"
+ target="_blank"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-file-text-o"
+ ></i>
+ </a>
+ <scroll-button
+ :disabled="isScrolledToTop"
+ direction="up"
+ @click="scrollUp"
+ />
+ <scroll-button
+ :disabled="isScrolledToBottom"
+ direction="down"
+ @click="scrollDown"
+ />
+ </div>
+ </div>
+ <pre
+ ref="buildTrace"
+ class="build-trace mb-0 h-100"
+ @scroll="scrollBuildLog"
+ >
+ <code
+ v-show="!detailJob.isLoading"
+ class="bash"
+ v-html="jobOutput"
+ >
+ </code>
+ <div
+ v-show="detailJob.isLoading"
+ class="build-loader-animation"
+ >
+ </div>
+ </pre>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue
new file mode 100644
index 00000000000..7e24974f7e5
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue
@@ -0,0 +1,47 @@
+<script>
+import Icon from '../../../../vue_shared/components/icon.vue';
+import CiIcon from '../../../../vue_shared/components/ci_icon.vue';
+
+export default {
+ components: {
+ Icon,
+ CiIcon,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ jobId() {
+ return `#${this.job.id}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="d-flex align-items-center">
+ <ci-icon
+ :status="job.status"
+ :borderless="true"
+ :size="24"
+ class="d-flex"
+ />
+ <span class="prepend-left-8">
+ {{ job.name }}
+ <a
+ :href="job.path"
+ target="_blank"
+ class="ide-external-link"
+ >
+ {{ jobId }}
+ <icon
+ :size="12"
+ name="external-link"
+ />
+ </a>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
new file mode 100644
index 00000000000..103a407987f
--- /dev/null
+++ b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
@@ -0,0 +1,66 @@
+<script>
+import { __ } from '../../../../locale';
+import Icon from '../../../../vue_shared/components/icon.vue';
+import tooltip from '../../../../vue_shared/directives/tooltip';
+
+const directions = {
+ up: 'up',
+ down: 'down',
+};
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ },
+ props: {
+ direction: {
+ type: String,
+ required: true,
+ validator(value) {
+ return Object.keys(directions).includes(value);
+ },
+ },
+ disabled: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ tooltipTitle() {
+ return this.direction === directions.up ? __('Scroll to top') : __('Scroll to bottom');
+ },
+ iconName() {
+ return `scroll_${this.direction}`;
+ },
+ },
+ methods: {
+ clickedScroll() {
+ this.$emit('click');
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-tooltip
+ :title="tooltipTitle"
+ class="controllers-buttons"
+ data-container="body"
+ data-placement="top"
+ >
+ <button
+ :disabled="disabled"
+ class="btn-scroll btn-transparent btn-blank"
+ type="button"
+ @click="clickedScroll"
+ >
+ <icon
+ :name="iconName"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/jobs/item.vue b/app/assets/javascripts/ide/components/jobs/item.vue
index c33936021d4..7f4695a0451 100644
--- a/app/assets/javascripts/ide/components/jobs/item.vue
+++ b/app/assets/javascripts/ide/components/jobs/item.vue
@@ -1,11 +1,9 @@
<script>
-import Icon from '../../../vue_shared/components/icon.vue';
-import CiIcon from '../../../vue_shared/components/ci_icon.vue';
+import JobDescription from './detail/description.vue';
export default {
components: {
- Icon,
- CiIcon,
+ JobDescription,
},
props: {
job: {
@@ -18,29 +16,29 @@ export default {
return `#${this.job.id}`;
},
},
+ methods: {
+ clickViewLog() {
+ this.$emit('clickViewLog', this.job);
+ },
+ },
};
</script>
<template>
<div class="ide-job-item">
- <ci-icon
- :status="job.status"
- :borderless="true"
- :size="24"
+ <job-description
+ :job="job"
+ class="append-right-default"
/>
- <span class="prepend-left-8">
- {{ job.name }}
- <a
- :href="job.path"
- target="_blank"
- class="ide-external-link"
+ <div class="ml-auto align-self-center">
+ <button
+ v-if="job.started"
+ type="button"
+ class="btn btn-default btn-sm"
+ @click="clickViewLog"
>
- {{ jobId }}
- <icon
- name="external-link"
- :size="12"
- />
- </a>
- </span>
+ {{ __('View log') }}
+ </button>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue
index bdd0364c9b9..3b16b860ecd 100644
--- a/app/assets/javascripts/ide/components/jobs/list.vue
+++ b/app/assets/javascripts/ide/components/jobs/list.vue
@@ -19,7 +19,7 @@ export default {
},
},
methods: {
- ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed']),
+ ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed', 'setDetailJob']),
},
};
</script>
@@ -38,6 +38,7 @@ export default {
:stage="stage"
@fetch="fetchJobs"
@toggleCollapsed="toggleStageCollapsed"
+ @clickViewLog="setDetailJob"
/>
</template>
</div>
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 5b24bb1f5a7..15e881b7bc8 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -48,6 +48,9 @@ export default {
toggleCollapsed() {
this.$emit('toggleCollapsed', this.stage.id);
},
+ clickViewLog(job) {
+ this.$emit('clickViewLog', job);
+ },
},
};
</script>
@@ -57,10 +60,10 @@ export default {
class="ide-stage card prepend-top-default"
>
<div
- class="card-header"
:class="{
'border-bottom-0': stage.isCollapsed
}"
+ class="card-header"
@click="toggleCollapsed"
>
<ci-icon
@@ -69,10 +72,10 @@ export default {
/>
<strong
v-tooltip="showTooltip"
+ ref="stageTitle"
:title="showTooltip ? stage.name : null"
data-container="body"
class="prepend-left-8 ide-stage-title"
- ref="stageTitle"
>
{{ stage.name }}
</strong>
@@ -90,8 +93,8 @@ export default {
/>
</div>
<div
- class="card-body"
v-show="!stage.isCollapsed"
+ class="card-body"
>
<loading-icon
v-if="showLoadingIcon"
@@ -101,6 +104,7 @@ export default {
v-for="job in stage.jobs"
:key="job.id"
:job="job"
+ @clickViewLog="clickViewLog"
/>
</template>
</div>
diff --git a/app/assets/javascripts/ide/components/merge_requests/dropdown.vue b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue
new file mode 100644
index 00000000000..4b9824bf04b
--- /dev/null
+++ b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue
@@ -0,0 +1,63 @@
+<script>
+import { mapGetters } from 'vuex';
+import Tabs from '../../../vue_shared/components/tabs/tabs';
+import Tab from '../../../vue_shared/components/tabs/tab.vue';
+import List from './list.vue';
+
+export default {
+ components: {
+ Tabs,
+ Tab,
+ List,
+ },
+ props: {
+ show: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters('mergeRequests', ['assignedData', 'createdData']),
+ createdMergeRequestLength() {
+ return this.createdData.mergeRequests.length;
+ },
+ assignedMergeRequestLength() {
+ return this.assignedData.mergeRequests.length;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown-menu ide-merge-requests-dropdown p-0">
+ <tabs
+ v-if="show"
+ stop-propagation
+ >
+ <tab active>
+ <template slot="title">
+ {{ __('Created by me') }}
+ <span class="badge badge-pill">
+ {{ createdMergeRequestLength }}
+ </span>
+ </template>
+ <list
+ :empty-text="__('You have not created any merge requests')"
+ type="created"
+ />
+ </tab>
+ <tab>
+ <template slot="title">
+ {{ __('Assigned to me') }}
+ <span class="badge badge-pill">
+ {{ assignedMergeRequestLength }}
+ </span>
+ </template>
+ <list
+ :empty-text="__('You do not have any assigned merge requests')"
+ type="assigned"
+ />
+ </tab>
+ </tabs>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue
new file mode 100644
index 00000000000..4e18376bd48
--- /dev/null
+++ b/app/assets/javascripts/ide/components/merge_requests/item.vue
@@ -0,0 +1,63 @@
+<script>
+import Icon from '../../../vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ currentId: {
+ type: String,
+ required: true,
+ },
+ currentProjectId: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isActive() {
+ return (
+ this.item.iid === parseInt(this.currentId, 10) &&
+ this.currentProjectId === this.item.projectPathWithNamespace
+ );
+ },
+ pathWithID() {
+ return `${this.item.projectPathWithNamespace}!${this.item.iid}`;
+ },
+ },
+ methods: {
+ clickItem() {
+ this.$emit('click', this.item);
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ type="button"
+ class="btn-link d-flex align-items-center"
+ @click="clickItem"
+ >
+ <span class="d-flex append-right-default ide-merge-request-current-icon">
+ <icon
+ v-if="isActive"
+ :size="18"
+ name="mobile-issue-close"
+ />
+ </span>
+ <span>
+ <strong>
+ {{ item.title }}
+ </strong>
+ <span class="ide-merge-request-project-path d-block mt-1">
+ {{ pathWithID }}
+ </span>
+ </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
new file mode 100644
index 00000000000..19d3e48ee10
--- /dev/null
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -0,0 +1,132 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import _ from 'underscore';
+import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import Item from './item.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ Item,
+ },
+ props: {
+ type: {
+ type: String,
+ required: true,
+ },
+ emptyText: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ search: '',
+ };
+ },
+ computed: {
+ ...mapGetters('mergeRequests', ['getData']),
+ ...mapState(['currentMergeRequestId', 'currentProjectId']),
+ data() {
+ return this.getData(this.type);
+ },
+ isLoading() {
+ return this.data.isLoading;
+ },
+ mergeRequests() {
+ return this.data.mergeRequests;
+ },
+ hasMergeRequests() {
+ return this.mergeRequests.length !== 0;
+ },
+ hasNoSearchResults() {
+ return this.search !== '' && !this.hasMergeRequests;
+ },
+ },
+ watch: {
+ isLoading: {
+ handler: 'focusSearch',
+ },
+ },
+ mounted() {
+ this.loadMergeRequests();
+ },
+ methods: {
+ ...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']),
+ loadMergeRequests() {
+ this.fetchMergeRequests({ type: this.type, search: this.search });
+ },
+ viewMergeRequest(item) {
+ this.openMergeRequest({
+ projectPath: item.projectPathWithNamespace,
+ id: item.iid,
+ });
+ },
+ searchMergeRequests: _.debounce(function debounceSearch() {
+ this.loadMergeRequests();
+ }, 250),
+ focusSearch() {
+ if (!this.isLoading) {
+ this.$nextTick(() => {
+ this.$refs.searchInput.focus();
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="dropdown-input mt-3 pb-3 mb-0 border-bottom">
+ <input
+ ref="searchInput"
+ :placeholder="__('Search merge requests')"
+ v-model="search"
+ type="search"
+ class="dropdown-input-field"
+ @input="searchMergeRequests"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ ></i>
+ </div>
+ <div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
+ <loading-icon
+ v-if="isLoading"
+ class="mt-3 mb-3 align-self-center ml-auto mr-auto"
+ size="2"
+ />
+ <ul
+ v-else
+ class="mb-3 w-100"
+ >
+ <template v-if="hasMergeRequests">
+ <li
+ v-for="item in mergeRequests"
+ :key="item.id"
+ >
+ <item
+ :item="item"
+ :current-id="currentMergeRequestId"
+ :current-project-id="currentProjectId"
+ @click="viewMergeRequest"
+ />
+ </li>
+ </template>
+ <li
+ v-else
+ class="ide-merge-requests-empty d-flex align-items-center justify-content-center"
+ >
+ <template v-if="hasNoSearchResults">
+ {{ __('No merge requests found') }}
+ </template>
+ <template v-else>
+ {{ emptyText }}
+ </template>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
index 179a589d1ac..821be319cce 100644
--- a/app/assets/javascripts/ide/components/mr_file_icon.vue
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -14,10 +14,10 @@ export default {
<template>
<icon
- name="git-merge"
v-tooltip
:title="__('Part of merge request changes')"
- css-classes="append-right-8"
:size="12"
+ name="git-merge"
+ css-classes="append-right-8"
/>
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index f0b29702497..1e398d7e1aa 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -55,10 +55,10 @@ export default {
<template>
<div class="ide-new-btn">
<div
- class="dropdown"
:class="{
show: dropdownOpen,
}"
+ class="dropdown"
>
<button
type="button"
@@ -67,19 +67,19 @@ export default {
@click.stop="openDropdown()"
>
<icon
- name="plus"
:size="12"
+ name="plus"
css-classes="float-left"
/>
<icon
- name="arrow-down"
:size="12"
+ name="arrow-down"
css-classes="float-left"
/>
</button>
<ul
- class="dropdown-menu dropdown-menu-right"
ref="dropdownMenu"
+ class="dropdown-menu dropdown-menu-right"
>
<li>
<a
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index dd2800179ff..1e9668d5154 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -71,18 +71,18 @@ export default {
>
<form
slot="body"
- @submit.prevent="createEntryInStore"
class="form-group row"
+ @submit.prevent="createEntryInStore"
>
<label class="label-light col-form-label col-sm-3">
{{ __('Name') }}
</label>
<div class="col-sm-9">
<input
+ ref="fieldName"
+ v-model="entryName"
type="text"
class="form-control"
- v-model="entryName"
- ref="fieldName"
/>
</div>
</form>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index c165af5ce52..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];
}
@@ -67,9 +68,9 @@
</a>
<input
id="file-upload"
+ ref="fileUpload"
type="file"
class="hidden"
- ref="fileUpload"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index 703c4a70cfa..5cd2c9ce188 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -4,6 +4,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import { rightSidebarViews } from '../../constants';
import PipelinesList from '../pipelines/list.vue';
+import JobsDetail from '../jobs/detail.vue';
+import ResizablePanel from '../resizable_panel.vue';
export default {
directives: {
@@ -12,9 +14,17 @@ export default {
components: {
Icon,
PipelinesList,
+ JobsDetail,
+ ResizablePanel,
},
computed: {
...mapState(['rightPane']),
+ pipelinesActive() {
+ return (
+ this.rightPane === rightSidebarViews.pipelines ||
+ this.rightPane === rightSidebarViews.jobsDetail
+ );
+ },
},
methods: {
...mapActions(['setRightPane']),
@@ -32,30 +42,34 @@ export default {
<div
class="multi-file-commit-panel ide-right-sidebar"
>
- <div
- class="multi-file-commit-panel-inner"
+ <resizable-panel
v-if="rightPane"
+ :collapsible="false"
+ :initial-width="350"
+ :min-size="350"
+ class="multi-file-commit-panel-inner"
+ side="right"
>
<component :is="rightPane" />
- </div>
+ </resizable-panel>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li>
<button
v-tooltip
- data-container="body"
- data-placement="left"
:title="__('Pipelines')"
- class="ide-sidebar-link is-right"
:class="{
- active: rightPane === $options.rightSidebarViews.pipelines
+ active: pipelinesActive
}"
+ data-container="body"
+ data-placement="left"
+ class="ide-sidebar-link is-right"
type="button"
@click="clickTab($event, $options.rightSidebarViews.pipelines)"
>
<icon
:size="16"
- name="pipeline"
+ name="rocket"
/>
</button>
</li>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 06455fac439..5757dfdc925 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -75,8 +75,8 @@ export default {
>
#{{ latestPipeline.id }}
<icon
- name="external-link"
:size="12"
+ name="external-link"
/>
</a>
</span>
@@ -94,7 +94,7 @@ export default {
<p class="append-bottom-0">
{{ __('Found errors in your .gitlab-ci.yml:') }}
</p>
- <p class="append-bottom-0">
+ <p class="append-bottom-0 break-word">
{{ latestPipeline.yamlError }}
</p>
<p
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index c5092d8e04d..50ab242ba2a 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -6,7 +6,7 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
import * as consts from '../stores/modules/commit/constants';
-import { activityBarViews } from '../constants';
+import { activityBarViews, stageKeys } from '../constants';
export default {
components: {
@@ -27,11 +27,14 @@ export default {
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
- ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']),
+ ...mapGetters('commit', ['discardDraftButtonDisabled']),
showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
+ activeFileKey() {
+ return this.activeFile ? this.activeFile.key : null;
+ },
},
watch: {
hasChanges() {
@@ -44,6 +47,7 @@ export default {
if (this.lastOpenedFile) {
this.openPendingTab({
file: this.lastOpenedFile,
+ keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged,
})
.then(changeViewer => {
if (changeViewer) {
@@ -62,6 +66,7 @@ export default {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
},
+ stageKeys,
};
</script>
@@ -72,8 +77,8 @@ export default {
<deprecated-modal
id="ide-create-branch-modal"
:primary-button-label="__('Create new branch')"
- kind="success"
:title="__('Branch has changed')"
+ kind="success"
@submit="forceCreateNewBranch"
>
<template slot="body">
@@ -85,22 +90,28 @@ export default {
v-if="showStageUnstageArea"
>
<commit-files-list
- class="is-first"
- icon-name="unstaged"
:title="__('Unstaged')"
+ :key-prefix="$options.stageKeys.unstaged"
:file-list="changedFiles"
+ :action-btn-text="__('Stage all changes')"
+ :active-file-key="activeFileKey"
action="stageAllChanges"
- :action-btn-text="__('Stage all')"
+ action-btn-icon="mobile-issue-close"
item-action-component="stage-button"
+ class="is-first"
+ icon-name="unstaged"
/>
<commit-files-list
- icon-name="staged"
:title="__('Staged')"
+ :key-prefix="$options.stageKeys.staged"
:file-list="stagedFiles"
+ :action-btn-text="__('Unstage all changes')"
+ :staged-list="true"
+ :active-file-key="activeFileKey"
action="unstageAllChanges"
- :action-btn-text="__('Unstage all')"
+ action-btn-icon="history"
item-action-component="unstage-button"
- :staged-list="true"
+ icon-name="staged"
/>
</template>
<empty-state
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 93453989c08..08ee12fd98f 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,16 +1,16 @@
<script>
-/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants';
-import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
import ExternalLink from './external_link.vue';
export default {
components: {
ContentViewer,
+ DiffViewer,
ExternalLink,
},
props: {
@@ -20,7 +20,13 @@ export default {
},
},
computed: {
- ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
+ ...mapState([
+ 'rightPanelCollapsed',
+ 'viewer',
+ 'panelResizing',
+ 'currentActivityView',
+ 'rightPane',
+ ]),
...mapGetters([
'currentMergeRequest',
'getStagedFile',
@@ -31,9 +37,18 @@ export default {
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
+ showContentViewer() {
+ return (
+ (this.shouldHideEditor || this.file.viewMode === 'preview') &&
+ (this.viewer !== viewerTypes.mr || !this.file.mrChange)
+ );
+ },
+ showDiffViewer() {
+ return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr;
+ },
editTabCSS() {
return {
- active: this.file.viewMode === 'edit',
+ active: this.file.viewMode === 'editor',
};
},
previewTabCSS() {
@@ -50,12 +65,12 @@ export default {
// Compare key to allow for files opened in review mode to be cached differently
if (oldVal.key !== this.file.key) {
- this.initMonaco();
+ this.initEditor();
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
- viewMode: 'edit',
+ viewMode: 'editor',
});
}
}
@@ -64,7 +79,7 @@ export default {
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
- viewMode: 'edit',
+ viewMode: 'editor',
});
}
},
@@ -79,20 +94,18 @@ export default {
this.editor.updateDimensions();
}
},
+ rightPane() {
+ this.editor.updateDimensions();
+ },
},
beforeDestroy() {
this.editor.dispose();
},
mounted() {
- if (this.editor && monaco) {
- this.initMonaco();
- } else {
- monacoLoader(['vs/editor/editor.main'], () => {
- this.editor = Editor.create(monaco);
-
- this.initMonaco();
- });
+ if (!this.editor) {
+ this.editor = Editor.create();
}
+ this.initEditor();
},
methods: {
...mapActions([
@@ -105,7 +118,7 @@ export default {
'updateViewer',
'removePendingTab',
]),
- initMonaco() {
+ initEditor() {
if (this.shouldHideEditor) return;
this.editor.clearEditor();
@@ -118,7 +131,7 @@ export default {
this.createEditorInstance();
})
.catch(err => {
- flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
+ flash('Error setting up editor. Please try again.', 'alert', document, null, false, true);
throw err;
});
},
@@ -197,14 +210,14 @@ export default {
>
<div class="ide-mode-tabs clearfix" >
<ul
- class="nav-links float-left"
v-if="!shouldHideEditor && isEditModeActive"
+ class="nav-links float-left"
>
<li :class="editTabCSS">
<a
href="javascript:void(0);"
role="button"
- @click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
+ @click.prevent="setFileViewMode({ file, viewMode: 'editor' })">
<template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
</template>
@@ -229,19 +242,27 @@ export default {
/>
</div>
<div
- v-show="!shouldHideEditor && file.viewMode === 'edit'"
+ v-show="!shouldHideEditor && file.viewMode ==='editor'"
ref="editor"
- class="multi-file-editor-holder"
:class="{
'is-readonly': isCommitModeActive,
}"
+ class="multi-file-editor-holder"
>
</div>
<content-viewer
- v-if="shouldHideEditor || file.viewMode === 'preview'"
+ v-if="showContentViewer"
:content="file.content || file.raw"
:path="file.rawPath || file.path"
:file-size="file.size"
:project-path="file.projectId"/>
+ <diff-viewer
+ v-if="showDiffViewer"
+ :diff-mode="file.mrChange.diffMode"
+ :new-path="file.mrChange.new_path"
+ :new-sha="currentMergeRequest.sha"
+ :old-path="file.mrChange.old_path"
+ :old-sha="currentMergeRequest.baseCommitSha"
+ :project-path="file.projectId"/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index f56aeced806..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>
@@ -120,17 +149,17 @@ export default {
<template>
<div>
<div
- class="file"
:class="fileClass"
- @click="clickFile"
+ class="file"
role="button"
+ @click="clickFile"
>
<div
class="file-name"
>
<span
- class="ide-file-name str-truncated"
:style="levelIndentation"
+ class="ide-file-name str-truncated"
>
<file-icon
:file-name="file.name"
@@ -156,10 +185,10 @@ export default {
<icon
v-tooltip
:title="folderChangesTooltip"
+ :size="12"
data-container="body"
data-placement="right"
name="file-modified"
- :size="12"
css-classes="prepend-left-5 multi-file-modified"
/>
</span>
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index 97589e116c5..76a3333be50 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -26,8 +26,8 @@ export default {
<template>
<span
- v-if="file.file_lock"
v-tooltip
+ v-if="file.file_lock"
:title="lockTooltip"
data-container="body"
>
diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue
index 3e47da88050..7a5ede82253 100644
--- a/app/assets/javascripts/ide/components/repo_loading_file.vue
+++ b/app/assets/javascripts/ide/components/repo_loading_file.vue
@@ -33,8 +33,8 @@
<td class="d-none d-sm-block">
<skeleton-loading-container
- class="animation-container-right"
:small="true"
+ class="animation-container-right"
/>
</td>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index fb26b973236..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) {
@@ -76,8 +78,8 @@ export default {
@mouseout="mouseOutTab"
>
<div
- class="multi-file-tab"
:title="tab.url"
+ class="multi-file-tab"
>
<file-icon
:file-name="tab.name"
@@ -89,16 +91,16 @@ export default {
/>
</div>
<button
+ :aria-label="closeLabel"
+ :disabled="tab.pending"
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
- :aria-label="closeLabel"
- :disabled="tab.pending"
>
<icon
v-if="!showChangedIcon"
- name="close"
:size="12"
+ name="close"
/>
<changed-file-icon
v-else
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 99e51097e12..c12a63e26be 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -52,8 +52,8 @@ export default {
<template>
<div class="multi-file-tabs">
<ul
- class="list-unstyled append-bottom-0"
ref="tabsScroller"
+ class="list-unstyled append-bottom-0"
>
<repo-tab
v-for="tab in files"
diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue
index 5ea2a2f6825..7277fcb7617 100644
--- a/app/assets/javascripts/ide/components/resizable_panel.vue
+++ b/app/assets/javascripts/ide/components/resizable_panel.vue
@@ -63,11 +63,11 @@ export default {
<template>
<div
- class="multi-file-commit-panel"
:class="{
'is-collapsed': collapsed && collapsible,
}"
:style="panelStyle"
+ class="multi-file-commit-panel"
@click="toggleFullbarCollapsed"
>
<slot></slot>
@@ -77,9 +77,9 @@ export default {
:start-size="initialWidth"
:min-size="minSize"
:max-size="$options.maxSize"
+ :side="side === 'right' ? 'left' : 'right'"
@resize-start="setResizingStatus(true)"
@resize-end="setResizingStatus(false)"
- :side="side === 'right' ? 'left' : 'right'"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 33cd20caf52..12e0c3aeef0 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -21,6 +21,19 @@ export const viewerTypes = {
diff: 'diff',
};
+export const diffModes = {
+ replaced: 'replaced',
+ new: 'new',
+ deleted: 'deleted',
+ renamed: 'renamed',
+};
+
export const rightSidebarViews = {
pipelines: 'pipelines-list',
+ jobsDetail: 'jobs-detail',
+};
+
+export const stageKeys = {
+ unstaged: 'unstaged',
+ staged: 'staged',
};
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/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index e5149b1f3ad..78e6f632728 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -1,32 +1,32 @@
+import { editor as monacoEditor, Uri } from 'monaco-editor';
import Disposable from './disposable';
import eventHub from '../../eventhub';
export default class Model {
- constructor(monaco, file, head = null) {
- this.monaco = monaco;
+ constructor(file, head = null) {
this.disposable = new Disposable();
this.file = file;
this.head = head;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
- (this.originalModel = this.monaco.editor.createModel(
+ (this.originalModel = monacoEditor.createModel(
head ? head.content : this.file.raw,
undefined,
- new this.monaco.Uri(null, null, `original/${this.path}`),
+ new Uri(false, false, `original/${this.path}`),
)),
- (this.model = this.monaco.editor.createModel(
+ (this.model = monacoEditor.createModel(
this.content,
undefined,
- new this.monaco.Uri(null, null, this.path),
+ new Uri(false, false, this.path),
)),
);
if (this.file.mrChange) {
this.disposable.add(
- (this.baseModel = this.monaco.editor.createModel(
+ (this.baseModel = monacoEditor.createModel(
this.file.baseRaw,
undefined,
- new this.monaco.Uri(null, null, `target/${this.path}`),
+ new Uri(false, false, `target/${this.path}`),
)),
);
}
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index 7f643969480..bd9b8fc3fcc 100644
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
@@ -3,8 +3,7 @@ import Disposable from './disposable';
import Model from './model';
export default class ModelManager {
- constructor(monaco) {
- this.monaco = monaco;
+ constructor() {
this.disposable = new Disposable();
this.models = new Map();
}
@@ -22,7 +21,7 @@ export default class ModelManager {
return this.getModel(file.key);
}
- const model = new Model(this.monaco, file, head);
+ const model = new Model(file, head);
this.models.set(model.path, model);
this.disposable.add(model);
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index f579424cf33..046e562ba2b 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -1,4 +1,4 @@
-/* global monaco */
+import { Range } from 'monaco-editor';
import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
@@ -16,7 +16,7 @@ export const getDiffChangeType = change => {
};
export const getDecorator = change => ({
- range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1),
+ range: new Range(change.lineNumber, 1, change.endLineNumber, 1),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js
index e74c4046330..78b2eab6399 100644
--- a/app/assets/javascripts/ide/lib/diff/diff_worker.js
+++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js
@@ -1,8 +1,10 @@
import { computeDiff } from './diff';
+// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', (e) => {
- const data = e.data;
+ const { data } = e;
+ // eslint-disable-next-line no-restricted-globals
self.postMessage({
path: data.path,
changes: computeDiff(data.originalContent, data.newContent),
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 9c3bb9cc17d..02038fcb534 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -1,4 +1,5 @@
import _ from 'underscore';
+import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor';
import store from '../stores';
import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
@@ -8,6 +9,11 @@ import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
import keymap from './keymap.json';
+function setupMonacoTheme() {
+ monacoEditor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
+ monacoEditor.setTheme('gitlab');
+}
+
export const clearDomElement = el => {
if (!el || !el.firstChild) return;
@@ -17,24 +23,22 @@ export const clearDomElement = el => {
};
export default class Editor {
- static create(monaco) {
- if (this.editorInstance) return this.editorInstance;
-
- this.editorInstance = new Editor(monaco);
-
+ static create() {
+ if (!this.editorInstance) {
+ this.editorInstance = new Editor();
+ }
return this.editorInstance;
}
- constructor(monaco) {
- this.monaco = monaco;
+ constructor() {
this.currentModel = null;
this.instance = null;
this.dirtyDiffController = null;
this.disposable = new Disposable();
- this.modelManager = new ModelManager(this.monaco);
+ this.modelManager = new ModelManager();
this.decorationsController = new DecorationsController(this);
- this.setupMonacoTheme();
+ setupMonacoTheme();
this.debouncedUpdate = _.debounce(() => {
this.updateDimensions();
@@ -46,7 +50,7 @@ export default class Editor {
clearDomElement(domElement);
this.disposable.add(
- (this.instance = this.monaco.editor.create(domElement, {
+ (this.instance = monacoEditor.create(domElement, {
...defaultEditorOptions,
})),
(this.dirtyDiffController = new DirtyDiffController(
@@ -66,7 +70,7 @@ export default class Editor {
clearDomElement(domElement);
this.disposable.add(
- (this.instance = this.monaco.editor.createDiffEditor(domElement, {
+ (this.instance = monacoEditor.createDiffEditor(domElement, {
...defaultEditorOptions,
quickSuggestions: false,
occurrencesHighlight: false,
@@ -122,17 +126,11 @@ export default class Editor {
modified: model.getModel(),
});
- this.monaco.editor.createDiffNavigator(this.instance, {
+ monacoEditor.createDiffNavigator(this.instance, {
alwaysRevealFirst: true,
});
}
- setupMonacoTheme() {
- this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
-
- this.monaco.editor.setTheme('gitlab');
- }
-
clearEditor() {
if (this.instance) {
this.instance.setModel(null);
@@ -200,7 +198,7 @@ export default class Editor {
const getKeyCode = key => {
const monacoKeyMod = key.indexOf('KEY_') === 0;
- return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
+ return monacoKeyMod ? KeyCode[key] : KeyMod[key];
};
keymap.forEach(command => {
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index 9f895d49f2e..e35595ab1fd 100644
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
@@ -12,5 +12,6 @@ export const defaultEditorOptions = {
export default [
{
readOnly: model => !!model.file.file_lock,
+ quickSuggestions: model => !(model.language === 'markdown'),
},
];
diff --git a/app/assets/javascripts/ide/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js
deleted file mode 100644
index 142a220097b..00000000000
--- a/app/assets/javascripts/ide/monaco_loader.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import monacoContext from 'monaco-editor/dev/vs/loader';
-
-monacoContext.require.config({
- paths: {
- vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
- },
-});
-
-// ignore CDN config and use local assets path for service worker which cannot be cross-domain
-const relativeRootPath = (gon && gon.relative_url_root) || '';
-const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`;
-window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` };
-
-// eslint-disable-next-line no-underscore-dangle
-window.__monaco_context__ = monacoContext;
-export default monacoContext.require;
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index e8b51f2b516..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' } });
+ 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 5ec9bd661bb..4aa151abcb7 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -1,29 +1,34 @@
-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,
mergeRequest: data,
});
- if (!state.currentMergeRequestId) {
- commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
- }
+ commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
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 {
@@ -32,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,
@@ -49,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 {
@@ -58,7 +70,7 @@ export const getMergeRequestChanges = (
});
export const getMergeRequestVersions = (
- { commit, state },
+ { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
@@ -75,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 46af47d2f81..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) => {
@@ -13,8 +16,7 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force
.then(data => {
commit(types.TOGGLE_LOADING, { entry: state });
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
- if (!state.currentProjectId)
- commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
+ commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data);
})
.catch(() => {
@@ -33,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' ||
@@ -52,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 {
@@ -81,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 0a0db4033c8..69b6fe2985b 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -49,31 +49,6 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true });
};
-export const checkCommitStatus = ({ rootState }) =>
- service
- .getBranchData(rootState.currentProjectId, rootState.currentBranchId)
- .then(({ data }) => {
- const { id } = data.commit;
- const selectedBranch =
- rootState.projects[rootState.currentProjectId].branches[rootState.currentBranchId];
-
- if (selectedBranch.workingReference !== id) {
- return true;
- }
-
- return false;
- })
- .catch(() =>
- flash(
- __('Error checking branch data. Please try again.'),
- 'alert',
- document,
- null,
- false,
- true,
- ),
- );
-
export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => {
const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = {
@@ -128,24 +103,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(getters.branchName, newBranch, state, rootState);
- const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
+ const stageFilesPromise = rootState.stagedFiles.length
+ ? Promise.resolve()
+ : dispatch('stageAllChanges', null, { root: true });
commit(types.UPDATE_LOADING, true);
- return getCommitStatus
- .then(
- branchChanged =>
- new Promise(resolve => {
- if (branchChanged) {
- // show the modal with a Bootstrap call
- $('#ide-create-branch-modal').modal('show');
- } else {
- resolve();
- }
- }),
- )
- .then(() => 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);
@@ -220,12 +195,16 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
);
})
.catch(err => {
- let errMsg = __('Error committing changes. Please try again.');
- if (err.response.data && err.response.data.message) {
- errMsg += ` (${stripHtml(err.response.data.message)})`;
+ 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);
+ window.dispatchEvent(new Event('resize'));
}
- flash(errMsg, 'alert', document, null, false, true);
- window.dispatchEvent(new Event('resize'));
commit(types.UPDATE_LOADING, false);
});
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 d3050183bd3..551dd322c9b 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
@@ -1,25 +1,48 @@
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';
+import * as rootTypes from '../../mutation_types';
-export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS);
-export const receiveMergeRequestsError = ({ commit }) => {
+export const requestMergeRequests = ({ commit }, type) =>
+ commit(types.REQUEST_MERGE_REQUESTS, type);
+export const receiveMergeRequestsError = ({ commit }, type) => {
flash(__('Error loading merge requests.'));
- commit(types.RECEIVE_MERGE_REQUESTS_ERROR);
+ commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type);
};
-export const receiveMergeRequestsSuccess = ({ commit }, data) =>
- commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data);
+export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) =>
+ commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { type, data });
-export const fetchMergeRequests = ({ dispatch, state: { scope, state } }, search = '') => {
- dispatch('requestMergeRequests');
- dispatch('resetMergeRequests');
+export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => {
+ const scope = scopes[type];
+ dispatch('requestMergeRequests', type);
+ dispatch('resetMergeRequests', type);
Api.mergeRequests({ scope, state, search })
- .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data))
- .catch(() => dispatch('receiveMergeRequestsError'));
+ .then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data }))
+ .catch(() => dispatch('receiveMergeRequestsError', type));
};
-export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS);
+export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type);
+
+export const openMergeRequest = ({ commit, dispatch }, { projectPath, id }) => {
+ commit(rootTypes.CLEAR_PROJECTS, null, { root: true });
+ commit(rootTypes.SET_CURRENT_MERGE_REQUEST, `${id}`, { root: true });
+ commit(rootTypes.RESET_OPEN_FILES, null, { root: true });
+ dispatch('setCurrentBranchId', '', { root: true });
+ dispatch('pipelines/stopPipelinePolling', null, { root: true })
+ .then(() => {
+ dispatch('pipelines/resetLatestPipeline', null, { root: true });
+ dispatch('pipelines/clearEtagPoll', null, { root: true });
+ })
+ .catch(e => {
+ throw e;
+ });
+ dispatch('setRightPane', null, { root: true });
+
+ router.push(`/project/${projectPath}/merge_requests/${id}`);
+};
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js
index 64b7763f257..a7085c7d04c 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js
@@ -1,6 +1,6 @@
export const scopes = {
- assignedToMe: 'assigned-to-me',
- createdByMe: 'created-by-me',
+ assigned: 'assigned-to-me',
+ created: 'created-by-me',
};
export const states = {
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js
new file mode 100644
index 00000000000..8e2b234be8d
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js
@@ -0,0 +1,4 @@
+export const getData = state => type => state[type];
+
+export const assignedData = state => state.assigned;
+export const createdData = state => state.created;
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
index 04e7e0f08f1..2e6dfb420f4 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
@@ -1,5 +1,6 @@
import state from './state';
import * as actions from './actions';
+import * as getters from './getters';
import mutations from './mutations';
export default {
@@ -7,4 +8,5 @@ export default {
state: state(),
actions,
mutations,
+ getters,
};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
index 98102a68e08..971da0806bd 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
@@ -2,15 +2,15 @@
import * as types from './mutation_types';
export default {
- [types.REQUEST_MERGE_REQUESTS](state) {
- state.isLoading = true;
+ [types.REQUEST_MERGE_REQUESTS](state, type) {
+ state[type].isLoading = true;
},
- [types.RECEIVE_MERGE_REQUESTS_ERROR](state) {
- state.isLoading = false;
+ [types.RECEIVE_MERGE_REQUESTS_ERROR](state, type) {
+ state[type].isLoading = false;
},
- [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) {
- state.isLoading = false;
- state.mergeRequests = data.map(mergeRequest => ({
+ [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, { type, data }) {
+ state[type].isLoading = false;
+ state[type].mergeRequests = data.map(mergeRequest => ({
id: mergeRequest.id,
iid: mergeRequest.iid,
title: mergeRequest.title,
@@ -20,7 +20,7 @@ export default {
.replace(`/merge_requests/${mergeRequest.iid}`, ''),
}));
},
- [types.RESET_MERGE_REQUESTS](state) {
- state.mergeRequests = [];
+ [types.RESET_MERGE_REQUESTS](state, type) {
+ state[type].mergeRequests = [];
},
};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
index 2947b686c1c..57eb6b04283 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
@@ -1,8 +1,13 @@
-import { scopes, states } from './constants';
+import { states } from './constants';
export default () => ({
- isLoading: false,
- mergeRequests: [],
- scope: scopes.assignedToMe,
+ created: {
+ isLoading: false,
+ mergeRequests: [],
+ },
+ assigned: {
+ isLoading: false,
+ mergeRequests: [],
+ },
state: states.opened,
});
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 1ebe487263b..fe1dc9ac8f8 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -4,6 +4,7 @@ import { __ } from '../../../../locale';
import flash from '../../../../flash';
import Poll from '../../../../lib/utils/poll';
import service from '../../../services';
+import { rightSidebarViews } from '../../../constants';
import * as types from './mutation_types';
let eTagPoll;
@@ -11,8 +12,12 @@ let eTagPoll;
export const clearEtagPoll = () => {
eTagPoll = null;
};
-export const stopPipelinePolling = () => eTagPoll && eTagPoll.stop();
-export const restartPipelinePolling = () => eTagPoll && eTagPoll.restart();
+export const stopPipelinePolling = () => {
+ if (eTagPoll) eTagPoll.stop();
+};
+export const restartPipelinePolling = () => {
+ if (eTagPoll) eTagPoll.restart();
+};
export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
export const receiveLatestPipelineError = ({ commit, dispatch }) => {
@@ -50,9 +55,9 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
Visibility.change(() => {
if (!Visibility.hidden()) {
- eTagPoll.restart();
+ dispatch('restartPipelinePolling');
} else {
- eTagPoll.stop();
+ dispatch('stopPipelinePolling');
}
});
};
@@ -77,4 +82,33 @@ export const fetchJobs = ({ dispatch }, stage) => {
export const toggleStageCollapsed = ({ commit }, stageId) =>
commit(types.TOGGLE_STAGE_COLLAPSE, stageId);
+export const setDetailJob = ({ commit, dispatch }, job) => {
+ commit(types.SET_DETAIL_JOB, job);
+ dispatch('setRightPane', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, {
+ root: true,
+ });
+};
+
+export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE);
+export const receiveJobTraceError = ({ commit }) => {
+ flash(__('Error fetching job trace'));
+ commit(types.RECEIVE_JOB_TRACE_ERROR);
+};
+export const receiveJobTraceSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_JOB_TRACE_SUCCESS, data);
+
+export const fetchJobTrace = ({ dispatch, state }) => {
+ dispatch('requestJobTrace');
+
+ return axios
+ .get(`${state.detailJob.path}/trace`, { params: { format: 'json' } })
+ .then(({ data }) => dispatch('receiveJobTraceSuccess', data))
+ .catch(() => dispatch('receiveJobTraceError'));
+};
+
+export const resetLatestPipeline = ({ commit }) => {
+ commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null);
+ commit(types.SET_DETAIL_JOB, null);
+};
+
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
index 3ddc8409c5b..f4c36b9d96f 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
@@ -7,3 +7,9 @@ export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR';
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE';
+
+export const SET_DETAIL_JOB = 'SET_DETAIL_JOB';
+
+export const REQUEST_JOB_TRACE = 'REQUEST_JOB_TRACE';
+export const RECEIVE_JOB_TRACE_ERROR = 'RECEIVE_JOB_TRACE_ERROR';
+export const RECEIVE_JOB_TRACE_SUCCESS = 'RECEIVE_JOB_TRACE_SUCCESS';
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
index 745797e1ee5..5a2213bbe89 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -63,4 +63,17 @@ export default {
isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed,
}));
},
+ [types.SET_DETAIL_JOB](state, job) {
+ state.detailJob = { ...job };
+ },
+ [types.REQUEST_JOB_TRACE](state) {
+ state.detailJob.isLoading = true;
+ },
+ [types.RECEIVE_JOB_TRACE_ERROR](state) {
+ state.detailJob.isLoading = false;
+ },
+ [types.RECEIVE_JOB_TRACE_SUCCESS](state, data) {
+ state.detailJob.isLoading = false;
+ state.detailJob.output = data.html;
+ },
};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
index 0f83b315fff..8651e267b53 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/state.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
@@ -3,4 +3,5 @@ export default () => ({
isLoadingJobs: false,
latestPipeline: null,
stages: [],
+ detailJob: null,
});
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
index 9f4b0d7d726..a6caca2d2dc 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
@@ -4,4 +4,8 @@ export const normalizeJob = job => ({
name: job.name,
status: job.status,
path: job.build_path,
+ rawPath: `${job.build_path}/raw`,
+ started: job.started,
+ output: '',
+ isLoading: false,
});
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index fbfb92105d6..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';
@@ -68,3 +69,8 @@ export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
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 eeaa7cb0ec3..702be2140e2 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -157,6 +157,15 @@ export default {
[types.SET_LINKS](state, links) {
Object.assign(state, { links });
},
+ [types.CLEAR_PROJECTS](state) {
+ Object.assign(state, { projects: {}, trees: {} });
+ },
+ [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/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 13f123b6630..46547820425 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
import * as types from '../mutation_types';
+import { diffModes } from '../../constants';
export default {
[types.SET_FILE_ACTIVE](state, { path, active }) {
@@ -46,6 +47,7 @@ export default {
baseRaw: null,
html: data.html,
size: data.size,
+ lastCommitSha: data.last_commit_sha,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
@@ -85,8 +87,19 @@ export default {
});
},
[types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
+ let diffMode = diffModes.replaced;
+ if (mrChange.new_file) {
+ diffMode = diffModes.new;
+ } else if (mrChange.deleted_file) {
+ diffMode = diffModes.deleted;
+ } else if (mrChange.renamed_file) {
+ diffMode = diffModes.renamed;
+ }
Object.assign(state.entries[file.path], {
- mrChange,
+ mrChange: {
+ ...mrChange,
+ diffMode,
+ },
});
},
[types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
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 e0b9766fbee..9e6b86dd844 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -17,6 +17,7 @@ export const dataStructure = () => ({
changed: false,
staged: false,
lastCommitPath: '',
+ lastCommitSha: '',
lastCommit: {
id: '',
url: '',
@@ -39,7 +40,7 @@ export const dataStructure = () => ({
editorColumn: 1,
fileLanguage: '',
eol: '',
- viewMode: 'edit',
+ viewMode: 'editor',
previewMode: null,
size: 0,
parentPath: null,
@@ -104,14 +105,15 @@ 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,
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
+ last_commit_id: newBranch ? undefined : f.lastCommitSha,
})),
start_branch: newBranch ? rootState.currentBranchId : undefined,
});
diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
index 0a1c253c637..fa35c215880 100644
--- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
+++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
@@ -1,6 +1,7 @@
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../utils';
+// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', e => {
const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
@@ -89,6 +90,7 @@ self.addEventListener('message', e => {
return acc;
}, {});
+ // eslint-disable-next-line no-restricted-globals
self.postMessage({
entries,
treeList: sortTree(treeList),
diff --git a/app/assets/javascripts/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/importer_status.js b/app/assets/javascripts/importer_status.js
index b469e1e2adc..f9ff0722c01 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -58,7 +58,7 @@ class ImporterStatus {
job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`);
$('table.import-jobs tbody').prepend(job);
- job.addClass('active');
+ job.addClass('table-active');
const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing');
job.find('.import-actions').html(sprintf(
_.escape(__('%{loadingIcon} Started')), {
@@ -67,7 +67,15 @@ class ImporterStatus {
false,
));
})
- .catch(() => flash(__('An error occurred while importing project')));
+ .catch((error) => {
+ let details = error;
+
+ if (error.response && error.response.data && error.response.data.errors) {
+ details = error.response.data.errors;
+ }
+
+ flash(__(`An error occurred while importing project: ${details}`));
+ });
}
autoUpdate() {
@@ -81,7 +89,7 @@ class ImporterStatus {
switch (job.import_status) {
case 'finished':
- jobItem.removeClass('active').addClass('success');
+ jobItem.removeClass('table-active').addClass('table-success');
statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`);
break;
case 'scheduled':
diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js
index 09cca1dc7d9..5c5a6e01848 100644
--- a/app/assets/javascripts/init_changes_dropdown.js
+++ b/app/assets/javascripts/init_changes_dropdown.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import stickyMonitor from './lib/utils/sticky';
+import { stickyMonitor } from './lib/utils/sticky';
export default (stickyTop) => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
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/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index e87a8ed7fea..b6364318537 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -226,7 +226,7 @@
.then(res => res.data)
.then(data => this.checkForSpam(data))
.then((data) => {
- if (location.pathname !== data.web_url) {
+ if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index ae577e04a56..1174177f561 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -110,25 +110,25 @@
<template>
<div
v-if="descriptionHtml"
- class="description"
:class="{
'js-task-list-container': canUpdate
}"
+ class="description"
>
<div
- class="wiki"
+ ref="gfm-content"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
- v-html="descriptionHtml"
- ref="gfm-content">
+ class="wiki"
+ v-html="descriptionHtml">
</div>
<textarea
- class="hidden js-task-list-field"
v-if="descriptionText"
v-model="descriptionText"
:data-update-url="updateUrl"
+ class="hidden js-task-list-field"
>
</textarea>
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index 7ef5e679881..597c6d69a81 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -38,7 +38,7 @@
},
deleteIssuable() {
// eslint-disable-next-line no-alert
- if (confirm('Issue will be removed! Are you sure?')) {
+ if (window.confirm('Issue will be removed! Are you sure?')) {
this.deleteLoading = true;
eventHub.$emit('delete.issuable');
@@ -51,16 +51,16 @@
<template>
<div class="prepend-top-default append-bottom-default clearfix">
<button
- class="btn btn-save float-left"
:class="{ disabled: formState.updateLoading || !isSubmitEnabled }"
- type="submit"
:disabled="formState.updateLoading || !isSubmitEnabled"
+ class="btn btn-save float-left"
+ type="submit"
@click.prevent="updateIssuable">
Save changes
<i
+ v-if="formState.updateLoading"
class="fa fa-spinner fa-spin"
- aria-hidden="true"
- v-if="formState.updateLoading">
+ aria-hidden="true">
</i>
</button>
<button
@@ -71,16 +71,16 @@
</button>
<button
v-if="shouldShowDeleteButton"
- class="btn btn-danger float-right append-right-default"
:class="{ disabled: deleteLoading }"
- type="button"
:disabled="deleteLoading"
+ class="btn btn-danger float-right append-right-default"
+ type="button"
@click="deleteIssuable">
Delete
<i
+ v-if="deleteLoading"
class="fa fa-spinner fa-spin"
- aria-hidden="true"
- v-if="deleteLoading">
+ aria-hidden="true">
</i>
</button>
</div>
diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue
index 01097b5b35e..5ff5b1630b1 100644
--- a/app/assets/javascripts/issue_show/components/edited.vue
+++ b/app/assets/javascripts/issue_show/components/edited.vue
@@ -37,16 +37,16 @@
Edited
<time-ago-tooltip
v-if="updatedAt"
- tooltip-placement="bottom"
:time="updatedAt"
+ tooltip-placement="bottom"
/>
<span
v-if="hasUpdatedBy"
>
by
<a
- class="author_link"
:href="updatedByPath"
+ class="author_link"
>
<span>{{ updatedByName }}</span>
</a>
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index d9fa2764d65..5f58f671c73 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -52,13 +52,13 @@
>
<textarea
id="issue-description"
+ ref="textarea"
+ slot="textarea"
+ v-model="formState.description"
class="note-textarea js-gfm-input js-autosize markdown-area"
data-supports-quick-actions="false"
aria-label="Description"
- v-model="formState.description"
- ref="textarea"
- slot="textarea"
- placeholder="Write a comment or drag your files here..."
+ placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable">
</textarea>
diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue
index 7db0488e306..e90d9fad94e 100644
--- a/app/assets/javascripts/issue_show/components/fields/description_template.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue
@@ -48,15 +48,15 @@
class="dropdown js-issuable-selector-wrap"
data-issuable-type="issue">
<button
+ ref="toggle"
+ :data-namespace-path="projectNamespace"
+ :data-project-path="projectPath"
+ :data-data="issuableTemplatesJson"
class="dropdown-menu-toggle js-issuable-selector"
type="button"
- ref="toggle"
data-field-name="issuable_template"
data-selected="null"
- data-toggle="dropdown"
- :data-namespace-path="projectNamespace"
- :data-project-path="projectPath"
- :data-data="issuableTemplatesJson">
+ data-toggle="dropdown">
<span class="dropdown-toggle-text">
Choose a template
</span>
diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue
index c3abb9fd9d5..7d1526a64b4 100644
--- a/app/assets/javascripts/issue_show/components/fields/title.vue
+++ b/app/assets/javascripts/issue_show/components/fields/title.vue
@@ -21,11 +21,11 @@
</label>
<input
id="issuable-title"
+ v-model="formState.title"
class="form-control"
type="text"
placeholder="Title"
aria-label="Title"
- v-model="formState.title"
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable" />
</fieldset>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index ab8bd34762f..5bfc072e3da 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -72,8 +72,8 @@
<locked-warning v-if="formState.lockedWarningVisible" />
<div class="row">
<div
- class="col-sm-4 col-lg-3"
- v-if="hasIssuableTemplates">
+ v-if="hasIssuableTemplates"
+ class="col-sm-4 col-lg-3">
<description-template
:form-state="formState"
:issuable-templates="issuableTemplates"
diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue
index 1c2789f154a..ad0d40faf32 100644
--- a/app/assets/javascripts/issue_show/components/locked_warning.vue
+++ b/app/assets/javascripts/issue_show/components/locked_warning.vue
@@ -2,7 +2,7 @@
export default {
computed: {
currentPath() {
- return location.pathname;
+ return window.location.pathname;
},
},
};
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index aec890a2ff6..12101c0daa5 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -67,11 +67,11 @@
<template>
<div class="title-container">
<h2
- class="title"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
+ class="title"
v-html="titleHtml"
>
</h2>
@@ -80,11 +80,11 @@
v-if="showInlineEditButton && canUpdate"
type="button"
class="btn btn-default btn-edit btn-svg js-issuable-edit"
- v-html="pencilIcon"
title="Edit title and description"
data-placement="bottom"
data-container="body"
@click="edit"
+ v-html="pencilIcon"
>
</button>
</div>
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index 611e8200b4d..d4f2a3ef7d3 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -1,14 +1,17 @@
import $ from 'jquery';
import _ from 'underscore';
-import StickyFill from 'stickyfilljs';
+import { polyfillSticky } from './lib/utils/sticky';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
+import { isScrolledToBottom, scrollDown } from './lib/utils/scroll_utils';
+import LogOutputBehaviours from './lib/utils/logoutput_behaviours';
-export default class Job {
+export default class Job extends LogOutputBehaviours {
constructor(options) {
+ super();
this.timeout = null;
this.state = null;
this.fetchingStatusFavicon = false;
@@ -29,10 +32,6 @@ export default class Job {
this.$buildTraceOutput = $('.js-build-output');
this.$topBar = $('.js-top-bar');
- // Scroll controllers
- this.$scrollTopBtn = $('.js-scroll-up');
- this.$scrollBottomBtn = $('.js-scroll-down');
-
clearTimeout(this.timeout);
this.initSidebar();
@@ -48,23 +47,14 @@ export default class Job {
.off('click', '.stage-item')
.on('click', '.stage-item', this.updateDropdown);
- // add event listeners to the scroll buttons
- this.$scrollTopBtn
- .off('click')
- .on('click', this.scrollToTop.bind(this));
-
- this.$scrollBottomBtn
- .off('click')
- .on('click', this.scrollToBottom.bind(this));
-
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window
.off('scroll')
.on('scroll', () => {
- if (!this.isScrolledToBottom()) {
+ if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false);
- } else if (this.isScrolledToBottom() && !this.isLogComplete) {
+ } else if (isScrolledToBottom() && !this.isLogComplete) {
this.toggleScrollAnimation(true);
}
this.scrollThrottled();
@@ -80,70 +70,11 @@ export default class Job {
}
initAffixTopArea() {
- /**
- If the browser does not support position sticky, it returns the position as static.
- If the browser does support sticky, then we allow the browser to handle it, if not
- then we use a polyfill
- */
- if (this.$topBar.css('position') !== 'static') return;
-
- StickyFill.add(this.$topBar);
- }
-
- // eslint-disable-next-line class-methods-use-this
- canScroll() {
- return $(document).height() > $(window).height();
- }
-
- toggleScroll() {
- const $document = $(document);
- const currentPosition = $document.scrollTop();
- const scrollHeight = $document.height();
-
- const windowHeight = $(window).height();
- if (this.canScroll()) {
- if (currentPosition > 0 &&
- (scrollHeight - currentPosition !== windowHeight)) {
- // User is in the middle of the log
-
- this.toggleDisableButton(this.$scrollTopBtn, false);
- this.toggleDisableButton(this.$scrollBottomBtn, false);
- } else if (currentPosition === 0) {
- // User is at Top of Log
-
- this.toggleDisableButton(this.$scrollTopBtn, true);
- this.toggleDisableButton(this.$scrollBottomBtn, false);
- } else if (this.isScrolledToBottom()) {
- // User is at the bottom of the build log.
-
- this.toggleDisableButton(this.$scrollTopBtn, false);
- this.toggleDisableButton(this.$scrollBottomBtn, true);
- }
- } else {
- this.toggleDisableButton(this.$scrollTopBtn, true);
- this.toggleDisableButton(this.$scrollBottomBtn, true);
- }
- }
- // eslint-disable-next-line class-methods-use-this
- isScrolledToBottom() {
- const $document = $(document);
-
- const currentPosition = $document.scrollTop();
- const scrollHeight = $document.height();
-
- const windowHeight = $(window).height();
-
- return scrollHeight - currentPosition === windowHeight;
- }
-
- // eslint-disable-next-line class-methods-use-this
- scrollDown() {
- const $document = $(document);
- $document.scrollTop($document.height());
+ polyfillSticky(this.$topBar);
}
scrollToBottom() {
- this.scrollDown();
+ scrollDown();
this.hasBeenScrolled = true;
this.toggleScroll();
}
@@ -154,12 +85,6 @@ export default class Job {
this.toggleScroll();
}
- // eslint-disable-next-line class-methods-use-this
- toggleDisableButton($button, disable) {
- if (disable && $button.prop('disabled')) return;
- $button.prop('disabled', disable);
- }
-
toggleScrollAnimation(toggle) {
this.$scrollBottomBtn.toggleClass('animate', toggle);
}
@@ -191,7 +116,7 @@ export default class Job {
this.state = log.state;
}
- this.isScrollInBottom = this.isScrolledToBottom();
+ this.isScrollInBottom = isScrolledToBottom();
if (log.append) {
this.$buildTraceOutput.append(log.html);
@@ -231,7 +156,7 @@ export default class Job {
})
.then(() => {
if (this.isScrollInBottom) {
- this.scrollDown();
+ scrollDown();
}
})
.then(() => this.toggleScroll());
@@ -239,7 +164,7 @@ export default class Job {
// 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/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
index c1044f4cd42..1e7f4b2c3f7 100644
--- a/app/assets/javascripts/jobs/components/header.vue
+++ b/app/assets/javascripts/jobs/components/header.vue
@@ -42,6 +42,9 @@ export default {
jobStarted() {
return !this.job.started === false;
},
+ headerTime() {
+ return this.jobStarted ? this.job.started : this.job.created_at;
+ },
},
watch: {
job() {
@@ -71,13 +74,13 @@ export default {
<ci-header
v-if="shouldRenderContent"
:status="status"
- item-name="Job"
:item-id="job.id"
- :time="job.created_at"
+ :time="headerTime"
:user="job.user"
:actions="actions"
:has-sidebar-button="true"
:should-render-triggered-label="jobStarted"
+ item-name="Job"
/>
<loading-icon
v-if="isLoading"
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index 8f3c66b0cbe..d2adf628050 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -101,8 +101,8 @@ export default {
{{ __('Retry') }}
</a>
<button
- type="button"
:aria-label="__('Toggle Sidebar')"
+ type="button"
class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
>
<i
@@ -114,20 +114,20 @@ export default {
</div>
<template v-if="shouldRenderContent">
<div
- class="block retry-link"
v-if="job.retry_path || job.new_issue_path"
+ class="block retry-link"
>
<a
v-if="job.new_issue_path"
- class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path"
+ class="js-new-issue btn btn-new btn-inverted"
>
{{ __('New issue') }}
</a>
<a
v-if="canUserRetry"
- class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path"
+ class="js-retry-job btn btn-inverted-secondary"
data-method="post"
rel="nofollow"
>
@@ -136,8 +136,8 @@ export default {
</div>
<div :class="{block : renderBlock }">
<p
- class="build-detail-row js-job-mr"
v-if="job.merge_request"
+ class="build-detail-row js-job-mr"
>
<span class="build-light-text">
{{ __('Merge Request:') }}
@@ -148,51 +148,51 @@ export default {
</p>
<detail-row
- class="js-job-duration"
v-if="job.duration"
- title="Duration"
:value="duration"
+ class="js-job-duration"
+ title="Duration"
/>
<detail-row
- class="js-job-finished"
v-if="job.finished_at"
- title="Finished"
:value="timeFormated(job.finished_at)"
+ class="js-job-finished"
+ title="Finished"
/>
<detail-row
- class="js-job-erased"
v-if="job.erased_at"
- title="Erased"
:value="timeFormated(job.erased_at)"
+ class="js-job-erased"
+ title="Erased"
/>
<detail-row
- class="js-job-queued"
v-if="job.queued"
- title="Queued"
:value="queued"
+ class="js-job-queued"
+ title="Queued"
/>
<detail-row
- class="js-job-timeout"
v-if="hasTimeout"
- title="Timeout"
:help-url="runnerHelpUrl"
:value="timeout"
+ class="js-job-timeout"
+ title="Timeout"
/>
<detail-row
- class="js-job-runner"
v-if="job.runner"
- title="Runner"
:value="runnerId"
+ class="js-job-runner"
+ title="Runner"
/>
<detail-row
- class="js-job-coverage"
v-if="job.coverage"
- title="Coverage"
:value="coverage"
+ class="js-job-coverage"
+ title="Coverage"
/>
<p
- class="build-detail-row js-job-tags"
v-if="job.tags.length"
+ class="build-detail-row js-job-tags"
>
<span class="build-light-text">
{{ __('Tags:') }}
@@ -210,8 +210,8 @@ export default {
class="btn-group prepend-top-5"
role="group">
<a
- class="js-cancel-job btn btn-sm btn-default"
:href="job.cancel_path"
+ class="js-cancel-job btn btn-sm btn-default"
data-method="post"
rel="nofollow"
>
@@ -221,8 +221,8 @@ export default {
</div>
</template>
<loading-icon
- class="prepend-top-10"
v-if="isLoading"
+ class="prepend-top-10"
size="2"
/>
</div>
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 8c3de6e4045..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';
@@ -13,6 +13,7 @@ export default class LabelManager {
this.otherLabels = otherLabels || $('.js-other-labels');
this.errorMessage = 'Unable to update label prioritization at this time';
this.emptyState = document.querySelector('#js-priority-labels-empty-state');
+ this.$badgeItemTemplate = $('#js-badge-item-template');
this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
filter: '.empty-message',
forceFallback: true,
@@ -63,7 +64,11 @@ export default class LabelManager {
$target = this.otherLabels;
$from = this.prioritizedLabels;
}
- $label.detach().appendTo($target);
+
+ const $detachedLabel = $label.detach();
+ this.toggleLabelPriorityBadge($detachedLabel, action);
+ $detachedLabel.appendTo($target);
+
if ($from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
@@ -88,6 +93,14 @@ export default class LabelManager {
}
}
+ toggleLabelPriorityBadge($label, action) {
+ if (action === 'remove') {
+ $('.js-priority-badge', $label).remove();
+ } else {
+ $('.label-links', $label).append(this.$badgeItemTemplate.clone().html());
+ }
+ }
+
onPrioritySortUpdate() {
this.savePrioritySort()
.catch(() => flash(this.errorMessage));
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index eafdaf4a672..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;
});
@@ -426,7 +427,7 @@ export default class LabelsSelect {
const tpl = _.template([
'<% _.each(labels, function(label){ %>',
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
- '<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
+ '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
'<%- label.title %>',
'</span>',
'</a>',
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index dbbf1637a47..9482d131344 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -44,8 +44,8 @@ export default class LazyLoader {
requestAnimationFrame(() => this.checkElementsInView());
}
checkElementsInView() {
- const scrollTop = pageYOffset;
- const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
+ const scrollTop = window.pageYOffset;
+ const visHeight = scrollTop + window.innerHeight + SCROLL_THRESHOLD;
// Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => {
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
index 3873f4528ce..c28ed04f94f 100644
--- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
+++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
@@ -93,7 +93,7 @@ export default class LinkedTabs {
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
- history.replaceState({
+ window.history.replaceState({
url: newState,
}, document.title, newState);
return newState;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8b5445d012b..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);
@@ -197,7 +227,10 @@ export const insertText = (target, text) => {
// eslint-disable-next-line no-param-reassign
target.value = newText;
// eslint-disable-next-line no-param-reassign
- target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
+ target.selectionStart = selectionStart + insertedText.length;
+
+ // eslint-disable-next-line no-param-reassign
+ target.selectionEnd = selectionStart + insertedText.length;
// Trigger autosave
target.dispatchEvent(new Event('input'));
@@ -209,7 +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 ||
@@ -222,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
@@ -238,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];
});
@@ -252,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];
});
@@ -292,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;
+ }, {});
};
/**
@@ -309,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
@@ -319,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);
};
@@ -368,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) {
@@ -384,6 +423,49 @@ export const backOff = (fn, timeout = 60000) => {
});
};
+export const createOverlayIcon = (iconPath, overlayPath) => {
+ const faviconImage = document.createElement('img');
+
+ return new Promise((resolve) => {
+ faviconImage.onload = () => {
+ const size = 32;
+
+ const canvas = document.createElement('canvas');
+ canvas.width = size;
+ canvas.height = size;
+
+ const context = canvas.getContext('2d');
+ context.clearRect(0, 0, size, size);
+ context.drawImage(
+ faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size,
+ );
+
+ const overlayImage = document.createElement('img');
+ overlayImage.onload = () => {
+ context.drawImage(
+ overlayImage, 0, 0, overlayImage.width, overlayImage.height, 0, 0, size, size,
+ );
+
+ const faviconWithOverlayUrl = canvas.toDataURL();
+
+ resolve(faviconWithOverlayUrl);
+ };
+ overlayImage.src = overlayPath;
+ };
+ faviconImage.src = iconPath;
+ });
+};
+
+export const setFaviconOverlay = (overlayPath) => {
+ const faviconEl = document.getElementById('favicon');
+
+ if (!faviconEl) { return null; }
+
+ const iconPath = faviconEl.getAttribute('data-original-href');
+
+ return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => faviconEl.setAttribute('href', faviconWithOverlayUrl));
+};
+
export const setFavicon = (faviconPath) => {
const faviconEl = document.getElementById('favicon');
if (faviconEl && faviconPath) {
@@ -393,20 +475,21 @@ export const setFavicon = (faviconPath) => {
export const resetFavicon = () => {
const faviconEl = document.getElementById('favicon');
- const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null;
+
if (faviconEl) {
+ const originalFavicon = faviconEl.getAttribute('data-original-href');
faviconEl.setAttribute('href', originalFavicon);
}
};
export const setCiStatusFavicon = pageUrl =>
- axios.get(pageUrl)
+ axios
+ .get(pageUrl)
.then(({ data }) => {
if (data && data.favicon) {
- setFavicon(data.favicon);
- } else {
- resetFavicon();
+ return setFaviconOverlay(data.favicon);
}
+ return resetFavicon();
})
.catch(resetFavicon);
@@ -423,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 0ff23bbb061..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.
@@ -79,37 +78,37 @@ export function getTimeago() {
if (!timeagoInstance) {
const localeRemaining = function getLocaleRemaining(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
- [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')],
- [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')],
+ [s__('Timeago|just now'), s__('Timeago|right now')],
+ [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')],
+ [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
[s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
- [s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')],
- [s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')],
- [s__('Timeago|a day ago'), s__('Timeago|1 day remaining')],
+ [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')],
+ [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')],
+ [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')],
[s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')],
- [s__('Timeago|a week ago'), s__('Timeago|1 week remaining')],
+ [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')],
[s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')],
- [s__('Timeago|a month ago'), s__('Timeago|1 month remaining')],
+ [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')],
[s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')],
- [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')],
+ [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
[s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
][index];
};
const locale = function getLocale(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
- [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')],
- [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')],
+ [s__('Timeago|just now'), s__('Timeago|right now')],
+ [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')],
+ [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
[s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
- [s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')],
- [s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')],
- [s__('Timeago|a day ago'), s__('Timeago|in 1 day')],
+ [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')],
+ [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')],
+ [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')],
[s__('Timeago|%s days ago'), s__('Timeago|in %s days')],
- [s__('Timeago|a week ago'), s__('Timeago|in 1 week')],
+ [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')],
[s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')],
- [s__('Timeago|a month ago'), s__('Timeago|in 1 month')],
+ [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')],
[s__('Timeago|%s months ago'), s__('Timeago|in %s months')],
- [s__('Timeago|a year ago'), s__('Timeago|in 1 year')],
+ [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
[s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
][index];
};
@@ -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>',
});
}
@@ -270,6 +270,15 @@ export const totalDaysInMonth = date => {
};
/**
+ * Returns number of days in a quarter from provided
+ * months array.
+ *
+ * @param {Array} quarter
+ */
+export const totalDaysInQuarter = quarter =>
+ quarter.reduce((acc, month) => acc + totalDaysInMonth(month), 0);
+
+/**
* Returns list of Dates referring to Sundays of the month
* based on provided date
*
@@ -309,42 +318,21 @@ export const getSundays = date => {
};
/**
- * Returns list of Dates representing a timeframe of Months from month of provided date (inclusive)
- * up to provided length
- *
- * For eg;
- * If current month is January 2018 and `length` provided is `6`
- * Then this method will return list of Date objects as follows;
- *
- * [ October 2017, November 2017, December 2017, January 2018, February 2018, March 2018 ]
- *
- * If current month is March 2018 and `length` provided is `3`
- * Then this method will return list of Date objects as follows;
- *
- * [ February 2018, March 2018, April 2018 ]
+ * Returns list of Dates representing a timeframe of months from startDate and length
*
+ * @param {Date} startDate
* @param {Number} length
- * @param {Date} date
*/
-export const getTimeframeWindow = (length, date) => {
- if (!length) {
+export const getTimeframeWindowFrom = (startDate, length) => {
+ if (!(startDate instanceof Date) || !length) {
return [];
}
- const currentDate = date instanceof Date ? date : new Date();
- const currentMonthIndex = Math.floor(length / 2);
- const timeframe = [];
-
- // Move date object backward to the first month of timeframe
- currentDate.setDate(1);
- currentDate.setMonth(currentDate.getMonth() - currentMonthIndex);
-
- // Iterate and update date for the size of length
+ // Iterate and set date for the size of length
// and push date reference to timeframe list
- for (let i = 0; i < length; i += 1) {
- timeframe.push(new Date(currentDate.getTime()));
- currentDate.setMonth(currentDate.getMonth() + 1);
- }
+ const timeframe = new Array(length)
+ .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]));
@@ -352,6 +340,30 @@ export const getTimeframeWindow = (length, date) => {
return timeframe;
};
+/**
+ * Returns count of day within current quarter from provided date
+ * and array of months for the quarter
+ *
+ * Eg;
+ * If date is 15 Feb 2018
+ * and quarter is [Jan, Feb, Mar]
+ *
+ * Then 15th Feb is 46th day of the quarter
+ * Where 31 (days in Jan) + 15 (date of Feb).
+ *
+ * @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);
+
window.gl = window.gl || {};
window.gl.utils = {
...(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/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
index bb151929431..229d53b18b0 100644
--- a/app/assets/javascripts/lib/utils/http_status.js
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -8,4 +8,5 @@ export default {
OK: 200,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
+ NOT_FOUND: 404,
};
diff --git a/app/assets/javascripts/lib/utils/logoutput_behaviours.js b/app/assets/javascripts/lib/utils/logoutput_behaviours.js
new file mode 100644
index 00000000000..1bf99d935ef
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/logoutput_behaviours.js
@@ -0,0 +1,46 @@
+import $ from 'jquery';
+import { canScroll, isScrolledToBottom, toggleDisableButton } from './scroll_utils';
+
+export default class LogOutputBehaviours {
+ constructor() {
+ // Scroll buttons
+ this.$scrollTopBtn = $('.js-scroll-up');
+ this.$scrollBottomBtn = $('.js-scroll-down');
+
+ this.$scrollTopBtn.off('click').on('click', this.scrollToTop.bind(this));
+ this.$scrollBottomBtn.off('click').on('click', this.scrollToBottom.bind(this));
+ }
+
+ toggleScroll() {
+ const $document = $(document);
+ const currentPosition = $document.scrollTop();
+ const scrollHeight = $document.height();
+
+ const windowHeight = $(window).height();
+ if (canScroll()) {
+ if (currentPosition > 0 && scrollHeight - currentPosition !== windowHeight) {
+ // User is in the middle of the log
+
+ toggleDisableButton(this.$scrollTopBtn, false);
+ toggleDisableButton(this.$scrollBottomBtn, false);
+ } else if (currentPosition === 0) {
+ // User is at Top of Log
+
+ toggleDisableButton(this.$scrollTopBtn, true);
+ toggleDisableButton(this.$scrollBottomBtn, false);
+ } else if (isScrolledToBottom()) {
+ // User is at the bottom of the build log.
+
+ toggleDisableButton(this.$scrollTopBtn, false);
+ toggleDisableButton(this.$scrollBottomBtn, true);
+ }
+ } else {
+ toggleDisableButton(this.$scrollTopBtn, true);
+ toggleDisableButton(this.$scrollBottomBtn, true);
+ }
+ }
+
+ toggleScrollAnimation(toggle) {
+ this.$scrollBottomBtn.toggleClass('animate', toggle);
+ }
+}
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index 973d6119158..305ad3e5e26 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, max-len */
+/* eslint-disable func-names, no-var, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, max-len */
function notificationGranted(message, opts, onclick) {
var notification;
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index a02c79b787e..afbab59055b 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -12,8 +12,8 @@ export function formatRelevantDigits(number) {
let digitsLeft = '';
let relevantDigits = 0;
let formattedNumber = '';
- if (!isNaN(Number(number))) {
- digitsLeft = number.toString().split('.')[0];
+ if (!Number.isNaN(Number(number))) {
+ [digitsLeft] = number.toString().split('.');
switch (digitsLeft.length) {
case 1:
relevantDigits = 3;
diff --git a/app/assets/javascripts/lib/utils/scroll_utils.js b/app/assets/javascripts/lib/utils/scroll_utils.js
new file mode 100644
index 00000000000..9313b570863
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/scroll_utils.js
@@ -0,0 +1,29 @@
+import $ from 'jquery';
+
+export const canScroll = () => $(document).height() > $(window).height();
+
+/**
+ * Checks if the entire page is scrolled down all the way to the bottom
+ */
+export const isScrolledToBottom = () => {
+ const $document = $(document);
+
+ const currentPosition = $document.scrollTop();
+ const scrollHeight = $document.height();
+
+ const windowHeight = $(window).height();
+
+ return scrollHeight - currentPosition === windowHeight;
+};
+
+export const scrollDown = () => {
+ const $document = $(document);
+ $document.scrollTop($document.height());
+};
+
+export const toggleDisableButton = ($button, disable) => {
+ if (disable && $button.prop('disabled')) return;
+ $button.prop('disabled', disable);
+};
+
+export default {};
diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js
index 098afcfa1b4..15a4dd62012 100644
--- a/app/assets/javascripts/lib/utils/sticky.js
+++ b/app/assets/javascripts/lib/utils/sticky.js
@@ -1,3 +1,5 @@
+import StickyFill from 'stickyfilljs';
+
export const createPlaceholder = () => {
const placeholder = document.createElement('div');
placeholder.classList.add('sticky-placeholder');
@@ -28,7 +30,16 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
}
};
-export default (el, stickyTop, insertPlaceholder = true) => {
+/**
+ * Create a listener that will toggle a 'is-stuck' class, based on the current scroll position.
+ *
+ * - If the current environment does not support `position: sticky`, do nothing.
+ *
+ * @param {HTMLElement} el The `position: sticky` element.
+ * @param {Number} stickyTop Used to determine when an element is stuck.
+ * @param {Boolean} insertPlaceholder Should a placeholder element be created when element is stuck?
+ */
+export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => {
if (!el) return;
if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return;
@@ -37,3 +48,13 @@ export default (el, stickyTop, insertPlaceholder = true) => {
passive: true,
});
};
+
+/**
+ * Polyfill the `position: sticky` behavior.
+ *
+ * - If the current environment supports `position: sticky`, do nothing.
+ * - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement.
+ */
+export const polyfillSticky = (el) => {
+ StickyFill.add(el);
+};
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/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index dd17544b656..72b72f4247d 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -85,9 +85,9 @@ export function redirectTo(url) {
}
export function webIDEUrl(route = undefined) {
- let returnUrl = `${gon.relative_url_root}/-/ide/`;
+ let returnUrl = `${gon.relative_url_root || ''}/-/ide/`;
if (route) {
- returnUrl += `project${route}`;
+ returnUrl += `project${route.replace(new RegExp(`^${gon.relative_url_root || ''}`), '')}`;
}
return returnUrl;
}
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index f2323f57455..291655235d5 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */
+/* eslint-disable func-names, no-var, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */
import $ from 'jquery';
@@ -35,7 +35,7 @@ const LineHighlighter = function(options = {}) {
options.highlightLineClass = options.highlightLineClass || 'hll';
options.fileHolderSelector = options.fileHolderSelector || '.file-holder';
options.scrollFileHolder = options.scrollFileHolder || false;
- options.hash = options.hash || location.hash;
+ options.hash = options.hash || window.location.hash;
this.options = options;
this._hash = options.hash;
@@ -142,12 +142,14 @@ LineHighlighter.prototype.highlightLine = function(lineNumber) {
//
// range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) {
- var i, lineNumber, ref, ref1, results;
if (range[1]) {
- results = [];
- for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
+ const results = [];
+ const ref = range[0] <= range[1] ? range : range.reverse();
+
+ for (let lineNumber = range[0]; lineNumber <= ref[1]; lineNumber += 1) {
results.push(this.highlightLine(lineNumber));
}
+
return results;
} else {
return this.highlightLine(range[0]);
@@ -170,7 +172,7 @@ LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
//
// This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) {
- return history.pushState({
+ return window.history.pushState({
url: value
// We're using pushState instead of assigning location.hash directly to
// prevent the page from scrolling on the hashchange event
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 9803bebfd10..c9ce838cd48 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -144,6 +144,7 @@ document.addEventListener('DOMContentLoaded', () => {
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
trigger: 'hover',
+ boundary: 'viewport',
placement(tip, el) {
return $(el).data('placement') || 'bottom';
},
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 2cb238529aa..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';
@@ -11,9 +11,18 @@ import { __ } from '~/locale';
global.mergeConflicts.diffFileEditor = Vue.extend({
props: {
- file: Object,
- onCancelDiscardConfirmation: Function,
- onAcceptDiscardConfirmation: Function
+ file: {
+ type: Object,
+ required: true,
+ },
+ onCancelDiscardConfirmation: {
+ type: Function,
+ required: true,
+ },
+ onAcceptDiscardConfirmation: {
+ type: Function,
+ required: true,
+ },
},
data() {
return {
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
index 56d6678e1bd..827cf5f478d 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
@@ -1,14 +1,19 @@
-/* eslint-disable no-param-reassign, comma-dangle */
+/* eslint-disable no-param-reassign */
import Vue from 'vue';
+import actionsMixin from '../mixins/line_conflict_actions';
+import utilsMixin from '../mixins/line_conflict_utils';
-((global) => {
+(global => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.inlineConflictLines = Vue.extend({
+ mixins: [utilsMixin, actionsMixin],
props: {
- file: Object
+ file: {
+ type: Object,
+ required: true,
+ },
},
- mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
});
})(window.gl || (window.gl = {}));
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 0fc4a13450a..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,15 +1,20 @@
-/* eslint-disable no-param-reassign, comma-dangle */
+/* eslint-disable no-param-reassign */
import Vue from 'vue';
+import actionsMixin from '../mixins/line_conflict_actions';
+import utilsMixin from '../mixins/line_conflict_utils';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLines = Vue.extend({
+ mixins: [utilsMixin, actionsMixin],
props: {
- file: Object
+ file: {
+ type: Object,
+ required: true,
+ },
},
- mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
template: `
<table>
<tr class="line_holder parallel" v-for="section in file.parallelLines">
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js
index c68b47c9348..64d69159222 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js
@@ -1,23 +1,16 @@
-/* eslint-disable no-param-reassign, comma-dangle */
import axios from '../lib/utils/axios_utils';
-((global) => {
- global.mergeConflicts = global.mergeConflicts || {};
-
- class mergeConflictsService {
- constructor(options) {
- this.conflictsPath = options.conflictsPath;
- this.resolveConflictsPath = options.resolveConflictsPath;
- }
-
- fetchConflictsData() {
- return axios.get(this.conflictsPath);
- }
+export default class MergeConflictsService {
+ constructor(options) {
+ this.conflictsPath = options.conflictsPath;
+ this.resolveConflictsPath = options.resolveConflictsPath;
+ }
- submitResolveConflicts(data) {
- return axios.post(this.resolveConflictsPath, data);
- }
+ fetchConflictsData() {
+ return axios.get(this.conflictsPath);
}
- global.mergeConflicts.mergeConflictsService = mergeConflictsService;
-})(window.gl || (window.gl = {}));
+ submitResolveConflicts(data) {
+ return axios.post(this.resolveConflictsPath, data);
+ }
+}
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 4abd5433bb5..7badd68089c 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -1,13 +1,9 @@
-/* eslint-disable new-cap, comma-dangle, no-new */
-
import $ from 'jquery';
import Vue from 'vue';
-import Flash from '../flash';
+import createFlash from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
-import './merge_conflict_service';
-import './mixins/line_conflict_utils';
-import './mixins/line_conflict_actions';
+import MergeConflictsService from './merge_conflict_service';
import './components/diff_file_editor';
import './components/inline_conflict_lines';
import './components/parallel_conflict_lines';
@@ -16,10 +12,10 @@ 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 mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({
+ const { mergeConflictsStore } = gl.mergeConflicts;
+ const mergeConflictsService = new MergeConflictsService({
conflictsPath: conflictsEl.dataset.conflictsPath,
- resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath
+ resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath,
});
initIssuableSidebar();
@@ -29,17 +25,26 @@ export default function initMergeConflicts() {
components: {
'diff-file-editor': gl.mergeConflicts.diffFileEditor,
'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
- 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
+ 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines,
},
data: mergeConflictsStore.state,
computed: {
- conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); },
- readyToCommit() { return mergeConflictsStore.isReadyToCommit(); },
- commitButtonText() { return mergeConflictsStore.getCommitButtonText(); },
- showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
+ conflictsCountText() {
+ return mergeConflictsStore.getConflictsCountText();
+ },
+ readyToCommit() {
+ return mergeConflictsStore.isReadyToCommit();
+ },
+ commitButtonText() {
+ return mergeConflictsStore.getCommitButtonText();
+ },
+ showDiffViewTypeSwitcher() {
+ return mergeConflictsStore.fileTextTypePresent();
+ },
},
created() {
- mergeConflictsService.fetchConflictsData()
+ mergeConflictsService
+ .fetchConflictsData()
.then(({ data }) => {
if (data.type === 'error') {
mergeConflictsStore.setFailedRequest(data.message);
@@ -87,9 +92,9 @@ export default function initMergeConflicts() {
})
.catch(() => {
mergeConflictsStore.setSubmitState(false);
- new Flash('Failed to save merge conflicts resolutions. Please try again!');
+ createFlash('Failed to save merge conflicts resolutions. Please try again!');
});
- }
- }
+ },
+ },
});
}
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js
index 53e000d7e9e..364ae2b2688 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js
@@ -1,13 +1,7 @@
-/* eslint-disable no-param-reassign, comma-dangle */
-
-((global) => {
- global.mergeConflicts = global.mergeConflicts || {};
-
- global.mergeConflicts.actions = {
- methods: {
- handleSelected(file, sectionId, selection) {
- gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
- }
- }
- };
-})(window.gl || (window.gl = {}));
+export default {
+ methods: {
+ handleSelected(file, sectionId, selection) {
+ gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
+ },
+ },
+};
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js
index 0f475f62ee6..d25032fb142 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js
@@ -1,19 +1,13 @@
-/* eslint-disable no-param-reassign, quote-props, comma-dangle */
-
-((global) => {
- global.mergeConflicts = global.mergeConflicts || {};
-
- global.mergeConflicts.utils = {
- methods: {
- lineCssClass(line) {
- return {
- 'head': line.isHead,
- 'origin': line.isOrigin,
- 'match': line.hasMatch,
- 'selected': line.isSelected,
- 'unselected': line.isUnselected
- };
- }
- }
- };
-})(window.gl || (window.gl = {}));
+export default {
+ methods: {
+ lineCssClass(line) {
+ return {
+ head: line.isHead,
+ origin: line.isOrigin,
+ match: line.hasMatch,
+ selected: line.isSelected,
+ unselected: line.isUnselected,
+ };
+ },
+ },
+};
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 493c119dc6f..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,
@@ -362,7 +403,7 @@ export default class MergeRequestTabs {
//
// status - Boolean, true to show, false to hide
toggleLoading(status) {
- $('.mr-loading-status .loading').toggleClass('hidden', !status);
+ $('.mr-loading-status .loading').toggleClass('hide', !status);
}
diffViewType() {
@@ -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.js b/app/assets/javascripts/milestone.js
index 325fa570f37..6da04020881 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -18,13 +18,13 @@ export default class Milestone {
return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
const $target = $(e.target);
- location.hash = $target.attr('href');
+ window.location.hash = $target.attr('href');
this.loadTab($target);
});
}
// eslint-disable-next-line class-methods-use-this
loadInitialTab() {
- const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
+ const $target = $(`.js-milestone-tabs a[href="${window.location.hash}"]`);
if ($target.length) {
$target.tab('show');
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index f8b3d3061f0..77acba6e355 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
+/* eslint-disable max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* global Issuable */
/* global ListMilestone */
@@ -16,10 +16,10 @@ export default class MilestoneSelect {
typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
- this.init(els, options);
+ MilestoneSelect.init(els, options);
}
- init(els, options) {
+ static init(els, options) {
let $els = $(els);
if (!els) {
@@ -56,7 +56,7 @@ export default class MilestoneSelect {
if (issueUpdateURL) {
milestoneLinkTemplate = _.template(
- '<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
+ '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
);
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
}
@@ -224,7 +224,6 @@ export default class MilestoneSelect {
$selectBox.hide();
$value.css('display', '');
if (data.milestone != null) {
- data.milestone.full_path = this.currentProject.full_path;
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index f5572be5fbf..e1c8b6a6d4a 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -139,7 +139,7 @@ export default {
this.updateAspectRatio = true;
},
toggleAspectRatio() {
- this.updatedAspectRatios = this.updatedAspectRatios += 1;
+ this.updatedAspectRatios += 1;
if (this.store.getMetricsCount() === this.updatedAspectRatios) {
this.updateAspectRatio = !this.updateAspectRatio;
this.updatedAspectRatios = 0;
@@ -174,7 +174,10 @@ export default {
:tags-path="tagsPath"
:show-legend="showLegend"
:small-graph="forceSmallGraph"
- />
+ >
+ <!-- EE content -->
+ {{ null }}
+ </graph>
</graph-group>
</div>
<empty-state
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index c77f451c2d3..82b9a4b1adb 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -107,8 +107,8 @@ export default {
<div class="state-button">
<a
v-if="currentState.buttonPath"
- class="btn btn-success"
:href="currentState.buttonPath"
+ class="btn btn-success"
>
{{ currentState.buttonText }}
</a>
@@ -116,8 +116,8 @@ export default {
<div class="state-button">
<a
v-if="currentState.secondaryButtonPath"
- class="btn"
:href="currentState.secondaryButtonPath"
+ class="btn"
>
{{ currentState.secondaryButtonText }}
</a>
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index de6755e0414..e5680a0499f 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -154,7 +154,7 @@ export default {
point.x = e.clientX;
point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
- point.x = point.x += 7;
+ point.x += 7;
const firstTimeSeries = this.timeSeries[0];
const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
@@ -232,20 +232,25 @@ export default {
@mouseover="showFlagContent = true"
@mouseleave="showFlagContent = false"
>
- <h5 class="text-center graph-title">
- {{ graphData.title }}
- </h5>
+ <div class="prometheus-graph-header">
+ <h5 class="prometheus-graph-title">
+ {{ graphData.title }}
+ </h5>
+ <div class="prometheus-graph-widgets">
+ <slot></slot>
+ </div>
+ </div>
<div
- class="prometheus-svg-container"
:style="paddingBottomRootSvg"
+ class="prometheus-svg-container"
>
<svg
- :viewBox="outerViewBox"
ref="baseSvg"
+ :viewBox="outerViewBox"
>
<g
- class="x-axis"
:transform="axisTransform"
+ class="x-axis"
/>
<g
class="y-axis"
@@ -260,9 +265,9 @@ export default {
:unit-of-display="unitOfDisplay"
/>
<svg
- class="graph-data"
- :viewBox="innerViewBox"
ref="graphData"
+ :viewBox="innerViewBox"
+ class="graph-data"
>
<graph-path
v-for="(path, index) in timeSeries"
@@ -282,11 +287,11 @@ export default {
:graph-height-offset="graphHeightOffset"
/>
<rect
- class="prometheus-graph-overlay"
+ ref="graphOverlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
+ class="prometheus-graph-overlay"
transform="translate(-5, 20)"
- ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)"
/>
</svg>
diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue
index fc4b3689dfd..8a604a51eb2 100644
--- a/app/assets/javascripts/monitoring/components/graph/axis.vue
+++ b/app/assets/javascripts/monitoring/components/graph/axis.vue
@@ -92,48 +92,48 @@ export default {
<template>
<g class="axis-label-container">
<line
+ :y1="yPosition"
+ :x2="graphWidth + 20"
+ :y2="yPosition"
class="label-x-axis-line"
stroke="#000000"
stroke-width="1"
x1="10"
- :y1="yPosition"
- :x2="graphWidth + 20"
- :y2="yPosition"
/>
<line
+ :x2="10"
+ :y2="yPosition"
class="label-y-axis-line"
stroke="#000000"
stroke-width="1"
x1="10"
y1="0"
- :x2="10"
- :y2="yPosition"
/>
<rect
- class="rect-axis-text"
:transform="rectTransform"
:width="yLabelWidth"
:height="yLabelHeight"
+ class="rect-axis-text"
/>
<text
+ ref="ylabel"
+ :transform="textTransform"
class="label-axis-text y-label-text"
text-anchor="middle"
- :transform="textTransform"
- ref="ylabel"
>
{{ yAxisLabelSentenceCase }}
</text>
<rect
- class="rect-axis-text"
:x="xPosition + 60"
:y="graphHeight - 80"
+ class="rect-axis-text"
width="35"
height="50"
/>
<text
- class="label-axis-text x-label-text"
:x="xPosition + 60"
:y="yPosition"
+ class="label-axis-text x-label-text"
dy=".35em"
>
{{ timeString }}
diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
index 4012191ceb9..a7289ed53e8 100644
--- a/app/assets/javascripts/monitoring/components/graph/deployment.vue
+++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue
@@ -33,18 +33,18 @@ export default {
:key="index"
:transform="transformDeploymentGroup(deployment)">
<rect
+ :height="calculatedHeight"
x="0"
y="0"
- :height="calculatedHeight"
width="3"
fill="url(#shadow-gradient)"
/>
<line
+ :y2="calculatedHeight"
class="deployment-line"
x1="0"
y1="0"
x2="0"
- :y2="calculatedHeight"
stroke="#000"
/>
</g>
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index 8a771107de8..92fe98508ad 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -97,7 +97,7 @@ export default {
? this.deploymentFlagData.seriesIndex
: indexFromCoordinates;
const value = series.values[index] && series.values[index].value;
- if (isNaN(value)) {
+ if (Number.isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
@@ -117,13 +117,13 @@ export default {
<template>
<div
- class="prometheus-graph-cursor"
:style="cursorStyle"
+ class="prometheus-graph-cursor"
>
<div
v-if="showFlagContent"
- class="prometheus-graph-flag popover"
:class="flagOrientation"
+ class="prometheus-graph-flag popover"
>
<div class="arrow"></div>
<div class="popover-title">
@@ -139,8 +139,8 @@ export default {
>
<div>
<icon
- name="commit"
:size="12"
+ name="commit"
/>
<a :href="deploymentFlagData.commitUrl">
{{ deploymentFlagData.sha.slice(0, 8) }}
@@ -150,8 +150,8 @@ export default {
v-if="deploymentFlagData.tag"
>
<icon
- name="label"
:size="12"
+ name="label"
/>
<a :href="deploymentFlagData.tagUrl">
{{ deploymentFlagData.ref }}
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index da9280cf1f1..3276f3a1ceb 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -31,8 +31,8 @@ export default {
<table class="prometheus-table">
<tr
v-for="(series, index) in timeSeries"
- :key="index"
v-if="series.shouldRenderLegend"
+ :key="index"
:class="isStable(series)"
>
<td>
@@ -40,11 +40,11 @@ export default {
</td>
<track-line :track="series" />
<td
- class="legend-metric-title"
- v-if="timeSeries.length > 1">
+ v-if="timeSeries.length > 1"
+ class="legend-metric-title">
<track-info
- :track="series"
- v-if="series.metricTag" />
+ v-if="series.metricTag"
+ :track="series" />
<track-info
v-else
:track="series">
@@ -62,8 +62,8 @@ export default {
:key="`track-line-${trackIndex}`"/>
<td :key="`track-info-${trackIndex}`">
<track-info
- class="legend-metric-title"
- :track="track" />
+ :track="track"
+ class="legend-metric-title" />
</td>
</template>
</tr>
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index 52f8aa2ee3f..a9b7ce586ce 100644
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
@@ -44,26 +44,26 @@ export default {
<template>
<g transform="translate(-5, 20)">
<circle
- class="circle-path"
+ v-if="showDot"
:cx="currentCoordinates.currentX"
:cy="currentCoordinates.currentY"
:fill="lineColor"
:stroke="lineColor"
+ class="circle-path"
r="3"
- v-if="showDot"
/>
<path
- class="metric-area"
:d="generatedAreaPath"
:fill="areaColor"
+ class="metric-area"
/>
<path
- class="metric-line"
:d="generatedLinePath"
:stroke="lineColor"
+ :stroke-dasharray="strokeDashArray"
+ class="metric-line"
fill="none"
stroke-width="1"
- :stroke-dasharray="strokeDashArray"
/>
</g>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue
index 18be65fd1ef..ba3f93b39ff 100644
--- a/app/assets/javascripts/monitoring/components/graph/track_line.vue
+++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue
@@ -24,11 +24,11 @@ export default {
<line
:stroke-dasharray="stylizedLine"
:stroke="track.lineColor"
- stroke-width="4"
:x1="0"
:x2="16"
:y1="4"
:y2="4"
+ stroke-width="4"
/>
</svg>
</td>
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 4d3f1f1a7cc..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);
@@ -73,7 +73,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
timeSeriesScaleX.ticks(d3.timeMinute, 60);
timeSeriesScaleY.domain(yDom);
- const defined = d => !isNaN(d.value) && d.value != null;
+ const defined = d => !Number.isNaN(d.value) && d.value != null;
const lineFunction = d3
.line()
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index e3c5bf06b3d..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 bd007c707f2..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({
@@ -112,7 +112,8 @@ export default (function() {
fill: "#444"
});
ref = this.days;
- for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) {
+
+ for (mm = 0, len = ref.length; mm < len; mm += 1) {
day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
// Dates
@@ -120,7 +121,7 @@ export default (function() {
font: "12px Monaco, monospace",
fill: "#BBB"
});
- cuday = day[0];
+ [cuday] = day;
}
if (cumonth !== day[1]) {
// Months
@@ -128,6 +129,8 @@ export default (function() {
font: "12px Monaco, monospace",
fill: "#EEE"
});
+
+ // eslint-disable-next-line prefer-destructuring
cumonth = day[1];
}
}
@@ -168,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();
@@ -206,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) {
@@ -241,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"
@@ -261,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
@@ -281,11 +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 = [];
- for (i = j = 0, len = ref.length; j < len; i = (j += 1)) {
+ var arrow, color, i, len, offset, parent, parentCommit, parentX1, parentX2, parentY, route;
+ const { r } = this;
+ const ref = commit.parents;
+ const results = [];
+
+ 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;
@@ -329,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/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue
index b4067d229aa..18cef82cec0 100644
--- a/app/assets/javascripts/notebook/cells/code.vue
+++ b/app/assets/javascripts/notebook/cells/code.vue
@@ -39,10 +39,10 @@ export default {
<template>
<div class="cell">
<code-cell
- type="input"
:raw-code="rawInputCode"
:count="cell.execution_count"
- :code-css-class="codeCssClass" />
+ :code-css-class="codeCssClass"
+ type="input" />
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue
index 0f3083f05b2..7d2a1a33b98 100644
--- a/app/assets/javascripts/notebook/cells/code/index.vue
+++ b/app/assets/javascripts/notebook/cells/code/index.vue
@@ -48,9 +48,9 @@
:type="promptType"
:count="count" />
<pre
- class="language-python"
- :class="codeCssClass"
ref="code"
+ :class="codeCssClass"
+ class="language-python"
v-text="code">
</pre>
</div>
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index 91b2269a83a..4183b976814 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -78,10 +78,10 @@
<template>
<component
:is="componentName"
- type="output"
:output-type="outputType"
:count="count"
:raw-code="rawCode"
:code-css-class="codeCssClass"
+ type="output"
/>
</template>
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index b2c1a26bbae..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) {
@@ -315,7 +253,7 @@ export default class Notes {
if (discussionNoteForm.length) {
if ($textarea.val() !== '') {
if (
- !confirm('Are you sure you want to cancel creating this comment?')
+ !window.confirm('Are you sure you want to cancel creating this comment?')
) {
return;
}
@@ -329,7 +267,7 @@ export default class Notes {
newText = $textarea.val();
if (originalText !== newText) {
if (
- !confirm('Are you sure you want to cancel editing this comment?')
+ !window.confirm('Are you sure you want to cancel editing this comment?')
) {
return;
}
@@ -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.
*/
@@ -1675,7 +1543,7 @@ export default class Notes {
<div class="note-header">
<div class="note-header-info">
<a href="/${_.escape(currentUsername)}">
- <span class="d-none d-sm-block">${_.escape(
+ <span class="d-none d-sm-inline-block">${_.escape(
currentUsername,
)}</span>
<span class="note-headline-light">${_.escape(
@@ -1694,7 +1562,7 @@ export default class Notes {
</li>`,
);
- $tempNote.find('.d-none.d-sm-block').text(_.escape(currentUserFullname));
+ $tempNote.find('.d-none.d-sm-inline-block').text(_.escape(currentUserFullname));
$tempNote
.find('.note-headline-light')
.text(`@${_.escape(currentUsername)}`);
@@ -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 72d7e22fba0..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
- issuable-type="issue"
- v-else-if="isLocked(getNoteableData) && !canCreateNote"
+ v-else-if="!canCreateNote"
+ :issuable-type="issuableTypeTitle"
/>
<ul
v-else-if="canCreateNote"
@@ -345,23 +340,23 @@ Please check your network connection and try again.`;
/>
<markdown-field
+ ref="markdownField"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
- :add-spacing-classes="false"
- ref="markdownField">
+ :add-spacing-classes="false">
<textarea
id="note-body"
+ ref="textarea"
+ slot="textarea"
+ 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"
- v-model="note"
- ref="textarea"
- slot="textarea"
- :disabled="isSubmitting"
- placeholder="Write a comment or drag your files here..."
+ placeholder="Write a comment or drag your files here…"
@keydown.up="editCurrentUserLastNote()"
@keydown.meta.enter="handleSave()"
@keydown.ctrl.enter="handleSave()">
@@ -372,10 +367,10 @@ js-gfm-input js-autosize markdown-area js-vue-textarea"
class="float-left btn-group
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
<button
- @click.prevent="handleSave()"
:disabled="isSubmitButtonDisabled"
class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
- type="submit">
+ type="submit"
+ @click.prevent="handleSave()">
{{ __(commentButtonTitle) }}
</button>
<button
@@ -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>
@@ -434,20 +429,20 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<loading-button
v-if="canUpdateIssue"
:loading="isToggleStateButtonLoading"
- @click="handleSave(true)"
:container-class="[
actionButtonClassNames,
'btn btn-comment btn-comment-and-close js-action-button'
]"
:disabled="isToggleStateButtonLoading || isSubmitting"
:label="issueActionButtonTitle"
+ @click="handleSave(true)"
/>
<button
- type="button"
v-if="note.length"
- @click="discard"
- class="btn btn-cancel js-note-discard">
+ type="button"
+ class="btn btn-cancel js-note-discard"
+ @click="discard">
Discard draft
</button>
</div>
diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue
index 94d9dc69964..fc7b52be241 100644
--- a/app/assets/javascripts/notes/components/diff_file_header.vue
+++ b/app/assets/javascripts/notes/components/diff_file_header.vue
@@ -29,12 +29,12 @@ export default {
<span>
<icon name="archive" />
<strong
- v-html="diffFile.submoduleLink"
class="file-title-name"
+ v-html="diffFile.submoduleLink"
></strong>
<clipboard-button
- title="Copy file path to clipboard"
:text="diffFile.submoduleLink"
+ title="Copy file path to clipboard"
css-class="btn-default btn-transparent btn-clipboard"
/>
</span>
@@ -48,16 +48,16 @@ export default {
<span v-html="diffFile.blobIcon"></span>
<span v-if="diffFile.renamedFile">
<strong
- class="file-title-name has-tooltip"
:title="diffFile.oldPath"
+ class="file-title-name has-tooltip"
data-container="body"
>
{{ diffFile.oldPath }}
</strong>
&rarr;
<strong
- class="file-title-name has-tooltip"
:title="diffFile.newPath"
+ class="file-title-name has-tooltip"
data-container="body"
>
{{ diffFile.newPath }}
@@ -66,8 +66,8 @@ export default {
<strong
v-else
- class="file-title-name has-tooltip"
:title="diffFile.oldPath"
+ class="file-title-name has-tooltip"
data-container="body"
>
{{ diffFile.filePath }}
@@ -78,8 +78,8 @@ export default {
</component>
<clipboard-button
- title="Copy file path to clipboard"
:text="diffFile.filePath"
+ title="Copy file path to clipboard"
css-class="btn-default btn-transparent btn-clipboard"
/>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index ee01ec85bbb..d321f2ce15e 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,13 +1,15 @@
<script>
-import $ from 'jquery';
-import syntaxHighlight from '~/syntax_highlight';
+import { mapState, mapActions } from 'vuex';
import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import DiffFileHeader from './diff_file_header.vue';
+import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
export default {
components: {
DiffFileHeader,
+ SkeletonLoadingContainer,
},
props: {
discussion: {
@@ -15,7 +17,24 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ error: false,
+ };
+ },
computed: {
+ ...mapState({
+ noteableData: state => state.notes.noteableData,
+ }),
+ hasTruncatedDiffLines() {
+ return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0;
+ },
+ isDiscussionsExpanded() {
+ return true; // TODO: @fatihacet - Fix this.
+ },
+ isCollapsed() {
+ return this.diffFile.collapsed || false;
+ },
isImageDiff() {
return !this.diffFile.text;
},
@@ -23,36 +42,46 @@ export default {
const { text } = this.diffFile;
return text ? 'text-file' : 'js-image-file';
},
- diffRows() {
- return $(this.discussion.truncatedDiffLines);
- },
diffFile() {
- return convertObjectPropsToCamelCase(this.discussion.diffFile);
+ return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true });
},
imageDiffHtml() {
return this.discussion.imageDiffHtml;
},
+ currentUser() {
+ return this.noteableData.current_user;
+ },
+ userColorScheme() {
+ return window.gon.user_color_scheme;
+ },
+ normalizedDiffLines() {
+ const lines = this.discussion.truncatedDiffLines || [];
+
+ return lines.map(line => trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line)));
+ },
},
mounted() {
if (this.isImageDiff) {
const canCreateNote = false;
const renderCommentBadge = true;
- imageDiffHelper.initImageDiff(
- this.$refs.fileHolder,
- canCreateNote,
- renderCommentBadge,
- );
- } else {
- const fileHolder = $(this.$refs.fileHolder);
- this.$nextTick(() => {
- syntaxHighlight(fileHolder);
- });
+ imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
+ } else if (!this.hasTruncatedDiffLines) {
+ this.fetchDiff();
}
},
methods: {
+ ...mapActions(['fetchDiscussionDiffLines']),
rowTag(html) {
return html.outerHTML ? 'tr' : 'template';
},
+ fetchDiff() {
+ this.error = false;
+ this.fetchDiscussionDiffLines(this.discussion)
+ .then(this.highlight)
+ .catch(() => {
+ this.error = true;
+ });
+ },
},
};
</script>
@@ -60,26 +89,62 @@ export default {
<template>
<div
ref="fileHolder"
- class="diff-file file-holder"
: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
- :is="rowTag(html)"
- :class="html.className"
- v-for="(html, index) in diffRows"
- v-html="html.outerHTML"
- :key="index"
- />
+ <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 cbe4774a360..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);
}
},
@@ -95,9 +100,9 @@ export default {
class="btn-group"
role="group">
<a
- :href="resolveAllDiscussionsIssuePath"
v-tooltip
- title="Resolve all discussions in new issue"
+ :href="resolveAllDiscussionsIssuePath"
+ :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>
@@ -108,11 +113,11 @@ export default {
class="btn-group"
role="group">
<button
- @click="jumpToFirstDiscussion"
v-tooltip
title="Jump to first unresolved discussion"
data-container="body"
- class="btn btn-default discussion-next-btn">
+ class="btn btn-default discussion-next-btn"
+ @click="jumpToFirstUnresolvedDiscussion">
<span v-html="nextDiscussionSvg"></span>
</button>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index 13283b187d1..de0a5f8489b 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -14,8 +14,8 @@ export default {
<div class="disabled-comment text-center">
<span class="issuable-note-warning inline">
<icon
- name="lock"
:size="16"
+ name="lock"
class="icon"
/>
<span>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 626b0799581..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,16 +134,16 @@ export default {
{{ accessLevel }}
</span>
<div
- v-if="resolvable"
+ v-if="canResolve"
class="note-actions-item">
<button
v-tooltip
- @click="onResolve"
:class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
:title="resolveButtonTitle"
:aria-label="resolveButtonTitle"
type="button"
- class="line-resolve-btn note-action-button">
+ class="line-resolve-btn note-action-button"
+ @click="onResolve">
<template v-if="!isResolving">
<div
v-if="isResolved"
@@ -164,16 +173,16 @@ export default {
>
<loading-icon :inline="true" />
<span
- v-html="emojiSmiling"
- class="link-highlight award-control-icon-neutral">
+ class="link-highlight award-control-icon-neutral"
+ v-html="emojiSmiling">
</span>
<span
- v-html="emojiSmiley"
- class="link-highlight award-control-icon-positive">
+ class="link-highlight award-control-icon-positive"
+ v-html="emojiSmiley">
</span>
<span
- v-html="emojiSmile"
- class="link-highlight award-control-icon-super-positive">
+ class="link-highlight award-control-icon-super-positive"
+ v-html="emojiSmile">
</span>
</a>
</div>
@@ -181,16 +190,16 @@ export default {
v-if="canEdit"
class="note-actions-item">
<button
- @click="onEdit"
v-tooltip
type="button"
title="Edit comment"
class="note-action-button js-note-edit btn btn-transparent"
data-container="body"
- data-placement="bottom">
+ data-placement="bottom"
+ @click="onEdit">
<span
- v-html="editSvg"
- class="link-highlight">
+ class="link-highlight"
+ v-html="editSvg">
</span>
</button>
</div>
@@ -216,11 +225,20 @@ 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
- @click.prevent="onDelete"
class="btn btn-transparent js-note-delete js-note-delete"
- type="button">
+ type="button"
+ @click.prevent="onDelete">
<span class="text-danger">
Delete comment
</span>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index e8fd155a1ee..225d9f18612 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -199,10 +199,11 @@ export default {
:key="index"
:class="getAwardClassBindings(awardList, awardName)"
:title="awardTitle(awardList)"
- @click="handleAward(awardName)"
class="btn award-control"
+ data-boundary="viewport"
data-placement="bottom"
- type="button">
+ type="button"
+ @click="handleAward(awardName)">
<span v-html="getAwardHTML(awardName)"></span>
<span class="award-control-text js-counter">
{{ awardList.length }}
@@ -217,19 +218,20 @@ 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
- v-html="emojiSmiling"
- class="award-control-icon award-control-icon-neutral">
+ class="award-control-icon award-control-icon-neutral"
+ v-html="emojiSmiling">
</span>
<span
- v-html="emojiSmiley"
- class="award-control-icon award-control-icon-positive">
+ class="award-control-icon award-control-icon-positive"
+ v-html="emojiSmiley">
</span>
<span
- v-html="emojiSmile"
- class="award-control-icon award-control-icon-super-positive">
+ class="award-control-icon award-control-icon-super-positive"
+ v-html="emojiSmile">
</span>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 0cb626c14f4..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);
},
},
};
@@ -80,20 +80,20 @@ export default {
<template>
<div
- :class="{ 'js-task-list-container': canEdit }"
ref="note-body"
+ :class="{ 'js-task-list-container': canEdit }"
class="note-body">
<div
- v-html="note.note_html"
- class="note-text md"></div>
+ class="note-text md"
+ v-html="note.note_html"></div>
<note-form
v-if="isEditing"
ref="noteForm"
- @handleFormUpdate="handleFormUpdate"
- @cancelFormEdition="formCancelHandler"
:is-editing="isEditing"
:note-body="noteBody"
:note-id="note.id"
+ @handleFormUpdate="handleFormUpdate"
+ @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 c59a2e7a406..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)"
@@ -165,15 +160,15 @@ export default {
:add-spacing-classes="false">
<textarea
id="note_note"
+ ref="textarea"
+ slot="textarea"
+ :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"
- :data-supports-quick-actions="!isEditing"
aria-label="Description"
- v-model="updatedNoteBody"
- ref="textarea"
- slot="textarea"
- placeholder="Write a comment or drag your files here..."
+ placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleUpdate()"
@keydown.ctrl.enter="handleUpdate()"
@keydown.up="editMyLastNote()"
@@ -182,24 +177,24 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
</markdown-field>
<div class="note-form-actions clearfix">
<button
- type="button"
- @click="handleUpdate()"
:disabled="isDisabled"
- class="js-vue-issue-save btn btn-save">
+ type="button"
+ class="js-vue-issue-save btn btn-save js-comment-button "
+ @click="handleUpdate()">
{{ saveButtonTitle }}
</button>
<button
- v-if="note.resolvable"
- @click.prevent="handleUpdate(true)"
+ v-if="discussion.resolvable"
class="btn btn-nr btn-default append-right-10 js-comment-resolve-button"
+ @click.prevent="handleUpdate(true)"
>
{{ resolveButtonTitle }}
</button>
<button
- @click="cancelHandler()"
- class="btn btn-cancel note-edit-cancel"
- type="button">
- Cancel
+ class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
+ type="button"
+ @click="cancelHandler()">
+ {{ __('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 a4081957207..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,
@@ -66,9 +61,9 @@ export default {
v-if="includeToggle"
class="discussion-actions">
<button
- @click="handleToggle"
class="note-action-button discussion-toggle-button js-vue-toggle-button"
- type="button">
+ type="button"
+ @click="handleToggle">
<i
:class="toggleChevronClass"
class="fa"
@@ -88,18 +83,16 @@ export default {
<template v-if="actionText">
{{ actionText }}
</template>
- <span
- v-if="actionTextHtml"
- v-html="actionTextHtml"
- class="system-note-message">
+ <span class="system-note-message">
+ <slot></slot>
</span>
<span class="system-note-separator">
&middot;
</span>
<a
:href="noteTimestampLink"
- @click="updateTargetNoteHash"
- class="note-timestamp system-note-separator">
+ class="note-timestamp system-note-separator"
+ @click="updateTargetNoteHash">
<time-ago-tooltip
:time="createdAt"
tooltip-placement="bottom"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 7f5aacaa3a2..bee635398b3 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,7 +1,11 @@
<script>
+import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionsSvg from 'icons/_next_discussion.svg';
+import { convertObjectPropsToCamelCase, scrollToElement } from '~/lib/utils/common_utils';
+import { truncateSha } from '~/lib/utils/text_utility';
+import systemNote from '~/vue_shared/components/notes/system_note.vue';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -17,9 +21,9 @@ import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import tooltip from '../../vue_shared/directives/tooltip';
-import { scrollToElement } from '../../lib/utils/common_utils';
export default {
+ name: 'NoteableDiscussion',
components: {
noteableNote,
diffWithNote,
@@ -30,16 +34,32 @@ export default {
noteForm,
placeholderNote,
placeholderSystemNote,
+ systemNote,
},
directives: {
tooltip,
},
mixins: [autosave, noteable, resolvable],
props: {
- note: {
+ discussion: {
type: Object,
required: true,
},
+ renderHeader: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ renderDiffFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ alwaysExpanded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -53,19 +73,27 @@ export default {
'getNoteableData',
'discussionCount',
'resolvedDiscussionCount',
+ 'allDiscussions',
'unresolvedDiscussions',
]),
- discussion() {
+ transformedDiscussion() {
return {
- ...this.note.notes[0],
- truncatedDiffLines: this.note.truncated_diff_lines,
- diffFile: this.note.diff_file,
- diffDiscussion: this.note.diff_discussion,
- imageDiffHtml: this.note.image_diff_html,
+ ...this.discussion.notes[0],
+ truncatedDiffLines: this.discussion.truncated_diff_lines || [],
+ truncatedDiffLinesPath: this.discussion.truncated_diff_lines_path,
+ diffFile: this.discussion.diff_file,
+ diffDiscussion: this.discussion.diff_discussion,
+ imageDiffHtml: this.discussion.image_diff_html,
+ active: this.discussion.active,
+ discussionPath: this.discussion.discussion_path,
+ resolved: this.discussion.resolved,
+ resolvedBy: this.discussion.resolved_by,
+ resolvedByPush: this.discussion.resolved_by_push,
+ resolvedAt: this.discussion.resolved_at,
};
},
author() {
- return this.discussion.author;
+ return this.transformedDiscussion.author;
},
canReply() {
return this.getNoteableData.current_user.can_create_note;
@@ -74,7 +102,7 @@ export default {
return this.getNoteableData.create_note_path;
},
lastUpdatedBy() {
- const { notes } = this.note;
+ const { notes } = this.discussion;
if (notes.length > 1) {
return notes[notes.length - 1].author;
@@ -83,7 +111,7 @@ export default {
return null;
},
lastUpdatedAt() {
- const { notes } = this.note;
+ const { notes } = this.discussion;
if (notes.length > 1) {
return notes[notes.length - 1].created_at;
@@ -91,27 +119,40 @@ export default {
return null;
},
- hasUnresolvedDiscussion() {
- return this.unresolvedDiscussions.length > 0;
+ resolvedText() {
+ return this.transformedDiscussion.resolvedByPush ? 'Automatically resolved' : 'Resolved';
+ },
+ hasMultipleUnresolvedDiscussions() {
+ return this.unresolvedDiscussions.length > 1;
+ },
+ shouldRenderDiffs() {
+ const { diffDiscussion, diffFile } = this.transformedDiscussion;
+
+ return diffDiscussion && diffFile && this.renderDiffFile;
},
wrapperComponent() {
- return this.discussion.diffDiscussion && this.discussion.diffFile
- ? diffWithNote
- : 'div';
+ return this.shouldRenderDiffs ? diffWithNote : 'div';
+ },
+ wrapperComponentProps() {
+ if (this.shouldRenderDiffs) {
+ return { discussion: convertObjectPropsToCamelCase(this.discussion) };
+ }
+
+ return {};
},
wrapperClass() {
- return this.isDiffDiscussion ? '' : 'card';
+ return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
},
},
mounted() {
if (this.isReplying) {
- this.initAutoSave(this.discussion.noteable_type);
+ this.initAutoSave(this.transformedDiscussion);
}
},
updated() {
if (this.isReplying) {
if (!this.autosave) {
- this.initAutoSave(this.discussion.noteable_type);
+ this.initAutoSave(this.transformedDiscussion);
} else {
this.setAutoSave();
}
@@ -127,7 +168,9 @@ export default {
'toggleDiscussion',
'removePlaceholderNotes',
'toggleResolveNote',
+ 'expandDiscussion',
]),
+ truncateSha,
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
@@ -136,23 +179,25 @@ export default {
return placeholderNote;
}
+ if (note.system) {
+ return systemNote;
+ }
+
return noteableNote;
},
componentData(note) {
- return note.isPlaceholderNote ? this.note.notes[0] : note;
+ return note.isPlaceholderNote ? this.discussion.notes[0] : note;
},
toggleDiscussionHandler() {
- this.toggleDiscussion({ discussionId: this.note.id });
+ this.toggleDiscussion({ discussionId: this.discussion.id });
},
showReplyForm() {
this.isReplying = true;
},
cancelReplyForm(shouldConfirm) {
if (shouldConfirm && this.$refs.noteForm.isDirty) {
- const msg = 'Are you sure you want to cancel creating this comment?';
-
// eslint-disable-next-line no-alert
- if (!confirm(msg)) {
+ if (!window.confirm('Are you sure you want to cancel creating this comment?')) {
return;
}
}
@@ -161,18 +206,23 @@ export default {
this.isReplying = false;
},
saveReply(noteText, form, callback) {
+ const postData = {
+ in_reply_to_discussion_id: this.discussion.reply_id,
+ target_type: this.getNoteableData.targetType,
+ note: { note: noteText },
+ };
+
+ if (this.discussion.for_commit) {
+ postData.note_project_id = this.discussion.project_id;
+ }
+
const replyData = {
endpoint: this.newNotePath,
flashContainer: this.$el,
- data: {
- in_reply_to_discussion_id: this.note.reply_id,
- target_type: this.noteableType,
- target_id: this.discussion.noteable_id,
- note: { note: noteText },
- },
+ data: postData,
};
- this.isReplying = false;
+ this.isReplying = false;
this.saveNote(replyData)
.then(() => {
this.resetAutoSave();
@@ -190,15 +240,19 @@ Please check your network connection and try again.`;
});
});
},
- jumpToDiscussion() {
+ jumpToNextDiscussion() {
+ const discussionIds = this.allDiscussions.map(d => d.id);
const unresolvedIds = this.unresolvedDiscussions.map(d => d.id);
- const index = unresolvedIds.indexOf(this.note.id);
+ const currentIndex = discussionIds.indexOf(this.discussion.id);
+ const remainingAfterCurrent = discussionIds.slice(currentIndex + 1);
+ const nextIndex = _.findIndex(remainingAfterCurrent, id => unresolvedIds.indexOf(id) > -1);
- if (index >= 0 && index !== unresolvedIds.length) {
- const nextId = unresolvedIds[index + 1];
+ if (nextIndex > -1) {
+ const nextId = remainingAfterCurrent[nextIndex];
const el = document.querySelector(`[data-discussion-id="${nextId}"]`);
if (el) {
+ this.expandDiscussion({ discussionId: nextId });
scrollToElement(el);
}
}
@@ -208,9 +262,7 @@ Please check your network connection and try again.`;
</script>
<template>
- <li
- :data-discussion-id="note.id"
- class="note note-discussion timeline-entry">
+ <li class="note note-discussion timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
@@ -221,20 +273,52 @@ Please check your network connection and try again.`;
/>
</div>
<div class="timeline-content">
- <div class="discussion">
- <div class="discussion-header">
+ <div
+ :data-discussion-id="transformedDiscussion.discussion_id"
+ class="discussion js-discussion-container"
+ >
+ <div
+ v-if="renderHeader"
+ class="discussion-header"
+ >
<note-header
:author="author"
- :created-at="discussion.created_at"
- :note-id="discussion.id"
+ :created-at="transformedDiscussion.created_at"
+ :note-id="transformedDiscussion.id"
:include-toggle="true"
- :expanded="note.expanded"
+ :expanded="discussion.expanded"
@toggleHandler="toggleDiscussionHandler"
- action-text="started a discussion"
- class="discussion"
+ >
+ <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,28 +344,29 @@ 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
- @click="showReplyForm"
type="button"
- class="js-vue-discussion-reply btn btn-text-field"
- title="Add a reply">Reply...</button>
+ 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
- @click="resolveHandler()"
type="button"
- class="btn btn-default"
+ class="btn btn-default mr-2"
+ @click="resolveHandler()"
>
<i
v-if="isResolving"
@@ -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"
>
@@ -301,26 +386,26 @@ Please check your network connection and try again.`;
class="btn-group"
role="group">
<a
- :href="note.resolve_with_issue_path"
v-tooltip
+ :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
- @click="jumpToDiscussion"
v-tooltip
class="btn btn-default discussion-next-btn"
title="Jump to next unresolved discussion"
data-container="body"
+ @click="jumpToNextDiscussion"
>
<span v-html="nextDiscussionsSvg"></span>
</button>
@@ -330,12 +415,12 @@ Please check your network connection and try again.`;
</template>
<note-form
v-if="isReplying"
- save-button-title="Comment"
- :note="note"
+ ref="noteForm"
+ :discussion="discussion"
:is-editing="false"
+ save-button-title="Comment"
@handleFormUpdate="saveReply"
- @cancelFormEdition="cancelReplyForm"
- ref="noteForm" />
+ @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 566f5c68e66..4ebeb5599f2 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -12,6 +12,7 @@ import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
export default {
+ name: 'NoteableNote',
components: {
userAvatarLink,
noteHeader,
@@ -34,26 +35,31 @@ export default {
};
},
computed: {
- ...mapGetters(['targetNoteHash', 'getUserData']),
+ ...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData']),
author() {
return this.note.author;
},
classNameBindings() {
return {
+ [`note-row-${this.note.id}`]: true,
'is-editing': this.isEditing && !this.isRequesting,
'is-requesting being-posted': this.isRequesting,
'disabled-content': this.isDeleting,
- target: this.targetNoteHash === this.noteAnchorId,
+ target: this.isTarget,
};
},
+ canResolve() {
+ return this.note.resolvable && !!this.getUserData.id;
+ },
canReportAsAbuse() {
- return (
- this.note.report_abuse_path && this.author.id !== this.getUserData.id
- );
+ return this.note.report_abuse_path && this.author.id !== this.getUserData.id;
},
noteAnchorId() {
return `note_${this.note.id}`;
},
+ isTarget() {
+ return this.targetNoteHash === this.noteAnchorId;
+ },
},
created() {
@@ -65,19 +71,20 @@ export default {
});
},
+ mounted() {
+ if (this.isTarget) {
+ this.scrollToNoteIfNeeded($(this.$el));
+ }
+ },
+
methods: {
- ...mapActions([
- 'deleteNote',
- 'updateNote',
- 'toggleResolveNote',
- 'scrollToNoteIfNeeded',
- ]),
+ ...mapActions(['deleteNote', 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded']),
editHandler() {
this.isEditing = true;
},
deleteHandler() {
// eslint-disable-next-line no-alert
- if (confirm('Are you sure you want to delete this comment?')) {
+ if (window.confirm('Are you sure you want to delete this comment?')) {
this.isDeleting = true;
this.deleteNote(this.note)
@@ -85,9 +92,7 @@ export default {
this.isDeleting = false;
})
.catch(() => {
- Flash(
- 'Something went wrong while deleting your note. Please try again.',
- );
+ Flash('Something went wrong while deleting your note. Please try again.');
this.isDeleting = false;
});
}
@@ -96,7 +101,7 @@ export default {
const data = {
endpoint: this.note.path,
note: {
- target_type: this.noteableType,
+ target_type: this.getNoteableData.targetType,
target_id: this.note.noteable_id,
note: { note: noteText },
},
@@ -118,8 +123,7 @@ export default {
this.isRequesting = false;
this.isEditing = true;
this.$nextTick(() => {
- const msg =
- 'Something went wrong while editing your comment. Please try again.';
+ const msg = 'Something went wrong while editing your comment. Please try again.';
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
@@ -129,8 +133,7 @@ export default {
formCancelHandler(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert
- if (!confirm('Are you sure you want to cancel editing this comment?'))
- return;
+ if (!window.confirm('Are you sure you want to cancel editing this comment?')) return;
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
@@ -143,7 +146,7 @@ export default {
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
- this.$refs.noteBody.$refs.noteForm.note.note = noteText;
+ this.$refs.noteBody.note.note = noteText;
},
},
};
@@ -151,10 +154,12 @@ export default {
<template>
<li
- class="note timeline-entry"
:id="noteAnchorId"
:class="classNameBindings"
- :data-award-url="note.toggle_award_path">
+ :data-award-url="note.toggle_award_path"
+ :data-note-id="note.id"
+ class="note timeline-entry"
+ >
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
@@ -170,16 +175,17 @@ export default {
:author="author"
:created-at="note.created_at"
:note-id="note.id"
- action-text="commented"
/>
<note-actions
:author-id="author.id"
:note-id="note.id"
+ :note-url="note.noteable_note_url"
:access-level="note.human_access"
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
+ :can-resolve="note.current_user.can_resolve"
:report-abuse-path="note.report_abuse_path"
:resolvable="note.resolvable"
:is-resolved="note.resolved"
@@ -191,12 +197,12 @@ export default {
/>
</div>
<note-body
+ ref="noteBody"
:note="note"
:can-edit="note.current_user.can_edit"
:is-editing="isEditing"
@handleFormUpdate="formUpdateHandler"
- @cancelFormEdition="formCancelHandler"
- ref="noteBody"
+ @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 c4de4826eda..2c3e07c0506 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -11,9 +11,10 @@ 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';
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
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/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js
new file mode 100644
index 00000000000..fa4a1c56b20
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/collapse_utils.js
@@ -0,0 +1,108 @@
+import { n__, s__, sprintf } from '~/locale';
+import { DESCRIPTION_TYPE } from '../constants';
+
+/**
+ * Changes the description from a note, returns 'changed the description n number of times'
+ */
+export const changeDescriptionNote = (note, descriptionChangedTimes, timeDifferenceMinutes) => {
+ const descriptionNote = Object.assign({}, note);
+
+ descriptionNote.note_html = sprintf(
+ s__(`MergeRequest|
+ %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}`),
+ {
+ paragraphStart: '<p dir="auto">',
+ paragraphEnd: '</p>',
+ descriptionChangedTimes,
+ timeDifferenceMinutes: n__('within %d minute ', 'within %d minutes ', timeDifferenceMinutes),
+ },
+ false,
+ );
+
+ descriptionNote.times_updated = descriptionChangedTimes;
+
+ return descriptionNote;
+};
+
+/**
+ * Checks the time difference between two notes from their 'created_at' dates
+ * returns an integer
+ */
+
+export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => {
+ const descriptionNoteBegin = new Date(noteBeggining.created_at);
+ const descriptionNoteEnd = new Date(noteEnd.created_at);
+ const timeDifferenceMinutes = (descriptionNoteEnd - descriptionNoteBegin) / 1000 / 60;
+
+ return Math.ceil(timeDifferenceMinutes);
+};
+
+/**
+ * Checks if a note is a system note and if the content is description
+ *
+ * @param {Object} note
+ * @returns {Boolean}
+ */
+export const isDescriptionSystemNote = note => note.system && note.note === DESCRIPTION_TYPE;
+
+/**
+ * Collapses the system notes of a description type, e.g. Changed the description, n minutes ago
+ * the notes will collapse as long as they happen no more than 10 minutes away from each away
+ * in between the notes can be anything, another type of system note
+ * (such as 'changed the weight') or a comment.
+ *
+ * @param {Array} notes
+ * @returns {Array}
+ */
+export const collapseSystemNotes = notes => {
+ let lastDescriptionSystemNote = null;
+ let lastDescriptionSystemNoteIndex = -1;
+ let descriptionChangedTimes = 1;
+
+ return notes.slice(0).reduce((acc, currentNote) => {
+ const note = currentNote.notes[0];
+
+ if (isDescriptionSystemNote(note)) {
+ // is it the first one?
+ if (!lastDescriptionSystemNote) {
+ lastDescriptionSystemNote = note;
+ lastDescriptionSystemNoteIndex = acc.length;
+ } else if (lastDescriptionSystemNote) {
+ const timeDifferenceMinutes = getTimeDifferenceMinutes(
+ lastDescriptionSystemNote,
+ note,
+ );
+
+ // are they less than 10 minutes appart?
+ if (timeDifferenceMinutes > 10) {
+ // reset counter
+ descriptionChangedTimes = 1;
+ // update the previous system note
+ lastDescriptionSystemNote = note;
+ lastDescriptionSystemNoteIndex = acc.length;
+ } else {
+ // increase counter
+ descriptionChangedTimes += 1;
+
+ // delete the previous one
+ acc.splice(lastDescriptionSystemNoteIndex, 1);
+
+ // replace the text of the current system note with the collapsed note.
+ currentNote.notes.splice(
+ 0,
+ 1,
+ changeDescriptionNote(note, descriptionChangedTimes, timeDifferenceMinutes),
+ );
+
+ // update the previous system note index
+ lastDescriptionSystemNoteIndex = acc.length;
+ }
+ }
+ }
+ acc.push(currentNote);
+ return acc;
+ }, []);
+};
+
+// for babel-rewire
+export default {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 787be6f4c99..a5518383d44 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,56 +1,91 @@
import _ from 'underscore';
+import * as constants from '../constants';
+import { collapseSystemNotes } from './collapse_utils';
+
+export const discussions = state => collapseSystemNotes(state.discussions);
-export const notes = state => state.notes;
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.forEach(n => {
if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system);
@@ -69,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/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
index ba1d8e4d8db..bc84666779e 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
+++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
@@ -40,8 +40,8 @@
<gl-modal
id="stop-jobs-modal"
:header-title-text="s__('AdminArea|Stop all jobs?')"
- footer-primary-button-variant="danger"
:footer-primary-button-text="s__('AdminArea|Stop jobs')"
+ footer-primary-button-variant="danger"
@submit="onSubmit"
>
{{ text }}
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index 343c65edb37..ff66d3a8ac4 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -83,9 +83,9 @@
id="delete-project-modal"
:title="title"
:text="text"
- kind="danger"
:primary-button-label="primaryButtonLabel"
:submit-disabled="!canSubmit"
+ kind="danger"
@submit="onSubmit"
@cancel="onCancel"
>
@@ -107,15 +107,15 @@
value="delete"
/>
<input
+ :value="csrfToken"
type="hidden"
name="authenticity_token"
- :value="csrfToken"
/>
<input
+ v-model="enteredProjectName"
name="projectName"
class="form-control"
type="text"
- v-model="enteredProjectName"
aria-labelledby="input-label"
autocomplete="off"
/>
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 9ce176744ba..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';
@@ -116,10 +116,10 @@
id="delete-user-modal"
:title="title"
:text="text"
- kind="danger"
:primary-button-label="primaryButtonLabel"
:secondary-button-label="secondaryButtonLabel"
:submit-disabled="!canSubmit"
+ kind="danger"
@submit="onSubmit"
@cancel="onCancel"
>
@@ -141,15 +141,15 @@
value="delete"
/>
<input
+ :value="csrfToken"
type="hidden"
name="authenticity_token"
- :value="csrfToken"
/>
<input
+ v-model="enteredUsername"
type="text"
name="username"
class="form-control"
- v-model="enteredUsername"
aria-labelledby="input-label"
autocomplete="off"
/>
@@ -160,11 +160,11 @@
slot-scope="props"
>
<button
+ :disabled="!canSubmit"
type="button"
class="btn js-secondary-button btn-warning"
- :disabled="!canSubmit"
- @click="onSecondaryAction"
data-dismiss="modal"
+ @click="onSecondaryAction"
>
{{ secondaryButtonLabel }}
</button>
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index c334eaa90f8..ff19b9a9c30 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';
@@ -61,7 +61,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/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index 16f792d635a..4061c11ba8f 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -96,8 +96,8 @@ Once deleted, it cannot be undone or recovered.`),
id="delete-milestone-modal"
:title="title"
:text="text"
- kind="danger"
:primary-button-label="s__('Milestones|Delete milestone')"
+ kind="danger"
@submit="onSubmit">
<template
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index 2bda2aeb3a1..2c683a39f42 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -53,8 +53,8 @@
<template>
<gl-modal
id="promote-milestone-modal"
- footer-primary-button-variant="warning"
:footer-primary-button-text="s__('Milestones|Promote Milestone')"
+ footer-primary-button-variant="warning"
@submit="onSubmit"
>
<template
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
index 653e2502d01..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';
@@ -36,7 +36,9 @@ export default (function() {
var author_graph, author_header;
author_header = _this.create_author_header(d);
$(".contributors-list").append(author_header);
- _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates);
+
+ author_graph = new ContributorsAuthorGraph(d.dates);
+ _this.authors[d.author_name] = author_graph;
return author_graph.draw();
};
})(this));
@@ -78,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);
@@ -100,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 77135ad1f0e..d12249bf612 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
+/* eslint-disable func-names, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
import _ from 'underscore';
export default {
@@ -111,10 +111,15 @@ export default {
parse_log_entry: function(log_entry, field, date_range) {
var parsed_entry;
parsed_entry = {};
+
parsed_entry.author_name = log_entry.author_name;
parsed_entry.author_email = log_entry.author_email;
parsed_entry.dates = {};
- parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
+
+ parsed_entry.commits = 0;
+ parsed_entry.additions = 0;
+ parsed_entry.deletions = 0;
+
_.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
return function(value, key) {
if (_this.in_range(value.date, date_range)) {
diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js
index 82143fa875a..56ab3fcdfcb 100644
--- a/app/assets/javascripts/pages/projects/init_blob.js
+++ b/app/assets/javascripts/pages/projects/init_blob.js
@@ -8,7 +8,8 @@ import initBlobBundle from '~/blob_edit/blob_bundle';
export default () => {
new LineHighlighter(); // eslint-disable-line no-new
- new BlobLinePermalinkUpdater( // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new BlobLinePermalinkUpdater(
document.querySelector('#blob-content-holder'),
'.diff-line-num[data-line-number]',
document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
@@ -19,12 +20,13 @@ export default () => {
new ShortcutsNavigation(); // eslint-disable-line no-new
- new ShortcutsBlob({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new ShortcutsBlob({
skipResetBindings: true,
fileBlobPermalinkUrl,
});
- new BlobForkSuggestion({ // eslint-disable-line no-new
+ new BlobForkSuggestion({
openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'),
forkButtons: document.querySelectorAll('.js-fork-suggestion-button'),
cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
diff --git a/app/assets/javascripts/pages/projects/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/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index ad6df51bb7a..5d2247f6c6d 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -71,8 +71,8 @@
<template>
<gl-modal
id="promote-label-modal"
- footer-primary-button-variant="warning"
:footer-primary-button-text="s__('Labels|Promote Label')"
+ footer-primary-button-variant="warning"
@submit="onSubmit"
>
<div
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/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index 2d18fa2044b..d0613804067 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -65,11 +65,11 @@
<div class="cron-preset-radio-input">
<input
id="custom"
- class="label-light"
- type="radio"
:name="inputNameAttribute"
:value="cronInterval"
:checked="isEditable"
+ class="label-light"
+ type="radio"
@click="toggleCustomInput(true)"
/>
@@ -90,11 +90,11 @@
<div class="cron-preset-radio-input">
<input
id="every-day"
- class="label-light"
- type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyDay"
+ class="label-light"
+ type="radio"
@click="toggleCustomInput(false)"
/>
@@ -109,11 +109,11 @@
<div class="cron-preset-radio-input">
<input
id="every-week"
- class="label-light"
- type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyWeek"
+ class="label-light"
+ type="radio"
@click="toggleCustomInput(false)"
/>
@@ -128,11 +128,11 @@
<div class="cron-preset-radio-input">
<input
id="every-month"
- class="label-light"
- type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyMonth"
+ class="label-light"
+ type="radio"
@click="toggleCustomInput(false)"
/>
@@ -147,13 +147,13 @@
<div class="cron-interval-input-wrapper">
<input
id="schedule_cron"
- class="form-control inline cron-interval-input"
- type="text"
:placeholder="__('Define a custom pattern with cron syntax')"
- required="true"
v-model="cronInterval"
:name="inputNameAttribute"
:disabled="!isEditable"
+ class="form-control inline cron-interval-input"
+ type="text"
+ required="true"
/>
</div>
</div>
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 6c2a785c0af..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
@@ -22,4 +22,18 @@ document.addEventListener('DOMContentLoaded', () => {
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
});
+
+ // hide extra auto devops settings based on data-attributes
+ const autoDevOpsSettings = document.querySelector('.js-auto-devops-settings');
+ const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
+
+ autoDevOpsSettings.addEventListener('click', event => {
+ const { target } = event;
+ if (target.classList.contains('js-toggle-extra-settings')) {
+ autoDevOpsExtraSettings.classList.toggle(
+ 'hidden',
+ !!(target.dataset && target.dataset.hideExtraSettings),
+ );
+ }
+ });
});
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/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
index 9b13b2a524f..06101290f6c 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
@@ -72,25 +72,25 @@
<template>
<div
- class="project-feature-controls"
:data-for="name"
+ class="project-feature-controls"
>
<input
v-if="name"
- type="hidden"
:name="name"
:value="value"
+ type="hidden"
/>
<project-feature-toggle
:value="featureEnabled"
- @change="toggleFeature"
:disabled-input="disabledInput"
+ @change="toggleFeature"
/>
<div class="select-wrapper">
<select
+ :disabled="displaySelectInput"
class="form-control project-repo-select select-control"
@change="selectOption"
- :disabled="displaySelectInput"
>
<option
v-for="[optionValue, optionName] in displayOptions"
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 06b0ab184ed..ae88b765abf 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -175,16 +175,16 @@
<div>
<div class="project-visibility-setting">
<project-setting-row
- label="Project visibility"
:help-path="visibilityHelpPath"
+ label="Project visibility"
>
<div class="project-feature-controls">
<div class="select-wrapper">
<select
- name="project[visibility_level]"
v-model="visibilityLevel"
- class="form-control select-control"
:disabled="!canChangeVisibilityLevel"
+ name="project[visibility_level]"
+ class="form-control select-control"
>
<option
:value="visibilityOptions.PRIVATE"
@@ -219,30 +219,30 @@
class="request-access"
>
<input
+ :value="requestAccessEnabled"
type="hidden"
name="project[request_access_enabled]"
- :value="requestAccessEnabled"
/>
<input
- type="checkbox"
v-model="requestAccessEnabled"
+ type="checkbox"
/>
Allow users to request access
</label>
</project-setting-row>
</div>
<div
- class="project-feature-settings"
:class="{ 'highlight-changes': highlightChangesClass }"
+ class="project-feature-settings"
>
<project-setting-row
label="Issues"
help-text="Lightweight issue tracking system for this project"
>
<project-feature-setting
- name="project[project_feature_attributes][issues_access_level]"
:options="featureAccessLevelOptions"
v-model="issuesAccessLevel"
+ name="project[project_feature_attributes][issues_access_level]"
/>
</project-setting-row>
<project-setting-row
@@ -250,9 +250,9 @@
help-text="View and edit files in this project"
>
<project-feature-setting
- name="project[project_feature_attributes][repository_access_level]"
:options="featureAccessLevelOptions"
v-model="repositoryAccessLevel"
+ name="project[project_feature_attributes][repository_access_level]"
/>
</project-setting-row>
<div class="project-feature-setting-group">
@@ -261,10 +261,10 @@
help-text="Submit changes to be merged upstream"
>
<project-feature-setting
- name="project[project_feature_attributes][merge_requests_access_level]"
:options="repoFeatureAccessLevelOptions"
v-model="mergeRequestsAccessLevel"
:disabled-input="!repositoryEnabled"
+ name="project[project_feature_attributes][merge_requests_access_level]"
/>
</project-setting-row>
<project-setting-row
@@ -272,34 +272,34 @@
help-text="Build, test, and deploy your changes"
>
<project-feature-setting
- name="project[project_feature_attributes][builds_access_level]"
:options="repoFeatureAccessLevelOptions"
v-model="buildsAccessLevel"
:disabled-input="!repositoryEnabled"
+ name="project[project_feature_attributes][builds_access_level]"
/>
</project-setting-row>
<project-setting-row
v-if="registryAvailable"
- label="Container registry"
:help-path="registryHelpPath"
+ label="Container registry"
help-text="Every project can have its own space to store its Docker images"
>
<project-feature-toggle
- name="project[container_registry_enabled]"
v-model="containerRegistryEnabled"
:disabled-input="!repositoryEnabled"
+ name="project[container_registry_enabled]"
/>
</project-setting-row>
<project-setting-row
v-if="lfsAvailable"
- label="Git Large File Storage"
:help-path="lfsHelpPath"
+ label="Git Large File Storage"
help-text="Manages large files such as audio, video, and graphics files"
>
<project-feature-toggle
- name="project[lfs_enabled]"
v-model="lfsEnabled"
:disabled-input="!repositoryEnabled"
+ name="project[lfs_enabled]"
/>
</project-setting-row>
</div>
@@ -308,9 +308,9 @@
help-text="Pages for project documentation"
>
<project-feature-setting
- name="project[project_feature_attributes][wiki_access_level]"
:options="featureAccessLevelOptions"
v-model="wikiAccessLevel"
+ name="project[project_feature_attributes][wiki_access_level]"
/>
</project-setting-row>
<project-setting-row
@@ -318,9 +318,9 @@
help-text="Share code pastes with others out of Git repository"
>
<project-feature-setting
- name="project[project_feature_attributes][snippets_access_level]"
:options="featureAccessLevelOptions"
v-model="snippetsAccessLevel"
+ name="project[project_feature_attributes][snippets_access_level]"
/>
</project-setting-row>
</div>
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/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
index 5765eed4d45..0289209ff1e 100644
--- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
@@ -50,8 +50,8 @@ export default {
<gl-modal
id="delete-wiki-modal"
:header-title-text="title"
- footer-primary-button-variant="danger"
:footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')"
+ footer-primary-button-variant="danger"
@submit="onSubmit"
>
{{ message }}
@@ -68,9 +68,9 @@ export default {
value="delete"
/>
<input
+ :value="csrfToken"
type="hidden"
name="authenticity_token"
- :value="csrfToken"
/>
</form>
</gl-modal>
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 825de01b5a2..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';
@@ -62,13 +62,13 @@ export default class UsernameValidator {
return this.setPendingState();
}
- if (!this.state.available) {
- return this.setUnavailableState();
- }
-
if (!this.state.valid) {
return this.setInvalidState();
}
+
+ if (!this.state.available) {
+ return this.setUnavailableState();
+ }
}
interceptInvalid(event) {
@@ -89,7 +89,6 @@ export default class UsernameValidator {
setAvailabilityState(usernameTaken) {
if (usernameTaken) {
- this.state.valid = false;
this.state.available = false;
} else {
this.state.available = true;
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/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 9404b06615e..a2ca03536f2 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -180,14 +180,14 @@ export default class UserTabs {
}
toggleLoading(status) {
- return this.$parentEl.find('.loading-status .loading').toggleClass('hidden', !status);
+ return this.$parentEl.find('.loading-status .loading').toggleClass('hide', !status);
}
setCurrentAction(source) {
let newState = source;
newState = newState.replace(/\/+$/, '');
newState += this.windowLocation.search + this.windowLocation.hash;
- history.replaceState(
+ window.history.replaceState(
{
url: newState,
},
diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue
index 00f32d9de78..2f480ecdc69 100644
--- a/app/assets/javascripts/pdf/index.vue
+++ b/app/assets/javascripts/pdf/index.vue
@@ -56,8 +56,8 @@
<template>
<div
- class="pdf-viewer"
- v-if="hasPDF">
+ v-if="hasPDF"
+ class="pdf-viewer">
<page
v-for="(page, index) in pages"
:key="index"
diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue
index fcba819beba..9f06833d560 100644
--- a/app/assets/javascripts/pdf/page/index.vue
+++ b/app/assets/javascripts/pdf/page/index.vue
@@ -43,9 +43,9 @@
<template>
<canvas
- class="pdf-page"
ref="canvas"
:data-page="number"
+ class="pdf-page"
>
</canvas>
</template>
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index db8a0055acd..dc7d6d29b8f 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -39,9 +39,9 @@ export default {
</script>
<template>
<div
+ v-if="currentRequest.details"
:id="`peek-view-${metric}`"
class="view"
- v-if="currentRequest.details"
>
<button
:data-target="`#modal-peek-${metric}-details`"
@@ -56,6 +56,7 @@ export default {
<gl-modal
:id="`modal-peek-${metric}-details`"
:header-title-text="header"
+ modal-size="xl"
class="performance-bar-modal"
>
<table
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 82b4ce083fb..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');
@@ -83,15 +83,16 @@ export default {
</script>
<template>
<button
- type="button"
- @click="onClickAction"
v-tooltip
:title="tooltipText"
+ :class="cssClass"
+ :disabled="isDisabled"
+ type="button"
class="js-ci-action btn btn-blank
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
- :class="cssClass"
data-container="body"
- :disabled="isDisabled"
+ data-boundary="viewport"
+ @click="onClickAction"
>
<icon :name="actionIcon"/>
</button>
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 e64afc94ef9..c32dc83da8e 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -82,12 +82,13 @@ export default {
<div class="ci-job-dropdown-container dropdown dropright">
<button
v-tooltip
+ :title="tooltipText"
type="button"
data-toggle="dropdown"
data-container="body"
data-boundary="viewport"
+ data-display="static"
class="dropdown-menu-toggle build-content"
- :title="tooltipText"
>
<job-name-component
@@ -108,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 dc16d395bcb..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"
>
@@ -107,11 +116,11 @@ export default {
</a>
<div
- v-else
v-tooltip
- class="js-job-component-tooltip non-details-job-component"
+ v-else
:title="tooltipText"
:class="cssClassJobName"
+ class="js-job-component-tooltip non-details-job-component"
data-html="true"
data-container="body"
>
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/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index f32368947e8..2c728582b7c 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -52,8 +52,8 @@ export default {
</script>
<template>
<li
- class="stage-column"
- :class="stageConnectorClass">
+ :class="stageConnectorClass"
+ class="stage-column">
<div class="stage-name">
{{ title }}
</div>
@@ -62,9 +62,9 @@ export default {
<li
v-for="(job, index) in jobs"
:key="job.id"
- class="build"
:class="buildConnnectorClass(index)"
:id="jobId(job)"
+ class="build"
>
<div class="curve"></div>
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index e08c2092680..001eaeaa065 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -1,92 +1,92 @@
<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">
<ci-header
v-if="shouldRenderContent"
:status="status"
- item-name="Pipeline"
:item-id="pipeline.id"
:time="pipeline.created_at"
:user="pipeline.user"
:actions="actions"
+ item-name="Pipeline"
@actionClicked="postAction"
/>
<loading-icon
diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue
index eba5678e3e5..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">
@@ -50,10 +50,10 @@
<loading-button
v-if="resetCachePath"
- @click="onClickResetCache"
:loading="isResetCacheButtonLoading"
- class="btn btn-default js-clear-cache"
:label="s__('Pipelines|Clear Runner Caches')"
+ class="btn btn-default js-clear-cache"
+ @click="onClickResetCache"
/>
<a
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 4d965733f95..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">
@@ -55,10 +55,10 @@
<span>by</span>
<user-avatar-link
v-if="user"
- class="js-pipeline-url-user"
:link-href="pipeline.user.path"
:img-src="pipeline.user.avatar_url"
:tooltip-text="pipeline.user.name"
+ class="js-pipeline-url-user"
/>
<span
v-if="!user"
@@ -67,31 +67,31 @@
</span>
<div class="label-container">
<span
- v-if="pipeline.flags.latest"
v-tooltip
+ v-if="pipeline.flags.latest"
class="js-pipeline-url-latest badge badge-success"
title="Latest pipeline for this branch">
latest
</span>
<span
- v-if="pipeline.flags.yaml_errors"
v-tooltip
- class="js-pipeline-url-yaml badge badge-danger"
- :title="pipeline.yaml_errors">
+ v-if="pipeline.flags.yaml_errors"
+ :title="pipeline.yaml_errors"
+ class="js-pipeline-url-yaml badge badge-danger">
yaml invalid
</span>
<span
- v-if="pipeline.flags.failure_reason"
v-tooltip
- class="js-pipeline-url-failure badge badge-danger"
- :title="pipeline.failure_reason">
+ v-if="pipeline.flags.failure_reason"
+ :title="pipeline.failure_reason"
+ class="js-pipeline-url-failure badge badge-danger">
error
</span>
<a
+ v-popover="popoverOptions"
v-if="pipeline.flags.auto_devops"
tabindex="0"
class="js-pipeline-url-autodevops badge badge-info autodevops-badge"
- v-popover="popoverOptions"
role="button">
Auto DevOps
</a>
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 497a09cec65..c9d2dc3a3c5 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -1,289 +1,289 @@
<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,
},
- scopes: {
- all: 'all',
- pending: 'pending',
- running: 'running',
- finished: 'finished',
- branches: 'branches',
- tags: 'tags',
+ canCreatePipeline: {
+ type: Boolean,
+ required: true,
},
- 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;
+ ciLintPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ resetCachePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ 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',
- if (this.isLoading) {
- return stateMap.loading;
- }
+ // 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.hasError) {
- return stateMap.error;
- }
+ if (this.isLoading) {
+ return stateMap.loading;
+ }
- if (this.state.pipelines.length) {
- return stateMap.tableList;
- }
+ if (this.hasError) {
+ return stateMap.error;
+ }
- if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) {
- return stateMap.emptyTab;
- }
+ if (this.state.pipelines.length) {
+ return stateMap.tableList;
+ }
- 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,
- )
- );
- },
+ 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,
+ )
+ );
+ },
- 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">
<div
- class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="shouldRenderTabs || shouldRenderButtons"
+ class="top-area scrolling-tabs-container inner-page-scroll-tabs"
>
<div class="fade-left">
<i
@@ -303,8 +303,8 @@
<navigation-tabs
v-if="shouldRenderTabs"
:tabs="tabs"
- @onChangeTab="onChangeTab"
scope="pipelines"
+ @onChangeTab="onChangeTab"
/>
<navigation-controls
@@ -312,8 +312,8 @@
:new-pipeline-path="newPipelinePath"
:reset-cache-path="resetCachePath"
:ci-lint-path="ciLintPath"
- @resetRunnersCache="handleResetRunnersCache"
:is-reset-cache-button-loading="isResetCacheButtonLoading"
+ @resetRunnersCache="handleResetRunnersCache"
/>
</div>
@@ -347,8 +347,8 @@
/>
<div
- class="table-holder"
v-else-if="stateToRender === $options.stateMap.tableList"
+ class="table-holder"
>
<pipelines-table-component
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index e9bc3cf14ca..1c8d7303c52 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -1,56 +1,56 @@
<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">
<button
v-tooltip
+ :disabled="isLoading"
type="button"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
- :disabled="isLoading"
>
<icon
name="play"
@@ -69,11 +69,11 @@
:key="i"
>
<button
+ :class="{ disabled: isActionDisabled(action) }"
+ :disabled="isActionDisabled(action)"
type="button"
class="js-pipeline-action-link no-btn btn"
@click="onClickAction(action.path)"
- :class="{ disabled: isActionDisabled(action) }"
- :disabled="isActionDisabled(action)"
>
{{ action.name }}
</button>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index 31fcc9dd412..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
@@ -42,9 +42,9 @@
v-for="(artifact, i) in artifacts"
:key="i">
<a
+ :href="artifact.path"
rel="nofollow"
download
- :href="artifact.path"
>
Download {{ artifact.name }} artifacts
</a>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index 4318abe97e0..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">
@@ -114,8 +122,8 @@
<modal
id="confirmation-modal"
:header-title-text="modalTitle"
- footer-primary-button-variant="danger"
:footer-primary-button-text="s__('Pipeline|Stop pipeline')"
+ footer-primary-button-variant="danger"
@submit="onSubmit"
>
<span v-html="modalText"></span>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index a3c17479e6f..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">
@@ -301,9 +299,9 @@
<div class="table-mobile-content">
<template v-if="pipeline.details.stages.length > 0">
<div
- class="stage-container dropdown js-mini-pipeline-graph"
v-for="(stage, index) in pipeline.details.stages"
- :key="index">
+ :key="index"
+ class="stage-container dropdown js-mini-pipeline-graph">
<pipeline-stage
:type="$options.pipelinesTable"
:stage="stage"
@@ -331,28 +329,28 @@
<pipelines-artifacts-component
v-if="pipeline.details.artifacts.length"
- class="d-none d-sm-none d-md-block"
:artifacts="pipeline.details.artifacts"
+ class="d-none d-sm-none d-md-block"
/>
<loading-button
v-if="pipeline.flags.retryable"
- @click="handleRetryClick"
- container-class="js-pipelines-retry-button btn btn-default btn-retry"
:loading="isRetrying"
:disabled="isRetrying"
+ container-class="js-pipelines-retry-button btn btn-default btn-retry"
+ @click="handleRetryClick"
>
<icon name="repeat" />
</loading-button>
<loading-button
v-if="pipeline.flags.cancelable"
- @click="handleCancelClick"
+ :loading="isCancelling"
+ :disabled="isCancelling"
data-toggle="modal"
data-target="#confirmation-modal"
container-class="js-pipelines-cancel-button btn btn-remove"
- :loading="isCancelling"
- :disabled="isCancelling"
+ @click="handleCancelClick"
>
<icon name="close" />
</loading-button>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index f9769815796..56fdb858088 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -158,22 +158,23 @@ export default {
<div class="dropdown">
<button
v-tooltip
+ id="stageDropdown"
+ ref="dropdown"
:class="triggerButtonClass"
- @click="onClickStage"
- class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
:title="stage.title"
+ class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
data-placement="top"
data-toggle="dropdown"
+ data-display="static"
type="button"
- id="stageDropdown"
aria-haspopup="true"
aria-expanded="false"
- ref="dropdown"
+ @click="onClickStage"
>
<span
- aria-hidden="true"
:aria-label="stage.title"
+ aria-hidden="true"
>
<icon :name="borderlessIcon" />
</span>
@@ -185,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 79dbdca4010..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">
@@ -66,8 +64,8 @@
</div>
<div class="table-mobile-content">
<p
- class="duration"
v-if="hasDuration"
+ class="duration"
>
<span v-html="iconTimerSvg">
</span>
@@ -75,8 +73,8 @@
</p>
<p
- class="finished-at d-none d-sm-none d-md-block"
v-if="hasFinishedTime"
+ class="finished-at d-none d-sm-none d-md-block"
>
<i
@@ -87,9 +85,9 @@
<time
v-tooltip
+ :title="tooltipTitle(finishedTime)"
data-placement="top"
- data-container="body"
- :title="tooltipTitle(finishedTime)">
+ data-container="body">
{{ timeFormated(finishedTime) }}
</time>
</p>
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/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index f50002afbf2..974629fa2af 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -80,10 +80,10 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
id="delete-account-modal"
:title="s__('Profiles|Delete your account?')"
:text="text"
- kind="danger"
:primary-button-label="s__('Profiles|Delete account')"
- @submit="onSubmit"
- :submit-disabled="!canSubmit()">
+ :submit-disabled="!canSubmit()"
+ kind="danger"
+ @submit="onSubmit">
<template
slot="body"
@@ -101,9 +101,9 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
value="delete"
/>
<input
+ :value="csrfToken"
type="hidden"
name="authenticity_token"
- :value="csrfToken"
/>
<p
@@ -114,18 +114,18 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
<input
v-if="confirmWithPassword"
+ v-model="enteredPassword"
name="password"
class="form-control"
type="password"
- v-model="enteredPassword"
aria-labelledby="input-label"
/>
<input
v-else
+ v-model="enteredUsername"
name="username"
class="form-control"
type="text"
- v-model="enteredUsername"
aria-labelledby="input-label"
/>
</form>
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index b37febe523c..ef484ddfd61 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -93,10 +93,10 @@ Please update your Git repository remotes as soon as possible.`),
</div>
<input
:id="$options.inputId"
- class="form-control"
- required="required"
v-model="newUsername"
:disabled="isRequestPending"
+ class="form-control"
+ required="required"
/>
</div>
<p class="form-text text-muted">
@@ -105,18 +105,18 @@ Please update your Git repository remotes as soon as possible.`),
</div>
<button
:data-target="`#${$options.modalId}`"
+ :disabled="isRequestPending || newUsername === username"
class="btn btn-warning"
type="button"
data-toggle="modal"
- :disabled="isRequestPending || newUsername === username"
>
{{ $options.buttonText }}
</button>
<gl-modal
:id="$options.modalId"
:header-title-text="s__('Profiles|Change username') + '?'"
- footer-primary-button-variant="warning"
:footer-primary-button-text="$options.buttonText"
+ footer-primary-button-variant="warning"
@submit="onConfirm"
>
<span v-html="modalText"></span>
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index 8f93156cdd1..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,12 +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 = [];
- 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 4c4acd487f8..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,10 +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 = [];
- for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) {
+
+ for (i = 0, len = filePaths.length; i < len; i += 1) {
filePath = filePaths[i];
if (i === 20) {
break;
@@ -150,7 +151,7 @@ export default class ProjectFindFile {
}
goToTree() {
- return location.href = this.options.treeUrl;
+ return window.location.href = this.options.treeUrl;
}
goToBlob() {
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index d2d26d6f67e..5a0d2b642eb 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -2,7 +2,7 @@ import { visitUrl } from './lib/utils/url_utility';
export default function projectImport() {
setTimeout(() => {
- visitUrl(location.href);
+ visitUrl(window.location.href);
}, 5000);
}
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index 6f06944ebb6..9049f87e037 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -3,6 +3,17 @@ import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
+const tooltipTitles = {
+ group: {
+ subscribed: __('Unsubscribe at group level'),
+ unsubscribed: __('Subscribe at group level'),
+ },
+ project: {
+ subscribed: __('Unsubscribe at project level'),
+ unsubscribed: __('Subscribe at project level'),
+ },
+};
+
export default class ProjectLabelSubscription {
constructor(container) {
this.$container = $(container);
@@ -15,12 +26,10 @@ export default class ProjectLabelSubscription {
event.preventDefault();
const $btn = $(event.currentTarget);
- const $span = $btn.find('span');
const url = $btn.attr('data-url');
const oldStatus = $btn.attr('data-status');
$btn.addClass('disabled');
- $span.toggleClass('hidden');
axios.post(url).then(() => {
let newStatus;
@@ -32,21 +41,28 @@ export default class ProjectLabelSubscription {
[newStatus, newAction] = ['unsubscribed', 'Subscribe'];
}
- $span.toggleClass('hidden');
$btn.removeClass('disabled');
this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction);
- this.$buttons.map((button) => {
+ this.$buttons.map((i, button) => {
const $button = $(button);
+ const originalTitle = $button.attr('data-original-title');
- if ($button.attr('data-original-title')) {
- $button.tooltip('hide').attr('data-original-title', newAction).tooltip('_fixTitle');
+ if (originalTitle) {
+ ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction);
}
return button;
});
}).catch(() => flash(__('There was an error subscribing to this label.')));
}
+
+ static setNewTitle($button, originalTitle, newStatus) {
+ const type = /group/.test(originalTitle) ? 'group' : 'project';
+ const newTitle = tooltipTitles[type][newStatus];
+
+ $button.attr('title', newTitle).tooltip('_fixTitle');
+ }
}
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/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
index ab7d2d41ece..d4497924ad8 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
@@ -89,14 +89,13 @@ export default {
<div>
<div
class="js-gcp-machine-type-dropdown dropdown"
- :class="{ 'gl-show-field-errors': hasErrors }"
>
<dropdown-hidden-input
:name="fieldName"
:value="selectedMachineType"
/>
<dropdown-button
- :class="{ 'gl-field-error-outline': hasErrors }"
+ :class="{ 'border-danger': hasErrors }"
:is-disabled="isDisabled"
:is-loading="isLoading"
:toggle-text="toggleText"
@@ -132,9 +131,12 @@ export default {
</div>
</div>
<span
- class="form-text text-muted"
- :class="{ 'gl-field-error': hasErrors }"
v-if="hasErrors"
+ :class="{
+ 'text-danger': hasErrors,
+ 'text-muted': !hasErrors
+ }"
+ class="form-text"
>
{{ errorMessage }}
</span>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
index 25350ef0fa9..08d0a122579 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
@@ -147,7 +147,6 @@ export default {
<div>
<div
class="js-gcp-project-id-dropdown dropdown"
- :class="{ 'gl-show-field-errors': hasErrors }"
>
<dropdown-hidden-input
:name="fieldName"
@@ -155,7 +154,7 @@ export default {
/>
<dropdown-button
:class="{
- 'gl-field-error-outline': hasErrors,
+ 'border-danger': hasErrors,
'read-only': hasOneProject
}"
:is-disabled="isDisabled"
@@ -193,8 +192,11 @@ export default {
</div>
</div>
<span
- class="form-text text-muted"
- :class="{ 'gl-field-error': hasErrors }"
+ :class="{
+ 'text-danger': hasErrors,
+ 'text-muted': !hasErrors
+ }"
+ class="form-text"
v-html="helpText"
></span>
</div>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
index 8ee4eefcd91..b5476684c6a 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
@@ -63,14 +63,13 @@ export default {
<div>
<div
class="js-gcp-zone-dropdown dropdown"
- :class="{ 'gl-show-field-errors': hasErrors }"
>
<dropdown-hidden-input
:name="fieldName"
:value="selectedZone"
/>
<dropdown-button
- :class="{ 'gl-field-error-outline': hasErrors }"
+ :class="{ 'border-danger': hasErrors }"
:is-disabled="isDisabled"
:is-loading="isLoading"
:toggle-text="toggleText"
@@ -106,9 +105,12 @@ export default {
</div>
</div>
<span
- class="form-text text-muted"
- :class="{ 'gl-field-error': hasErrors }"
v-if="hasErrors"
+ :class="{
+ 'text-danger': hasErrors,
+ 'text-muted': !hasErrors
+ }"
+ class="form-text"
>
{{ errorMessage }}
</span>
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 888b1d6ce33..002edb4663c 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -90,7 +90,7 @@ const bindEvents = () => {
function chooseTemplate() {
$('.template-option').hide();
$projectFieldsForm.addClass('selected');
- $selectedIcon.removeClass('active');
+ $selectedIcon.removeClass('d-block');
const value = $(this).val();
const templates = {
rails: {
@@ -109,7 +109,7 @@ const bindEvents = () => {
const selectedTemplate = templates[value];
$selectedTemplateText.text(selectedTemplate.text);
- $(selectedTemplate.icon).addClass('active');
+ $(selectedTemplate.icon).addClass('d-block');
$templateProjectNameInput.focus();
}
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 63f20a0041d..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;
@@ -100,9 +100,9 @@
<template>
<div>
<loading-icon
+ v-if="isLoading"
label="Loading pipeline status"
size="3"
- v-if="isLoading"
/>
<a
v-else
@@ -112,8 +112,8 @@
v-tooltip
:title="statusTitle"
:aria-label="statusTitle"
- data-container="body"
:status="ciStatus"
+ data-container="body"
/>
</a>
</div>
diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue
index 0bbd8a41753..73d49488299 100644
--- a/app/assets/javascripts/projects_dropdown/components/app.vue
+++ b/app/assets/javascripts/projects_dropdown/components/app.vue
@@ -132,14 +132,14 @@ export default {
<div>
<search/>
<loading-icon
- class="loading-animation prepend-top-20"
- size="2"
v-if="isLoadingProjects"
:label="s__('ProjectsDropdown|Loading projects')"
+ class="loading-animation prepend-top-20"
+ size="2"
/>
<div
- class="section-header"
v-if="isFrequentsListVisible"
+ class="section-header"
>
{{ s__('ProjectsDropdown|Frequently visited') }}
</div>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue
index 246dbeaaded..625e0aa548c 100644
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue
+++ b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue
@@ -37,14 +37,14 @@
class="list-unstyled"
>
<li
- class="section-empty"
v-if="isListEmpty"
+ class="section-empty"
>
{{ listEmptyMessage }}
</li>
<projects-list-item
- v-else
v-for="(project, index) in projects"
+ v-else
:key="index"
:project-id="project.id"
:project-name="project.name"
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
index 759cdd1ded9..eafbf6c99e2 100644
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
+++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
@@ -79,36 +79,36 @@
class="projects-list-item-container"
>
<a
- class="clearfix"
:href="webUrl"
+ class="clearfix"
>
<div
class="project-item-avatar-container"
>
<img
v-if="hasAvatar"
- class="avatar s32"
:src="avatarUrl"
+ class="avatar s32"
/>
<identicon
v-else
- size-class="s32"
:entity-id="projectId"
:entity-name="projectName"
+ size-class="s32"
/>
</div>
<div
class="project-item-metadata-container"
>
<div
- class="project-title"
:title="projectName"
+ class="project-title"
v-html="highlightedProjectName"
>
</div>
<div
- class="project-namespace"
:title="namespace"
+ class="project-namespace"
>{{ truncatedNamespace }}</div>
</div>
</a>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
index 8d0c29177e6..76e9cb9e53f 100644
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
+++ b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
@@ -48,8 +48,8 @@ export default {
{{ listEmptyMessage }}
</li>
<projects-list-item
- v-else
v-for="(project, index) in projects"
+ v-else
:key="index"
:project-id="project.id"
:project-name="project.name"
diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue
index 7fcd62dcdb8..28f2a18f2a6 100644
--- a/app/assets/javascripts/projects_dropdown/components/search.vue
+++ b/app/assets/javascripts/projects_dropdown/components/search.vue
@@ -49,11 +49,11 @@
class="search-input-container d-none d-sm-block"
>
<input
- type="search"
- class="form-control"
ref="search"
v-model="searchQuery"
:placeholder="s__('ProjectsDropdown|Search your projects')"
+ type="search"
+ class="form-control"
/>
<i
v-if="!searchQuery"
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/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index 1a75fdd75db..078ccbbbac2 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -107,7 +107,7 @@ export default class PrometheusMetrics {
if (data && data.success) {
stop(data);
} else {
- this.backOffRequestCounter = this.backOffRequestCounter += 1;
+ this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
diff --git a/app/assets/javascripts/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/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index ea0f7199a70..31f88675912 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -48,8 +48,8 @@
/>
<collapsible-container
- v-else-if="!isLoading && repos.length"
v-for="(item, index) in repos"
+ v-else-if="!isLoading && repos.length"
:key="index"
:repo="item"
/>
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index 2fc3778820b..4116c4a0489 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -62,15 +62,15 @@
<div class="container-image-head">
<button
type="button"
- @click="toggleRepo"
class="js-toggle-repo btn-link"
+ @click="toggleRepo"
>
<i
- class="fa"
:class="{
'fa-chevron-right': !isOpen,
'fa-chevron-up': isOpen,
}"
+ class="fa"
aria-hidden="true"
>
</i>
@@ -86,12 +86,12 @@
<div class="controls d-none d-sm-block float-right">
<button
+ v-tooltip
v-if="repo.canDelete"
- type="button"
- class="js-remove-repo btn btn-danger"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
- v-tooltip
+ type="button"
+ class="js-remove-repo btn btn-danger"
@click="handleDeleteRepository"
>
<i
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index e4a4b3bb129..9f4973c3490 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -118,13 +118,13 @@
<td class="content">
<button
+ v-tooltip
v-if="item.canDelete"
- type="button"
- class="js-delete-registry btn btn-danger d-none d-sm-block float-right"
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
+ type="button"
+ class="js-delete-registry btn btn-danger d-none d-sm-block float-right"
data-container="body"
- v-tooltip
@click="handleDeleteRegistry(item)"
>
<i
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 2da022fde63..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();
}
}
@@ -438,7 +438,7 @@ export default class SearchAutocomplete {
}
onClick(item, $el, e) {
- if (location.pathname.indexOf(item.url) !== -1) {
+ if (window.location.pathname.indexOf(item.url) !== -1) {
if (!e.metaKey) e.preventDefault();
if (!this.badgePresent) {
if (item.category === 'Projects') {
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index eecde4550f9..37b4a2a4c63 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -42,8 +42,8 @@ export default function initSettingsPanels() {
}
});
- if (location.hash) {
- const $target = $(location.hash);
+ if (window.location.hash) {
+ const $target = $(window.location.hash);
if ($target.length && $target.hasClass('settings')) {
expandSection($target);
}
diff --git a/app/assets/javascripts/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_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 1e246a56b85..8658081c6c2 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -13,8 +13,8 @@ export default class ShortcutsFindFile extends ShortcutsNavigation {
element === this.projectFindFile.inputElement[0] &&
(combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')
) {
- // when press up/down key in textbox, cusor prevent to move to home/end
- event.preventDefault();
+ // when press up/down key in textbox, cursor prevent to move to home/end
+ e.preventDefault();
return false;
}
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 193788f754f..e9451be31fd 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -9,12 +9,10 @@ export default class ShortcutsIssuable extends Shortcuts {
constructor(isMergeRequest) {
super();
- this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form');
-
Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
- Mousetrap.bind('r', this.replyWithSelectedText.bind(this));
+ Mousetrap.bind('r', ShortcutsIssuable.replyWithSelectedText);
Mousetrap.bind('e', ShortcutsIssuable.editIssue);
if (isMergeRequest) {
@@ -24,11 +22,16 @@ export default class ShortcutsIssuable extends Shortcuts {
}
}
- replyWithSelectedText() {
+ static replyWithSelectedText() {
+ const $replyField = $('.js-main-target-form .js-vue-comment-form');
const documentFragment = window.gl.utils.getSelectedFragment();
+ if (!$replyField.length) {
+ return false;
+ }
+
if (!documentFragment) {
- this.$replyField.focus();
+ $replyField.focus();
return false;
}
@@ -39,21 +42,22 @@ export default class ShortcutsIssuable extends Shortcuts {
return false;
}
- const quote = _.map(selected.split('\n'), val => `${(`> ${val}`).trim()}\n`);
+ const quote = _.map(selected.split('\n'), val => `${`> ${val}`.trim()}\n`);
// If replyField already has some content, add a newline before our quote
- const separator = (this.$replyField.val().trim() !== '' && '\n\n') || '';
- this.$replyField.val((a, current) => `${current}${separator}${quote.join('')}\n`)
+ const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
+ $replyField
+ .val((a, current) => `${current}${separator}${quote.join('')}\n`)
.trigger('input')
.trigger('change');
// Trigger autosize
const event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
- this.$replyField.get(0).dispatchEvent(event);
+ $replyField.get(0).dispatchEvent(event);
// Focus the input field
- this.$replyField.focus();
+ $replyField.focus();
return false;
}
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 5a374d84796..d22a1e1ac66 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -125,13 +125,13 @@ export default {
<template>
<div>
<div
- class="sidebar-collapsed-icon sidebar-collapsed-user"
- :class="{ 'multiple-users': hasMoreThanOneAssignee }"
v-tooltip
+ :class="{ 'multiple-users': hasMoreThanOneAssignee }"
+ :title="collapsedTooltipTitle"
+ class="sidebar-collapsed-icon sidebar-collapsed-user"
data-container="body"
data-placement="left"
data-boundary="viewport"
- :title="collapsedTooltipTitle"
>
<i
v-if="hasNoUsers"
@@ -140,17 +140,17 @@ export default {
>
</i>
<button
- type="button"
- class="btn-link"
v-for="(user, index) in users"
v-if="shouldRenderCollapsedAssignee(index)"
:key="user.id"
+ type="button"
+ class="btn-link"
>
<img
- width="24"
- class="avatar avatar-inline s24"
:alt="assigneeAlt(user)"
:src="avatarUrl(user)"
+ width="24"
+ class="avatar avatar-inline s24"
/>
<span class="author">
{{ user.name }}
@@ -186,14 +186,14 @@ export default {
</template>
<template v-else-if="hasOneUser">
<a
- class="author_link bold"
:href="assigneeUrl(firstUser)"
+ class="author_link bold"
>
<img
- width="32"
- class="avatar avatar-inline s32"
:alt="assigneeAlt(firstUser)"
:src="avatarUrl(firstUser)"
+ width="32"
+ class="avatar avatar-inline s32"
/>
<span class="author">
{{ firstUser.name }}
@@ -206,23 +206,23 @@ export default {
<template v-else>
<div class="user-list">
<div
- class="user-item"
v-for="(user, index) in users"
v-if="renderAssignee(index)"
:key="user.id"
+ class="user-item"
>
<a
+ :href="assigneeUrl(user)"
+ :data-title="user.name"
class="user-link has-tooltip"
data-container="body"
data-placement="bottom"
- :href="assigneeUrl(user)"
- :data-title="user.name"
>
<img
- width="32"
- class="avatar avatar-inline s32"
:alt="assigneeAlt(user)"
:src="avatarUrl(user)"
+ width="32"
+ class="avatar avatar-inline s32"
/>
</a>
</div>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index b04a2eff798..123c92aff64 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -90,12 +90,12 @@ export default {
/>
<assignees
v-if="!store.isFetching.assignees"
- class="value"
:root-path="store.rootPath"
:users="store.assignees"
:editable="store.editable"
- @assign-self="assignSelf"
:issuable-type="issuableType"
+ class="value"
+ @assign-self="assignSelf"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 3f6e2f05396..2b8d6207dea 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -54,7 +54,7 @@ export default {
updateConfidentialAttribute(confidential) {
this.service
.update('issue', { confidential })
- .then(() => location.reload())
+ .then(() => window.location.reload())
.catch(() => {
Flash(
__(
@@ -70,13 +70,13 @@ export default {
<template>
<div class="block issuable-sidebar-item confidentiality">
<div
- class="sidebar-collapsed-icon"
- @click="toggleForm"
v-tooltip
+ :title="tooltipLabel"
+ class="sidebar-collapsed-icon"
data-container="body"
data-placement="left"
data-boundary="viewport"
- :title="tooltipLabel"
+ @click="toggleForm"
>
<icon
:name="confidentialityIcon"
@@ -104,8 +104,8 @@ export default {
v-if="!isConfidential"
class="no-value sidebar-item-value">
<icon
- name="eye"
:size="16"
+ name="eye"
aria-hidden="true"
class="sidebar-item-icon inline"
/>
@@ -115,8 +115,8 @@ export default {
v-else
class="value sidebar-item-value hide-collapsed">
<icon
- name="eye-slash"
:size="16"
+ name="eye-slash"
aria-hidden="true"
class="sidebar-item-icon inline is-active"
/>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index d392977e5e2..4906dad22e1 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -44,14 +44,14 @@ export default {
<div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<p
- class="text"
v-if="isLocked"
+ class="text"
v-html="unlockWarning">
</p>
<p
- class="text"
v-else
+ class="text"
v-html="lockWarning">
</p>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index fb69c741dcd..8bbc59f623a 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -76,7 +76,7 @@ export default {
.update(this.issuableType, {
discussion_locked: locked,
})
- .then(() => location.reload())
+ .then(() => window.location.reload())
.catch(() =>
Flash(
this.__(
@@ -94,13 +94,13 @@ export default {
<template>
<div class="block issuable-sidebar-item lock">
<div
- class="sidebar-collapsed-icon"
- @click="toggleForm"
v-tooltip
+ :title="tooltipLabel"
+ class="sidebar-collapsed-icon"
data-container="body"
data-placement="left"
data-boundary="viewport"
- :title="tooltipLabel"
+ @click="toggleForm"
>
<icon
:name="lockIcon"
@@ -134,8 +134,8 @@ export default {
class="value sidebar-item-value"
>
<icon
- name="lock"
:size="16"
+ name="lock"
aria-hidden="true"
class="sidebar-item-icon inline is-active"
/>
@@ -147,8 +147,8 @@ export default {
class="no-value sidebar-item-value hide-collapsed"
>
<icon
- name="lock-open"
:size="16"
+ name="lock-open"
aria-hidden="true"
class="sidebar-item-icon inline"
/>
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 0a945fc7fd5..33dd6c981b6 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -80,12 +80,12 @@
<template>
<div>
<div
- class="sidebar-collapsed-icon"
v-tooltip
+ :title="participantLabel"
+ class="sidebar-collapsed-icon"
data-container="body"
data-placement="left"
data-boundary="viewport"
- :title="participantLabel"
@click="onClickCollapsedIcon"
>
<i
@@ -119,15 +119,15 @@
class="participants-author js-participants-author"
>
<a
- class="author_link"
:href="participant.web_url"
+ class="author_link"
>
<user-avatar-image
:lazy="true"
:img-src="participant.avatar_url"
- css-classes="avatar-inline"
:size="24"
:tooltip-text="participant.name"
+ css-classes="avatar-inline"
tooltip-placement="bottom"
/>
</a>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 6745c1aafff..448c8fc3602 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -97,9 +97,9 @@
</span>
<toggle-button
ref="toggleButton"
- class="float-right hide-collapsed js-issuable-subscribe-button"
:is-loading="showLoadingState"
:value="subscribed"
+ class="float-right hide-collapsed js-issuable-subscribe-button"
@change="toggleSubscription"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 209af1ce152..1d030c4f67f 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -110,12 +110,12 @@
<template>
<div
- class="sidebar-collapsed-icon"
v-tooltip
+ :title="tooltipText"
+ class="sidebar-collapsed-icon"
data-container="body"
data-placement="left"
data-boundary="viewport"
- :title="tooltipText"
>
<icon name="timer" />
<div class="time-tracking-collapsed-summary">
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index 6f79310b1cc..d335c3f55af 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -1,8 +1,12 @@
<script>
import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time';
+import tooltip from '../../../vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingComparisonPane',
+ directives: {
+ tooltip,
+ },
props: {
timeSpent: {
type: Number,
@@ -50,19 +54,17 @@ export default {
<template>
<div class="time-tracking-comparison-pane">
<div
+ v-tooltip
+ :title="timeRemainingTooltip"
+ :class="timeRemainingStatusClass"
class="compare-meter"
data-toggle="tooltip"
data-placement="top"
role="timeRemainingDisplay"
- :aria-valuenow="timeRemainingTooltip"
- :title="timeRemainingTooltip"
- :data-original-title="timeRemainingTooltip"
- :class="timeRemainingStatusClass"
>
<div
- class="meter-container"
- role="timeSpentPercent"
:aria-valuenow="timeRemainingPercent"
+ class="meter-container"
>
<div
:style="{ width: timeRemainingPercent }"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
index 825063d9ba6..19ec0f05a26 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -45,8 +45,8 @@ export default {
<p v-html="spendText">
</p>
<a
- class="btn btn-default learn-more-button"
:href="href"
+ class="btn btn-default learn-more-button"
>
{{ __('Learn more') }}
</a>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 7d56d2fa5ee..ca3b9338c29 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -101,8 +101,8 @@ export default {
<template>
<div
- class="time_tracker time-tracking-component-wrap"
v-cloak
+ class="time_tracker time-tracking-component-wrap"
>
<time-tracking-collapsed-state
:show-comparison-state="showComparisonState"
@@ -116,8 +116,8 @@ export default {
<div class="title hide-collapsed">
{{ __('Time tracking') }}
<div
- class="help-button float-right"
v-if="!showHelpState"
+ class="help-button float-right"
@click="toggleHelpState(true)"
>
<i
@@ -127,8 +127,8 @@ export default {
</i>
</div>
<div
- class="close-help-button float-right"
v-if="showHelpState"
+ class="close-help-button float-right"
@click="toggleHelpState(false)"
>
<i
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 3086e7d0fc9..655bf9198b7 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -75,7 +75,6 @@ function mountLockComponent(mediator) {
function mountParticipantsComponent(mediator) {
const el = document.querySelector('.js-sidebar-participants-entry-point');
- // eslint-disable-next-line no-new
if (!el) return;
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index d86557e870a..d9ca5e46770 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -80,7 +80,7 @@ export default class SidebarMediator {
return this.service.moveIssue(this.store.moveToProjectId)
.then(response => response.json())
.then((data) => {
- if (location.pathname !== data.web_url) {
+ if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
});
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 1afff0dba38..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';
@@ -11,7 +11,7 @@ import syntaxHighlight from './syntax_highlight';
const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
const ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
-const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
+const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <button class="click-to-expand btn btn-link">Click to expand it.</button></div>';
export default class SingleFileDiff {
constructor(file) {
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/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js
index 81ec483f2d9..873a506a92f 100644
--- a/app/assets/javascripts/snippet/snippet_embed.js
+++ b/app/assets/javascripts/snippet/snippet_embed.js
@@ -1,5 +1,5 @@
export default () => {
- const { protocol, host, pathname } = location;
+ const { protocol, host, pathname } = window.location;
const shareBtn = document.querySelector('.js-share-btn');
const embedBtn = document.querySelector('.js-embed-btn');
const snippetUrlArea = document.querySelector('.js-snippet-url-area');
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index f52990ba232..37f3dd4b496 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */
+/* eslint-disable consistent-return, no-else-return */
import $ from 'jquery';
diff --git a/app/assets/javascripts/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 cd954f75613..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,7 +258,7 @@ function UsersSelect(currentUser, els, options = {}) {
showDivider = 0;
if (firstUser) {
// Move current user to the front of the list
- for (index = j = 0, len = users.length; j < len; index = (j += 1)) {
+ for (index = 0, len = users.length; index < len; index += 1) {
obj = users[index];
if (obj.username === firstUser) {
users.splice(index, 1);
@@ -501,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) {
@@ -553,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
};
@@ -561,7 +560,8 @@ function UsersSelect(currentUser, els, options = {}) {
if (firstUser) {
// Move current user to the front of the list
ref = data.results;
- for (index = j = 0, len = ref.length; j < len; index = (j += 1)) {
+
+ for (index = 0, len = ref.length; index < len; index += 1) {
obj = ref[index];
if (obj.username === firstUser) {
data.results.splice(index, 1);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 1608858da22..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,16 +112,15 @@ 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
- v-if="hasDeploymentTime"
v-tooltip
+ v-if="hasDeploymentTime"
:title="deployment.deployed_at_formatted"
class="js-deploy-time"
>
@@ -127,9 +128,9 @@ export default {
</span>
<loading-button
v-if="deployment.stop_url"
+ :loading="isStopping"
container-class="btn btn-default btn-sm prepend-left-default"
label="Stop environment"
- :loading="isStopping"
@click="stopEnvironment"
/>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
index f012f9c6772..5e76f6b1cac 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
@@ -105,7 +105,7 @@ export default {
MRWidgetService.fetchMetrics(this.metricsUrl)
.then((res) => {
if (res.status === statusCodes.NO_CONTENT) {
- this.backOffRequestCounter = this.backOffRequestCounter += 1;
+ this.backOffRequestCounter += 1;
/* eslint-disable no-unused-expressions */
this.backOffRequestCounter < 3 ? next() : stop(res);
} else {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
index 8338fde61c7..22c2f74f900 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
@@ -35,17 +35,17 @@
<template>
<a
:href="authorUrl"
- class="author-link inline"
:v-tooltip="showAuthorTooltip"
:title="author.name"
+ class="author-link inline"
>
<img
:src="avatarUrl"
class="avatar avatar-inline s16"
/>
<span
- class="author"
v-if="showAuthorName"
+ class="author"
>
{{ author.name }}
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
index 644e4b7d81a..ba16cb9e2c8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
@@ -1,11 +1,15 @@
<script>
+ import tooltip from '~/vue_shared/directives/tooltip';
import MrWidgetAuthor from './mr_widget_author.vue';
export default {
- name: 'MRWidgetAuthorTime',
+ name: 'MrWidgetAuthorTime',
components: {
MrWidgetAuthor,
},
+ directives: {
+ tooltip,
+ },
props: {
actionText: {
type: String,
@@ -31,9 +35,8 @@
{{ actionText }}
<mr-widget-author :author="author" />
<time
+ v-tooltip
:title="dateTitle"
- data-toggle="tooltip"
- data-placement="top"
data-container="body"
>
{{ dateReadable }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 51a0fda6555..3ce9d8dc26a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -59,11 +59,11 @@ export default {
<strong>
{{ s__("mrWidget|Request to merge") }}
<span
- class="label-branch js-source-branch"
:class="{ 'label-truncated': isSourceBranchLong }"
:title="isSourceBranchLong ? mr.sourceBranch : ''"
- data-placement="bottom"
:v-tooltip="isSourceBranchLong"
+ class="label-branch js-source-branch"
+ data-placement="bottom"
v-html="mr.sourceBranchLink"
>
</span>
@@ -77,10 +77,10 @@ export default {
{{ s__("mrWidget|into") }}
<span
- class="label-branch"
:v-tooltip="isTargetBranchLong"
:class="{ 'label-truncatedtooltip': isTargetBranchLong }"
:title="isTargetBranchLong ? mr.targetBranch : ''"
+ class="label-branch"
data-placement="bottom"
>
<a
@@ -108,9 +108,9 @@ export default {
{{ s__("mrWidget|Web IDE") }}
</a>
<button
+ :disabled="mr.sourceBranchRemoved"
data-target="#modal_merge_info"
data-toggle="modal"
- :disabled="mr.sourceBranchRemoved"
class="btn btn-sm btn-default inline js-check-out-branch"
type="button"
>
@@ -134,8 +134,8 @@ export default {
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a
- class="js-download-email-patches"
:href="mr.emailPatchesPath"
+ class="js-download-email-patches"
download
>
{{ s__("mrWidget|Email patches") }}
@@ -143,8 +143,8 @@ export default {
</li>
<li>
<a
- class="js-download-plain-diff"
:href="mr.plainDiffPath"
+ class="js-download-plain-diff"
download
>
{{ s__("mrWidget|Plain diff") }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 48dff8c4916..2f0b5e12c12 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -67,8 +67,8 @@ export default {
</template>
<template v-else-if="hasPipeline">
<a
- class="append-right-10"
:href="status.details_path"
+ class="append-right-10"
>
<ci-icon :status="status" />
</a>
@@ -96,8 +96,8 @@ export default {
<span class="mr-widget-pipeline-graph">
<span
- class="stage-cell"
v-if="hasStages"
+ class="stage-cell"
>
<div
v-for="(stage, i) in pipeline.details.stages"
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/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
index 460437ceeff..56879c04d16 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
@@ -25,9 +25,9 @@
</span>
<i
v-tooltip
- class="fa fa-question-circle"
:title="tooltipTitle"
:aria-label="tooltipTitle"
+ class="fa fa-question-circle"
>
</i>
</p>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
index b404a592234..2133124347c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
@@ -39,10 +39,10 @@
{{ s__("mrWidget|This merge request failed to be merged automatically") }}
</span>
<button
- @click="refreshWidget"
:disabled="isRefreshing"
type="button"
class="btn btn-sm btn-default"
+ @click="refreshWidget"
>
<loading-icon
v-if="isRefreshing"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
index caeaac75b45..ae6630dcd6f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
@@ -11,8 +11,8 @@
<template>
<div class="mr-widget-body media">
<status-icon
- status="loading"
:show-disabled-button="true"
+ status="loading"
/>
<div class="media-body space-children">
<span class="bold">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
index 68b691fc914..25a57e520ee 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
@@ -1,11 +1,11 @@
<script>
- import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
+ import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetClosed',
components: {
- mrWidgetAuthorTime,
+ MrWidgetAuthorTime,
statusIcon,
},
props: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 5c1500ab801..dff9ec657b9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -20,8 +20,8 @@
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index 05fecd4de35..c302960f16e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -1,5 +1,6 @@
<script>
import { n__ } from '~/locale';
+import { stripHtml } from '~/lib/utils/text_utility';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
@@ -27,6 +28,9 @@ export default {
},
computed: {
+ mergeError() {
+ return this.mr.mergeError ? stripHtml(this.mr.mergeError, ' ').trim() : '';
+ },
timerText() {
return n__(
'Refreshing in a second to show the updated status...',
@@ -76,16 +80,16 @@ export default {
</template>
<template v-else>
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
<span class="bold">
<span
- class="has-error-message"
v-if="mr.mergeError"
+ class="has-error-message"
>
- {{ mr.mergeError }}.
+ {{ mergeError }}.
</span>
<span v-else>
{{ s__("mrWidget|Merge failed.") }}
@@ -97,9 +101,9 @@ export default {
</span>
</span>
<button
- @click="refresh"
class="btn btn-default btn-sm js-refresh-button"
type="button"
+ @click="refresh"
>
{{ s__("mrWidget|Refresh now") }}
</button>
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 e8352c362d6..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" />
@@ -90,11 +90,11 @@
</span>
<a
v-if="mr.canCancelAutomaticMerge"
- @click.prevent="cancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
href="#"
- class="btn btn-sm btn-default js-cancel-auto-merge">
+ class="btn btn-sm btn-default js-cancel-auto-merge"
+ @click.prevent="cancelAutomaticMerge">
<i
v-if="isCancellingAutoMerge"
class="fa fa-spinner fa-spin"
@@ -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") }}
@@ -127,10 +127,10 @@
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
- @click.prevent="removeSourceBranch"
role="button"
class="btn btn-sm btn-default js-remove-source-branch"
href="#"
+ @click.prevent="removeSourceBranch"
>
<i
v-if="isRemovingSourceBranch"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index eb581b807a2..1a444c04a1d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -4,7 +4,7 @@
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
- import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
+ import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
@@ -14,7 +14,7 @@
tooltip,
},
components: {
- mrWidgetAuthorTime,
+ MrWidgetAuthorTime,
loadingIcon,
statusIcon,
ClipboardButton,
@@ -116,44 +116,44 @@
:date-readable="mr.metrics.readableMergedAt"
/>
<a
- v-if="mr.canRevertInCurrentMR"
v-tooltip
+ v-if="mr.canRevertInCurrentMR"
+ :title="revertTitle"
class="btn btn-close btn-sm"
href="#modal-revert-commit"
data-toggle="modal"
data-container="body"
- :title="revertTitle"
>
{{ revertLabel }}
</a>
<a
- v-else-if="mr.revertInForkPath"
v-tooltip
- class="btn btn-close btn-sm"
- data-method="post"
+ v-else-if="mr.revertInForkPath"
:href="mr.revertInForkPath"
:title="revertTitle"
+ class="btn btn-close btn-sm"
+ data-method="post"
>
{{ revertLabel }}
</a>
<a
- v-if="mr.canCherryPickInCurrentMR"
v-tooltip
+ v-if="mr.canCherryPickInCurrentMR"
+ :title="cherryPickTitle"
class="btn btn-default btn-sm"
href="#modal-cherry-pick-commit"
data-toggle="modal"
data-container="body"
- :title="cherryPickTitle"
>
{{ cherryPickLabel }}
</a>
<a
- v-else-if="mr.cherryPickInForkPath"
v-tooltip
- class="btn btn-default btn-sm"
- data-method="post"
+ v-else-if="mr.cherryPickInForkPath"
:href="mr.cherryPickInForkPath"
:title="cherryPickTitle"
+ class="btn btn-default btn-sm"
+ data-method="post"
>
{{ cherryPickLabel }}
</a>
@@ -173,7 +173,7 @@
</a>
<clipboard-button
:title="__('Copy commit SHA to clipboard')"
- :text="mr.shortMergeCommitSha"
+ :text="mr.mergeCommitSha"
css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha"
/>
</p>
@@ -186,10 +186,10 @@
>
<span>{{ s__("mrWidget|You can remove source branch now") }}</span>
<button
- @click="removeSourceBranch"
:disabled="isMakingRequest"
type="button"
class="btn btn-sm btn-default js-remove-branch-button"
+ @click="removeSourceBranch"
>
{{ s__("mrWidget|Remove Source Branch") }}
</button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
index 718c0e4b3c6..b0e96f74626 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
@@ -39,8 +39,8 @@
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
@@ -51,9 +51,9 @@
{{ missingBranchNameMessage }}
<i
v-tooltip
- class="fa fa-question-circle"
:title="message"
:aria-label="message"
+ class="fa fa-question-circle"
>
</i>
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue
index e4af50b09f8..92eee2cf5dd 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue
@@ -12,8 +12,8 @@
<template>
<div class="mr-widget-body media">
<status-icon
- status="success"
:show-disabled-button="true"
+ status="success"
/>
<div class="media-body space-children">
<span class="bold">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue
index 6d7cc03f7ad..37ee5215cea 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue
@@ -11,8 +11,8 @@
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
<span class="bold">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 143fd328d88..2d8c3d6be87 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -110,9 +110,9 @@
js-toggle-container accept-action media space-children"
>
<button
+ :disabled="isMakingRequest"
type="button"
class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button"
- :disabled="isMakingRequest"
@click="rebase"
>
<loading-icon v-if="isMakingRequest" />
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/components/states/mr_widget_squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
index 875c3323dbb..25c1044fe2b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
@@ -37,23 +37,23 @@ export default {
<div class="accept-control inline">
<label class="merge-param-checkbox">
<input
+ :disabled="isMergeButtonDisabled"
+ v-model="squashBeforeMerge"
type="checkbox"
name="squash"
class="qa-squash-checkbox"
- :disabled="isMergeButtonDisabled"
- v-model="squashBeforeMerge"
@change="updateSquashModel"
/>
{{ __('Squash commits') }}
</label>
<a
+ v-tooltip
:href="mr.squashBeforeMergeHelpPath"
data-title="About this feature"
data-placement="bottom"
target="_blank"
rel="noopener noreferrer nofollow"
data-container="body"
- v-tooltip
>
<icon
name="question-o"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
index 8d55477929f..2bb1a34412e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
@@ -12,8 +12,8 @@ export default {
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
<span class="bold">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 3a194320bd8..fe777a07189 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -235,11 +235,11 @@ export default {
<div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5">
<button
- @click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled"
:class="mergeButtonClass"
type="button"
- class="qa-merge-button">
+ class="qa-merge-button"
+ @click="handleMergeButtonClick()">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
@@ -265,28 +265,28 @@ export default {
role="menu">
<li>
<a
- @click.prevent="handleMergeButtonClick(true)"
class="merge_when_pipeline_succeeds"
- href="#">
+ href="#"
+ @click.prevent="handleMergeButtonClick(true)">
<span class="media">
<span
- v-html="successSvg"
class="merge-opt-icon"
- aria-hidden="true"></span>
+ aria-hidden="true"
+ v-html="successSvg"></span>
<span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
</span>
</a>
</li>
<li>
<a
- @click.prevent="handleMergeButtonClick(false, true)"
class="accept-merge-request"
- href="#">
+ href="#"
+ @click.prevent="handleMergeButtonClick(false, true)">
<span class="media">
<span
- v-html="warningSvg"
class="merge-opt-icon"
- aria-hidden="true"></span>
+ aria-hidden="true"
+ v-html="warningSvg"></span>
<span class="media-body merge-opt-title">Merge immediately</span>
</span>
</a>
@@ -299,8 +299,8 @@ export default {
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
- class="js-remove-source-branch-checkbox"
:disabled="isRemoveSourceBranchButtonDisabled"
+ class="js-remove-source-branch-checkbox"
type="checkbox"/> Remove source branch
</label>
@@ -317,10 +317,10 @@ export default {
</span>
<button
v-else
- @click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled"
class="js-modify-commit-message-button btn btn-default btn-sm"
- type="button">
+ type="button"
+ @click="toggleCommitMessageEditor">
Modify commit message
</button>
</template>
@@ -356,8 +356,8 @@ export default {
</p>
<div class="hint">
<a
- @click.prevent="updateCommitMessage"
href="#"
+ @click.prevent="updateCommitMessage"
>
{{ commitMessageLinkTitle }}
</a>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
index 7cc07401911..16c903c923f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
@@ -12,8 +12,8 @@ export default {
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
<span class="bold">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index 8ea3f22ecc2..5eb2058a03b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -18,8 +18,8 @@ export default {
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="true"
+ status="warning"
/>
<div class="media-body space-children">
<span class="bold">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index fe2608e8212..89c9a41f316 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -43,8 +43,8 @@ export default {
<template>
<div class="mr-widget-body media">
<status-icon
- status="warning"
:show-disabled-button="Boolean(mr.removeWIPPath)"
+ status="warning"
/>
<div class="media-body space-children">
<span class="bold">
@@ -60,10 +60,10 @@ export default {
</span>
<button
v-if="mr.removeWIPPath"
- @click="removeWIP"
:disabled="isMakingRequest"
type="button"
- class="btn btn-default btn-xs js-remove-wip">
+ class="btn btn-default btn-sm js-remove-wip"
+ @click="removeWIP">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
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 f69fe03fcb3..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
@@ -36,7 +36,7 @@ import {
notify,
SourceBranchRemovalStatus,
} from './dependencies';
-import { setFavicon } from '../lib/utils/common_utils';
+import { setFaviconOverlay } from '../lib/utils/common_utils';
export default {
el: '#js-vue-mr-widget',
@@ -159,8 +159,9 @@ export default {
},
setFaviconHelper() {
if (this.mr.ciStatusFaviconPath) {
- setFavicon(this.mr.ciStatusFaviconPath);
+ return setFaviconOverlay(this.mr.ciStatusFaviconPath);
}
+ return Promise.resolve();
},
fetchDeployments() {
return this.service.fetchDeployments()
@@ -171,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() {
@@ -190,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}"`;
@@ -210,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) => {
@@ -265,10 +266,10 @@ export default {
/>
<section
- v-if="mr.maintainerEditAllowed"
+ v-if="mr.allowCollaboration"
class="mr-info-list mr-links"
>
- {{ s__("mrWidget|Allows edits from maintainers") }}
+ {{ s__("mrWidget|Allows commits from members who can merge to the target branch") }}
</section>
<mr-widget-related-links
@@ -282,8 +283,8 @@ export default {
/>
</div>
<div
- class="mr-widget-footer"
v-if="shouldRenderMergeHelp"
+ class="mr-widget-footer"
>
<mr-widget-merge-help />
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index e5b7e1f1c68..c881cd496d1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -26,6 +26,7 @@ export default class MergeRequestStore {
this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
+ this.mergeCommitSha = data.merge_commit_sha;
this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
@@ -83,7 +84,7 @@ export default class MergeRequestStore {
this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing;
- this.maintainerEditAllowed = data.allow_maintainer_to_push;
+ this.allowCollaboration = data.allow_collaboration;
// Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 0d64efcbf68..a2518e2a611 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -50,9 +50,9 @@ export default {
</script>
<template>
<a
+ v-tooltip
:href="status.details_path"
:class="cssClass"
- v-tooltip
:title="!showText ? status.text : ''"
>
<ci-icon :status="status" />
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index cb2cc3901ad..dc5760bce28 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -49,14 +49,14 @@ export default {
<template>
<button
- type="button"
- class="btn"
+ v-tooltip
:class="cssClass"
:title="title"
:data-clipboard-text="text"
- v-tooltip
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
+ type="button"
+ class="btn"
>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 8f250a6c989..13bca99dcb3 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -124,11 +124,11 @@ export default {
</div>
<a
- class="ref-name"
- :href="commitRef.ref_url"
v-tooltip
- data-container="body"
+ :href="commitRef.ref_url"
:title="commitRef.name"
+ class="ref-name"
+ data-container="body"
>
{{ commitRef.name }}
</a>
@@ -139,8 +139,8 @@ export default {
/>
<a
- class="commit-sha"
:href="commitUrl"
+ class="commit-sha"
>
{{ shortSha }}
</a>
@@ -152,15 +152,15 @@ export default {
>
<user-avatar-link
v-if="hasAuthor"
- class="avatar-image-container"
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
+ class="avatar-image-container"
/>
<a
- class="commit-row-message"
:href="commitUrl"
+ class="commit-row-message"
>
{{ title }}
</a>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index 7b5367ac19b..f1ef50d0e3d 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -32,7 +32,10 @@ export default {
<div class="file-container">
<div class="file-content">
<p class="prepend-top-10 file-info">
- {{ fileName }} ({{ fileSizeReadable }})
+ {{ fileName }}
+ <template v-if="fileSize > 0">
+ ({{ fileSizeReadable }})
+ </template>
</p>
<a
:href="path"
@@ -41,9 +44,9 @@ export default {
download
target="_blank">
<icon
+ :size="16"
name="download"
css-classes="float-left append-right-8"
- :size="16"
/>
{{ __('Download') }}
</a>
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 a5999f909ca..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
@@ -1,4 +1,5 @@
<script>
+import _ from 'underscore';
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default {
@@ -12,6 +13,10 @@ export default {
required: false,
default: 0,
},
+ renderInfo: {
+ type: Boolean,
+ default: true,
+ },
},
data() {
return {
@@ -26,14 +31,34 @@ export default {
return numberToHumanSize(this.fileSize);
},
},
+ beforeDestroy() {
+ window.removeEventListener('resize', this.resizeThrottled, false);
+ },
+ mounted() {
+ // The onImgLoad may have happened before the control was actually mounted
+ this.onImgLoad();
+ this.resizeThrottled = _.throttle(this.onImgLoad, 400);
+ window.addEventListener('resize', this.resizeThrottled, false);
+ },
methods: {
onImgLoad() {
- const contentImg = this.$refs.contentImg;
- this.isZoomable =
- contentImg.naturalWidth > contentImg.width || contentImg.naturalHeight > contentImg.height;
+ const { contentImg } = this.$refs;
+
+ if (contentImg) {
+ this.isZoomable =
+ contentImg.naturalWidth > contentImg.width ||
+ contentImg.naturalHeight > contentImg.height;
+
+ this.width = contentImg.naturalWidth;
+ this.height = contentImg.naturalHeight;
- this.width = contentImg.naturalWidth;
- this.height = contentImg.naturalHeight;
+ this.$emit('imgLoaded', {
+ width: this.width,
+ height: this.height,
+ renderedWidth: contentImg.clientWidth,
+ renderedHeight: contentImg.clientHeight,
+ });
+ }
},
onImgClick() {
if (this.isZoomable) this.isZoomed = !this.isZoomed;
@@ -47,20 +72,22 @@ export default {
<div class="file-content image_file">
<img
ref="contentImg"
- :class="{ 'isZoomable': isZoomable, 'isZoomed': isZoomed }"
+ :class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }"
:src="path"
:alt="path"
@load="onImgLoad"
@click="onImgClick"/>
- <p class="file-info prepend-top-10">
+ <p
+ v-if="renderInfo"
+ class="file-info prepend-top-10">
<template v-if="fileSize>0">
{{ fileSizeReadable }}
</template>
<template v-if="fileSize>0 && width && height">
- -
+ |
</template>
<template v-if="width && height">
- {{ width }} x {{ height }}
+ W: {{ width }} | H: {{ height }}
</template>
</p>
</div>
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/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
index 424af5a0293..9c1e5c68649 100644
--- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
@@ -86,8 +86,8 @@
<div class="modal-open">
<div
:id="id"
- class="modal"
:class="id ? '' : 'd-block'"
+ class="modal"
role="dialog"
tabindex="-1"
>
@@ -105,9 +105,9 @@
<button
type="button"
class="close float-right"
- @click="emitCancel($event)"
data-dismiss="modal"
aria-label="Close"
+ @click="emitCancel($event)"
>
<span aria-hidden="true">&times;</span>
</button>
@@ -115,22 +115,22 @@
</div>
<div class="modal-body">
<slot
- name="body"
:text="text"
+ name="body"
>
<p>{{ text }}</p>
</slot>
</div>
<div
- class="modal-footer"
v-if="!hideFooter"
+ class="modal-footer"
>
<button
+ :class="btnCancelKindClass"
type="button"
class="btn"
- :class="btnCancelKindClass"
- @click="emitCancel($event)"
data-dismiss="modal"
+ @click="emitCancel($event)"
>
{{ closeButtonLabel }}
</button>
@@ -151,12 +151,12 @@
<button
v-if="primaryButtonLabel"
- type="button"
- class="btn js-primary-button"
:disabled="submitDisabled"
:class="btnKindClass"
- @click="emitSubmit($event)"
+ type="button"
+ class="btn js-primary-button"
data-dismiss="modal"
+ @click="emitSubmit($event)"
>
{{ primaryButtonLabel }}
</button>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js b/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js
new file mode 100644
index 00000000000..6c1840361af
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js
@@ -0,0 +1,12 @@
+export const diffModes = {
+ replaced: 'replaced',
+ new: 'new',
+ deleted: 'deleted',
+ renamed: 'renamed',
+};
+
+export const imageViewMode = {
+ twoup: 'twoup',
+ swipe: 'swipe',
+ onion: 'onion',
+};
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
new file mode 100644
index 00000000000..d3cbe3c7e74
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -0,0 +1,74 @@
+<script>
+import { viewerInformationForPath } from '../content_viewer/lib/viewer_utils';
+import ImageDiffViewer from './viewers/image_diff_viewer.vue';
+import DownloadDiffViewer from './viewers/download_diff_viewer.vue';
+
+export default {
+ props: {
+ diffMode: {
+ type: String,
+ required: true,
+ },
+ newPath: {
+ type: String,
+ required: true,
+ },
+ newSha: {
+ type: String,
+ required: true,
+ },
+ oldPath: {
+ type: String,
+ required: true,
+ },
+ oldSha: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ viewer() {
+ if (!this.newPath) return null;
+
+ const previewInfo = viewerInformationForPath(this.newPath);
+ if (!previewInfo) return DownloadDiffViewer;
+
+ switch (previewInfo.id) {
+ case 'image':
+ return ImageDiffViewer;
+ 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 `${this.basePath}${this.projectPath}/raw/${this.oldSha}/${this.oldPath}`;
+ },
+ fullNewPath() {
+ return `${this.basePath}${this.projectPath}/raw/${this.newSha}/${this.newPath}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="viewer"
+ class="diff-file preview-container">
+ <component
+ :is="viewer"
+ :diff-mode="diffMode"
+ :new-path="fullNewPath"
+ :old-path="fullOldPath"
+ :project-path="projectPath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue
new file mode 100644
index 00000000000..50389b6ae63
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue
@@ -0,0 +1,69 @@
+<script>
+import DownloadViewer from '../../content_viewer/viewers/download_viewer.vue';
+import { diffModes } from '../constants';
+
+export default {
+ components: {
+ DownloadViewer,
+ },
+ props: {
+ diffMode: {
+ type: String,
+ required: true,
+ },
+ newPath: {
+ type: String,
+ required: true,
+ },
+ oldPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ diffModes,
+};
+</script>
+
+<template>
+ <div class="diff-file-container">
+ <div class="diff-viewer">
+ <div
+ v-if="diffMode === $options.diffModes.replaced"
+ class="two-up view row">
+ <div class="col-sm-6 deleted">
+ <download-viewer
+ :path="oldPath"
+ :project-path="projectPath"
+ />
+ </div>
+ <div class="col-sm-6 added">
+ <download-viewer
+ :path="newPath"
+ :project-path="projectPath"
+ />
+ </div>
+ </div>
+ <div
+ v-else-if="diffMode === $options.diffModes.new"
+ class="added">
+ <download-viewer
+ :path="newPath"
+ :project-path="projectPath"
+ />
+ </div>
+ <div
+ v-else
+ class="deleted">
+ <download-viewer
+ :path="oldPath"
+ :project-path="projectPath"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
new file mode 100644
index 00000000000..38e881d17a2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
@@ -0,0 +1,160 @@
+<script>
+import { pixeliseValue } from '../../../lib/utils/dom_utils';
+import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
+
+export default {
+ components: {
+ ImageViewer,
+ },
+ props: {
+ newPath: {
+ type: String,
+ required: true,
+ },
+ oldPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ onionMaxWidth: undefined,
+ onionMaxHeight: undefined,
+ onionOldImgInfo: null,
+ onionNewImgInfo: null,
+ onionDraggerPos: 0,
+ onionOpacity: 1,
+ dragging: false,
+ };
+ },
+ computed: {
+ onionMaxPixelWidth() {
+ return pixeliseValue(this.onionMaxWidth);
+ },
+ onionMaxPixelHeight() {
+ return pixeliseValue(this.onionMaxHeight);
+ },
+ onionDraggerPixelPos() {
+ return pixeliseValue(this.onionDraggerPos);
+ },
+ },
+ beforeDestroy() {
+ document.body.removeEventListener('mouseup', this.stopDrag);
+ this.$refs.dragger.removeEventListener('mousedown', this.startDrag);
+ },
+ methods: {
+ dragMove(e) {
+ if (!this.dragging) return;
+ const left = e.pageX - this.$refs.dragTrack.getBoundingClientRect().left;
+ const dragTrackWidth =
+ this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100;
+
+ let leftValue = left;
+ if (leftValue < 0) leftValue = 0;
+ if (leftValue > dragTrackWidth) leftValue = dragTrackWidth;
+
+ this.onionOpacity = left / dragTrackWidth;
+ this.onionDraggerPos = leftValue;
+ },
+ startDrag() {
+ this.dragging = true;
+ document.body.style.userSelect = 'none';
+ document.body.addEventListener('mousemove', this.dragMove);
+ },
+ stopDrag() {
+ this.dragging = false;
+ document.body.style.userSelect = '';
+ document.body.removeEventListener('mousemove', this.dragMove);
+ },
+ prepareOnionSkin() {
+ if (this.onionOldImgInfo && this.onionNewImgInfo) {
+ this.onionMaxWidth = Math.max(
+ this.onionOldImgInfo.renderedWidth,
+ this.onionNewImgInfo.renderedWidth,
+ );
+ this.onionMaxHeight = Math.max(
+ this.onionOldImgInfo.renderedHeight,
+ this.onionNewImgInfo.renderedHeight,
+ );
+
+ this.onionOpacity = 1;
+ this.onionDraggerPos =
+ this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100;
+
+ document.body.addEventListener('mouseup', this.stopDrag);
+ }
+ },
+ onionNewImgLoaded(imgInfo) {
+ this.onionNewImgInfo = imgInfo;
+ this.prepareOnionSkin();
+ },
+ onionOldImgLoaded(imgInfo) {
+ this.onionOldImgInfo = imgInfo;
+ this.prepareOnionSkin();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="onion-skin view">
+ <div
+ :style="{
+ 'width': onionMaxPixelWidth,
+ 'height': onionMaxPixelHeight,
+ 'user-select': dragging === true ? 'none' : '',
+ }"
+ class="onion-skin-frame">
+ <div
+ :style="{
+ 'width': onionMaxPixelWidth,
+ 'height': onionMaxPixelHeight,
+ }"
+ class="frame deleted">
+ <image-viewer
+ key="onionOldImg"
+ :render-info="false"
+ :path="oldPath"
+ :project-path="projectPath"
+ @imgLoaded="onionOldImgLoaded"
+ />
+ </div>
+ <div
+ ref="addedFrame"
+ :style="{
+ 'opacity': onionOpacity,
+ 'width': onionMaxPixelWidth,
+ 'height': onionMaxPixelHeight,
+ }"
+ class="added frame">
+ <image-viewer
+ key="onionNewImg"
+ :render-info="false"
+ :path="newPath"
+ :project-path="projectPath"
+ @imgLoaded="onionNewImgLoaded"
+ />
+ </div>
+ <div class="controls">
+ <div class="transparent"></div>
+ <div
+ ref="dragTrack"
+ class="drag-track"
+ @mousedown="startDrag"
+ @mouseup="stopDrag">
+ <div
+ ref="dragger"
+ :style="{ 'left': onionDraggerPixelPos }"
+ class="dragger">
+ </div>
+ </div>
+ <div class="opaque"></div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
new file mode 100644
index 00000000000..86366c799a2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
@@ -0,0 +1,158 @@
+<script>
+import _ from 'underscore';
+import { pixeliseValue } from '../../../lib/utils/dom_utils';
+import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
+
+export default {
+ components: {
+ ImageViewer,
+ },
+ props: {
+ newPath: {
+ type: String,
+ required: true,
+ },
+ oldPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ dragging: false,
+ swipeOldImgInfo: null,
+ swipeNewImgInfo: null,
+ swipeMaxWidth: undefined,
+ swipeMaxHeight: undefined,
+ swipeBarPos: 1,
+ swipeWrapWidth: undefined,
+ };
+ },
+ computed: {
+ swipeMaxPixelWidth() {
+ return pixeliseValue(this.swipeMaxWidth);
+ },
+ swipeMaxPixelHeight() {
+ return pixeliseValue(this.swipeMaxHeight);
+ },
+ swipeWrapPixelWidth() {
+ return pixeliseValue(this.swipeWrapWidth);
+ },
+ swipeBarPixelPos() {
+ return pixeliseValue(this.swipeBarPos);
+ },
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', this.resizeThrottled, false);
+ document.body.removeEventListener('mouseup', this.stopDrag);
+ document.body.removeEventListener('mousemove', this.dragMove);
+ },
+ mounted() {
+ window.addEventListener('resize', this.resize, false);
+ },
+ methods: {
+ dragMove(e) {
+ if (!this.dragging) return;
+
+ let leftValue = e.pageX - this.$refs.swipeFrame.getBoundingClientRect().left;
+ const spaceLeft = 20;
+ const { clientWidth } = this.$refs.swipeFrame;
+ if (leftValue <= 0) {
+ leftValue = 0;
+ } else if (leftValue > clientWidth - spaceLeft) {
+ leftValue = clientWidth - spaceLeft;
+ }
+
+ this.swipeWrapWidth = this.swipeMaxWidth - leftValue;
+ this.swipeBarPos = leftValue;
+ },
+ startDrag() {
+ this.dragging = true;
+ document.body.style.userSelect = 'none';
+ document.body.addEventListener('mousemove', this.dragMove);
+ },
+ stopDrag() {
+ this.dragging = false;
+ document.body.style.userSelect = '';
+ document.body.removeEventListener('mousemove', this.dragMove);
+ },
+ prepareSwipe() {
+ if (this.swipeOldImgInfo && this.swipeNewImgInfo) {
+ // Add 2 for border width
+ this.swipeMaxWidth =
+ Math.max(this.swipeOldImgInfo.renderedWidth, this.swipeNewImgInfo.renderedWidth) + 2;
+ this.swipeWrapWidth = this.swipeMaxWidth;
+ this.swipeMaxHeight =
+ Math.max(this.swipeOldImgInfo.renderedHeight, this.swipeNewImgInfo.renderedHeight) + 2;
+
+ document.body.addEventListener('mouseup', this.stopDrag);
+ }
+ },
+ swipeNewImgLoaded(imgInfo) {
+ this.swipeNewImgInfo = imgInfo;
+ this.prepareSwipe();
+ },
+ swipeOldImgLoaded(imgInfo) {
+ this.swipeOldImgInfo = imgInfo;
+ this.prepareSwipe();
+ },
+ resize: _.throttle(function throttledResize() {
+ this.swipeBarPos = 0;
+ }, 400),
+ },
+};
+</script>
+
+<template>
+ <div class="swipe view">
+ <div
+ ref="swipeFrame"
+ :style="{
+ 'width': swipeMaxPixelWidth,
+ 'height': swipeMaxPixelHeight,
+ }"
+ class="swipe-frame">
+ <div class="frame deleted">
+ <image-viewer
+ key="swipeOldImg"
+ ref="swipeOldImg"
+ :render-info="false"
+ :path="oldPath"
+ :project-path="projectPath"
+ @imgLoaded="swipeOldImgLoaded"
+ />
+ </div>
+ <div
+ ref="swipeWrap"
+ :style="{
+ 'width': swipeWrapPixelWidth,
+ 'height': swipeMaxPixelHeight,
+ }"
+ class="swipe-wrap">
+ <div class="frame added">
+ <image-viewer
+ key="swipeNewImg"
+ :render-info="false"
+ :path="newPath"
+ :project-path="projectPath"
+ @imgLoaded="swipeNewImgLoaded"
+ />
+ </div>
+ </div>
+ <span
+ ref="swipeBar"
+ :style="{ 'left': swipeBarPixelPos }"
+ class="swipe-bar"
+ @mousedown="startDrag"
+ @mouseup="stopDrag">
+ <span class="top-handle"></span>
+ <span class="bottom-handle"></span>
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
new file mode 100644
index 00000000000..9c19266ecdf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
@@ -0,0 +1,41 @@
+<script>
+import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
+
+export default {
+ components: {
+ ImageViewer,
+ },
+ props: {
+ newPath: {
+ type: String,
+ required: true,
+ },
+ oldPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="two-up view row">
+ <div class="col-sm-6 frame deleted">
+ <image-viewer
+ :path="oldPath"
+ :project-path="projectPath"
+ />
+ </div>
+ <div class="col-sm-6 frame added">
+ <image-viewer
+ :path="newPath"
+ :project-path="projectPath"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
new file mode 100644
index 00000000000..1af85283277
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
@@ -0,0 +1,109 @@
+<script>
+import ImageViewer from '../../content_viewer/viewers/image_viewer.vue';
+import TwoUpViewer from './image_diff/two_up_viewer.vue';
+import SwipeViewer from './image_diff/swipe_viewer.vue';
+import OnionSkinViewer from './image_diff/onion_skin_viewer.vue';
+import { diffModes, imageViewMode } from '../constants';
+
+export default {
+ components: {
+ ImageViewer,
+ TwoUpViewer,
+ SwipeViewer,
+ OnionSkinViewer,
+ },
+ props: {
+ diffMode: {
+ type: String,
+ required: true,
+ },
+ newPath: {
+ type: String,
+ required: true,
+ },
+ oldPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ mode: imageViewMode.twoup,
+ };
+ },
+ methods: {
+ changeMode(newMode) {
+ this.mode = newMode;
+ },
+ },
+ diffModes,
+ imageViewMode,
+};
+</script>
+
+<template>
+ <div class="diff-file-container">
+ <div
+ v-if="diffMode === $options.diffModes.replaced"
+ class="diff-viewer">
+ <div class="image js-replaced-image">
+ <two-up-viewer
+ v-if="mode === $options.imageViewMode.twoup"
+ v-bind="$props"/>
+ <swipe-viewer
+ v-else-if="mode === $options.imageViewMode.swipe"
+ v-bind="$props"/>
+ <onion-skin-viewer
+ v-else-if="mode === $options.imageViewMode.onion"
+ v-bind="$props"/>
+ </div>
+ <div class="view-modes">
+ <ul class="view-modes-menu">
+ <li
+ :class="{
+ active: mode === $options.imageViewMode.twoup
+ }"
+ @click="changeMode($options.imageViewMode.twoup)">
+ {{ s__('ImageDiffViewer|2-up') }}
+ </li>
+ <li
+ :class="{
+ active: mode === $options.imageViewMode.swipe
+ }"
+ @click="changeMode($options.imageViewMode.swipe)">
+ {{ s__('ImageDiffViewer|Swipe') }}
+ </li>
+ <li
+ :class="{
+ active: mode === $options.imageViewMode.onion
+ }"
+ @click="changeMode($options.imageViewMode.onion)">
+ {{ s__('ImageDiffViewer|Onion skin') }}
+ </li>
+ </ul>
+ </div>
+ <div class="note-container"></div>
+ </div>
+ <div
+ v-else-if="diffMode === $options.diffModes.new"
+ class="diff-viewer added">
+ <image-viewer
+ :path="newPath"
+ :project-path="projectPath"
+ />
+ </div>
+ <div
+ v-else
+ class="diff-viewer deleted">
+ <image-viewer
+ :path="oldPath"
+ :project-path="projectPath"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index c159333d89a..3cba0c5e633 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -28,11 +28,11 @@ export default {
<template>
<button
+ :disabled="isDisabled || isLoading"
class="dropdown-menu-toggle dropdown-menu-full-width"
type="button"
data-toggle="dropdown"
aria-expanded="false"
- :disabled="isDisabled || isLoading"
>
<loading-icon
v-show="isLoading"
@@ -42,8 +42,8 @@ export default {
{{ toggleText }}
</span>
<span
- class="dropdown-toggle-icon"
v-show="!isLoading"
+ class="dropdown-toggle-icon"
>
<i
class="fa fa-chevron-down"
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue
index 1fe27eb97ab..b7a4613bdd2 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue
@@ -15,8 +15,8 @@ export default {
<template>
<input
- type="hidden"
:name="name"
:value="value"
+ type="hidden"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
index c2145a26e64..7f1912f6405 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
@@ -23,10 +23,10 @@ export default {
<template>
<div class="dropdown-input">
<input
- class="dropdown-input-field"
- type="search"
v-model="searchQuery"
:placeholder="placeholderText"
+ class="dropdown-input-field"
+ type="search"
autocomplete="off"
/>
<i
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index 9295be3e2b2..e6e92594b65 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -1,5 +1,7 @@
<script>
import { __ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+
/**
* Port of detail_behavior expand button.
*
@@ -12,6 +14,9 @@ import { __ } from '~/locale';
*/
export default {
name: 'ExpandButton',
+ components: {
+ Icon,
+ },
data() {
return {
isCollapsed: true,
@@ -22,6 +27,9 @@ export default {
return __('Click to expand text');
},
},
+ destroyed() {
+ this.isCollapsed = true;
+ },
methods: {
onClick() {
this.isCollapsed = !this.isCollapsed;
@@ -32,12 +40,15 @@ export default {
<template>
<span>
<button
- type="button"
v-show="isCollapsed"
- class="text-expander btn-blank"
:aria-label="ariaLabel"
+ type="button"
+ class="text-expander btn-blank"
@click="onClick">
- ...
+ <icon
+ :size="12"
+ name="ellipsis_h"
+ />
</button>
<span v-if="!isCollapsed">
<slot name="expanded"></slot>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index be2755452e2..878c805ada5 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -73,8 +73,8 @@ export default {
<template>
<span>
<svg
- :class="[iconSizeClass, cssClasses]"
v-if="!loading && !folder"
+ :class="[iconSizeClass, cssClasses]"
>
<use v-bind="{ 'xlink:href':spriteHref }" />
</svg>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
index 9ffbaae3ea5..9bca1993ccc 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
+++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
@@ -513,7 +513,7 @@ const fileNameIcons = {
'credits.md': 'credits',
'credits.md.rendered': 'credits',
'.flowconfig': 'flow',
- 'favicon.ico': 'favicon',
+ 'favicon.png': 'favicon',
'karma.conf.js': 'karma',
'karma.conf.ts': 'karma',
'karma.conf.coffee': 'karma',
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue
index d5d5a7d3798..b298b989203 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue
@@ -1,15 +1,21 @@
<script>
const buttonVariants = ['danger', 'primary', 'success', 'warning'];
+const sizeVariants = ['sm', 'md', 'lg', 'xl'];
export default {
name: 'GlModal',
-
props: {
id: {
type: String,
required: false,
default: null,
},
+ modalSize: {
+ type: String,
+ required: false,
+ default: 'md',
+ validator: value => sizeVariants.includes(value),
+ },
headerTitleText: {
type: String,
required: false,
@@ -27,7 +33,11 @@ export default {
default: '',
},
},
-
+ computed: {
+ modalSizeClass() {
+ return this.modalSize === 'md' ? '' : `modal-${this.modalSize}`;
+ },
+ },
methods: {
emitCancel(event) {
this.$emit('cancel', event);
@@ -47,6 +57,7 @@ export default {
role="dialog"
>
<div
+ :class="modalSizeClass"
class="modal-dialog"
role="document"
>
@@ -59,10 +70,10 @@ export default {
</slot>
</h4>
<button
+ :aria-label="s__('Modal|Close')"
type="button"
class="close js-modal-close-action"
data-dismiss="modal"
- :aria-label="s__('Modal|Close')"
@click="emitCancel($event)"
>
<span aria-hidden="true">&times;</span>
@@ -85,9 +96,9 @@ export default {
{{ s__('Modal|Cancel') }}
</button>
<button
+ :class="`btn-${footerPrimaryButtonVariant}`"
type="button"
class="btn js-modal-primary-action"
- :class="`btn-${footerPrimaryButtonVariant}`"
data-dismiss="modal"
@click="emitSubmit($event)"
>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index ca17fa06a00..62d35f6547d 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -117,8 +117,8 @@ export default {
</section>
<section
- class="header-action-buttons"
v-if="actions.length"
+ class="header-action-buttons"
>
<template
v-for="(action, i) in actions"
@@ -135,21 +135,21 @@ export default {
<a
v-else-if="action.type === 'ujs-link'"
:href="action.path"
- data-method="post"
- rel="nofollow"
:class="action.cssClass"
:key="i"
+ data-method="post"
+ rel="nofollow"
>
{{ action.label }}
</a>
<button
v-else-if="action.type === 'button'"
- @click="onClickAction(action)"
:disabled="action.isLoading"
:class="action.cssClass"
- type="button"
:key="i"
+ type="button"
+ @click="onClickAction(action)"
>
{{ action.label }}
<i
@@ -162,11 +162,11 @@ export default {
</template>
<button
v-if="hasSidebarButton"
+ id="toggleSidebar"
type="button"
class="btn btn-default d-block d-sm-none d-md-none
sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
aria-label="Toggle Sidebar"
- id="toggleSidebar"
>
<i
class="fa fa-angle-double-left"
diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue
index 23010f40f26..4ffc811e714 100644
--- a/app/assets/javascripts/vue_shared/components/identicon.vue
+++ b/app/assets/javascripts/vue_shared/components/identicon.vue
@@ -43,9 +43,9 @@ export default {
<template>
<div
- class="avatar identicon"
:class="sizeClass"
- :style="identiconStyles">
+ :style="identiconStyles"
+ class="avatar identicon">
{{ identiconTitle }}
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
index 3d39b3ab173..ca8ce554588 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -33,11 +33,11 @@
<template>
<div class="issuable-note-warning">
<icon
+ v-if="!isLockedAndConfidential"
:name="warningIcon"
:size="16"
class="icon inline"
aria-hidden="true"
- v-if="!isLockedAndConfidential"
/>
<span v-if="isLockedAndConfidential">
diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js
new file mode 100644
index 00000000000..02f28da8bb0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js
@@ -0,0 +1,5 @@
+export function pixeliseValue(val) {
+ return val ? `${val}px` : '';
+}
+
+export default {};
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
index 88c13a1f340..2ff0c056b9c 100644
--- a/app/assets/javascripts/vue_shared/components/loading_button.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -54,19 +54,19 @@
<template>
<button
- @click="onClick"
- type="button"
:class="containerClass"
:disabled="loading || disabled"
+ type="button"
+ @click="onClick"
>
<transition name="fade">
<loading-icon
v-if="loading"
:inline="true"
- class="js-loading-button-icon"
:class="{
'append-right-5': label
}"
+ class="js-loading-button-icon"
/>
</transition>
<transition name="fade">
diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue
index 12a75e016d7..db22c5f02cd 100644
--- a/app/assets/javascripts/vue_shared/components/loading_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue
@@ -35,10 +35,10 @@
:is="rootElementType"
class="loading-container text-center">
<i
- class="fa fa-spin fa-spinner"
:class="cssClass"
- aria-hidden="true"
:aria-label="label"
+ class="fa fa-spin fa-spinner"
+ aria-hidden="true"
>
</i>
</component>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 12c7d125062..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');
@@ -117,17 +125,17 @@
<template>
<div
- class="md-area js-vue-markdown-field"
+ ref="gl-form"
:class="{ 'prepend-top-default append-bottom-default': addSpacingClasses }"
- ref="gl-form">
+ class="md-area js-vue-markdown-field">
<markdown-header
:preview-markdown="previewMarkdown"
@preview-markdown="showPreviewTab"
@write-markdown="showWriteTab"
/>
<div
- class="md-write-holder"
v-show="!previewMarkdown"
+ class="md-write-holder"
>
<div class="zen-backdrop">
<slot name="textarea"></slot>
@@ -137,8 +145,8 @@
aria-label="Enter zen mode"
>
<icon
- name="screen-normal"
:size="32"
+ name="screen-normal"
/>
</a>
<markdown-toolbar
@@ -149,8 +157,8 @@
</div>
</div>
<div
- class="md md-preview-holder md-preview"
v-show="previewMarkdown"
+ class="md md-preview-holder md-preview js-vue-md-preview"
>
<div
ref="markdown-preview"
@@ -164,8 +172,8 @@
<template v-if="previewMarkdown && !markdownPreviewLoading">
<div
v-if="referencedCommands"
- v-html="referencedCommands"
class="referenced-commands"
+ v-html="referencedCommands"
>
</div>
<div
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index db453c30576..83171ae50b8 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -54,8 +54,8 @@
<div class="md-header">
<ul class="nav-links clearfix">
<li
- class="md-header-tab"
:class="{ active: !previewMarkdown }"
+ class="md-header-tab"
>
<a
class="js-write-link"
@@ -67,11 +67,11 @@
</a>
</li>
<li
- class="md-header-tab"
:class="{ active: previewMarkdown }"
+ 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)"
@@ -80,8 +80,8 @@
</a>
</li>
<li
- class="md-header-toolbar"
:class="{ active: !previewMarkdown }"
+ class="md-header-toolbar"
>
<toolbar-button
tag="**"
@@ -94,8 +94,8 @@
icon="italic"
/>
<toolbar-button
- tag="> "
:prepend="true"
+ tag="> "
button-title="Insert a quote"
icon="quote"
/>
@@ -106,20 +106,20 @@
icon="code"
/>
<toolbar-button
- tag="* "
:prepend="true"
+ tag="* "
button-title="Add a bullet list"
icon="list-bulleted"
/>
<toolbar-button
- tag="1. "
:prepend="true"
+ tag="1. "
button-title="Add a numbered list"
icon="list-numbered"
/>
<toolbar-button
- tag="* [ ] "
:prepend="true"
+ tag="* [ ] "
button-title="Add a task list"
icon="task-done"
/>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 2d2d69ebeb2..9f1e009efdd 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -39,15 +39,15 @@
<template>
<button
v-tooltip
- type="button"
- class="toolbar-btn js-md"
- tabindex="-1"
- data-container="body"
:data-md-tag="tag"
:data-md-block="tagBlock"
:data-md-prepend="prepend"
:title="buttonTitle"
:aria-label="buttonTitle"
+ type="button"
+ class="toolbar-btn js-md"
+ tabindex="-1"
+ data-container="body"
>
<icon
:name="icon"
diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.vue b/app/assets/javascripts/vue_shared/components/memory_graph.vue
index b07f6b07afe..522091ea889 100644
--- a/app/assets/javascripts/vue_shared/components/memory_graph.vue
+++ b/app/assets/javascripts/vue_shared/components/memory_graph.vue
@@ -113,19 +113,19 @@ export default {
<template>
<div class="memory-graph-container">
<svg
- class="has-tooltip"
:title="getFormattedMedian"
:width="width"
:height="height"
+ class="has-tooltip"
xmlns="http://www.w3.org/2000/svg">
<path
:d="pathD"
:viewBox="pathViewBox"
/>
<circle
- r="1.5"
:cx="dotX"
:cy="dotY"
+ r="1.5"
tranform="translate(0 -1)"
/>
</svg>
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index 08d4936f480..99d61b5639d 100644
--- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -59,9 +59,9 @@ export default {
}"
>
<a
+ :class="`js-${scope}-tab-${tab.scope}`"
role="button"
@click="onTabClick(tab)"
- :class="`js-${scope}-tab-${tab.scope}`"
>
{{ tab.name }}
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index eccba61a8c0..38115f268bb 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -54,7 +54,7 @@
<div class="note-header">
<div class="note-header-info">
<a :href="getUserData.path">
- <span class="d-none d-sm-block">{{ getUserData.name }}</span>
+ <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span>
<span class="note-headline-light">@{{ getUserData.username }}</span>
</a>
</div>
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/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue
index abbe9a22717..8c2dcc2d902 100644
--- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue
+++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue
@@ -82,9 +82,9 @@
<template>
<div
- class="dragHandle"
:class="className"
:style="cursorStyle"
+ class="dragHandle"
@mousedown="startDrag"
@dblclick="resetSize"
></div>
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
index 279cc1de5bb..97ca4d93bd7 100644
--- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
+++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
@@ -85,7 +85,6 @@
<template>
<img
v-tooltip
- class="avatar"
:class="{
lazy: lazy,
[avatarSizeClass]: true,
@@ -99,5 +98,6 @@
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
+ class="avatar"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index 21ffdc1dc86..a2a9a5e6987 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -66,10 +66,10 @@
<template>
<deprecated-modal
- kind="warning"
- class="recaptcha-modal js-recaptcha-modal"
:hide-footer="true"
:title="__('Please solve the reCAPTCHA')"
+ kind="warning"
+ class="recaptcha-modal js-recaptcha-modal"
@cancel="close"
>
<div slot="body">
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 71ec34f2c7a..74998a4787d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -97,8 +97,8 @@
<template>
<div
- class="block"
:class="blockClass"
+ class="block"
>
<div class="issuable-sidebar-header">
<toggle-sidebar
@@ -107,8 +107,8 @@
/>
</div>
<collapsed-calendar-icon
- class="sidebar-collapsed-icon"
:text="collapsedText"
+ class="sidebar-collapsed-icon"
/>
<div class="title">
{{ label }}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index f155ac2be02..a3fc358130f 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -143,8 +143,8 @@ export default {
:value="label.id"
/>
<div
- class="dropdown"
ref="dropdown"
+ class="dropdown"
>
<dropdown-button
:ability-name="abilityName"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
index 47497c1de98..48d2f16f554 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
@@ -53,10 +53,7 @@ export default {
<template>
<button
- type="button"
ref="dropdownButton"
- class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
- data-toggle="dropdown"
:class="{ 'js-extra-options': showExtraOptions }"
:data-ability-name="abilityName"
:data-field-name="fieldName"
@@ -64,6 +61,9 @@ export default {
:data-labels="labelsPath"
:data-namespace-path="namespace"
:data-show-any="showExtraOptions"
+ type="button"
+ class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
+ data-toggle="dropdown"
>
<span class="dropdown-toggle-text">
{{ dropdownToggleText }}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
index 3c400afdc1d..fe895136ccc 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
@@ -19,9 +19,9 @@ export default {
<div class="dropdown-page-two dropdown-new-label">
<div class="dropdown-title">
<button
+ :aria-label="__('Go back')"
type="button"
class="dropdown-title-button dropdown-menu-back"
- :aria-label="__('Go back')"
>
<i
aria-hidden="true"
@@ -32,9 +32,9 @@ export default {
</button>
{{ headerTitle }}
<button
+ :aria-label="__('Close')"
type="button"
class="dropdown-title-button dropdown-menu-close"
- :aria-label="__('Close')"
>
<i
aria-hidden="true"
@@ -48,19 +48,19 @@ export default {
<div class="dropdown-labels-error js-label-error"></div>
<input
id="new_label_name"
+ :placeholder="__('Name new label')"
type="text"
class="default-dropdown-input"
- :placeholder="__('Name new label')"
/>
<div class="suggest-colors suggest-colors-dropdown">
<a
v-for="(color, index) in suggestedColors"
- href="#"
:key="index"
:data-color="color"
:style="{
backgroundColor: color,
}"
+ href="#"
>
&nbsp;
</a>
@@ -69,9 +69,9 @@ export default {
<div class="dropdown-label-color-preview js-dropdown-label-color-preview"></div>
<input
id="new_label_color"
+ :placeholder="__('Assign custom color like #FF0000')"
type="text"
class="default-dropdown-input"
- :placeholder="__('Assign custom color like #FF0000')"
/>
</div>
<div class="clearfix">
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue
index 5f61e9fbe80..d64ad016f9b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue
@@ -34,9 +34,9 @@ export default {
</li>
<li>
<a
+ :href="labelsWebUrl"
data-is-link="true"
class="dropdown-external-link"
- :href="labelsWebUrl"
>
{{ manageLabelsTitle }}
</a>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
index 7664acdf19c..e98b6392827 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
@@ -6,9 +6,9 @@ export default {};
<div class="dropdown-title">
<span>{{ __('Assign labels') }}</span>
<button
+ :aria-label="__('Close')"
type="button"
class="dropdown-title-button dropdown-menu-close"
- :aria-label="__('Close')"
>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
index ae633460c95..80d65a2a534 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
@@ -5,10 +5,10 @@ export default {};
<template>
<div class="dropdown-input">
<input
+ :placeholder="__('Search')"
autocomplete="off"
class="dropdown-input-field"
type="search"
- :placeholder="__('Search')"
/>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
index 69d588eb25d..10e990f8a80 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
@@ -35,7 +35,12 @@ export default {
</script>
<template>
- <div class="hide-collapsed value issuable-show-labels js-value">
+ <div
+ :class="{
+ 'has-labels':!isEmpty,
+ }"
+ class="hide-collapsed value issuable-show-labels js-value"
+ >
<span
v-if="isEmpty"
class="text-secondary"
@@ -43,18 +48,18 @@ export default {
<slot>{{ __('None') }}</slot>
</span>
<a
- v-else
v-for="label in labels"
+ v-else
:key="label.id"
:href="labelFilterUrl(label)"
>
<span
v-tooltip
- class="label color-label"
- data-placement="bottom"
- data-container="body"
:style="labelStyle(label)"
:title="label.description"
+ class="badge color-label"
+ data-placement="bottom"
+ data-container="body"
>
{{ label.title }}
</span>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
index 68fa2ab8d01..af297f3c408 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -37,10 +37,10 @@ export default {
<template>
<div
v-tooltip
+ :title="labelsList"
class="sidebar-collapsed-icon"
data-placement="left"
data-container="body"
- :title="labelsList"
@click="handleClick"
>
<i
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 de6f8c32e74..ac2e99abe77 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -28,21 +28,21 @@ export default {
<template>
<button
+ v-tooltip
+ :title="tooltipLabel"
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
- @click="toggle"
- v-tooltip
data-container="body"
data-placement="left"
- :title="tooltipLabel"
+ @click="toggle"
>
<i
- aria-label="toggle collapse"
- class="fa"
:class="{
'fa-angle-double-right': !collapsed,
'fa-angle-double-left': collapsed
}"
+ aria-label="toggle collapse"
+ class="fa"
>
</i>
</button>
diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
index 16304e4815d..4a5ffbe5d5a 100644
--- a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
+++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
@@ -22,10 +22,10 @@
<template>
<div
- class="animation-container"
:class="{
'animation-container-small': small,
}"
+ class="animation-container"
>
<div
v-for="(css, index) in lineClasses"
diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
index 86f06c8d266..b1c2df54ef6 100644
--- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
@@ -84,8 +84,8 @@ export default {
<template>
<div
- class="stacked-progress-bar"
:class="cssClass"
+ class="stacked-progress-bar"
>
<span
v-if="!totalCount"
@@ -96,30 +96,30 @@ export default {
<span
v-tooltip
v-if="successPercent"
- class="status-green"
- data-placement="bottom"
:title="successTooltip"
:style="successBarStyle"
+ class="status-green"
+ data-placement="bottom"
>
{{ successPercent }}%
</span>
<span
v-tooltip
v-if="neutralPercent"
- class="status-neutral"
- data-placement="bottom"
:title="neutralTooltip"
:style="neutralBarStyle"
+ class="status-neutral"
+ data-placement="bottom"
>
{{ neutralPercent }}%
</span>
<span
v-tooltip
v-if="failurePercent"
- class="status-red"
- data-placement="bottom"
:title="failureTooltip"
:style="failureBarStyle"
+ class="status-red"
+ data-placement="bottom"
>
{{ failurePercent }}%
</span>
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue
index 22fc5757447..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) {
@@ -124,15 +124,18 @@
break;
}
},
+ hideOnSmallScreen(item) {
+ return !item.first && !item.last && !item.next && !item.prev && !item.active;
+ },
},
};
</script>
<template>
<div
v-if="showPagination"
- class="gl-pagination"
+ class="gl-pagination prepend-top-default"
>
- <ul class="pagination clearfix">
+ <ul class="pagination justify-content-center">
<li
v-for="(item, index) in getItems"
:key="index"
@@ -142,12 +145,17 @@
'js-next-button': item.next,
'js-last-button': item.last,
'js-first-button': item.first,
+ 'd-none d-md-block': hideOnSmallScreen(item),
separator: item.separator,
active: item.active,
- disabled: item.disabled
+ disabled: item.disabled || item.separator
}"
+ class="page-item"
>
- <a @click.prevent="changePage(item.title, item.disabled)">
+ <a
+ class="page-link"
+ @click.prevent="changePage(item.title, item.disabled)"
+ >
{{ item.title }}
</a>
</li>
diff --git a/app/assets/javascripts/vue_shared/components/tabs/tab.vue b/app/assets/javascripts/vue_shared/components/tabs/tab.vue
index 2a35d6bc151..1c6011dcfd0 100644
--- a/app/assets/javascripts/vue_shared/components/tabs/tab.vue
+++ b/app/assets/javascripts/vue_shared/components/tabs/tab.vue
@@ -26,15 +26,20 @@ export default {
created() {
this.isTab = true;
},
+ updated() {
+ if (this.$parent) {
+ this.$parent.$forceUpdate();
+ }
+ },
};
</script>
<template>
<div
- class="tab-pane"
:class="{
active: localActive
}"
+ class="tab-pane"
role="tabpanel"
>
<slot></slot>
diff --git a/app/assets/javascripts/vue_shared/components/tabs/tabs.js b/app/assets/javascripts/vue_shared/components/tabs/tabs.js
index 4362264caa5..9b9e4bb47bd 100644
--- a/app/assets/javascripts/vue_shared/components/tabs/tabs.js
+++ b/app/assets/javascripts/vue_shared/components/tabs/tabs.js
@@ -1,4 +1,11 @@
export default {
+ props: {
+ stopPropagation: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
data() {
return {
currentIndex: 0,
@@ -13,7 +20,12 @@ export default {
this.tabs = this.$children.filter(child => child.isTab);
this.currentIndex = this.tabs.findIndex(tab => tab.localActive);
},
- setTab(index) {
+ setTab(e, index) {
+ if (this.stopPropagation) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
this.tabs[this.currentIndex].localActive = false;
this.tabs[index].localActive = true;
@@ -36,7 +48,7 @@ export default {
href: '#',
},
on: {
- click: () => this.setTab(i),
+ click: e => this.setTab(e, i),
},
},
tab.$slots.title || tab.title,
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index 09031d3ffa1..a897300b62b 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -63,26 +63,26 @@
<label class="toggle-wrapper">
<input
v-if="name"
- type="hidden"
:name="name"
:value="value"
+ type="hidden"
/>
<button
- type="button"
- class="project-feature-toggle"
:aria-label="ariaLabel"
:class="{
'is-checked': value,
'is-disabled': disabledInput,
'is-loading': isLoading
}"
+ type="button"
+ class="project-feature-toggle"
@click="toggleFeature"
>
<loadingIcon class="loading-icon" />
<span class="toggle-icon">
<icon
- css-classes="toggle-icon-svg"
- :name="toggleIcon"/>
+ :name="toggleIcon"
+ css-classes="toggle-icon-svg"/>
</span>
</button>
</label>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index cc9cc46bb4c..3a413c74410 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -85,7 +85,6 @@ export default {
<template>
<img
v-tooltip
- class="avatar"
:class="{
lazy: lazy,
[avatarSizeClass]: true,
@@ -99,5 +98,7 @@ export default {
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
+ class="avatar"
+ data-boundary="window"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index 6955d164def..01c36fec41a 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -84,8 +84,8 @@ export default {
<template>
<a
- class="user-avatar-link"
- :href="linkHref">
+ :href="linkHref"
+ class="user-avatar-link">
<user-avatar-image
:img-src="imgSrc"
:img-alt="imgAlt"
@@ -94,8 +94,8 @@ export default {
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
/><span
- v-if="shouldShowUsername"
v-tooltip
+ v-if="shouldShowUsername"
:title="tooltipText"
:tooltip-placement="tooltipPlacement"
>{{ username }}</span>
diff --git a/app/assets/javascripts/vue_shared/models/assignee.js b/app/assets/javascripts/vue_shared/models/assignee.js
new file mode 100644
index 00000000000..4a29b0d0581
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/models/assignee.js
@@ -0,0 +1,13 @@
+export default class ListAssignee {
+ constructor(obj, defaultAvatar) {
+ this.id = obj.id;
+ this.name = obj.name;
+ this.username = obj.username;
+ this.avatar = obj.avatar_url || obj.avatar || defaultAvatar;
+ this.path = obj.path;
+ this.state = obj.state;
+ this.webUrl = obj.web_url || obj.webUrl;
+ }
+}
+
+window.ListAssignee = ListAssignee;
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 d8e57834f9e..ded33e8b151 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -24,16 +24,60 @@ html {
font-size: 14px;
}
+legend {
+ border-bottom: 1px solid $border-color;
+ margin-bottom: 20px;
+}
+
button,
html [type="button"],
[type="reset"],
-[type="submit"] {
+[type="submit"],
+[role="button"] {
// Override bootstrap reboot
-webkit-appearance: inherit;
+ cursor: pointer;
}
-[role="button"] {
- cursor: pointer;
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ color: $gl-text-color;
+ font-weight: 600;
+}
+
+h1,
+.h1,
+h2,
+.h2,
+h3,
+.h3 {
+ margin-top: 20px;
+ margin-bottom: 10px;
+}
+
+h4,
+.h4,
+h5,
+.h5,
+h6,
+.h6 {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+h5,
+.h5 {
+ font-size: $gl-font-size;
+}
+
+input[type="file"] {
+ // Bootstrap 4 file input height is taller by default
+ // which makes them look ugly
+ line-height: 1;
}
b,
@@ -45,7 +89,12 @@ a {
color: $gl-link-color;
}
+hr {
+ overflow: hidden;
+}
+
.form-group.row .col-form-label {
+ padding-top: 0;
// Bootstrap 4 aligns labels to the left
// for horizontal forms
@include media-breakpoint-up(md) {
@@ -53,10 +102,25 @@ a {
}
}
+kbd {
+ display: inline-block;
+}
+
code {
padding: 2px 4px;
+ color: $red-600;
background-color: $red-100;
border-radius: 3px;
+
+ .code > & {
+ background-color: inherit;
+ padding: unset;
+ }
+
+ .build-trace & {
+ background-color: inherit;
+ padding: inherit;
+ }
}
table {
@@ -87,6 +151,16 @@ table {
color: $gl-text-color-secondary !important;
}
+.bg-success,
+.bg-primary,
+.bg-info,
+.bg-danger,
+.bg-warning {
+ .card-header {
+ color: $white-light;
+ }
+}
+
// Polyfill deprecated selectors
.hidden {
@@ -104,7 +178,16 @@ table {
display: none;
}
-.badge {
+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,
+.label {
padding: 4px 5px;
font-size: 12px;
font-style: normal;
@@ -139,6 +222,19 @@ table {
&:not(:last-of-type) {
border-bottom: 1px solid $well-inner-border;
}
+
+ p,
+ ol,
+ ul,
+ .form-group {
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ .badge.badge-gray {
+ background-color: $well-expand-item;
}
}
@@ -160,9 +256,21 @@ table {
}
}
+.card-header {
+ h3.card-title,
+ h4.card-title {
+ margin-top: 0;
+ }
+}
+
.nav-tabs {
+ // Override bootstrap's default border
+ border-bottom: 0;
+
.nav-link {
- border: 0;
+ border-top: 0;
+ border-left: 0;
+ border-right: 0;
}
.nav-item {
@@ -173,3 +281,60 @@ table {
pre code {
white-space: pre-wrap;
}
+
+.alert,
+.flash-notice {
+ border-radius: 0;
+}
+
+.alert-success {
+ background-color: $green-500;
+ border-color: $green-500;
+}
+
+.alert-info {
+ background-color: $blue-500;
+ border-color: $blue-500;
+}
+
+.alert-warning {
+ background-color: $orange-500;
+ border-color: $orange-500;
+}
+
+.alert-danger {
+ background-color: $red-500;
+ border-color: $red-500;
+}
+
+.alert-success,
+.alert-info,
+.alert-warning,
+.alert-danger,
+.flash-notice {
+ color: $white-light;
+
+ h4,
+ a:not(.btn),
+ .alert-link {
+ color: $white-light;
+ }
+}
+
+input[type=color].form-control {
+ height: $input-height;
+}
+
+.toggle-sidebar-button {
+ .collapse-text,
+ .icon-angle-double-left,
+ .icon-angle-double-right {
+ color: $gl-text-color-secondary;
+ }
+}
+
+.project-templates-buttons {
+ .btn {
+ vertical-align: unset;
+ }
+}
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 c5be27f2d29..340fddd398b 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -13,9 +13,11 @@
&.diff-collapsed {
padding: 5px;
+ line-height: 34px;
.click-to-expand {
cursor: pointer;
+ vertical-align: initial;
}
}
}
@@ -348,11 +350,6 @@
}
}
-.flex-container-block {
- display: -webkit-flex;
- display: flex;
-}
-
.flex-right {
margin-left: auto;
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 0115f542c88..523fcb05a87 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -121,10 +121,6 @@
&.btn-sm {
margin-left: $btn-sm-side-margin;
}
-
- &.btn-xs {
- margin-left: $btn-xs-side-margin;
- }
}
@mixin btn-svg {
@@ -150,10 +146,6 @@
line-height: 18px;
}
- &.btn-xs {
- padding: 2px 5px;
- }
-
&.btn-success,
&.btn-new,
&.btn-create,
@@ -497,6 +489,10 @@ fieldset[disabled] .btn,
}
}
+[readonly] {
+ cursor: default;
+}
+
.btn-no-padding {
padding: 0;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 1e7b9534275..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;
@@ -305,14 +300,6 @@ img.emoji {
margin-bottom: 10px;
}
-.btn-sign-in {
- text-shadow: none;
-
- @include media-breakpoint-up(sm) {
- margin-top: 8px;
- }
-}
-
.side-filters {
fieldset {
margin-bottom: 15px;
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index 1a415e1b852..ea4cb9a0b75 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -26,19 +26,25 @@
margin-right: 2px;
width: $contextual-sidebar-width;
- a {
+ > a,
+ > button {
transition: padding $sidebar-transition-duration;
font-weight: $gl-font-weight-bold;
display: flex;
+ width: 100%;
align-items: center;
padding: 10px 16px 10px 10px;
color: $gl-text-color;
- }
+ background-color: transparent;
+ border: 0;
+ text-align: left;
- &:hover,
- a:hover {
- background-color: $link-hover-background;
- color: $gl-text-color;
+ &:hover,
+ &:focus {
+ background-color: $link-hover-background;
+ color: $gl-text-color;
+ outline: 0;
+ }
}
.avatar-container {
@@ -62,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;
@@ -71,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;
}
}
@@ -208,7 +213,7 @@
> li {
> a {
@include media-breakpoint-up(sm) {
- margin-right: 2px;
+ margin-right: 1px;
}
&:hover {
@@ -218,7 +223,7 @@
&.is-showing-fly-out {
> a {
- margin-right: 2px;
+ margin-right: 1px;
}
.sidebar-sub-level-items {
@@ -311,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;
@@ -373,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/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index b91d579cae6..74475daae14 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -35,6 +35,12 @@
@include media-breakpoint-down(xs) {
width: 100%;
}
+
+ &.projects-dropdown-menu {
+ padding: 0;
+ overflow-y: initial;
+ max-height: initial;
+ }
}
.dropdown-toggle,
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 f77ec4b6a2c..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 {
@@ -400,3 +414,51 @@ span.idiff {
color: $common-gray-light;
border: 1px solid $common-gray-light;
}
+
+.preview-container {
+ height: 100%;
+ overflow: auto;
+
+ .file-container {
+ background-color: $gray-darker;
+ display: flex;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+
+ text-align: center;
+
+ .file-content {
+ padding: $gl-padding;
+ max-width: 100%;
+ max-height: 100%;
+
+ img {
+ max-width: 90%;
+ max-height: 70vh;
+ }
+
+ .is-zoomable {
+ cursor: pointer;
+ cursor: zoom-in;
+
+ &.is-zoomed {
+ cursor: pointer;
+ cursor: zoom-out;
+ max-width: none;
+ max-height: none;
+ margin-right: $gl-padding;
+ }
+ }
+ }
+
+ .file-info {
+ font-size: $label-font-size;
+ color: $diff-image-info-color;
+ }
+ }
+
+ .md-previewer {
+ padding: $gl-padding;
+ }
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 0ee5748952a..551a7e852ae 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -299,6 +299,7 @@
height: 14px;
width: 14px;
vertical-align: middle;
+ margin-bottom: 4px;
}
.dropdown-toggle-text {
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 52c3f18a682..e4bcb92876d 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -10,6 +10,20 @@
@extend .alert;
background-color: $blue-500;
margin: 0;
+
+ &.flash-notice-persistent {
+ background-color: $blue-100;
+ color: $gl-text-color;
+
+ a {
+ color: $gl-link-color;
+
+ &:hover {
+ color: $gl-link-hover-color;
+ text-decoration: none;
+ }
+ }
+ }
}
.flash-warning {
@@ -28,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 c76ea532912..282e424fc38 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -170,7 +170,7 @@ label {
}
.form-control::-webkit-input-placeholder {
- color: $gl-text-color-secondary;
+ color: $placeholder-text-color;
}
.input-group {
@@ -201,6 +201,10 @@ label {
}
.gl-show-field-errors {
+ .form-control {
+ height: 34px;
+ }
+
.gl-field-success-outline {
border: 1px solid $green-600;
@@ -239,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/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index e378e84ca1b..1cf12b1a015 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -19,6 +19,7 @@
.gfm-color_chip {
display: inline-block;
+ line-height: 1;
margin: 0 0 2px 4px;
vertical-align: middle;
border-radius: 3px;
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 11e21edfc1b..aaa8bed3df0 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -3,26 +3,26 @@
*/
@mixin gitlab-theme(
- $color-100,
- $color-200,
- $color-500,
- $color-700,
- $color-800,
- $color-900,
+ $location-badge-color,
+ $search-and-nav-links,
+ $active-tab-border,
+ $border-and-box-shadow,
+ $sidebar-text,
+ $nav-svg-color,
$color-alternate
) {
// Header
.navbar-gitlab {
- background-color: $color-900;
+ background-color: $nav-svg-color;
.navbar-collapse {
- color: $color-200;
+ color: $search-and-nav-links;
}
.container-fluid {
.navbar-toggler {
- border-left: 1px solid lighten($color-700, 10%);
+ border-left: 1px solid lighten($border-and-box-shadow, 10%);
}
}
@@ -31,40 +31,40 @@
> li {
> a:hover,
> a:focus {
- background-color: rgba($color-200, 0.2);
+ background-color: rgba($search-and-nav-links, 0.2);
}
&.active > a,
- &.dropdown.open > a {
- color: $color-900;
+ &.dropdown.show > a {
+ color: $nav-svg-color;
background-color: $color-alternate;
}
&.line-separator {
- border-left: 1px solid rgba($color-200, 0.2);
+ border-left: 1px solid rgba($search-and-nav-links, 0.2);
}
}
}
.navbar-sub-nav {
- color: $color-200;
+ color: $search-and-nav-links;
}
.nav {
> li {
- color: $color-200;
+ color: $search-and-nav-links;
> a {
&.header-user-dropdown-toggle {
.header-user-avatar {
- border-color: $color-200;
+ border-color: $search-and-nav-links;
}
}
&:hover,
&:focus {
@include media-breakpoint-up(sm) {
- background-color: rgba($color-200, 0.2);
+ background-color: rgba($search-and-nav-links, 0.2);
}
svg {
@@ -74,13 +74,13 @@
}
&.active > a,
- &.dropdown.open > a {
- color: $color-900;
+ &.dropdown.show > a {
+ color: $nav-svg-color;
background-color: $color-alternate;
&:hover {
svg {
- fill: $color-900;
+ fill: $nav-svg-color;
}
}
}
@@ -88,7 +88,7 @@
.impersonated-user,
.impersonated-user:hover {
svg {
- fill: $color-900;
+ fill: $nav-svg-color;
}
}
}
@@ -99,34 +99,34 @@
> a {
&:hover,
&:focus {
- background-color: rgba($color-200, 0.2);
+ background-color: rgba($search-and-nav-links, 0.2);
}
}
}
.search {
form {
- background-color: rgba($color-200, 0.2);
+ background-color: rgba($search-and-nav-links, 0.2);
&:hover {
- background-color: rgba($color-200, 0.3);
+ background-color: rgba($search-and-nav-links, 0.3);
}
}
.location-badge {
- color: $color-100;
- background-color: rgba($color-200, 0.1);
- border-right: 1px solid $color-800;
+ color: $location-badge-color;
+ background-color: rgba($search-and-nav-links, 0.1);
+ border-right: 1px solid $sidebar-text;
}
.search-input::placeholder {
- color: rgba($color-200, 0.8);
+ color: rgba($search-and-nav-links, 0.8);
}
.search-input-wrap {
.search-icon,
.clear-icon {
- fill: rgba($color-200, 0.8);
+ fill: rgba($search-and-nav-links, 0.8);
}
}
@@ -141,38 +141,34 @@
.search-input-wrap {
.search-icon {
- fill: rgba($color-200, 0.8);
+ fill: rgba($search-and-nav-links, 0.8);
}
}
}
}
- .btn-sign-in {
- background-color: $color-100;
- color: $color-900;
- }
// Sidebar
.nav-sidebar li.active {
- box-shadow: inset 4px 0 0 $color-700;
+ box-shadow: inset 4px 0 0 $border-and-box-shadow;
> a {
- color: $color-800;
+ color: $sidebar-text;
}
svg {
- fill: $color-800;
+ fill: $sidebar-text;
}
}
.sidebar-top-level-items > li.active .badge.badge-pill {
- color: $color-800;
+ color: $sidebar-text;
}
.nav-links li {
&.active a,
a.active {
- border-bottom: 2px solid $color-500;
+ border-bottom: 2px solid $active-tab-border;
.badge.badge-pill {
font-weight: $gl-font-weight-bold;
@@ -181,27 +177,23 @@
}
.branch-header-title {
- color: $color-700;
- }
-
- .ide-file-list .file.file-active {
- color: $color-700;
+ color: $border-and-box-shadow;
}
.ide-sidebar-link {
&.active {
- color: $color-700;
- box-shadow: inset 3px 0 $color-700;
+ color: $border-and-box-shadow;
+ box-shadow: inset 3px 0 $border-and-box-shadow;
&.is-right {
- box-shadow: inset -3px 0 $color-700;
+ box-shadow: inset -3px 0 $border-and-box-shadow;
}
}
}
}
body {
- &.ui_indigo {
+ &.ui-indigo {
@include gitlab-theme(
$indigo-100,
$indigo-200,
@@ -213,19 +205,19 @@ body {
);
}
- &.ui_dark {
+ &.ui-light-indigo {
@include gitlab-theme(
- $theme-gray-100,
- $theme-gray-200,
- $theme-gray-500,
- $theme-gray-700,
- $theme-gray-800,
- $theme-gray-900,
+ $indigo-100,
+ $indigo-200,
+ $indigo-500,
+ $indigo-500,
+ $indigo-700,
+ $indigo-700,
$white-light
);
}
- &.ui_blue {
+ &.ui-blue {
@include gitlab-theme(
$theme-blue-100,
$theme-blue-200,
@@ -237,7 +229,19 @@ body {
);
}
- &.ui_green {
+ &.ui-light-blue {
+ @include gitlab-theme(
+ $theme-light-blue-100,
+ $theme-light-blue-200,
+ $theme-light-blue-500,
+ $theme-light-blue-500,
+ $theme-light-blue-700,
+ $theme-light-blue-700,
+ $white-light
+ );
+ }
+
+ &.ui-green {
@include gitlab-theme(
$theme-green-100,
$theme-green-200,
@@ -249,7 +253,55 @@ body {
);
}
- &.ui_light {
+ &.ui-light-green {
+ @include gitlab-theme(
+ $theme-green-100,
+ $theme-green-200,
+ $theme-green-500,
+ $theme-green-500,
+ $theme-light-green-700,
+ $theme-light-green-700,
+ $white-light
+ );
+ }
+
+ &.ui-red {
+ @include gitlab-theme(
+ $theme-red-100,
+ $theme-red-200,
+ $theme-red-500,
+ $theme-red-700,
+ $theme-red-800,
+ $theme-red-900,
+ $white-light
+ );
+ }
+
+ &.ui-light-red {
+ @include gitlab-theme(
+ $theme-light-red-100,
+ $theme-light-red-200,
+ $theme-light-red-500,
+ $theme-light-red-500,
+ $theme-light-red-700,
+ $theme-light-red-700,
+ $white-light
+ );
+ }
+
+ &.ui-dark {
+ @include gitlab-theme(
+ $theme-gray-100,
+ $theme-gray-200,
+ $theme-gray-500,
+ $theme-gray-700,
+ $theme-gray-800,
+ $theme-gray-900,
+ $white-light
+ );
+ }
+
+ &.ui-light {
@include gitlab-theme(
$theme-gray-900,
$theme-gray-700,
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 2085e5646ef..5789c3fa1b1 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -139,6 +139,8 @@
}
.nav {
+ flex-wrap: nowrap;
+
> li:not(.d-none) a {
@include media-breakpoint-down(xs) {
margin-left: 0;
@@ -158,11 +160,12 @@
}
.navbar-toggler {
+ position: relative;
right: -10px;
border-radius: 0;
min-width: 45px;
padding: 0;
- margin-right: -7px;
+ margin: $gl-padding-8 -7px $gl-padding-8 0;
font-size: 14px;
text-align: center;
color: currentColor;
@@ -186,6 +189,7 @@
display: -webkit-flex;
display: flex;
padding-right: 10px;
+ flex-direction: row;
}
li {
@@ -264,6 +268,8 @@
.navbar-sub-nav,
.navbar-nav {
+ align-items: center;
+
> li {
> a:hover,
> a:focus {
@@ -290,6 +296,10 @@
margin: 8px;
}
}
+
+ .dropdown-menu {
+ position: absolute;
+ }
}
.navbar-sub-nav {
@@ -297,12 +307,6 @@
display: flex;
margin: 0 0 0 6px;
- .projects-dropdown-menu {
- padding: 0;
- overflow-y: initial;
- max-height: initial;
- }
-
.dropdown-chevron {
position: relative;
top: -1px;
@@ -443,12 +447,18 @@
}
.btn-sign-in {
- margin-top: 3px;
+ background-color: $indigo-100;
+ color: $indigo-900;
font-weight: $gl-font-weight-bold;
+ line-height: 18px;
&:hover {
background-color: $white-light;
}
+
+ @include media-breakpoint-down(xs) {
+ margin-top: $gl-padding-4;
+ }
}
.navbar-nav {
@@ -517,7 +527,7 @@
.header-user {
.dropdown-menu {
width: auto;
- min-width: 160px;
+ min-width: unset;
margin-top: 4px;
color: $gl-text-color;
left: auto;
@@ -529,6 +539,10 @@
display: block;
}
}
+
+ svg {
+ vertical-align: text-top;
+ }
}
}
@@ -548,7 +562,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/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 0536c39cee7..52b5f059f20 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -54,10 +54,6 @@ body {
&.limit-container-width {
max-width: $limited-layout-width;
}
-
- &.limit-container-width-sm {
- max-width: $limited-layout-width-sm;
- }
}
.alert-wrapper {
@@ -115,9 +111,3 @@ body {
.with-performance-bar .layout-page {
margin-top: $header-height + $performance-bar-height;
}
-
-.vertical-center {
- min-height: 100vh;
- display: flex;
- align-items: center;
-}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index b893151e4fe..7290a174668 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -61,10 +61,6 @@
padding-top: 0;
line-height: 19px;
- &.btn.btn-xs {
- padding: 2px 5px;
- }
-
&:focus {
margin-top: -10px;
padding-top: 10px;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index d76cf8f8182..0b645eb811b 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -186,6 +186,7 @@
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
display: flex;
+ flex-wrap: nowrap;
&::-webkit-scrollbar {
display: none;
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index a7896cc3fc3..ffb40166c15 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -1,3 +1,7 @@
+.modal-xl {
+ max-width: 98%;
+}
+
.modal-header {
background-color: $modal-body-bg;
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index d3e013590b6..61d02511ff4 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -1,91 +1,14 @@
.gl-pagination {
- text-align: center;
- border-top: 1px solid $border-color;
- margin: 0;
- margin-top: 0;
-
- .pagination {
- padding: 0;
- margin: 20px 0;
-
- a {
- cursor: pointer;
- }
-
- .separator,
- .separator:hover {
- a {
- cursor: default;
- background-color: $gray-light;
- padding: $gl-vert-padding;
- }
- }
+ a {
+ color: inherit;
+ text-decoration: none;
}
-
-
- .gap,
- .gap:hover {
- background-color: $gray-light;
- padding: $gl-vert-padding;
- cursor: default;
- }
-}
-
-.card > .gl-pagination {
- margin: 0;
}
-/**
- * Extra-small screen pagination.
- */
-@media (max-width: 320px) {
- .gl-pagination {
- .first,
- .last {
- display: none;
- }
-
- .page-item {
- display: none;
-
- &.active {
- display: inline;
- }
- }
- }
-}
-
-/**
- * Small screen pagination
- */
-@include media-breakpoint-down(xs) {
- .gl-pagination {
- .pagination li a {
- padding: 6px 10px;
- }
-
- .page-item {
- display: none;
-
- &.active {
- display: inline;
- }
- }
- }
-}
-
-/**
- * Medium screen pagination
- */
-@media (min-width: map-get($grid-breakpoints, xs)) and (max-width: map-get($grid-breakpoints, sm)) {
- .gl-pagination {
- .page-item {
- display: none;
-
- &.active,
- &.sibling {
- display: inline;
- }
+.page-item {
+ &.active {
+ .page-link {
+ z-index: 3;
}
}
}
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 847fc8c0792..9dbb04e5443 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -197,7 +197,7 @@
flex-flow: row wrap;
.nav-controls {
- $controls-margin: $btn-xs-side-margin - 2px;
+ $controls-margin: $btn-margin-5 - 2px;
flex: 0 0 100%;
&.controls-flex {
@@ -230,6 +230,8 @@
}
.scrolling-tabs-container {
+ position: relative;
+
.merge-request-tabs-container & {
overflow: hidden;
}
@@ -345,7 +347,7 @@
.empty-state .project-item-select-holder.btn-group {
float: none;
- display: inline-block;
+ justify-content: center;
.btn {
// overrides styles applied to plain `.empty-state .btn`
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index a10bd1544c5..339388392df 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -1,5 +1,6 @@
.table-holder {
margin: 0;
+ overflow: auto;
}
table {
@@ -38,6 +39,11 @@ table {
&.wide {
width: 55%;
}
+
+ &.table-th-transparent {
+ background: none;
+ color: $gl-text-color-secondary;
+ }
}
td {
@@ -45,9 +51,91 @@ table {
}
}
}
+
+ &.responsive-table {
+ @include media-breakpoint-down(sm) {
+ thead {
+ display: none;
+ }
+
+ &,
+ tbody,
+ td {
+ display: block;
+ }
+
+ td {
+ color: $gl-text-color-secondary;
+ }
+
+ tbody td.responsive-table-cell {
+ padding: $gl-padding 0;
+ width: 100%;
+ display: flex;
+ text-align: right;
+ align-items: center;
+ justify-content: space-between;
+
+ &[data-column]::before {
+ content: attr(data-column);
+ display: block;
+ text-align: left;
+ padding-right: $gl-padding;
+ color: $gl-text-color-secondary;
+ }
+
+ &:not([data-column]) {
+ flex-direction: row-reverse;
+ }
+ }
+
+ tr.responsive-table-border-start,
+ tr.responsive-table-border-end {
+ display: block;
+ border: solid $gl-text-color-quaternary;
+ padding-left: 0;
+ padding-right: 0;
+
+ > td {
+ border-color: $gl-text-color-quaternary;
+
+ &,
+ &:last-child {
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
+ }
+ }
+ }
+
+ tr.responsive-table-border-start {
+ border-width: 1px 1px 0;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ padding-top: 0;
+ padding-bottom: 0;
+
+ > td:first-child {
+ border-top: 0; // always have the <table> top border
+ }
+
+ > td:last-child {
+ border-bottom: 1px solid $gl-text-color-quaternary;
+ }
+ }
+
+ tr.responsive-table-border-end {
+ border-width: 0 1px 1px;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
+ margin-bottom: 2 * $gl-padding;
+
+ > :last-child {
+ border-bottom: 0;
+ }
+ }
+ }
+ }
}
-.responsive-table {
+.responsive-table:not(table) {
@include media-breakpoint-down(sm) {
th {
width: 100%;
diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss
index 744fd0ff796..7cda674e5c8 100644
--- a/app/assets/stylesheets/framework/terms.scss
+++ b/app/assets/stylesheets/framework/terms.scss
@@ -11,15 +11,15 @@
padding-top: $gl-padding;
}
- .panel {
- .panel-heading {
+ .card {
+ .card-header {
display: -webkit-flex;
display: flex;
align-items: center;
justify-content: space-between;
line-height: $line-height-base;
- .title {
+ .card-title {
display: flex;
align-items: center;
@@ -34,6 +34,8 @@
.navbar-collapse {
padding-right: 0;
+ flex-grow: 0;
+ flex-basis: auto;
.navbar-nav {
margin: 0;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 75c11590547..dfb145debe7 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -4,7 +4,7 @@
padding: 0;
&::before {
- @include notes-media('max', map-get($grid-breakpoints, xs)) {
+ @include notes-media('max', map-get($grid-breakpoints, sm)) {
background: none;
}
}
@@ -34,7 +34,7 @@
.timeline-entry-inner {
position: relative;
- @include notes-media('max', map-get($grid-breakpoints, xs)) {
+ @include notes-media('max', map-get($grid-breakpoints, sm)) {
.timeline-icon {
display: none;
}
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
index d5cc78a6680..20394cc1e52 100644
--- a/app/assets/stylesheets/framework/toggle.scss
+++ b/app/assets/stylesheets/framework/toggle.scss
@@ -42,6 +42,10 @@
background: none;
}
+ &:focus {
+ outline: none;
+ }
+
.toggle-icon {
position: relative;
display: block;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 97b821e0cb9..9e77ea03a24 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -114,26 +114,27 @@
font-size: 0.95em;
}
+ blockquote,
.blockquote {
color: $gl-grayish-blue;
font-size: inherit;
padding: 8px 24px;
margin: 16px 0;
border-left: 3px solid $white-dark;
- }
- .blockquote:dir(rtl) {
- border-left: 0;
- border-right: 3px solid $white-dark;
- }
+ &:dir(rtl) {
+ border-left: 0;
+ border-right: 3px solid $white-dark;
+ }
- .blockquote p {
- color: $gl-grayish-blue !important;
- font-size: inherit;
- line-height: 1.5;
+ p {
+ color: $gl-grayish-blue !important;
+ font-size: inherit;
+ line-height: 1.5;
- &:last-child {
- margin: 0;
+ &:last-child {
+ margin: 0;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 946223cfff0..7808f6d3a25 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -99,7 +99,7 @@ $theme-gray-200: #dfdfdf;
$theme-gray-300: #cccccc;
$theme-gray-400: #bababa;
$theme-gray-500: #a7a7a7;
-$theme-gray-600: #949494;
+$theme-gray-600: #919191;
$theme-gray-700: #707070;
$theme-gray-800: #4f4f4f;
$theme-gray-900: #2e2e2e;
@@ -117,6 +117,15 @@ $theme-blue-800: #25496e;
$theme-blue-900: #1a3652;
$theme-blue-950: #0f2235;
+$theme-light-blue-50: #f2f7fc;
+$theme-light-blue-100: #ebf1f7;
+$theme-light-blue-200: #c9dcf2;
+$theme-light-blue-300: #83abd4;
+$theme-light-blue-400: #4d86bf;
+$theme-light-blue-500: #367cc2;
+$theme-light-blue-600: #3771ab;
+$theme-light-blue-700: #2261a1;
+
$theme-green-50: #f2faf6;
$theme-green-100: #e4f3ea;
$theme-green-200: #c0dfcd;
@@ -129,6 +138,29 @@ $theme-green-800: #145d33;
$theme-green-900: #0d4524;
$theme-green-950: #072d16;
+$theme-light-green-700: #156b39;
+
+$theme-red-50: #fcf4f2;
+$theme-red-100: #fae9e6;
+$theme-red-200: #ebcac5;
+$theme-red-300: #d99b91;
+$theme-red-400: #b0655a;
+$theme-red-500: #ad4a3b;
+$theme-red-600: #9e4133;
+$theme-red-700: #912f20;
+$theme-red-800: #78291d;
+$theme-red-900: #691a16;
+$theme-red-950: #36140f;
+
+$theme-light-red-50: #fff6f5;
+$theme-light-red-100: #fae2de;
+$theme-light-red-200: #f7d5d0;
+$theme-light-red-300: #d9796a;
+$theme-light-red-400: #cf604e;
+$theme-light-red-500: #c24b38;
+$theme-light-red-600: #b03927;
+$theme-light-red-700: #a62e21;
+
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424;
@@ -142,11 +174,6 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
$border-gray-dark: darken($white-normal, $darken-border-factor);
/*
- * Override Bootstrap 4 variables
- */
-$secondary: $gray-light;
-
-/*
* UI elements
*/
$border-color: #e5e5e5;
@@ -164,7 +191,7 @@ $gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
$gl-text-color: #2e2e2e;
$gl-text-color-secondary: #707070;
-$gl-text-color-tertiary: #949494;
+$gl-text-color-tertiary: #919191;
$gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, 0.85);
@@ -206,7 +233,7 @@ $md-area-border: #ddd;
/*
* Code
*/
-$code_font_size: 12px;
+$code_font_size: 90%;
$code_line_height: 1.6;
/*
@@ -217,10 +244,11 @@ $tooltip-font-size: 12px;
/*
* Padding
*/
-$gl-padding-24: 24px;
-$gl-padding: 16px;
-$gl-padding-8: 8px;
$gl-padding-4: 4px;
+$gl-padding-8: 8px;
+$gl-padding: 16px;
+$gl-padding-24: 24px;
+$gl-padding-32: 32px;
$gl-col-padding: 15px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px;
@@ -238,7 +266,6 @@ $header-height: 40px;
$ide-statusbar-height: 25px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
-$limited-layout-width-sm: 790px;
$container-text-max-width: 540px;
$gl-avatar-size: 40px;
$error-exclamation-point: $red-500;
@@ -251,7 +278,7 @@ $active-item-blue: $blue-500;
$layout-link-gray: #7e7c7c;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
-$btn-xs-side-margin: 5px;
+$btn-margin-5: 5px;
$issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary;
$sidebar-block-hover-color: #ebebeb;
@@ -408,6 +435,22 @@ $badge-bg: rgba(0, 0, 0, 0.07);
$badge-color: $gl-text-color-secondary;
/*
+* Pagination
+*/
+$pagination-padding-y: 6px;
+$pagination-padding-x: 16px;
+$pagination-line-height: 20px;
+$pagination-border-color: $border-color;
+$pagination-active-bg: $blue-600;
+$pagination-active-border-color: $blue-600;
+$pagination-hover-bg: $blue-50;
+$pagination-hover-border-color: $border-color;
+$pagination-hover-color: $gl-text-color;
+$pagination-disabled-color: #cdcdcd;
+$pagination-disabled-bg: $gray-light;
+$pagination-disabled-border-color: $border-color;
+
+/*
* Status icons
*/
$status-icon-size: 22px;
@@ -776,3 +819,19 @@ $modal-body-height: 134px;
Prometheus
*/
$prometheus-table-row-highlight-color: $theme-gray-100;
+
+$priority-label-empty-state-width: 114px;
+
+/*
+ * Override Bootstrap 4 variables
+ */
+
+$secondary: $gray-light;
+$input-disabled-bg: $gray-light;
+$input-border-color: $theme-gray-200;
+$input-color: $gl-text-color;
+$font-family-sans-serif: $regular_font;
+$font-family-monospace: $monospace_font;
+$input-line-height: 20px;
+$btn-line-height: 20px;
+$table-accent-bg: $gray-light;
diff --git a/app/assets/stylesheets/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/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index b5eda79e5ed..1835c4364d3 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -138,6 +138,7 @@ pre {
margin: 0;
}
+blockquote,
.blockquote {
color: $gl-grayish-blue;
padding: 0 0 0 15px;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 1c3d312f7ac..750d2c8b990 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -80,6 +80,7 @@
overflow-x: scroll;
white-space: nowrap;
min-height: 200px;
+ display: flex;
@include media-breakpoint-only(sm) {
height: calc(100vh - #{$issue-board-list-difference-sm});
@@ -110,17 +111,15 @@
.board {
display: inline-block;
- width: calc(85vw - 15px);
+ flex: 1;
+ min-width: 300px;
+ max-width: 400px;
height: 100%;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
white-space: normal;
vertical-align: top;
- @include media-breakpoint-up(sm) {
- width: 400px;
- }
-
&.is-expandable {
.board-header {
cursor: pointer;
@@ -128,6 +127,8 @@
}
&.is-collapsed {
+ flex: none;
+ min-width: 0;
width: 50px;
.board-header {
@@ -282,9 +283,6 @@
box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none;
- // as a fallback, hide overflow content so that dragging and dropping still works
- overflow: hidden;
-
&:not(:last-child) {
margin-bottom: 5px;
}
@@ -292,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/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 9ee02ca1d83..f030189af06 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -12,26 +12,22 @@
@keyframes blinking-dots {
0% {
background-color: rgba($white-light, 1);
- box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 0.2);
}
25% {
background-color: rgba($white-light, 0.4);
- box-shadow: 12px 0 0 0 rgba($white-light, 2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ box-shadow: 12px 0 0 0 rgba($white-light, 2), 24px 0 0 0 rgba($white-light, 0.2);
}
75% {
background-color: rgba($white-light, 0.4);
- box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 1);
+ box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 1);
}
100% {
background-color: rgba($white-light, 1);
- box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 0.2);
}
}
@@ -71,10 +67,15 @@
.bash {
display: block;
}
+
+ &.build-trace-rounded {
+ border-radius: $border-radius-base;
+ }
}
.top-bar {
height: 35px;
+ min-height: 35px;
background: $gray-light;
border: 1px solid $border-color;
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 3e4d123242c..56beb7718a4 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -13,6 +13,10 @@
max-width: 100%;
}
+.clusters-error-alert {
+ width: 100%;
+}
+
.clusters-container {
.nav-bar-right {
padding: $gl-padding-top $gl-padding;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index a4ca82de90e..f75be4e01cd 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -135,10 +135,10 @@
}
.text-expander {
- display: inline-block;
+ display: inline-flex;
background: $white-light;
color: $gl-text-color-secondary;
- padding: 0 4px;
+ padding: 1px $gl-padding-4;
cursor: pointer;
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
@@ -180,6 +180,11 @@
.commit-content {
padding-right: 10px;
white-space: normal;
+
+ .commit-title {
+ display: flex;
+ align-items: center;
+ }
}
.commit-actions {
@@ -193,6 +198,10 @@
display: inline-flex;
}
+ .ci-status-icon svg {
+ vertical-align: text-bottom;
+ }
+
> .ci-status-link,
> .btn,
> .commit-sha-group {
@@ -249,16 +258,19 @@
.generic_commit_status {
a,
button {
- color: $gl-text-color;
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/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 7b36bcb3c7d..2e007c52592 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -23,7 +23,8 @@
position: relative;
line-height: 35px;
display: flex;
- flex-grow: 1;
+ flex: 1 1;
+ min-width: 0;
@include media-breakpoint-up(sm) {
padding-left: 0;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index f06c9dcdf8c..a90a9c6e486 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: text-bottom;
}
}
@@ -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 {
@@ -189,8 +200,22 @@
img {
border: 1px solid $white-light;
- background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%),
- linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%);
+ background-image: linear-gradient(
+ 45deg,
+ $border-color 25%,
+ transparent 25%,
+ transparent 75%,
+ $border-color 75%,
+ $border-color 100%
+ ),
+ linear-gradient(
+ 45deg,
+ $border-color 25%,
+ transparent 25%,
+ transparent 75%,
+ $border-color 75%,
+ $border-color 100%
+ );
background-size: 10px 10px;
background-position: 0 0, 5px 5px;
max-width: 100%;
@@ -395,6 +420,69 @@
.line_content {
white-space: pre-wrap;
}
+
+ .diff-file-container {
+ .frame.deleted {
+ border: 0;
+ background-color: inherit;
+
+ .image_file img {
+ border: 1px solid $deleted;
+ }
+ }
+
+ .frame.added {
+ border: 0;
+ background-color: inherit;
+
+ .image_file img {
+ border: 1px solid $added;
+ }
+ }
+
+ .swipe.view,
+ .onion-skin.view {
+ .swipe-wrap {
+ top: 0;
+ right: 0;
+ }
+
+ .frame.deleted {
+ top: 0;
+ right: 0;
+ }
+
+ .swipe-bar {
+ top: 0;
+
+ .top-handle {
+ top: -14px;
+ left: -7px;
+ }
+
+ .bottom-handle {
+ bottom: -14px;
+ left: -7px;
+ }
+ }
+
+ .file-container {
+ display: inline-block;
+
+ .file-content {
+ padding: 0;
+
+ img {
+ max-width: none;
+ }
+ }
+ }
+ }
+
+ .onion-skin.view .controls {
+ bottom: -25px;
+ }
+ }
}
.file-content .diff-file {
@@ -414,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;
@@ -536,7 +628,7 @@
margin-right: 0;
border-color: $white-light;
cursor: pointer;
- transition: all .1s ease-out;
+ transition: all 0.1s ease-out;
@for $i from 1 through 4 {
&:nth-child(#{$i}) {
@@ -563,7 +655,7 @@
height: 24px;
border-radius: 50%;
padding: 0;
- transition: transform .1s ease-out;
+ transition: transform 0.1s ease-out;
z-index: 100;
.collapse-icon {
@@ -600,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 {
@@ -644,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;
@@ -708,11 +805,35 @@
width: 100%;
height: 10px;
background-color: $white-light;
- background-image: linear-gradient(45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
- linear-gradient(225deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
- linear-gradient(135deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
- linear-gradient(-45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%);
- background-position: 5px 5px,0 5px,0 5px,5px 5px;
+ background-image: linear-gradient(
+ 45deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%
+ ),
+ linear-gradient(
+ 225deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%
+ ),
+ linear-gradient(
+ 135deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%
+ ),
+ linear-gradient(
+ -45deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%
+ );
+ background-position: 5px 5px, 0 5px, 0 5px, 5px 5px;
background-size: 10px 10px;
background-repeat: repeat;
}
@@ -750,11 +871,16 @@
.frame.click-to-comment {
position: relative;
cursor: image-url('illustrations/image_comment_light_cursor.svg')
- $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
+ $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
+ auto;
// Retina cursor
- cursor: -webkit-image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x, image-url('illustrations/image_comment_light_cursor@2x.svg') 2x)
- $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
+ cursor: -webkit-image-set(
+ image-url('illustrations/image_comment_light_cursor.svg') 1x,
+ image-url('illustrations/image_comment_light_cursor@2x.svg') 2x
+ )
+ $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
+ auto;
.comment-indicator {
position: absolute;
@@ -840,7 +966,7 @@
.diff-notes-collapse,
.note,
- .discussion-reply-holder, {
+ .discussion-reply-holder {
display: none;
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index cd0d67613c3..06f08ae2215 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -23,7 +23,6 @@
}
.btn-group {
-
> a {
color: $gl-text-color-secondary;
}
@@ -245,6 +244,7 @@
.prometheus-graph {
flex: 1 0 auto;
min-width: 450px;
+ max-width: 100%;
padding: $gl-padding / 2;
h5 {
@@ -256,6 +256,17 @@
}
}
+.prometheus-graph-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: $gl-padding-8;
+
+ h5 {
+ margin: 0;
+ }
+}
+
.prometheus-graph-cursor {
position: absolute;
background: $theme-gray-600;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index c2b42e02eee..05bf5596fb3 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -425,7 +425,7 @@
margin-left: 5px;
> .btn {
- margin-right: $btn-xs-side-margin;
+ margin-right: $btn-margin-5;
}
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 4aea9740735..f9fd9f1ab8b 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -485,6 +485,15 @@
.sidebar-collapsed-user {
padding-bottom: 0;
margin-bottom: 10px;
+
+ .author_link {
+ padding-left: 0;
+
+ .avatar {
+ position: static;
+ margin: 0;
+ }
+ }
}
.issuable-header-btn {
@@ -689,6 +698,8 @@
font-size: 14px;
line-height: 24px;
align-self: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.js-issuable-selector-wrap {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index e178371d21f..391dfea0703 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -57,67 +57,6 @@
border-bottom-left-radius: $border-radius-base;
}
-.label-row {
- .label-name {
- display: inline-block;
- margin-bottom: 10px;
-
- @include media-breakpoint-up(sm) {
- width: 200px;
- margin-left: $gl-padding * 2;
- margin-bottom: 0;
- }
-
- .badge {
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 100%;
- }
- }
-
- .label-type {
- display: block;
- margin-bottom: 10px;
- margin-left: 50px;
-
- @include media-breakpoint-up(sm) {
- display: inline-block;
- width: 100px;
- margin-left: 10px;
- margin-bottom: 0;
- vertical-align: top;
- }
- }
-
- .label-description {
- display: block;
- margin-bottom: 10px;
-
- .description-text {
- margin-bottom: $gl-padding;
- }
-
- a {
- color: $blue-600;
- }
-
- @include media-breakpoint-up(sm) {
- display: inline-block;
- max-width: 50%;
- margin-left: 10px;
- margin-bottom: 0;
- vertical-align: top;
- }
- }
-
- .badge {
- padding: 4px $grid-size;
- font-size: $label-font-size;
- position: relative;
- top: ($grid-size / 2);
- }
-}
-
.color-label {
padding: 0 $grid-size;
line-height: 16px;
@@ -133,26 +72,31 @@
}
.manage-labels-list {
- @media(min-width: map-get($grid-breakpoints, md)) {
- &.content-list li {
- padding: $gl-padding 0;
- }
- }
-
> li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light;
- cursor: move;
- cursor: -webkit-grab;
- cursor: -moz-grab;
-
- &:active {
- cursor: -webkit-grabbing;
- cursor: -moz-grabbing;
- }
+ margin-bottom: 5px;
+ display: flex;
+ justify-content: space-between;
+ padding: $gl-padding;
+ border-radius: $border-radius-default;
+ border: 1px solid $theme-gray-100;
&.sortable-ghost {
opacity: 0.3;
}
+
+ .prioritized-labels & {
+ box-shadow: 0 1px 2px $issue-boards-card-shadow;
+ cursor: move;
+ cursor: -webkit-grab;
+ cursor: -moz-grab;
+ border: 0;
+
+ &:active {
+ cursor: -webkit-grabbing;
+ cursor: -moz-grabbing;
+ }
+ }
}
.btn-action {
@@ -171,28 +115,11 @@
}
}
- .dropdown {
- @include media-breakpoint-up(sm) {
- float: right;
- }
- }
-
- @include media-breakpoint-down(xs) {
- .dropdown-menu {
- min-width: 100%;
- }
+ .color-label {
+ padding: $gl-padding-4 $grid-size;
}
}
-.draggable-handler {
- display: inline-block;
- vertical-align: top;
- margin: 5px 0;
- opacity: 0;
- transition: opacity .3s;
- color: $gray-darkest;
-}
-
.prioritized-labels {
margin-bottom: 30px;
@@ -215,22 +142,6 @@
}
}
-.toggle-priority {
- display: inline-block;
- vertical-align: top;
-
- button {
- border-color: transparent;
- padding: 5px 8px;
- vertical-align: top;
- font-size: 14px;
-
- &:hover {
- border-color: transparent;
- }
- }
-}
-
.filtered-labels {
font-size: 0;
padding: 12px 16px;
@@ -284,10 +195,8 @@
}
.label-subscribe-button {
- @media(min-width: map-get($grid-breakpoints, md)) {
- min-width: 105px;
- margin-left: $gl-padding;
- }
+ width: 105px;
+ font-weight: 200;
.label-subscribe-button-icon {
&[disabled] {
@@ -313,7 +222,7 @@
.label-link {
display: inline-flex;
- vertical-align: top;
+ vertical-align: text-bottom;
&:hover .color-label {
text-decoration: underline;
@@ -324,3 +233,95 @@
font-size: $label-font-size;
}
}
+
+.labels-container {
+ background-color: $gray-light;
+ border-radius: $border-radius-default;
+ padding: $gl-padding $gl-padding-8;
+}
+
+.label-actions-list {
+ list-style: none;
+ flex-shrink: 0;
+ padding: 0;
+}
+
+.label-badge {
+ color: $theme-gray-900;
+ font-weight: $gl-font-weight-normal;
+ padding: $gl-padding-4 $gl-padding-8;
+ border-radius: $border-radius-default;
+ font-size: $label-font-size;
+}
+
+.label-badge-blue {
+ background-color: $theme-blue-100;
+}
+
+.label-badge-gray {
+ background-color: $theme-gray-100;
+}
+
+.label-links {
+ list-style: none;
+ padding: 0;
+ white-space: nowrap;
+}
+
+.label-link-item {
+ padding: 0;
+}
+
+.label-list-item {
+ .content-list &::before,
+ .content-list &::after {
+ content: none;
+ }
+
+ .label-name {
+ width: 150px;
+ flex-shrink: 0;
+
+ .badge {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+ }
+ }
+
+ .label-description {
+ flex-grow: 1;
+
+ a {
+ color: $blue-600;
+ }
+ }
+
+ .label {
+ padding: 4px $grid-size;
+ font-size: $label-font-size;
+ position: relative;
+ top: $gl-padding-4;
+ }
+
+ .label-action {
+ color: $theme-gray-800;
+ cursor: pointer;
+
+ svg {
+ fill: $theme-gray-800;
+ }
+
+ &:hover {
+ color: $blue-600;
+
+ svg {
+ fill: $blue-600;
+ }
+ }
+ }
+}
+
+.priority-labels-empty-state .svg-content img {
+ max-width: $priority-label-empty-state-width;
+}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 9914555d309..5fdb2b4a90a 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -121,10 +121,6 @@
background: transparent;
border: 0;
outline: 0;
-
- @include media-breakpoint-up(sm) {
- right: 160px;
- }
}
.flex-project-members-panel {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 9eceb3e9a33..efd730af558 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -51,12 +51,6 @@
opacity: 0.3;
}
- &.btn-xs {
- line-height: 1;
- padding: 5px 10px;
- margin-top: 1px;
- }
-
&.dropdown-toggle {
.fa {
color: inherit;
@@ -605,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 {
@@ -678,6 +670,7 @@
.merge-request-tabs {
display: flex;
+ flex-wrap: nowrap;
margin-bottom: 0;
padding: 0;
}
@@ -744,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 3b037d066dc..5e5696b1602 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -129,7 +129,7 @@
.icon svg {
position: relative;
top: 2px;
- margin-right: $btn-xs-side-margin;
+ margin-right: $btn-margin-5;
width: $gl-font-size;
height: $gl-font-size;
fill: $orange-600;
@@ -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/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index f85f66b9c0b..52332ac97dd 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -36,6 +36,7 @@
}
.table-holder {
+ overflow: unset;
width: 100%;
}
@@ -321,18 +322,17 @@
}
.build-failures {
+ th {
+ border-top: 0;
+ }
+
.build-state {
padding: 20px 2px;
.build-name {
- float: right;
font-weight: $gl-font-weight-normal;
}
- .ci-status-icon-failed svg {
- vertical-align: middle;
- }
-
.stage {
color: $gl-text-color-secondary;
font-weight: $gl-font-weight-normal;
@@ -344,6 +344,81 @@
border: 0;
line-height: initial;
}
+
+ .build-trace-row td {
+ border-top: 0;
+ border-bottom-width: 1px;
+ border-bottom-style: solid;
+ padding-top: 0;
+ }
+
+ .build-trace {
+ width: 100%;
+ text-align: left;
+ margin-top: $gl-padding;
+ }
+
+ .build-name {
+ width: 196px;
+
+ a {
+ font-weight: $gl-font-weight-bold;
+ color: $gl-text-color;
+ text-decoration: none;
+
+ &:focus,
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .build-actions {
+ width: 70px;
+ text-align: right;
+ }
+
+ .build-stage {
+ width: 140px;
+ }
+
+ .ci-status-icon-failed {
+ padding: 10px 0 10px 12px;
+ width: 12px + 24px; // padding-left + svg width
+ }
+
+ .build-icon svg {
+ width: 24px;
+ height: 24px;
+ vertical-align: middle;
+ }
+
+ .build-state,
+ .build-trace-row {
+ > td:last-child {
+ padding-right: 0;
+ }
+ }
+
+ @include media-breakpoint-down(sm) {
+ td:empty {
+ display: none;
+ }
+
+ .ci-table {
+ margin-top: 2 * $gl-padding;
+ }
+
+ .build-trace-container {
+ padding-top: $gl-padding;
+ padding-bottom: $gl-padding;
+ }
+
+ .build-trace {
+ margin-bottom: 0;
+ margin-top: 0;
+ }
+ }
}
.pipeline-tab-content {
@@ -926,10 +1001,10 @@ button.mini-pipeline-graph-dropdown-toggle {
/**
* Center dropdown menu in mini graph
*/
- &.dropdown-menu {
+ .dropdown &.dropdown-menu {
transform: translate(-80%, 0);
- @media(min-width: map-get($grid-breakpoints, md)) {
+ @media (min-width: map-get($grid-breakpoints, md)) {
transform: translate(-50%, 0);
right: auto;
left: 50%;
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index 68d40b56133..a353f301d07 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -1,25 +1,3 @@
-@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) {
- .one {
- background-color: $color-1;
- border-top-left-radius: $border-radius-default;
- }
-
- .two {
- background-color: $color-2;
- border-top-right-radius: $border-radius-default;
- }
-
- .three {
- background-color: $color-3;
- border-bottom-left-radius: $border-radius-default;
- }
-
- .four {
- background-color: $color-4;
- border-bottom-right-radius: $border-radius-default;
- }
-}
-
.multi-file-editor-options {
label {
margin-right: 20px;
@@ -38,49 +16,67 @@
.application-theme {
label {
- margin-right: 20px;
+ margin: 0 $gl-padding-32 $gl-padding 0;
text-align: center;
}
.preview {
font-size: 0;
- margin-bottom: 10px;
+ height: 48px;
+ border-radius: 4px;
+ min-width: 112px;
+ margin-bottom: $gl-padding-8;
+
+ &.ui-indigo {
+ background-color: $indigo-900;
+ }
- &.indigo {
- @include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500);
+ &.ui-light-indigo {
+ background-color: $indigo-700;
}
- &.dark {
- @include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600);
+ &.ui-blue {
+ background-color: $theme-blue-900;
}
- &.light {
- @include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100);
+ &.ui-light-blue {
+ background-color: $theme-light-blue-700;
}
- &.blue {
- @include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500);
+ &.ui-green {
+ background-color: $theme-green-900;
}
- &.green {
- @include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500);
+ &.ui-light-green {
+ background-color: $theme-light-green-700;
+ }
+
+ &.ui-red {
+ background-color: $theme-red-900;
+ }
+
+ &.ui-light-red {
+ background-color: $theme-light-red-700;
+ }
+
+ &.ui-dark {
+ background-color: $theme-gray-900;
+ }
+
+ &.ui-light {
+ background-color: $theme-gray-200;
}
}
.preview-row {
display: block;
}
-
- .quadrant {
- display: inline-block;
- height: 50px;
- width: 80px;
- }
}
.syntax-theme {
label {
- margin-right: 20px;
+ margin-right: $gl-padding-32;
+ margin-bottom: $gl-padding;
text-align: center;
.preview {
@@ -89,7 +85,6 @@
img {
border-radius: 4px;
-
max-width: 100%;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 22964163e95..aa83e5bdebc 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -354,12 +354,6 @@
min-width: 200px;
}
-.deploy-keys {
- .scrolling-tabs-container {
- position: relative;
- }
-}
-
.deploy-key {
// Ensure that the fingerprint does not overflow on small screens
.fingerprint {
@@ -503,6 +497,12 @@
&:not(:first-child) {
border-top: 1px solid $border-color;
}
+
+ .btn-template-icon {
+ position: absolute;
+ left: $gl-padding;
+ top: $gl-padding;
+ }
}
.template-title {
@@ -520,12 +520,6 @@
}
}
- svg {
- position: absolute;
- left: $gl-padding;
- top: $gl-padding;
- }
-
.project-fields-form {
display: none;
@@ -536,34 +530,23 @@
}
.template-input-group {
- position: relative;
-
- @include media-breakpoint-up(sm) {
- display: flex;
- }
-
- .input-group-prepend,
- .input-group-append {
+ .input-group-prepend {
flex: 1;
- text-align: left;
- padding-left: ($gl-padding * 3);
- background-color: $white-light;
}
- .selected-template {
- line-height: 20px;
+ .input-group-text {
+ width: 100%;
+ background-color: $white-light;
}
.selected-icon {
+ padding-right: $gl-padding;
+
svg {
display: none;
top: 7px;
height: 20px;
width: 20px;
-
- &.active {
- display: block;
- }
}
}
}
@@ -875,7 +858,6 @@ pre.light-well {
.git-clone-holder {
width: 380px;
- height: 28px;
.btn-clipboard {
border: 1px solid $border-color;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 6bbcb15329c..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;
}
@@ -183,7 +184,7 @@
svg {
position: relative;
- top: -1px;
+ top: -2px;
}
.ide-file-changed-icon {
@@ -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 {
@@ -335,7 +345,6 @@
img {
max-width: 90%;
- max-height: 90%;
}
.isZoomable {
@@ -458,9 +467,9 @@
width: auto;
margin-right: 0;
- a:hover,
- a:focus {
- text-decoration: none;
+ > a,
+ > button {
+ height: 60px;
}
}
@@ -541,32 +550,12 @@
margin-right: -$grid-size;
min-height: 60px;
- .multi-file-commit-list-item {
- margin-left: 0;
- margin-right: 0;
- }
-
&.form-text.text-muted {
margin-left: 0;
right: 0;
}
}
-.multi-file-commit-list-item {
- .multi-file-discard-btn {
- display: none;
- margin-top: -2px;
- margin-left: auto;
- color: $gl-link-color;
- }
-
- &:hover {
- .multi-file-discard-btn {
- display: flex;
- }
- }
-}
-
.multi-file-addition,
.multi-file-addition-solid {
color: $green-500;
@@ -596,7 +585,7 @@
}
}
-.multi-file-commit-list-item,
+.multi-file-commit-list-path,
.ide-file-list .file {
display: flex;
align-items: center;
@@ -608,16 +597,20 @@
&:hover,
&:focus {
- background: $white-normal;
+ background: $theme-gray-100;
+ }
+
+ &:active {
+ background: $theme-gray-200;
}
}
.multi-file-commit-list-path {
- padding: 0;
- background: none;
- border: 0;
- text-align: left;
- width: 100%;
+ cursor: pointer;
+
+ &.is-active {
+ background-color: $white-normal;
+ }
&:hover,
&:focus {
@@ -632,17 +625,23 @@
}
.multi-file-commit-list-file-path {
- @include str-truncated(100%);
-
- &:hover {
- text-decoration: underline;
- }
+ @include str-truncated(calc(100% - 30px));
&:active {
text-decoration: none;
}
}
+.multi-file-discard-btn {
+ top: 4px;
+ right: 8px;
+ bottom: 4px;
+
+ svg {
+ top: 0;
+ }
+}
+
.multi-file-commit-form {
position: relative;
background-color: $white-light;
@@ -718,9 +717,17 @@
}
.ide-new-btn {
+ .btn {
+ padding-top: 3px;
+ padding-bottom: 3px;
+ }
+
+ .dropdown {
+ display: flex;
+ }
+
.dropdown-toggle svg {
- margin-top: -2px;
- margin-bottom: 2px;
+ top: 0;
}
.dropdown-menu {
@@ -829,18 +836,20 @@
}
.ide-staged-action-btn {
- margin-left: auto;
- line-height: 22px;
+ width: 22px;
+ margin-left: -1px;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+
+ > svg {
+ top: 0;
+ }
}
.ide-commit-file-count {
min-width: 22px;
- margin-left: auto;
background-color: $gray-light;
- border-radius: $border-radius-default;
border: 1px solid $white-dark;
- line-height: 20px;
- text-align: center;
}
.ide-commit-radios {
@@ -877,6 +886,7 @@
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
outline: 0;
+ cursor: pointer;
svg {
margin: 0 auto;
@@ -1120,7 +1130,12 @@
.ide-context-header {
.avatar {
- flex: 0 0 40px;
+ flex: 0 0 38px;
+ }
+
+ .ide-merge-requests-dropdown.dropdown-menu {
+ width: 385px;
+ max-height: initial;
}
}
@@ -1130,11 +1145,20 @@
.sidebar-context-title {
white-space: nowrap;
}
+
+ .ide-sidebar-branch-title {
+ min-width: 50px;
+ }
}
.ide-external-link {
+ position: relative;
+
svg {
display: none;
+ position: absolute;
+ top: 2px;
+ right: -$gl-padding;
}
&:hover,
@@ -1165,6 +1189,8 @@
display: flex;
flex-direction: column;
height: 100%;
+ margin-top: -$grid-size;
+ margin-bottom: -$grid-size;
.empty-state {
margin-top: auto;
@@ -1181,6 +1207,17 @@
margin: 0;
}
}
+
+ .build-trace,
+ .top-bar {
+ margin-left: -$gl-padding;
+ }
+
+ &.build-page .top-bar {
+ top: 0;
+ font-size: 12px;
+ border-top-right-radius: $border-radius-default;
+ }
}
.ide-pipeline-list {
@@ -1189,7 +1226,7 @@
}
.ide-pipeline-header {
- min-height: 50px;
+ min-height: 55px;
padding-left: $gl-padding;
padding-right: $gl-padding;
@@ -1209,8 +1246,7 @@
.ci-status-icon {
display: flex;
justify-content: center;
- height: 20px;
- margin-top: -2px;
+ min-width: 24px;
overflow: hidden;
}
}
@@ -1240,3 +1276,56 @@
overflow: hidden;
text-overflow: ellipsis;
}
+
+.ide-job-header {
+ min-height: 60px;
+}
+
+.ide-merge-requests-dropdown {
+ .nav-links li {
+ width: 50%;
+ padding-left: 0;
+ padding-right: 0;
+
+ a {
+ text-align: center;
+
+ &:not(.active) {
+ background-color: $gray-light;
+ }
+ }
+ }
+
+ .dropdown-input {
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
+
+ .fa {
+ right: 26px;
+ }
+ }
+
+ .btn-link {
+ padding-top: $gl-padding;
+ padding-bottom: $gl-padding;
+ }
+}
+
+.ide-merge-request-current-icon {
+ min-width: 18px;
+}
+
+.ide-merge-requests-empty {
+ height: 230px;
+}
+
+.ide-merge-requests-dropdown-content {
+ min-height: 230px;
+ max-height: 470px;
+}
+
+.ide-merge-request-project-path {
+ font-size: 12px;
+ line-height: 16px;
+ color: $gl-text-color-secondary;
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index a35c4ff7c80..2d66f336076 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -18,7 +18,8 @@
.file-finder-input:hover,
.issuable-search-form:hover,
.search-text-input:hover,
-.form-control:hover {
+.form-control:hover,
+:not[readonly] {
border-color: lighten($dropdown-input-focus-border, 20%);
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
}
@@ -28,7 +29,7 @@ input[type="checkbox"]:hover {
}
.search {
- margin: 4px 8px 0;
+ margin: 0 8px;
form {
@extend .form-control;
@@ -113,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 2b3773eebad..e264b06c4b2 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -52,7 +52,7 @@
.settings-content {
max-height: 1px;
- overflow-y: scroll;
+ overflow-y: hidden;
padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out;
// Keep the section from expanding when we scroll over it
@@ -102,10 +102,6 @@
.form-text.text-muted {
margin-top: 0;
}
-
- .label-light {
- margin-bottom: 0;
- }
}
.settings-list-icon {
@@ -131,12 +127,9 @@
color: $gl-danger;
}
-.service-settings .form-control-label {
- padding-top: 0;
-}
-
.integration-settings-form {
- .card.card-body {
+ .card.card-body,
+ .info-well {
padding: $gl-padding / 2;
box-shadow: none;
}
@@ -174,7 +167,7 @@
.option-description,
.option-disabled-reason {
- margin-left: 45px;
+ margin-left: 30px;
color: $project-option-descr-color;
}
@@ -262,25 +255,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;
}
@@ -296,7 +276,8 @@
}
.btn-clipboard {
- margin-left: 5px;
+ background-color: $white-light;
+ border: 1px solid $theme-gray-200;
}
.deploy-token-help-block {
diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss
index a355e2dee24..777fdb3581e 100644
--- a/app/assets/stylesheets/pages/settings_ci_cd.scss
+++ b/app/assets/stylesheets/pages/settings_ci_cd.scss
@@ -16,3 +16,12 @@
.registry-placeholder {
min-height: 60px;
}
+
+.auto-devops-card {
+ margin-bottom: $gl-vert-padding;
+
+ > .card-body {
+ border-radius: $card-border-radius;
+ padding: $gl-padding $gl-padding-24;
+ }
+}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 06ef58531d7..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;
@@ -15,6 +14,7 @@
color: $perf-bar-text;
select {
+ color: $perf-bar-text;
width: 200px;
}
@@ -81,7 +81,7 @@
.view {
margin-right: 15px;
- float: left;
+ flex-shrink: 0;
&:last-child {
margin-right: 0;
@@ -106,12 +106,12 @@
}
.performance-bar-modal {
- .modal-footer {
- display: none;
+ .modal-body {
+ padding: 0;
}
- .modal-dialog {
- width: 860px;
+ .modal-footer {
+ display: none;
}
}
}
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index 90ccd4abd90..bb10928a037 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -22,9 +22,9 @@
header,
nav,
-nav.main-nav,
nav.navbar-collapse,
nav.navbar-collapse.collapse,
+.nav-sidebar,
.profiler-results,
.tree-ref-holder,
.tree-holder .breadcrumb,
@@ -38,7 +38,8 @@ ul.notes-form,
.edit-link,
.note-action-button,
.right-sidebar,
-.flash-container {
+.flash-container,
+#js-peek {
display: none !important;
}
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index ea302f17d16..9aaec905734 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -41,6 +41,13 @@ class Admin::AppearancesController < Admin::ApplicationController
redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.'
end
+ def favicon
+ @appearance.remove_favicon!
+ @appearance.save
+
+ redirect_to admin_appearances_path, notice: 'Favicon was succesfully removed.'
+ end
+
private
# Use callbacks to share common setup or constraints between actions.
@@ -61,6 +68,8 @@ class Admin::AppearancesController < Admin::ApplicationController
logo_cache
header_logo
header_logo_cache
+ favicon
+ favicon_cache
new_project_guidelines
updated_by
]
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 2b47819303e..fb788c47ef1 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -9,7 +9,7 @@ class Admin::HooksController < Admin::ApplicationController
end
def create
- @hook = SystemHook.new(hook_params)
+ @hook = SystemHook.new(hook_params.to_h)
if @hook.save
redirect_to admin_hooks_path, notice: 'Hook was successfully created.'
diff --git a/app/controllers/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 db8a8cdc0d2..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?
@@ -91,6 +91,10 @@ class ApplicationController < ActionController::Base
payload[:user_id] = logged_user.try(:id)
payload[:username] = logged_user.try(:username)
end
+
+ if response.status == 422 && response.body.present? && response.content_type == 'application/json'.freeze
+ payload[:response] = response.body
+ end
end
# Controllers such as GitHttpController may use alternative methods
@@ -130,12 +134,17 @@ class ApplicationController < ActionController::Base
end
def access_denied!(message = nil)
+ # If we display a custom access denied message to the user, we don't want to
+ # hide existence of the resource, rather tell them they cannot access it using
+ # the provided message
+ status = message.present? ? :forbidden : :not_found
+
respond_to do |format|
- format.any { head :not_found }
+ format.any { head status }
format.html do
render "errors/access_denied",
layout: "errors",
- status: 404,
+ status: status,
locals: { message: message }
end
end
@@ -275,8 +284,10 @@ class ApplicationController < ActionController::Base
return unless current_user
return if current_user.terms_accepted?
+ message = _("Please accept the Terms of Service before continuing.")
+
if sessionless_user?
- render_403
+ access_denied!(message)
else
# Redirect to the destination if the request is a get.
# Redirect to the source if it was a post, so the user can re-submit after
@@ -287,7 +298,7 @@ class ApplicationController < ActionController::Base
URI(request.referer).path if request.referer
end
- flash[:notice] = _("Please accept the Terms of Service before continuing.")
+ flash[:notice] = message
redirect_to terms_path(redirect: redirect_path), status: :found
end
end
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
index 381fd4d7508..e8b5934f2a9 100644
--- a/app/controllers/boards/lists_controller.rb
+++ b/app/controllers/boards/lists_controller.rb
@@ -56,8 +56,12 @@ module Boards
private
+ def list_creation_attrs
+ %i[label_id]
+ end
+
def list_params
- params.require(:list).permit(:label_id)
+ params.require(:list).permit(list_creation_attrs)
end
def move_params
@@ -65,11 +69,15 @@ module Boards
end
def serialize_as_json(resource)
- resource.as_json(
+ resource.as_json(serialization_attrs)
+ end
+
+ def serialization_attrs
+ {
only: [:id, :list_type, :position],
methods: [:title],
label: true
- )
+ }
end
end
end
diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb
index 7409b2e89a5..10b9852e329 100644
--- a/app/controllers/concerns/internal_redirect.rb
+++ b/app/controllers/concerns/internal_redirect.rb
@@ -23,6 +23,10 @@ module InternalRedirect
nil
end
+ def sanitize_redirect(url_or_path)
+ safe_redirect_path(url_or_path) || safe_redirect_path_for_url(url_or_path)
+ end
+
def host_allowed?(uri)
uri.host == request.host &&
uri.port == request.port
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index c925b4aada5..ba510968684 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -7,6 +7,19 @@ module IssuableActions
before_action :authorize_admin_issuable!, only: :bulk_update
end
+ def permitted_keys
+ [
+ :issuable_ids,
+ :assignee_id,
+ :milestone_id,
+ :state_event,
+ :subscription_event,
+ label_ids: [],
+ add_label_ids: [],
+ remove_label_ids: []
+ ]
+ end
+
def show
respond_to do |format|
format.html
@@ -77,7 +90,7 @@ module IssuableActions
end
def discussions
- notes = issuable.notes
+ notes = issuable.discussion_notes
.inc_relations_for_view
.includes(:noteable)
.fresh
@@ -140,24 +153,15 @@ module IssuableActions
end
def bulk_update_params
- permitted_keys = [
- :issuable_ids,
- :assignee_id,
- :milestone_id,
- :state_event,
- :subscription_event,
- label_ids: [],
- add_label_ids: [],
- remove_label_ids: []
- ]
+ permitted_keys_array = permitted_keys.dup
if resource_name == 'issue'
- permitted_keys << { assignee_ids: [] }
+ permitted_keys_array << { assignee_ids: [] }
else
- permitted_keys.unshift(:assignee_id)
+ permitted_keys_array.unshift(:assignee_id)
end
- params.require(:update).permit(permitted_keys)
+ params.require(:update).permit(permitted_keys_array)
end
def resource_name
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index ca1b80a36a0..2ef2ee76855 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -95,12 +95,7 @@ module IssuableCollections
elsif @group
@filter_params[:group_id] = @group.id
@filter_params[:include_subgroups] = true
- else
- # TODO: this filter ignore issues/mr created in public or
- # internal repos where you are not a member. Enable this filter
- # or improve current implementation to filter only issues you
- # created or assigned or mentioned
- # @filter_params[:authorized_only] = true
+ @filter_params[:use_cte_for_search] = true
end
@filter_params.permit(finder_type.valid_params)
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index b6eb7d292fc..9d58656773d 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -1,6 +1,7 @@
module IssuesAction
extend ActiveSupport::Concern
include IssuableCollections
+ include IssuesCalendar
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def issues
@@ -17,18 +18,9 @@ module IssuesAction
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
def issues_calendar
- @issues = issuables_collection
- .non_archived
- .with_due_date
- .limit(100)
-
- respond_to do |format|
- format.ics { response.headers['Content-Disposition'] = 'inline' }
- end
+ render_issues_calendar(issuables_collection)
end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
private
diff --git a/app/controllers/concerns/issues_calendar.rb b/app/controllers/concerns/issues_calendar.rb
new file mode 100644
index 00000000000..671a204621d
--- /dev/null
+++ b/app/controllers/concerns/issues_calendar.rb
@@ -0,0 +1,24 @@
+module IssuesCalendar
+ extend ActiveSupport::Concern
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def render_issues_calendar(issuables)
+ @issues = issuables
+ .non_archived
+ .with_due_date
+ .limit(100)
+
+ respond_to do |format|
+ format.ics do
+ # NOTE: with text/calendar as Content-Type, the browser always downloads
+ # the content as a file (even ignoring the Content-Disposition
+ # header). We want to display the content inline when accessed
+ # from GitLab, similarly to the RSS feed.
+ if request.referer&.start_with?(::Settings.gitlab.base_url)
+ response.headers['Content-Type'] = 'text/plain'
+ end
+ end
+ end
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 0c34e49206a..fe9a030cdf2 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -237,10 +237,6 @@ module NotesActions
def use_note_serializer?
return false if params['html']
- if noteable.is_a?(MergeRequest)
- cookies[:vue_mr_discussions] == 'true'
- else
- noteable.discussions_rendered_on_frontend?
- end
+ noteable.discussions_rendered_on_frontend?
end
end
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index b9b9b6e4e88..16374146ae4 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -1,8 +1,14 @@
module UploadsActions
+ extend ActiveSupport::Concern
+
include Gitlab::Utils::StrongMemoize
include SendFileUpload
- UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo).freeze
+ UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
+
+ included do
+ prepend_before_action :set_html_format, only: :show
+ end
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
@@ -31,11 +37,23 @@ module UploadsActions
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
+ uploaders = [uploader, *uploader.versions.values]
+ uploader = uploaders.find { |version| version.filename == params[:filename] }
+
+ return render_404 unless uploader
+
send_upload(uploader, attachment: uploader.filename, disposition: disposition)
end
private
+ # Explicitly set the format.
+ # Otherwise rails 5 will set it from a file extension.
+ # See https://github.com/rails/rails/commit/84e8accd6fb83031e4c27e44925d7596655285f7#diff-2b8f2fbb113b55ca8e16001c393da8f1
+ def set_html_format
+ request.format = :html
+ end
+
def uploader_class
raise NotImplementedError
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 68d328fa797..ff133001b84 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -54,7 +54,7 @@ class DashboardController < Dashboard::ApplicationController
return unless @no_filters_set
respond_to do |format|
- format.html
+ format.html { render }
format.atom { head :bad_request }
end
end
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
new file mode 100644
index 00000000000..0a1cf169aca
--- /dev/null
+++ b/app/controllers/graphql_controller.rb
@@ -0,0 +1,45 @@
+class GraphqlController < ApplicationController
+ # Unauthenticated users have access to the API for public data
+ skip_before_action :authenticate_user!
+
+ before_action :check_graphql_feature_flag!
+
+ def execute
+ variables = Gitlab::Graphql::Variables.new(params[:variables]).to_h
+ query = params[:query]
+ operation_name = params[:operationName]
+ context = {
+ current_user: current_user
+ }
+ result = GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
+ render json: result
+ end
+
+ rescue_from StandardError do |exception|
+ log_exception(exception)
+
+ render_error("Internal server error")
+ end
+
+ rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
+ render_error(exception.message, status: :unprocessable_entity)
+ end
+
+ private
+
+ # Overridden from the ApplicationController to make the response look like
+ # a GraphQL response. That is nicely picked up in Graphiql.
+ def render_404
+ render_error("Not found!", status: :not_found)
+ end
+
+ def render_error(message, status: 500)
+ error = { errors: [message: message] }
+
+ render json: error, status: status
+ end
+
+ def check_graphql_feature_flag!
+ render_404 unless Feature.enabled?(:graphql)
+ end
+end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index ef3eba80154..ef5d5e5c742 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -3,8 +3,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
include MembersPresentation
include SortingHelper
+ def self.admin_not_required_endpoints
+ %i[index leave request_access]
+ end
+
# Authorize
- before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
+ before_action :authorize_admin_group_member!, except: admin_not_required_endpoints
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite,
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 58be330f466..863f50e8e66 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -2,6 +2,7 @@ class Groups::LabelsController < Groups::ApplicationController
include ToggleSubscriptionAction
before_action :label, only: [:edit, :update, :destroy]
+ before_action :available_labels, only: [:index]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
before_action :save_previous_label_path, only: [:edit]
@@ -12,17 +13,8 @@ class Groups::LabelsController < Groups::ApplicationController
format.html do
@labels = @group.labels.page(params[:page])
end
-
format.json do
- available_labels = LabelsFinder.new(
- current_user,
- group_id: @group.id,
- only_group_labels: params[:only_group_labels],
- include_ancestor_groups: params[:include_ancestor_groups],
- include_descendant_groups: params[:include_descendant_groups]
- ).execute
-
- render json: LabelSerializer.new.represent_appearance(available_labels)
+ render json: LabelSerializer.new.represent_appearance(@available_labels)
end
end
end
@@ -113,4 +105,15 @@ class Groups::LabelsController < Groups::ApplicationController
def save_previous_label_path
session[:previous_labels_path] = URI(request.referer || '').path
end
+
+ def available_labels
+ @available_labels ||=
+ LabelsFinder.new(
+ current_user,
+ group_id: @group.id,
+ only_group_labels: params[:only_group_labels],
+ include_ancestor_groups: params[:include_ancestor_groups],
+ include_descendant_groups: params[:include_descendant_groups]
+ ).execute
+ end
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 5903689dc62..9bd51de7e97 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -76,12 +76,15 @@ class Groups::MilestonesController < Groups::ApplicationController
def milestones
milestones = MilestonesFinder.new(search_params).execute
- legacy_milestones = GroupMilestone.build_collection(group, group_projects, params)
@sort = params[:sort] || 'due_date_asc'
MilestoneArray.sort(milestones + legacy_milestones, @sort)
end
+ def legacy_milestones
+ GroupMilestone.build_collection(group, group_projects, params)
+ end
+
def milestone
@milestone =
if params[:title]
diff --git a/app/controllers/groups/shared_projects_controller.rb b/app/controllers/groups/shared_projects_controller.rb
index f2f835767e0..7dec1f5f402 100644
--- a/app/controllers/groups/shared_projects_controller.rb
+++ b/app/controllers/groups/shared_projects_controller.rb
@@ -24,7 +24,9 @@ module Groups
# Make the `search` param consistent for the frontend,
# which will be using `filter`.
params[:search] ||= params[:filter] if params[:filter]
- params.permit(:sort, :search)
+ # Don't show archived projects
+ params[:non_archived] = true
+ params.permit(:sort, :search, :non_archived)
end
end
end
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/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 663269a0f92..5766c6924cd 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -25,4 +25,8 @@ class Import::BaseController < ApplicationController
current_user.namespace
end
+
+ def project_save_error(project)
+ project.errors.full_messages.join(', ')
+ end
end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 77af5fb9c4f..fa31933e778 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -55,7 +55,7 @@ class Import::BitbucketController < Import::BaseController
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
- render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
+ render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
else
render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 25ec13b8075..2d665e05ac3 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -66,7 +66,7 @@ class Import::FogbugzController < Import::BaseController
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
- render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
+ render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index f67ec4c248b..c9870332c0f 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -48,7 +48,7 @@ class Import::GithubController < Import::BaseController
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
- render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
+ render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
else
render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index 39e2e9e094b..fccbdbca0f6 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -32,7 +32,7 @@ class Import::GitlabController < Import::BaseController
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
- render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
+ render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
else
render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
index 9b26a00f7c7..3bce27e810a 100644
--- a/app/controllers/import/google_code_controller.rb
+++ b/app/controllers/import/google_code_controller.rb
@@ -92,7 +92,7 @@ class Import::GoogleCodeController < Import::BaseController
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
- render json: { errors: project.errors.full_messages }, status: :unprocessable_entity
+ render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
end
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/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index abc283d7aa9..6484a713f8e 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -7,6 +7,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep]
before_action :extract_ref_name_and_path
+ before_action :set_request_format, only: [:file]
before_action :validate_artifacts!
before_action :entry, only: [:file]
@@ -101,4 +102,12 @@ class Projects::ArtifactsController < Projects::ApplicationController
render_404 unless @entry.exists?
end
+
+ def set_request_format
+ request.format = :html if set_request_format?
+ end
+
+ def set_request_format?
+ request.format != :json
+ end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 0c1c286a0a4..ebc61264b39 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -3,10 +3,11 @@ class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
include CreatesCommit
include RendersBlob
+ include NotesHelper
include ActionView::Helpers::SanitizeHelper
-
prepend_before_action :authenticate_user!, only: [:edit]
+ before_action :set_request_format, only: [:edit, :show, :update]
before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code!
@@ -92,6 +93,7 @@ class Projects::BlobController < Projects::ApplicationController
@lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines
@form = UnfoldForm.new(params)
+
@lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe)
if @form.bottom?
@@ -102,11 +104,50 @@ class Projects::BlobController < Projects::ApplicationController
@match_line = "@@ -#{line}+#{line} @@"
end
- render layout: false
+ # We can keep only 'render_diff_lines' from this conditional when
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/44988 is done
+ if rendered_for_merge_request?
+ render_diff_lines
+ else
+ render layout: false
+ end
end
private
+ # Converts a String array to Gitlab::Diff::Line array
+ def render_diff_lines
+ @lines.map! do |line|
+ # These are marked as context lines but are loaded from blobs.
+ # We also have context lines loaded from diffs in other places.
+ diff_line = Gitlab::Diff::Line.new(line, 'context', nil, nil, nil)
+ diff_line.rich_text = line
+ diff_line
+ end
+
+ add_match_line
+
+ render json: @lines
+ end
+
+ def add_match_line
+ return unless @form.unfold?
+
+ if @form.bottom? && @form.to < @blob.lines.size
+ old_pos = @form.to - @form.offset
+ new_pos = @form.to
+ elsif @form.since != 1
+ old_pos = new_pos = @form.since
+ end
+
+ # Match line is not needed when it reaches the top limit or bottom limit of the file.
+ return unless new_pos
+
+ @match_line = Gitlab::Diff::Line.new(@match_line, 'match', nil, old_pos, new_pos)
+
+ @form.bottom? ? @lines.push(@match_line) : @lines.unshift(@match_line)
+ end
+
def blob
@blob ||= @repository.blob_at(@commit.id, @path)
@@ -188,6 +229,18 @@ class Projects::BlobController < Projects::ApplicationController
.last_for_path(@repository, @ref, @path).sha
end
+ # In Rails 4.2 if params[:format] is empty, Rails set it to :html
+ # But since Rails 5.0 the framework now looks for an extension.
+ # E.g. for `blob/master/CHANGELOG.md` in Rails 4 the format would be `:html`, but in Rails 5 on it'd be `:md`
+ # This before_action explicitly sets the `:html` format for all requests unless `:format` is set by a client e.g. by JS for XHR requests.
+ def set_request_format
+ request.format = :html if set_request_format?
+ end
+
+ def set_request_format?
+ params[:id].present? && params[:format].blank? && request.format != "json"
+ end
+
def show_html
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@@ -197,15 +250,14 @@ class Projects::BlobController < Projects::ApplicationController
end
def show_json
- json = blob_json(@blob)
- return render_404 unless json
-
+ set_last_commit_sha
path_segments = @path.split('/')
path_segments.pop
tree_path = path_segments.join('/')
- render json: json.merge(
+ json = {
id: @blob.id,
+ last_commit_sha: @last_commit_sha,
path: blob.path,
name: blob.name,
extension: blob.extension,
@@ -221,6 +273,10 @@ class Projects::BlobController < Projects::ApplicationController
commits_path: project_commits_path(project, @id),
tree_path: project_tree_path(project, File.join(@ref, tree_path)),
permalink: project_blob_path(project, File.join(@commit.id, @path))
- )
+ }
+
+ json.merge!(blob_json(@blob) || {}) unless params[:viewer] == 'none'
+
+ render json: json
end
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index b7b36f770f5..cd7250b10fc 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -31,7 +31,10 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
- render
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/48097
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render
+ end
end
format.json do
branches = BranchesFinder.new(@repository, params).execute
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
index 4d758402850..a5c82caa897 100644
--- a/app/controllers/projects/clusters/applications_controller.rb
+++ b/app/controllers/projects/clusters/applications_controller.rb
@@ -42,6 +42,6 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
owner: current_user
}
- Applications::CreateService.new(current_user, oauth_application_params).execute
+ Applications::CreateService.new(current_user, oauth_application_params).execute(request)
end
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/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 35c36c725e2..7c897b2d86c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -4,6 +4,7 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableActions
include ToggleAwardEmoji
include IssuableCollections
+ include IssuesCalendar
include SpammableActions
prepend_before_action :authenticate_user!, only: [:new]
@@ -40,14 +41,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def calendar
- @issues = @issuables
- .non_archived
- .with_due_date
- .limit(100)
-
- respond_to do |format|
- format.ics { response.headers['Content-Disposition'] = 'inline' }
- end
+ render_issues_calendar(@issuables)
end
def new
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index dd12d30a085..63f0aea3195 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -160,7 +160,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/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 43d8867a536..45c98d60822 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -18,7 +18,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def upload_authorize
set_workhorse_internal_api_content_type
- authorized = LfsObjectUploader.workhorse_authorize
+ authorized = LfsObjectUploader.workhorse_authorize(has_length: true)
authorized.merge!(LfsOid: oid, LfsSize: size)
render json: authorized
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 29632bef7e5..8e4aeec16dc 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -15,7 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
def merge_request_params_attributes
[
- :allow_maintainer_to_push,
+ :allow_collaboration,
:assignee_id,
:description,
:force_remove_source_branch,
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 ecea6e1b2bf..a7c5f858c42 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -28,21 +28,23 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def show
- validates_merge_request
- close_merge_request_without_source_project
- check_if_can_be_merged
-
- # Return if the response has already been rendered
- return if response_body
+ close_merge_request_if_no_source_project
+ mark_merge_request_mergeable
respond_to do |format|
format.html do
+ # use next to appease Rubocop
+ next render('invalid') if target_branch_missing?
+
# Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request)
@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)
@@ -116,7 +118,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
format.json do
- render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+ render json: serializer.represent(@merge_request, serializer: 'basic')
end
end
rescue ActiveRecord::StaleObjectError
@@ -234,20 +236,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
- def validates_merge_request
- # Show git not found page
- # if there is no saved commits between source & target branch
- if @merge_request.has_no_commits?
- # and if target branch doesn't exist
- return invalid_mr unless @merge_request.target_branch_exists?
- end
- end
-
- def invalid_mr
- # Render special view for MR with removed target branch
- render 'invalid'
- end
-
def merge_params
params.permit(merge_params_attributes)
end
@@ -261,7 +249,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.head_pipeline && @merge_request.head_pipeline.active?
end
- def close_merge_request_without_source_project
+ def close_merge_request_if_no_source_project
if !@merge_request.source_project && @merge_request.open?
@merge_request.close
end
@@ -269,7 +257,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private
- def check_if_can_be_merged
+ def target_branch_missing?
+ @merge_request.has_no_commits? && !@merge_request.target_branch_exists?
+ end
+
+ def mark_merge_request_mergeable
@merge_request.check_if_can_be_merged
end
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index c5a044541f1..594563d1f6f 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -1,4 +1,5 @@
class Projects::MilestonesController < Projects::ApplicationController
+ include Gitlab::Utils::StrongMemoize
include MilestoneActions
before_action :check_issuables_available!
@@ -76,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)
@@ -103,7 +104,7 @@ class Projects::MilestonesController < Projects::ApplicationController
protected
def milestones
- @milestones ||= begin
+ strong_memoize(:milestones) do
MilestonesFinder.new(search_params).execute
end
end
@@ -121,10 +122,10 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def search_params
- if @project.group && can?(current_user, :read_group, @project.group)
- group = @project.group
+ if request.format.json? && @project.group && can?(current_user, :read_group, @project.group)
+ groups = @project.group.self_and_ancestors_ids
end
- params.permit(:state).merge(project_ids: @project.id, group_ids: group&.id)
+ params.permit(:state).merge(project_ids: @project.id, group_ids: groups)
end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 6b40fc2fe68..768595ceeb4 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -23,8 +23,6 @@ class Projects::PipelinesController < Projects::ApplicationController
@finished_count = limited_pipelines_count(project, 'finished')
@pipelines_count = limited_pipelines_count(project)
- Gitlab::Ci::Pipeline::Preloader.preload(@pipelines)
-
respond_to do |format|
format.html
format.json do
@@ -34,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
- .represent(@pipelines, disable_coverage: true),
+ .represent(@pipelines, disable_coverage: true, preload: true),
count: {
all: @pipelines_count,
running: @running_count,
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 1d850baf012..fb3f6eec2bd 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -41,7 +41,7 @@ module Projects
:runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_human_readable, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
- auto_devops_attributes: [:id, :domain, :enabled]
+ auto_devops_attributes: [:id, :domain, :enabled, :deploy_strategy]
)
end
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 a93b116c6fe..c2492a137fb 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
@@ -247,13 +247,13 @@ class ProjectsController < Projects::ApplicationController
if find_branches
branches = BranchesFinder.new(@repository, params).execute.take(100).map(&:name)
- options[s_('RefSwitcher|Branches')] = branches
+ options['Branches'] = branches
end
if find_tags && @repository.tag_count.nonzero?
tags = TagsFinder.new(@repository, params).execute.take(100).map(&:name)
- options[s_('RefSwitcher|Tags')] = tags
+ options['Tags'] = tags
end
# If reference is commit id - we should add it to branch/tag selectbox
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index f5a222b3a48..e6d6965036e 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -3,6 +3,9 @@ class RegistrationsController < Devise::RegistrationsController
include AcceptsPendingInvitations
before_action :whitelist_query_limiting, only: [:destroy]
+ before_action :ensure_terms_accepted,
+ if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms? },
+ only: [:create]
def new
redirect_to(new_user_session_path)
@@ -18,7 +21,9 @@ class RegistrationsController < Devise::RegistrationsController
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
accept_pending_invitations
- super
+ super do |new_user|
+ persist_accepted_terms_if_required(new_user)
+ end
else
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
flash.delete :recaptcha_error
@@ -40,6 +45,16 @@ class RegistrationsController < Devise::RegistrationsController
protected
+ def persist_accepted_terms_if_required(new_user)
+ return unless new_user.persisted?
+ return unless Gitlab::CurrentSettings.current_application_settings.enforce_terms?
+
+ if terms_accepted?
+ terms = ApplicationSetting::Term.latest
+ Users::RespondToTermsService.new(new_user, terms).execute(accepted: true)
+ end
+ end
+
def destroy_confirmation_valid?
if current_user.confirm_deletion_with_password?
current_user.valid_password?(params[:password])
@@ -91,4 +106,14 @@ class RegistrationsController < Devise::RegistrationsController
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42380')
end
+
+ def ensure_terms_accepted
+ return if terms_accepted?
+
+ redirect_to new_user_session_path, alert: _('You must accept our Terms of Service and privacy policy in order to register an account')
+ end
+
+ def terms_accepted?
+ Gitlab::Utils.to_boolean(params[:terms_opt_in])
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1a339f76d26..7aa277b3614 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,25 @@ 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!
+
+ unless verify_recaptcha
+ 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 log_failed_login
Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}")
end
@@ -152,6 +177,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/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb
index ab685b9106e..1b1560a2a00 100644
--- a/app/controllers/users/terms_controller.rb
+++ b/app/controllers/users/terms_controller.rb
@@ -2,6 +2,7 @@ module Users
class TermsController < ApplicationController
include InternalRedirect
+ skip_before_action :authenticate_user!
skip_before_action :enforce_terms!
skip_before_action :check_password_expiration
skip_before_action :check_two_factor_requirement
@@ -13,6 +14,10 @@ module Users
def index
@redirect = redirect_path
+
+ if current_user && @term.accepted_by_user?(current_user)
+ flash.now[:notice] = "You have already accepted the Terms of Service as #{current_user.to_reference}"
+ end
end
def accept
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 067aff408df..2a656c0d31c 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -3,17 +3,29 @@ class GroupMembersFinder
@group = group
end
- def execute
+ def execute(include_descendants: false)
group_members = @group.members
+ wheres = []
- return group_members unless @group.parent
+ return group_members unless @group.parent || include_descendants
- parents_members = GroupMember.non_request
- .where(source_id: @group.ancestors.select(:id))
- .where.not(user_id: @group.users.select(:id))
+ wheres << "members.id IN (#{group_members.select(:id).to_sql})"
- wheres = ["members.id IN (#{group_members.select(:id).to_sql})"]
- wheres << "members.id IN (#{parents_members.select(:id).to_sql})"
+ if @group.parent
+ parents_members = GroupMember.non_request
+ .where(source_id: @group.ancestors.select(:id))
+ .where.not(user_id: @group.users.select(:id))
+
+ wheres << "members.id IN (#{parents_members.select(:id).to_sql})"
+ end
+
+ if include_descendants
+ descendant_members = GroupMember.non_request
+ .where(source_id: @group.descendants.select(:id))
+ .where.not(user_id: @group.users.select(:id))
+
+ wheres << "members.id IN (#{descendant_members.select(:id).to_sql})"
+ end
GroupMember.where(wheres.join(' OR '))
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index c6ef79ce15e..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
@@ -23,6 +23,7 @@
# created_before: datetime
# updated_after: datetime
# updated_before: datetime
+# use_cte_for_search: boolean
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
@@ -54,6 +55,7 @@ class IssuableFinder
sort
state
include_subgroups
+ use_cte_for_search
]
end
@@ -74,19 +76,21 @@ class IssuableFinder
items = init_collection
items = filter_items(items)
- # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far
- items = by_project(items)
+ # This has to be last as we may use a CTE as an optimization fence by
+ # passing the use_cte_for_search param
+ # https://www.postgresql.org/docs/current/static/queries-with.html
+ items = by_search(items)
sort(items)
end
def filter_items(items)
+ items = by_project(items)
items = by_scope(items)
items = by_created_at(items)
items = by_updated_at(items)
items = by_state(items)
items = by_group(items)
- items = by_search(items)
items = by_assignee(items)
items = by_author(items)
items = by_non_archived(items)
@@ -107,7 +111,6 @@ class IssuableFinder
#
def count_by_state
count_params = params.merge(state: nil, sort: nil)
- labels_count = label_names.any? ? label_names.count : 1
finder = self.class.new(current_user, count_params)
counts = Hash.new(0)
@@ -116,6 +119,11 @@ class IssuableFinder
# per issuable, so we have to count those in Ruby - which is bad, but still
# better than performing multiple queries.
#
+ # This does not apply when we are using a CTE for the search, as the labels
+ # GROUP BY is inside the subquery in that case, so we set labels_count to 1.
+ labels_count = label_names.any? ? label_names.count : 1
+ labels_count = 1 if use_cte_for_search?
+
finder.execute.reorder(nil).group(:state).count.each do |key, value|
counts[Array(key).last.to_sym] += value / labels_count
end
@@ -159,10 +167,7 @@ class IssuableFinder
finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute
else
- opts = { current_user: current_user }
- opts[:project_ids_relation] = item_project_ids(items) if items
-
- ProjectsFinder.new(opts).execute
+ ProjectsFinder.new(current_user: current_user).execute
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
@@ -306,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
@@ -329,8 +336,24 @@ class IssuableFinder
items
end
+ def use_cte_for_search?
+ return false unless search
+ return false unless Gitlab::Database.postgresql?
+
+ params[:use_cte_for_search]
+ end
+
def by_search(items)
- search ? items.full_search(search) : items
+ return items unless search
+
+ if use_cte_for_search?
+ cte = Gitlab::SQL::RecursiveCTE.new(klass.table_name)
+ cte << items
+
+ items = klass.with(cte.to_arel).from(klass.table_name)
+ end
+
+ items.full_search(search)
end
def by_iids(items)
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 3626670d141..24a6b9349a0 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -136,8 +136,4 @@ class IssuesFinder < IssuableFinder
items
end
end
-
- def item_project_ids(items)
- items&.reorder(nil)&.select(:project_id)
- end
end
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index 4734d97b8c7..4c893ae2de6 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -7,12 +7,12 @@ class MembersFinder
@group = project.group
end
- def execute
+ def execute(include_descendants: false)
project_members = project.project_members
project_members = project_members.non_invite unless can?(current_user, :admin_project, project)
if group
- group_members = GroupMembersFinder.new(group).execute
+ group_members = GroupMembersFinder.new(group).execute(include_descendants: include_descendants)
group_members = group_members.non_invite
union = Gitlab::SQL::Union.new([project_members, group_members], remove_duplicates: false)
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index e2240e5e0d8..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
@@ -56,8 +56,4 @@ class MergeRequestsFinder < IssuableFinder
items.where(target_branch: target_branch)
end
-
- def item_project_ids(items)
- items&.reorder(nil)&.select(:target_project_id)
- end
end
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/snippets_finder.rb b/app/finders/snippets_finder.rb
index d498a2d6d11..9d3772d7541 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -54,7 +54,10 @@ class SnippetsFinder < UnionFinder
end
def authorized_snippets
- Snippet.where(feature_available_projects.or(not_project_related))
+ # This query was intentionally converted to a raw one to get it work in Rails 5.0.
+ # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
+ # Please convert it back when on rails 5.2 as it works again as expected since 5.2.
+ Snippet.where("#{feature_available_projects} OR #{not_project_related}")
.public_or_visible_to_user(current_user)
end
@@ -86,18 +89,20 @@ class SnippetsFinder < UnionFinder
def feature_available_projects
# Don't return any project related snippets if the user cannot read cross project
- return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project)
+ return table[:id].eq(nil).to_sql unless Ability.allowed?(current_user, :read_cross_project)
projects = projects_for_user do |part|
part.with_feature_available_for_user(:snippets, current_user)
end.select(:id)
- arel_query = Arel::Nodes::SqlLiteral.new(projects.to_sql)
- table[:project_id].in(arel_query)
+ # This query was intentionally converted to a raw one to get it work in Rails 5.0.
+ # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
+ # Please convert it back when on rails 5.2 as it works again as expected since 5.2.
+ "snippets.project_id IN (#{projects.to_sql})"
end
def not_project_related
- table[:project_id].eq(nil)
+ table[:project_id].eq(nil).to_sql
end
def table
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/functions/base_function.rb b/app/graphql/functions/base_function.rb
new file mode 100644
index 00000000000..42fb8f99acc
--- /dev/null
+++ b/app/graphql/functions/base_function.rb
@@ -0,0 +1,4 @@
+module Functions
+ class BaseFunction < GraphQL::Function
+ end
+end
diff --git a/app/graphql/functions/echo.rb b/app/graphql/functions/echo.rb
new file mode 100644
index 00000000000..e5bf109b8d7
--- /dev/null
+++ b/app/graphql/functions/echo.rb
@@ -0,0 +1,13 @@
+module Functions
+ class Echo < BaseFunction
+ argument :text, GraphQL::STRING_TYPE
+
+ description "Testing endpoint to validate the API with"
+
+ def call(obj, args, ctx)
+ username = ctx[:current_user]&.username
+
+ "#{username.inspect} says: #{args[:text]}"
+ end
+ end
+end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
new file mode 100644
index 00000000000..de4fc1d8e32
--- /dev/null
+++ b/app/graphql/gitlab_schema.rb
@@ -0,0 +1,8 @@
+class GitlabSchema < GraphQL::Schema
+ use BatchLoader::GraphQL
+ use Gitlab::Graphql::Authorize
+ use Gitlab::Graphql::Present
+
+ query(Types::QueryType)
+ # mutation(Types::MutationType)
+end
diff --git a/app/graphql/mutations/.keep b/app/graphql/mutations/.keep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/app/graphql/mutations/.keep
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
new file mode 100644
index 00000000000..89b7f9dad6f
--- /dev/null
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -0,0 +1,4 @@
+module Resolvers
+ class BaseResolver < GraphQL::Schema::Resolver
+ end
+end
diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb
new file mode 100644
index 00000000000..4eb28aaed6c
--- /dev/null
+++ b/app/graphql/resolvers/full_path_resolver.rb
@@ -0,0 +1,19 @@
+module Resolvers
+ module FullPathResolver
+ extend ActiveSupport::Concern
+
+ prepended do
+ argument :full_path, GraphQL::ID_TYPE,
+ required: true,
+ description: 'The full path of the project or namespace, e.g., "gitlab-org/gitlab-ce"'
+ end
+
+ def model_by_full_path(model, full_path)
+ BatchLoader.for(full_path).batch(key: "#{model.model_name.param_key}:full_path") do |full_paths, loader|
+ # `with_route` avoids an N+1 calculating full_path
+ results = model.where_full_path_in(full_paths).with_route
+ results.each { |project| loader.call(project.full_path, project) }
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb
new file mode 100644
index 00000000000..9f2d348e95f
--- /dev/null
+++ b/app/graphql/resolvers/merge_request_resolver.rb
@@ -0,0 +1,20 @@
+module Resolvers
+ class MergeRequestResolver < BaseResolver
+ argument :iid, GraphQL::ID_TYPE,
+ required: true,
+ description: 'The IID of the merge request, e.g., "1"'
+
+ type Types::MergeRequestType, null: true
+
+ alias_method :project, :object
+
+ def resolve(iid:)
+ return unless project.present?
+
+ BatchLoader.for(iid.to_s).batch(key: project.id) do |iids, loader|
+ results = project.merge_requests.where(iid: iids)
+ results.each { |mr| loader.call(mr.iid.to_s, mr) }
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/project_resolver.rb b/app/graphql/resolvers/project_resolver.rb
new file mode 100644
index 00000000000..ec115bad896
--- /dev/null
+++ b/app/graphql/resolvers/project_resolver.rb
@@ -0,0 +1,11 @@
+module Resolvers
+ class ProjectResolver < BaseResolver
+ prepend FullPathResolver
+
+ type Types::ProjectType, null: true
+
+ def resolve(full_path:)
+ model_by_full_path(Project, full_path)
+ end
+ end
+end
diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb
new file mode 100644
index 00000000000..b45a845f74f
--- /dev/null
+++ b/app/graphql/types/base_enum.rb
@@ -0,0 +1,4 @@
+module Types
+ class BaseEnum < GraphQL::Schema::Enum
+ end
+end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
new file mode 100644
index 00000000000..c5740a334d7
--- /dev/null
+++ b/app/graphql/types/base_field.rb
@@ -0,0 +1,5 @@
+module Types
+ class BaseField < GraphQL::Schema::Field
+ prepend Gitlab::Graphql::Authorize
+ end
+end
diff --git a/app/graphql/types/base_input_object.rb b/app/graphql/types/base_input_object.rb
new file mode 100644
index 00000000000..309e336e6c8
--- /dev/null
+++ b/app/graphql/types/base_input_object.rb
@@ -0,0 +1,4 @@
+module Types
+ class BaseInputObject < GraphQL::Schema::InputObject
+ end
+end
diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb
new file mode 100644
index 00000000000..69e72dc5808
--- /dev/null
+++ b/app/graphql/types/base_interface.rb
@@ -0,0 +1,5 @@
+module Types
+ module BaseInterface
+ include GraphQL::Schema::Interface
+ end
+end
diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb
new file mode 100644
index 00000000000..754adf4c04d
--- /dev/null
+++ b/app/graphql/types/base_object.rb
@@ -0,0 +1,8 @@
+module Types
+ class BaseObject < GraphQL::Schema::Object
+ prepend Gitlab::Graphql::Present
+ prepend Gitlab::Graphql::ExposePermissions
+
+ field_class Types::BaseField
+ end
+end
diff --git a/app/graphql/types/base_scalar.rb b/app/graphql/types/base_scalar.rb
new file mode 100644
index 00000000000..c0aa38be239
--- /dev/null
+++ b/app/graphql/types/base_scalar.rb
@@ -0,0 +1,4 @@
+module Types
+ class BaseScalar < GraphQL::Schema::Scalar
+ end
+end
diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb
new file mode 100644
index 00000000000..36337fc6ee5
--- /dev/null
+++ b/app/graphql/types/base_union.rb
@@ -0,0 +1,4 @@
+module Types
+ class BaseUnion < GraphQL::Schema::Union
+ end
+end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
new file mode 100644
index 00000000000..a1f3c0dd8c0
--- /dev/null
+++ b/app/graphql/types/merge_request_type.rb
@@ -0,0 +1,49 @@
+module Types
+ class MergeRequestType < BaseObject
+ expose_permissions Types::PermissionTypes::MergeRequest
+
+ present_using MergeRequestPresenter
+
+ graphql_name 'MergeRequest'
+
+ field :id, GraphQL::ID_TYPE, null: false
+ field :iid, GraphQL::ID_TYPE, null: false
+ field :title, GraphQL::STRING_TYPE, null: false
+ field :description, GraphQL::STRING_TYPE, null: true
+ field :state, GraphQL::STRING_TYPE, null: true
+ field :created_at, Types::TimeType, null: false
+ field :updated_at, Types::TimeType, null: false
+ field :source_project, Types::ProjectType, null: true
+ field :target_project, Types::ProjectType, null: false
+ # Alias for target_project
+ field :project, Types::ProjectType, null: false
+ field :project_id, GraphQL::INT_TYPE, null: false, method: :target_project_id
+ field :source_project_id, GraphQL::INT_TYPE, null: true
+ field :target_project_id, GraphQL::INT_TYPE, null: false
+ field :source_branch, GraphQL::STRING_TYPE, null: false
+ field :target_branch, GraphQL::STRING_TYPE, null: false
+ field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false
+ field :merge_when_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true
+ field :diff_head_sha, GraphQL::STRING_TYPE, null: true
+ field :merge_commit_sha, GraphQL::STRING_TYPE, null: true
+ field :user_notes_count, GraphQL::INT_TYPE, null: true
+ field :should_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :should_remove_source_branch?, null: true
+ field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true
+ field :merge_status, GraphQL::STRING_TYPE, null: true
+ field :in_progress_merge_commit_sha, GraphQL::STRING_TYPE, null: true
+ field :merge_error, GraphQL::STRING_TYPE, null: true
+ field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true
+ field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false
+ field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true
+ field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false
+ field :diff_head_sha, GraphQL::STRING_TYPE, null: true
+ field :merge_commit_message, GraphQL::STRING_TYPE, null: true
+ field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false
+ field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false
+ field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true
+ field :web_url, GraphQL::STRING_TYPE, null: true
+ field :upvotes, GraphQL::INT_TYPE, null: false
+ field :downvotes, GraphQL::INT_TYPE, null: false
+ field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
new file mode 100644
index 00000000000..06ed91c1658
--- /dev/null
+++ b/app/graphql/types/mutation_type.rb
@@ -0,0 +1,7 @@
+module Types
+ class MutationType < BaseObject
+ graphql_name "Mutation"
+
+ # TODO: Add Mutations as fields
+ 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/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
new file mode 100644
index 00000000000..a832e8b4bde
--- /dev/null
+++ b/app/graphql/types/project_type.rb
@@ -0,0 +1,74 @@
+module Types
+ class ProjectType < BaseObject
+ expose_permissions Types::PermissionTypes::Project
+
+ graphql_name 'Project'
+
+ field :id, GraphQL::ID_TYPE, null: false
+
+ field :full_path, GraphQL::ID_TYPE, null: false
+ field :path, GraphQL::STRING_TYPE, null: false
+
+ field :name_with_namespace, GraphQL::STRING_TYPE, null: false
+ field :name, GraphQL::STRING_TYPE, null: false
+
+ field :description, GraphQL::STRING_TYPE, null: true
+
+ field :default_branch, GraphQL::STRING_TYPE, null: true
+ field :tag_list, GraphQL::STRING_TYPE, null: true
+
+ field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true
+ field :http_url_to_repo, GraphQL::STRING_TYPE, null: true
+ field :web_url, GraphQL::STRING_TYPE, null: true
+
+ field :star_count, GraphQL::INT_TYPE, null: false
+ field :forks_count, GraphQL::INT_TYPE, null: false
+
+ field :created_at, Types::TimeType, null: true
+ field :last_activity_at, Types::TimeType, null: true
+
+ field :archived, GraphQL::BOOLEAN_TYPE, null: true
+
+ field :visibility, GraphQL::STRING_TYPE, null: true
+
+ field :container_registry_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :shared_runners_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true
+
+ field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (project, args, ctx) do
+ project.avatar_url(only_path: false)
+ end
+
+ %i[issues merge_requests wiki snippets].each do |feature|
+ field "#{feature}_enabled", GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do
+ project.feature_available?(feature, ctx[:current_user])
+ end
+ end
+
+ field :jobs_enabled, GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do
+ project.feature_available?(:builds, ctx[:current_user])
+ end
+
+ field :public_jobs, GraphQL::BOOLEAN_TYPE, method: :public_builds, null: true
+
+ field :open_issues_count, GraphQL::INT_TYPE, null: true, resolve: -> (project, args, ctx) do
+ project.open_issues_count if project.feature_available?(:issues, ctx[:current_user])
+ end
+
+ field :import_status, GraphQL::STRING_TYPE, null: true
+ field :ci_config_path, GraphQL::STRING_TYPE, null: true
+
+ field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true
+ field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true
+ field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true
+
+ field :merge_request,
+ Types::MergeRequestType,
+ null: true,
+ resolver: Resolvers::MergeRequestResolver do
+ authorize :read_merge_request
+ end
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
new file mode 100644
index 00000000000..010ec2d7942
--- /dev/null
+++ b/app/graphql/types/query_type.rb
@@ -0,0 +1,14 @@
+module Types
+ class QueryType < BaseObject
+ graphql_name 'Query'
+
+ field :project, Types::ProjectType,
+ null: true,
+ resolver: Resolvers::ProjectResolver,
+ description: "Find a project" do
+ authorize :read_project
+ end
+
+ field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new
+ end
+end
diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb
new file mode 100644
index 00000000000..2333d82ad1e
--- /dev/null
+++ b/app/graphql/types/time_type.rb
@@ -0,0 +1,14 @@
+module Types
+ class TimeType < BaseScalar
+ graphql_name 'Time'
+ description 'Time represented in ISO 8601'
+
+ def self.coerce_input(value, ctx)
+ Time.parse(value)
+ end
+
+ def self.coerce_result(value, ctx)
+ value.iso8601
+ end
+ 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/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index adc423af9e1..ef1bf283d0c 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -36,7 +36,7 @@ module ApplicationSettingsHelper
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
- def restricted_level_checkboxes(help_block_id, checkbox_name)
+ def restricted_level_checkboxes(help_block_id, checkbox_name, options = {})
Gitlab::VisibilityLevel.values.map do |level|
checked = restricted_visibility_levels(true).include?(level)
css_class = checked ? 'active' : ''
@@ -46,6 +46,7 @@ module ApplicationSettingsHelper
check_box_tag(checkbox_name, level, checked,
autocomplete: 'off',
'aria-describedby' => help_block_id,
+ 'class' => options[:class],
id: tag_name) + visibility_level_icon(level) + visibility_level_label(level)
end
end
@@ -53,7 +54,7 @@ module ApplicationSettingsHelper
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
- def import_sources_checkboxes(help_block_id)
+ def import_sources_checkboxes(help_block_id, options = {})
Gitlab::ImportSources.options.map do |name, source|
checked = Gitlab::CurrentSettings.import_sources.include?(source)
css_class = checked ? 'active' : ''
@@ -63,6 +64,7 @@ module ApplicationSettingsHelper
check_box_tag(checkbox_name, source, checked,
autocomplete: 'off',
'aria-describedby' => help_block_id,
+ 'class' => options[:class],
id: name.tr(' ', '_')) + name
end
end
diff --git a/app/helpers/favicon_helper.rb b/app/helpers/favicon_helper.rb
new file mode 100644
index 00000000000..3a5342a8d9d
--- /dev/null
+++ b/app/helpers/favicon_helper.rb
@@ -0,0 +1,7 @@
+module FaviconHelper
+ def favicon_extension_whitelist
+ FaviconUploader::EXTENSION_WHITELIST
+ .map { |extension| "'.#{extension}'"}
+ .to_sentence
+ end
+end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 58372edff3c..2f304b040c7 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -60,7 +60,7 @@ module IconsHelper
def spinner(text = nil, visible = false)
css_class = 'loading'
- css_class << ' hidden' unless visible
+ css_class << ' hide' unless visible
content_tag :div, class: css_class do
icon('spinner spin') + text
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 8572c2b7276..9f501ea55fb 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -157,7 +157,7 @@ module IssuablesHelper
output = ""
output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do
- author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline-block", tooltip: true)
+ author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline", tooltip: true)
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none")
end
@@ -165,7 +165,7 @@ module IssuablesHelper
output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!'))
output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block")
- output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none d-lg-none d-xl-inline-block")
+ output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none")
output.html_safe
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index e1b0e7a4a3e..c7df25cecef 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -211,6 +211,14 @@ module LabelsHelper
end
end
+ def label_status_tooltip(label, status)
+ type = label.is_a?(ProjectLabel) ? 'project' : 'group'
+ level = status.unsubscribed? ? type : status.sub('-level', '')
+ action = status.unsubscribed? ? 'Subscribe' : 'Unsubscribe'
+
+ "#{action} at #{level} level"
+ end
+
# Required for Banzai::Filter::LabelReferenceFilter
module_function :render_colored_label, :text_color_for_bg, :escape_once
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 74251c260f0..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
@@ -126,8 +128,8 @@ module MergeRequestsHelper
link_to(url[merge_request.project, merge_request], data: data_attrs, &block)
end
- def allow_maintainer_push_unavailable_reason(merge_request)
- return if merge_request.can_allow_maintainer_to_push?(current_user)
+ def allow_collaboration_unavailable_reason(merge_request)
+ return if merge_request.can_allow_collaboration?(current_user)
minimum_visibility = [merge_request.target_project.visibility_level,
merge_request.source_project.visibility_level].min
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/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index a8397b03d63..68d892393ef 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -39,10 +39,7 @@ module PageLayoutHelper
end
def favicon
- return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY'])
- return 'favicon-blue.ico' if Rails.env.development?
-
- 'favicon.ico'
+ Gitlab::Favicon.main
end
def page_image
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 55078e1a2d2..c7a434ea092 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)
@@ -171,11 +172,12 @@ module ProjectsHelper
key = [
project.route.cache_key,
project.cache_key,
+ project.last_activity_date,
controller.controller_name,
controller.action_name,
Gitlab::CurrentSettings.cache_key,
"cross-project:#{can?(current_user, :read_cross_project)}",
- 'v2.5'
+ 'v2.6'
]
key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
@@ -238,6 +240,14 @@ module ProjectsHelper
"git push --set-upstream #{repository_url}/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)"
end
+ def show_xcode_link?(project = @project)
+ browser.platform.mac? && project.repository.xcode_project?
+ end
+
+ def xcode_uri_to_repo(project = @project)
+ "xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}"
+ end
+
private
def get_project_nav_tabs(project, current_user)
@@ -341,11 +351,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
@@ -381,11 +395,11 @@ module ProjectsHelper
def project_status_css_class(status)
case status
when "started"
- "active"
+ "table-active"
when "failed"
- "danger"
+ "table-danger"
when "finished"
- "success"
+ "table-success"
end
end
@@ -398,13 +412,17 @@ module ProjectsHelper
@ref || @repository.try(:root_ref)
end
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1235
def sanitize_repo_path(project, message)
return '' unless message.present?
exports_path = File.join(Settings.shared['path'], 'tmp/project_exports')
filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]")
- disk_path = Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path
+ disk_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path
+ end
+
filtered_message.gsub(disk_path.chomp('/'), "[REPOS PATH]")
end
@@ -488,4 +506,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/search_helper.rb b/app/helpers/search_helper.rb
index 761c1252fc8..f7dafca7834 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -25,14 +25,22 @@ module SearchHelper
return unless collection.count > 0
from = collection.offset_value + 1
- to = collection.offset_value + collection.length
+ to = collection.offset_value + collection.count
count = collection.total_count
"Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\""
end
+ def find_project_for_result_blob(result)
+ @project
+ end
+
def parse_search_result(result)
- Gitlab::ProjectSearchResults.parse_search_result(result)
+ result
+ end
+
+ def search_blob_title(project, filename)
+ filename
end
private
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index 9f78b80c71d..a82271ce0ee 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -6,7 +6,7 @@ module WorkhorseHelper
headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob))
headers['Content-Disposition'] = 'inline'
headers['Content-Type'] = safe_content_type(blob)
- head :ok # 'render nothing: true' messes up the Content-Type
+ render plain: ""
end
# Send a Git diff through Workhorse
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/appearance.rb b/app/models/appearance.rb
index 67cc84a9140..b770aadef0e 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -14,6 +14,7 @@ class Appearance < ActiveRecord::Base
mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
+ mount_uploader :favicon, FaviconUploader
# Overrides CacheableAttributes.current_without_cache
def self.current_without_cache
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/application_setting/term.rb b/app/models/application_setting/term.rb
index e8ce0ccbb71..3b1dfe7e4ef 100644
--- a/app/models/application_setting/term.rb
+++ b/app/models/application_setting/term.rb
@@ -1,6 +1,7 @@
class ApplicationSetting
class Term < ActiveRecord::Base
include CacheMarkdownField
+ has_many :term_agreements
validates :terms, presence: true
@@ -9,5 +10,10 @@ class ApplicationSetting
def self.latest
order(:id).last
end
+
+ def accepted_by_user?(user)
+ user.accepted_term_id == id ||
+ term_agreements.accepted.where(user: user).exists?
+ end
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 75fd55a8f7b..41446946a5e 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -55,12 +55,18 @@ module Ci
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end
+
+ scope :without_archived_trace, ->() do
+ where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
+ end
+
scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :ref_protected, -> { where(protected: true) }
+ scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) }
scope :matches_tag_ids, -> (tag_ids) do
matcher = ::ActsAsTaggableOn::Tagging
@@ -144,6 +150,7 @@ module Ci
after_transition any => [:success] do |build|
build.run_after_commit do
BuildSuccessWorker.perform_async(id)
+ PagesWorker.perform_async(:deploy, id) if build.pages_generator?
end
end
@@ -183,6 +190,11 @@ module Ci
pipeline.manual_actions.where.not(name: name)
end
+ def pages_generator?
+ Gitlab.config.pages.enabled &&
+ self.name == 'pages'
+ end
+
def playable?
action? && (manual? || retryable?)
end
@@ -402,8 +414,6 @@ module Ci
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :job_hooks)
project.execute_services(build_data.dup, :job_hooks)
- PagesService.new(build_data).execute
- project.running_or_pending_build_count(force: true)
end
def browsable_artifacts?
@@ -601,6 +611,7 @@ module Ci
variables
.concat(pipeline.persisted_variables)
.append(key: 'CI_JOB_ID', value: id.to_s)
+ .append(key: 'CI_JOB_URL', value: Gitlab::Routing.url_helpers.project_job_url(project, self))
.append(key: 'CI_JOB_TOKEN', value: token, public: false)
.append(key: 'CI_BUILD_ID', value: id.to_s)
.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb
index 87898b086c6..9c1046e8715 100644
--- a/app/models/ci/group.rb
+++ b/app/models/ci/group.rb
@@ -31,6 +31,14 @@ module Ci
end
end
+ def self.fabricate(stage)
+ stage.statuses.ordered.latest
+ .sort_by(&:sortable_name).group_by(&:group_name)
+ .map do |group_name, grouped_statuses|
+ self.new(stage, name: group_name, jobs: grouped_statuses)
+ end
+ end
+
private
def commit_statuses
diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb
index 9b536af672b..ce691875e42 100644
--- a/app/models/ci/legacy_stage.rb
+++ b/app/models/ci/legacy_stage.rb
@@ -16,11 +16,7 @@ module Ci
end
def groups
- @groups ||= statuses.ordered.latest
- .sort_by(&:sortable_name).group_by(&:group_name)
- .map do |group_name, grouped_statuses|
- Ci::Group.new(self, name: group_name, jobs: grouped_statuses)
- end
+ @groups ||= Ci::Group.fabricate(self)
end
def to_param
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 53af87a271a..e5caa3ffa41 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -7,13 +7,19 @@ module Ci
include Presentable
include Gitlab::OptimisticLocking
include Gitlab::Utils::StrongMemoize
+ include AtomicInternalId
+ include EnumWithNil
belongs_to :project, inverse_of: :pipelines
belongs_to :user
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
- has_many :stages
+ has_internal_id :iid, scope: :project, presence: false, init: ->(s) do
+ s&.project&.pipelines&.maximum(:iid) || s&.project&.pipelines&.count
+ end
+
+ has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
@@ -49,7 +55,7 @@ module Ci
after_create :keep_around_commits, unless: :importing?
- enum source: {
+ enum_with_nil source: {
unknown: nil,
push: 1,
web: 2,
@@ -59,7 +65,7 @@ module Ci
external: 6
}
- enum config_source: {
+ enum_with_nil config_source: {
unknown_source: nil,
repository_source: 1,
auto_devops_source: 2
@@ -249,6 +255,20 @@ module Ci
stage unless stage.statuses_count.zero?
end
+ ##
+ # TODO We do not completely switch to persisted stages because of
+ # race conditions with setting statuses gitlab-ce#23257.
+ #
+ def ordered_stages
+ return legacy_stages unless complete?
+
+ if Feature.enabled?('ci_pipeline_persisted_stages')
+ stages
+ else
+ legacy_stages
+ end
+ end
+
def legacy_stages
# TODO, this needs refactoring, see gitlab-ce#26481.
@@ -411,7 +431,7 @@ module Ci
def number_of_warnings
BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader|
- Build.where(commit_id: pipeline_ids)
+ ::Ci::Build.where(commit_id: pipeline_ids)
.latest
.failed_but_allowed
.group(:commit_id)
@@ -503,7 +523,8 @@ module Ci
def update_status
retry_optimistic_lock(self) do
- case latest_builds_status
+ case latest_builds_status.to_s
+ when 'created' then nil
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
@@ -511,6 +532,9 @@ module Ci
when 'canceled' then cancel
when 'skipped' then skip
when 'manual' then block
+ else
+ raise HasStatus::UnknownStatusError,
+ "Unknown status `#{latest_builds_status}`"
end
end
end
@@ -525,17 +549,21 @@ module Ci
def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI_PIPELINE_ID', value: id.to_s) if persisted?
+ break variables unless persisted?
+
+ variables.append(key: 'CI_PIPELINE_ID', value: id.to_s)
+ variables.append(key: 'CI_PIPELINE_URL', value: Gitlab::Routing.url_helpers.project_pipeline_url(project, self))
end
end
def predefined_variables
Gitlab::Ci::Variables::Collection.new
+ .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
@@ -575,17 +603,6 @@ module Ci
@latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a
end
- # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
- # They always return `false`.
- # These methods overwrite autogenerated ones to return correct results.
- def unknown?
- Gitlab.rails5? ? source.nil? : super
- end
-
- def unknown_source?
- Gitlab.rails5? ? config_source.nil? : super
- end
-
private
def ci_yaml_from_repo
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 57edd6a4956..8c9aacca8de 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -219,10 +219,8 @@ module Ci
cache_attributes(values)
- if persist_cached_data?
- self.assign_attributes(values)
- self.save if self.changed?
- end
+ # We save data without validation, it will always change due to `contacted_at`
+ self.update_columns(values) if persist_cached_data?
end
def pick_build!(build)
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 5a1eeb966aa..ea07f37e6c1 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -68,16 +68,44 @@ module Ci
def update_status
retry_optimistic_lock(self) do
case statuses.latest.status
+ when 'created' then nil
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
when 'failed' then drop
when 'canceled' then cancel
when 'manual' then block
- when 'skipped' then skip
- else skip
+ when 'skipped', nil then skip
+ else
+ raise HasStatus::UnknownStatusError,
+ "Unknown status `#{statuses.latest.status}`"
end
end
end
+
+ def groups
+ @groups ||= Ci::Group.fabricate(self)
+ end
+
+ def has_warnings?
+ number_of_warnings.positive?
+ end
+
+ def number_of_warnings
+ BatchLoader.for(id).batch(default_value: 0) do |stage_ids, loader|
+ ::Ci::Build.where(stage_id: stage_ids)
+ .latest
+ .failed_but_allowed
+ .group(:stage_id)
+ .count
+ .each { |id, amount| loader.call(id, amount) }
+ end
+ end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Stage::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
end
end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index c702c4ee807..48137c2ed68 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -3,7 +3,7 @@ module Clusters
class Prometheus < ActiveRecord::Base
include PrometheusAdapter
- VERSION = "2.0.0".freeze
+ VERSION = '6.7.3'.freeze
self.table_name = 'clusters_applications_prometheus'
@@ -37,6 +37,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name,
chart: chart,
+ version: version,
values: values
)
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 25eac5160f1..36631d57ad1 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -11,12 +11,12 @@ module Clusters
attr_encrypted :password,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base,
+ key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
attr_encrypted :token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base,
+ key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
before_validation :enforce_namespace_to_lower_case
diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb
index eb2e42fd3fe..4db1bb35c12 100644
--- a/app/models/clusters/providers/gcp.rb
+++ b/app/models/clusters/providers/gcp.rb
@@ -11,7 +11,7 @@ module Clusters
attr_encrypted :access_token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base,
+ key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
validates :gcp_project_id,
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index a7d05722287..97516079b66 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -3,6 +3,7 @@ class CommitStatus < ActiveRecord::Base
include Importable
include AfterCommitQueue
include Presentable
+ include EnumWithNil
self.table_name = 'ci_builds'
@@ -39,7 +40,7 @@ class CommitStatus < ActiveRecord::Base
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
- enum failure_reason: {
+ enum_with_nil failure_reason: {
unknown_failure: nil,
script_failure: 1,
api_failure: 2,
@@ -190,11 +191,4 @@ class CommitStatus < ActiveRecord::Base
v =~ /\d+/ ? v.to_i : v
end
end
-
- # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
- # They always return `false`.
- # This method overwrites the autogenerated one to return correct result.
- def unknown_failure?
- Gitlab.rails5? ? failure_reason.nil? : super
- end
end
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index 22f516a172f..164c704260e 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -25,9 +25,13 @@ module AtomicInternalId
extend ActiveSupport::Concern
module ClassMethods
- def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName
- before_validation(on: :create) do
+ def has_internal_id(column, scope:, init:, presence: true) # rubocop:disable Naming/PredicateName
+ before_validation :"ensure_#{scope}_#{column}!", on: :create
+ validates column, presence: presence
+
+ define_method("ensure_#{scope}_#{column}!") do
scope_value = association(scope).reader
+
if read_attribute(column).blank? && scope_value
scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
usage = self.class.table_name.to_sym
@@ -35,13 +39,9 @@ module AtomicInternalId
new_iid = InternalId.generate_next(self, scope_attrs, usage, init)
write_attribute(column, new_iid)
end
- end
- validates column, presence: true, numericality: true
+ read_attribute(column)
+ end
end
end
-
- def to_param
- iid.to_s
- end
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 13246a774e3..095897b08e3 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -4,11 +4,14 @@ module Avatarable
included do
prepend ShadowMethods
include ObjectStorage::BackgroundMove
+ include Gitlab::Utils::StrongMemoize
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
mount_uploader :avatar, AvatarUploader
+
+ after_initialize :add_avatar_to_batch
end
module ShadowMethods
@@ -18,6 +21,17 @@ module Avatarable
avatar_path(only_path: args.fetch(:only_path, true)) || super
end
+
+ def retrieve_upload(identifier, paths)
+ upload = retrieve_upload_from_batch(identifier)
+
+ # This fallback is needed when deleting an upload, because we may have
+ # already been removed from the DB. We have to check an explicit `#nil?`
+ # because it's a BatchLoader instance.
+ upload = super if upload.nil?
+
+ upload
+ end
end
def avatar_type
@@ -52,4 +66,37 @@ module Avatarable
url_base + avatar.local_url
end
+
+ # Path that is persisted in the tracking Upload model. Used to fetch the
+ # upload from the model.
+ def upload_paths(identifier)
+ avatar_mounter.blank_uploader.store_dirs.map { |store, path| File.join(path, identifier) }
+ end
+
+ private
+
+ def retrieve_upload_from_batch(identifier)
+ BatchLoader.for(identifier: identifier, model: self).batch(key: self.class) do |upload_params, loader, args|
+ model_class = args[:key]
+ paths = upload_params.flat_map do |params|
+ params[:model].upload_paths(params[:identifier])
+ end
+
+ Upload.where(uploader: AvatarUploader, path: paths).find_each do |upload|
+ model = model_class.instantiate('id' => upload.model_id)
+
+ loader.call({ model: model, identifier: File.basename(upload.path) }, upload)
+ end
+ end
+ end
+
+ def add_avatar_to_batch
+ return unless avatar_mounter
+
+ avatar_mounter.read_identifiers.each { |identifier| retrieve_upload_from_batch(identifier) }
+ end
+
+ def avatar_mounter
+ strong_memoize(:avatar_mounter) { _mounter(:avatar) }
+ end
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index db8cf322ef7..9f6358cecbe 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -114,7 +114,7 @@ module CacheMarkdownField
end
def latest_cached_markdown_version
- return CacheMarkdownField::CACHE_REDCARPET_VERSION unless cached_markdown_version
+ return CacheMarkdownField::CACHE_COMMONMARK_VERSION unless cached_markdown_version
if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
CacheMarkdownField::CACHE_REDCARPET_VERSION
diff --git a/app/models/concerns/enum_with_nil.rb b/app/models/concerns/enum_with_nil.rb
new file mode 100644
index 00000000000..6b37903da20
--- /dev/null
+++ b/app/models/concerns/enum_with_nil.rb
@@ -0,0 +1,33 @@
+module EnumWithNil
+ extend ActiveSupport::Concern
+
+ included do
+ def self.enum_with_nil(definitions)
+ # use original `enum` to auto-define all methods
+ enum(definitions)
+
+ # override auto-defined methods only for the
+ # key which uses nil value
+ definitions.each do |name, values|
+ next unless key_with_nil = values.key(nil)
+
+ # E.g. for enum_with_nil failure_reason: { unknown_failure: nil }
+ # this overrides auto-generated method `unknown_failure?`
+ define_method("#{key_with_nil}?") do
+ Gitlab.rails5? ? self[name].nil? : super()
+ end
+
+ # E.g. for enum_with_nil failure_reason: { unknown_failure: nil }
+ # this overrides auto-generated method `failure_reason`
+ define_method(name) do
+ orig = super()
+
+ return orig unless Gitlab.rails5?
+ return orig unless orig.nil?
+
+ self.class.public_send(name.to_s.pluralize).key(nil) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 7c3ed96bc28..72c236a0fc7 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -11,6 +11,8 @@ module HasStatus
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
+ UnknownStatusError = Class.new(StandardError)
+
class_methods do
def status_sql
scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all
diff --git a/app/models/concerns/iid_routes.rb b/app/models/concerns/iid_routes.rb
new file mode 100644
index 00000000000..246748cf52c
--- /dev/null
+++ b/app/models/concerns/iid_routes.rb
@@ -0,0 +1,9 @@
+module IidRoutes
+ ##
+ # This automagically enforces all related routes to use `iid` instead of `id`
+ # If you want to use `iid` for some routes and `id` for other routes, this module should not to be included,
+ # instead you should define `iid` or `id` explictly at each route generators. e.g. pipeline_path(project.id, pipeline.iid)
+ def to_param
+ iid.to_s
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 44150b37708..b93c1145f82 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -107,6 +107,10 @@ module Issuable
false
end
+ def etag_caching_enabled?
+ false
+ end
+
def has_multiple_assignees?
assignees.count > 1
end
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index bfda5b1678b..e3a7f2d5498 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -8,8 +8,8 @@ module ProtectedRefAccess
].freeze
HUMAN_ACCESS_LEVELS = {
- Gitlab::Access::MASTER => "Masters".freeze,
- Gitlab::Access::DEVELOPER => "Developers + Masters".freeze,
+ Gitlab::Access::MASTER => "Maintainers".freeze,
+ Gitlab::Access::DEVELOPER => "Developers + Maintainers".freeze,
Gitlab::Access::NO_ACCESS => "No one".freeze
}.freeze
diff --git a/app/models/concerns/redis_cacheable.rb b/app/models/concerns/redis_cacheable.rb
index b5425295130..3bdc1330d23 100644
--- a/app/models/concerns/redis_cacheable.rb
+++ b/app/models/concerns/redis_cacheable.rb
@@ -48,7 +48,7 @@ module RedisCacheable
def cast_value_from_cache(attribute, value)
if Gitlab.rails5?
- self.class.type_for_attribute(attribute).cast(value)
+ self.class.type_for_attribute(attribute.to_s).cast(value)
else
self.class.column_for_attribute(attribute).type_cast_from_database(value)
end
diff --git a/app/models/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/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb
index e7cfffb775b..4245d083a49 100644
--- a/app/models/concerns/with_uploads.rb
+++ b/app/models/concerns/with_uploads.rb
@@ -36,4 +36,8 @@ module WithUploads
upload.destroy
end
end
+
+ def retrieve_upload(_identifier, paths)
+ uploads.find_by(path: paths)
+ end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 254764eefde..ac86e9e8de0 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -1,5 +1,6 @@
class Deployment < ActiveRecord::Base
include AtomicInternalId
+ include IidRoutes
belongs_to :project, required: true
belongs_to :environment, required: true
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 8fb77a7869d..9c171de7fc3 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -11,6 +11,7 @@ class Group < Namespace
include GroupDescendant
include TokenAuthenticatable
include WithUploads
+ include Gitlab::Utils::StrongMemoize
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
@@ -26,7 +27,11 @@ class Group < Namespace
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, through: :project_group_links, source: :project
+
+ # Overridden on another method
+ # Left here just to be dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
+
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
has_many :custom_attributes, class_name: 'GroupCustomAttribute'
@@ -88,6 +93,15 @@ class Group < Namespace
end
end
+ # Overrides notification_settings has_many association
+ # This allows to apply notification settings from parent groups
+ # to child groups and projects.
+ def notification_settings
+ source_type = self.class.base_class.name
+
+ NotificationSetting.where(source_type: source_type, source_id: self_and_ancestors_ids)
+ end
+
def to_reference(_from = nil, full: nil)
"#{self.class.reference_prefix}#{full_path}"
end
@@ -141,13 +155,14 @@ class Group < Namespace
)
end
- def add_user(user, access_level, current_user: nil, expires_at: nil)
+ def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false)
GroupMember.add_user(
self,
user,
access_level,
current_user: current_user,
- expires_at: expires_at
+ expires_at: expires_at,
+ ldap: ldap
)
end
@@ -195,6 +210,10 @@ class Group < Namespace
owners.include?(user) && owners.size == 1
end
+ def ldap_synced?
+ false
+ end
+
def post_create_hook
Gitlab::AppLogger.info("Group \"#{name}\" was created")
@@ -220,6 +239,12 @@ class Group < Namespace
members_with_parents.pluck(:user_id)
end
+ def self_and_ancestors_ids
+ strong_memoize(:self_and_ancestors_ids) do
+ self_and_ancestors.pluck(:id)
+ end
+ end
+
def members_with_parents
# Avoids an unnecessary SELECT when the group has no parents
source_ids =
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index f7f930e86ed..f50f28deffe 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -14,7 +14,7 @@ class InternalId < ActiveRecord::Base
belongs_to :project
belongs_to :namespace
- enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4 }
+ enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5 }
validates :usage, presence: true
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 41a290f34b4..4715d942c8d 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -2,6 +2,7 @@ require 'carrierwave/orm/activerecord'
class Issue < ActiveRecord::Base
include AtomicInternalId
+ include IidRoutes
include Issuable
include Noteable
include Referable
@@ -47,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) }
@@ -55,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) }
@@ -307,6 +308,10 @@ class Issue < ActiveRecord::Base
end
end
+ def etag_caching_enabled?
+ true
+ end
+
def discussions_rendered_on_frontend?
true
end
diff --git a/app/models/label.rb b/app/models/label.rb
index de7f1d56c64..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
@@ -137,6 +142,10 @@ class Label < ActiveRecord::Base
priority.try(:priority)
end
+ def priority?
+ priorities.present?
+ end
+
def template?
template
end
diff --git a/app/models/list.rb b/app/models/list.rb
index 5daf35ef845..4edcfa78835 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -2,17 +2,27 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
- enum list_type: { backlog: 0, label: 1, closed: 2 }
+ enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
validates :label_id, uniqueness: { scope: :board_id }, if: :label?
- validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :label?
+ validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :movable?
before_destroy :can_be_destroyed
- scope :destroyable, -> { where(list_type: list_types[:label]) }
- scope :movable, -> { where(list_type: list_types[:label]) }
+ scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) }
+ scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) }
+
+ class << self
+ def destroyable_types
+ [:label]
+ end
+
+ def movable_types
+ [:label]
+ end
+ end
def destroyable?
label?
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 79fc155fd3c..b4090fd8baf 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1,5 +1,6 @@
class MergeRequest < ActiveRecord::Base
include AtomicInternalId
+ include IidRoutes
include Issuable
include Noteable
include Referable
@@ -127,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)
@@ -368,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.
@@ -609,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
@@ -705,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
@@ -1114,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
@@ -1124,21 +1135,24 @@ class MergeRequest < ActiveRecord::Base
project.merge_requests.merged.where(author_id: author_id).empty?
end
- def allow_maintainer_to_push
- maintainer_push_possible? && super
+ # TODO: remove once production database rename completes
+ alias_attribute :allow_collaboration, :allow_maintainer_to_push
+
+ def allow_collaboration
+ collaborative_push_possible? && allow_maintainer_to_push
end
- alias_method :allow_maintainer_to_push?, :allow_maintainer_to_push
+ alias_method :allow_collaboration?, :allow_collaboration
- def maintainer_push_possible?
+ def collaborative_push_possible?
source_project.present? && for_fork? &&
target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
!ProtectedBranch.protected?(source_project, source_branch)
end
- def can_allow_maintainer_to_push?(user)
- maintainer_push_possible? &&
+ def can_allow_collaboration?(user)
+ collaborative_push_possible? &&
Ability.allowed?(user, :push_code, source_project)
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/milestone.rb b/app/models/milestone.rb
index d14e3a4ded5..d05dcfd083a 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -9,6 +9,7 @@ class Milestone < ActiveRecord::Base
include CacheMarkdownField
include AtomicInternalId
+ include IidRoutes
include Sortable
include Referable
include StripAttribute
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 02f7a9b1e4f..abc40d9016e 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -384,6 +384,7 @@ class Note < ActiveRecord::Base
def expire_etag_cache
return unless noteable&.discussions_rendered_on_frontend?
+ return unless noteable&.etag_caching_enabled?
Gitlab::EtagCaching::Store.new.touch(etag_key)
end
@@ -435,6 +436,10 @@ class Note < ActiveRecord::Base
super.merge(noteable: noteable)
end
+ def retrieve_upload(_identifier, paths)
+ Upload.find_by(model: self, path: paths)
+ end
+
private
def keep_around_commit
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 2c3580bbdc6..1a03dd9df56 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -1,4 +1,6 @@
class NotificationRecipient
+ include Gitlab::Utils::StrongMemoize
+
attr_reader :user, :type, :reason
def initialize(user, type, **opts)
unless NotificationSetting.levels.key?(type) || type == :subscription
@@ -64,7 +66,7 @@ class NotificationRecipient
return false unless @target
return false unless @target.respond_to?(:subscriptions)
- subscription = @target.subscriptions.find_by_user_id(@user.id)
+ subscription = @target.subscriptions.find { |subscription| subscription.user_id == @user.id }
subscription && !subscription.subscribed
end
@@ -142,10 +144,33 @@ class NotificationRecipient
return project_setting unless project_setting.nil? || project_setting.global?
- group_setting = @group && user.notification_settings_for(@group)
+ group_setting = closest_non_global_group_notification_settting
- return group_setting unless group_setting.nil? || group_setting.global?
+ return group_setting unless group_setting.nil?
user.global_notification_setting
end
+
+ # Returns the notificaton_setting of the lowest group in hierarchy with non global level
+ def closest_non_global_group_notification_settting
+ return unless @group
+ return if indexed_group_notification_settings.empty?
+
+ notification_setting = nil
+
+ @group.self_and_ancestors_ids.each do |id|
+ notification_setting = indexed_group_notification_settings[id]
+ break if notification_setting
+ end
+
+ notification_setting
+ end
+
+ def indexed_group_notification_settings
+ strong_memoize(:indexed_group_notification_settings) do
+ @group.notification_settings.where(user_id: user.id)
+ .where.not(level: NotificationSetting.levels[:global])
+ .index_by(&:source_id)
+ end
+ end
end
diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb
index 82c1c4de3a0..355624fd552 100644
--- a/app/models/personal_snippet.rb
+++ b/app/models/personal_snippet.rb
@@ -1,2 +1,3 @@
class PersonalSnippet < Snippet
+ include WithUploads
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 32298fc7f5c..d91d7dcfe9a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -25,6 +25,7 @@ class Project < ActiveRecord::Base
include FastDestroyAll::Helpers
include WithUploads
include BatchDestroyDependentAssociations
+ extend Gitlab::Cache::RequestCache
extend Gitlab::ConfigHelper
@@ -68,7 +69,7 @@ class Project < ActiveRecord::Base
add_authentication_token_field :runners_token
- before_validation :mark_remote_mirrors_for_removal, if: -> { ActiveRecord::Base.connection.table_exists?(:remote_mirrors) }
+ before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
before_save :ensure_runners_token
@@ -228,6 +229,7 @@ class Project < ActiveRecord::Base
has_many :commit_statuses
has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
+ has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
# Ci::Build objects store data on the file system such as artifact files and
# build traces. Currently there's no efficient way of removing this data in
@@ -291,6 +293,7 @@ class Project < ActiveRecord::Base
validates :name, uniqueness: { scope: :namespace_id }
validates :import_url, url: { protocols: %w(http https ssh git),
allow_localhost: false,
+ enforce_user: true,
ports: VALID_IMPORT_PORTS }, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
@@ -674,6 +677,12 @@ class Project < ActiveRecord::Base
end
end
+ def human_import_status_name
+ ensure_import_state
+
+ import_state.human_status_name
+ end
+
def import_schedule
ensure_import_state(force: true)
@@ -1425,8 +1434,14 @@ class Project < ActiveRecord::Base
Ci::Runner.from("(#{union.to_sql}) ci_runners")
end
+ def active_runners
+ strong_memoize(:active_runners) do
+ all_runners.active
+ end
+ end
+
def any_runners?(&block)
- all_runners.active.any?(&block)
+ active_runners.any?(&block)
end
def valid_runners_token?(token)
@@ -1602,6 +1617,7 @@ class Project < ActiveRecord::Base
def after_import
repository.after_import
+ wiki.repository.after_import
import_finish
remove_import_jid
update_project_counter_caches
@@ -1649,12 +1665,6 @@ class Project < ActiveRecord::Base
import_state.update_column(:jid, nil)
end
- def running_or_pending_build_count(force: false)
- Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
- builds.running_or_pending.count(:all)
- end
- end
-
# Lazy loading of the `pipeline_status` attribute
def pipeline_status
@pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
@@ -1974,18 +1984,18 @@ class Project < ActiveRecord::Base
.limit(1)
.select(1)
source_of_merge_requests.opened
- .where(allow_maintainer_to_push: true)
+ .where(allow_collaboration: true)
.where('EXISTS (?)', developer_access_exists)
end
- def branch_allows_maintainer_push?(user, branch_name)
+ def branch_allows_collaboration?(user, branch_name)
return false unless user
cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push"
- memoized_results = strong_memoize(:branch_allows_maintainer_push) do
+ memoized_results = strong_memoize(:branch_allows_collaboration) do
Hash.new do |result, cache_key|
- result[cache_key] = fetch_branch_allows_maintainer_push?(user, branch_name)
+ result[cache_key] = fetch_branch_allows_collaboration?(user, branch_name)
end
end
@@ -2004,6 +2014,15 @@ class Project < ActiveRecord::Base
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
end
+ def any_lfs_file_locks?
+ lfs_file_locks.any?
+ end
+ request_cache(:any_lfs_file_locks?) { self.id }
+
+ def auto_cancel_pending_pipelines?
+ auto_cancel_pending_pipelines == 'enabled'
+ end
+
private
def storage
@@ -2127,18 +2146,22 @@ class Project < ActiveRecord::Base
raise ex
end
- def fetch_branch_allows_maintainer_push?(user, branch_name)
+ def fetch_branch_allows_collaboration?(user, branch_name)
check_access = -> do
next false if empty_repo?
- merge_request = source_of_merge_requests.opened
- .where(allow_maintainer_to_push: true)
- .find_by(source_branch: branch_name)
- merge_request&.can_be_merged_by?(user)
+ merge_requests = source_of_merge_requests.opened
+ .where(allow_collaboration: true)
+
+ if branch_name
+ merge_requests.find_by(source_branch: branch_name)&.can_be_merged_by?(user)
+ else
+ merge_requests.any? { |merge_request| merge_request.can_be_merged_by?(user) }
+ end
end
if RequestStore.active?
- RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_maintainer_push") do
+ RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
check_access.call
end
else
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index ed6c1eddbc1..faa831b1949 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -1,11 +1,18 @@
class ProjectAutoDevops < ActiveRecord::Base
belongs_to :project
+ enum deploy_strategy: {
+ continuous: 0,
+ manual: 1
+ }
+
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
+ after_save :create_gitlab_deploy_token, if: :needs_to_create_deploy_token?
+
def instance_domain
Gitlab::CurrentSettings.auto_devops_domain
end
@@ -20,6 +27,30 @@ class ProjectAutoDevops < ActiveRecord::Base
variables.append(key: 'AUTO_DEVOPS_DOMAIN',
value: domain.presence || instance_domain)
end
+
+ if manual?
+ variables.append(key: 'STAGING_ENABLED', value: '1')
+ variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: '1')
+ end
end
end
+
+ private
+
+ def create_gitlab_deploy_token
+ project.deploy_tokens.create!(
+ name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME,
+ read_registry: true
+ )
+ end
+
+ def needs_to_create_deploy_token?
+ auto_devops_enabled? &&
+ !project.public? &&
+ !project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present?
+ end
+
+ def auto_devops_enabled?
+ Gitlab::CurrentSettings.auto_devops_enabled? || enabled?
+ 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/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb
index 22a65b5145e..f710fa85b5d 100644
--- a/app/models/project_services/chat_message/base_message.rb
+++ b/app/models/project_services/chat_message/base_message.rb
@@ -26,13 +26,18 @@ module ChatMessage
end
end
- def pretext
+ def summary
return message if markdown
format(message)
end
+ def pretext
+ summary
+ end
+
def fallback
+ format(message)
end
def attachments
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index 2135122278a..96fd23aede3 100644
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -23,10 +23,6 @@ module ChatMessage
''
end
- def fallback
- format(message)
- end
-
def attachments
return message if markdown
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index ae0debbd3ac..a60b4c7fd0d 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -155,6 +155,7 @@ class ChatNotificationService < Service
end
def notify_for_ref?(data)
+ return true if data[:object_kind] == 'tag_push'
return true if data.dig(:object_attributes, :tag)
return true unless notify_only_default_branch?
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 84248f9590b..8a6b0ed1a5f 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -43,13 +43,18 @@ class GemnasiumService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
+ # Gitaly: this class will be removed https://gitlab.com/gitlab-org/gitlab-ee/issues/6010
+ repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ project.repository.path_to_repo
+ end
+
Gemnasium::GitlabService.execute(
ref: data[:ref],
before: data[:before],
after: data[:after],
token: token,
api_key: api_key,
- repo: project.repository.path_to_repo # Gitaly: fixed by https://gitlab.com/gitlab-org/security-products/gemnasium-migration/issues/9
+ repo: repo_path
)
end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index eb3261c902f..412d62388f0 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -265,7 +265,7 @@ class JiraService < IssueTrackerService
title: title,
status: status,
icon: {
- title: 'GitLab', url16x16: asset_url('favicon.ico', host: gitlab_config.url)
+ title: 'GitLab', url16x16: asset_url(Gitlab::Favicon.main, host: gitlab_config.url)
}
}
}
diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb
index 2facff53e26..99500caec0e 100644
--- a/app/models/project_services/microsoft_teams_service.rb
+++ b/app/models/project_services/microsoft_teams_service.rb
@@ -44,7 +44,7 @@ class MicrosoftTeamsService < ChatNotificationService
def notify(message, opts)
MicrosoftTeams::Notifier.new(webhook).ping(
title: message.project_name,
- pretext: message.pretext,
+ summary: message.summary,
activity: message.activity,
attachments: message.attachments
)
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/project_wiki.rb b/app/models/project_wiki.rb
index f799a0b4227..a6f94b3e3b0 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -140,10 +140,6 @@ class ProjectWiki
[title, title_array.join("/")]
end
- def search_files(query)
- repository.search_files_by_content(query, default_branch)
- end
-
def repository
@repository ||= Repository.new(full_path, @project, disk_path: disk_path, is_wiki: true)
end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index cb361a66591..dff99cfca35 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -5,7 +5,7 @@ class ProtectedBranch < ActiveRecord::Base
protected_ref_access_levels :merge, :push
def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
- # Masters, owners and admins are allowed to create the default branch
+ # Maintainers, owners and admins are allowed to create the default branch
if default_branch_protected? && project.empty_repo?
return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
end
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index 5cd222e18a4..c4b5dd2dc96 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -16,7 +16,7 @@ class RemoteMirror < ActiveRecord::Base
belongs_to :project, inverse_of: :remote_mirrors
- validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true }
+ validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true }
before_save :set_new_remote_name, if: :mirror_url_changed?
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 82cf47ba04e..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
@@ -154,7 +154,10 @@ class Repository
# Returns a list of commits that are not present in any reference
def new_commits(newrev)
- refs = ::Gitlab::Git::RevList.new(raw, newrev: newrev).new_refs
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1233
+ refs = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ ::Gitlab::Git::RevList.new(raw, newrev: newrev).new_refs
+ end
refs.map { |sha| commit(sha.strip) }
end
@@ -270,6 +273,20 @@ class Repository
end
end
+ def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:)
+ raw_repository.archive_metadata(
+ ref,
+ storage_path,
+ project.path,
+ format,
+ append_sha: append_sha
+ )
+ end
+
+ def cached_methods
+ CACHED_METHODS
+ end
+
def expire_tags_cache
expire_method_caches(%i(tag_names tag_count))
@tags = nil
@@ -410,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.
@@ -837,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
@@ -946,6 +963,10 @@ class Repository
blob_data_at(sha, path)
end
+ def lfsconfig_for(sha)
+ blob_data_at(sha, '.lfsconfig')
+ end
+
def fetch_ref(source_repository, source_ref:, target_ref:)
raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
end
diff --git a/app/models/term_agreement.rb b/app/models/term_agreement.rb
index 8458a231bbd..c317bd0c90b 100644
--- a/app/models/term_agreement.rb
+++ b/app/models/term_agreement.rb
@@ -2,5 +2,7 @@ class TermAgreement < ActiveRecord::Base
belongs_to :term, class_name: 'ApplicationSetting::Term'
belongs_to :user
+ scope :accepted, -> { where(accepted: true) }
+
validates :user, :term, presence: true
end
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index f4c5c581a11..659146f43e4 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -19,4 +19,9 @@ class Timelog < ActiveRecord::Base
errors.add(:base, 'Issue or Merge Request ID is required')
end
end
+
+ # Rails5 defaults to :touch_later, overwrite for normal touch
+ def belongs_to_touch_method
+ :touch
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index e219ab800ad..48629c58490 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')) }
@@ -1038,7 +1038,10 @@ class User < ActiveRecord::Base
def notification_settings_for(source)
if notification_settings.loaded?
- notification_settings.find { |notification| notification.source == source }
+ notification_settings.find do |notification|
+ notification.source_type == source.class.base_class.name &&
+ notification.source_id == source.id
+ end
else
notification_settings.find_or_initialize_by(source: source)
end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 8b65758f3e8..1c0cc7425ec 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -14,8 +14,8 @@ module Ci
@subject.triggered_by?(@user)
end
- condition(:branch_allows_maintainer_push) do
- @subject.project.branch_allows_maintainer_push?(@user, @subject.ref)
+ condition(:branch_allows_collaboration) do
+ @subject.project.branch_allows_collaboration?(@user, @subject.ref)
end
rule { protected_ref }.policy do
@@ -25,7 +25,7 @@ module Ci
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build
- rule { can?(:public_access) & branch_allows_maintainer_push }.policy do
+ rule { can?(:public_access) & branch_allows_collaboration }.policy do
enable :update_build
enable :update_commit_status
end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index 540e4235299..b81329d0625 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -4,13 +4,13 @@ module Ci
condition(:protected_ref) { ref_protected?(@user, @subject.project, @subject.tag?, @subject.ref) }
- condition(:branch_allows_maintainer_push) do
- @subject.project.branch_allows_maintainer_push?(@user, @subject.ref)
+ condition(:branch_allows_collaboration) do
+ @subject.project.branch_allows_collaboration?(@user, @subject.ref)
end
rule { protected_ref }.prevent :update_pipeline
- rule { can?(:public_access) & branch_allows_maintainer_push }.policy do
+ rule { can?(:public_access) & branch_allows_collaboration }.policy do
enable :update_pipeline
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 99a0d7118f2..199bcf92b21 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -45,7 +45,7 @@ class ProjectPolicy < BasePolicy
desc "User has developer access"
condition(:developer) { team_access_level >= Gitlab::Access::DEVELOPER }
- desc "User has master access"
+ desc "User has maintainer access"
condition(:master) { team_access_level >= Gitlab::Access::MASTER }
desc "Project is public"
@@ -297,6 +297,7 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:build))
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
prevent(*create_read_update_admin_destroy(:environment))
+ prevent(*create_read_update_admin_destroy(:cluster))
prevent(*create_read_update_admin_destroy(:deployment))
end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index ad839d9840a..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,29 @@ 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
+ # safely short-circuit it.
+ if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
+ merge_request.mergeable_discussions_state?
+ else
+ false
+ end
+ end
+
+ def web_url
+ Gitlab::UrlBuilder.build(merge_request)
+ end
+
+ def subscribed?
+ merge_request.subscribed?(current_user, merge_request.target_project)
+ end
+
private
def cached_can_be_reverted?
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..aa289a96975 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -1,25 +1,46 @@
class DiffFileEntity < Grape::Entity
+ include RequestAwareEntity
+ include BlobHelper
+ include CommitsHelper
include DiffHelper
include SubmoduleHelper
include BlobHelper
include IconsHelper
- include ActionView::Helpers::TagHelper
+ include TreeHelper
+ include ChecksCollaboration
+ include Gitlab::Utils::StrongMemoize
expose :submodule?, as: :submodule
expose :submodule_link do |diff_file|
- submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository).first
+ memoized_submodule_links(diff_file).first
+ end
+
+ expose :submodule_tree_url do |diff_file|
+ memoized_submodule_links(diff_file).last
end
- expose :blob_path do |diff_file|
- diff_file.blob.path
+ expose :blob, using: BlobEntity
+
+ expose :can_modify_blob do |diff_file|
+ merge_request = options[:merge_request]
+
+ if merge_request&.source_project && current_user
+ can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
+ else
+ false
+ end
end
- expose :blob_icon do |diff_file|
- blob_icon(diff_file.b_mode, diff_file.file_path)
+ expose :file_hash do |diff_file|
+ Digest::SHA1.hexdigest(diff_file.file_path)
end
expose :file_path
+ expose :too_large?, as: :too_large
+ expose :collapsed?, as: :collapsed
+ expose :new_file?, as: :new_file
+
expose :deleted_file?, as: :deleted_file
expose :renamed_file?, as: :renamed_file
expose :old_path
@@ -28,6 +49,36 @@ class DiffFileEntity < Grape::Entity
expose :a_mode
expose :b_mode
expose :text?, as: :text
+ expose :added_lines
+ expose :removed_lines
+ expose :diff_refs
+ expose :content_sha
+ expose :stored_externally?, as: :stored_externally
+ expose :external_storage
+
+ expose :load_collapsed_diff_url, if: -> (diff_file, options) { diff_file.text? && options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+ project = merge_request.target_project
+
+ next unless project
+
+ diff_for_path_namespace_project_merge_request_path(
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid,
+ old_path: diff_file.old_path,
+ new_path: diff_file.new_path,
+ file_identifier: diff_file.file_identifier
+ )
+ end
+
+ expose :formatted_external_url, if: -> (_, options) { options[:environment] } do |diff_file|
+ options[:environment].formatted_external_url
+ end
+
+ expose :external_url, if: -> (_, options) { options[:environment] } do |diff_file|
+ options[:environment].external_url_for(diff_file.new_path, diff_file.content_sha)
+ end
expose :old_path_html do |diff_file|
old_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
@@ -38,4 +89,64 @@ class DiffFileEntity < Grape::Entity
_, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
new_path
end
+
+ expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+
+ options = merge_request.persisted? ? { from_merge_request_iid: merge_request.iid } : {}
+
+ next unless merge_request.source_project
+
+ project_edit_blob_path(merge_request.source_project,
+ tree_join(merge_request.source_branch, diff_file.new_path),
+ options)
+ end
+
+ expose :view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+
+ project = merge_request.target_project
+
+ next unless project
+
+ project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path))
+ end
+
+ expose :replaced_view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image'
+ image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha
+
+ merge_request = options[:merge_request]
+ project = merge_request.target_project
+
+ next unless project
+
+ project_blob_path(project, tree_join(diff_file.old_content_sha, diff_file.old_path)) if image_diff && image_replaced
+ end
+
+ expose :context_lines_path, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
+ project_blob_diff_path(diff_file.repository.project, tree_join(diff_file.content_sha, diff_file.file_path))
+ end
+
+ # Used for inline diffs
+ expose :highlighted_diff_lines, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
+ diff_file.diff_lines_for_serializer
+ end
+
+ # Used for parallel diffs
+ expose :parallel_diff_lines, if: -> (diff_file, _) { diff_file.text? }
+
+ def current_user
+ request.current_user
+ end
+
+ def memoized_submodule_links(diff_file)
+ strong_memoize(:submodule_links) do
+ if diff_file.submodule?
+ submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository)
+ else
+ []
+ end
+ end
+ end
end
diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb
new file mode 100644
index 00000000000..bb804e5347a
--- /dev/null
+++ b/app/serializers/diffs_entity.rb
@@ -0,0 +1,65 @@
+class DiffsEntity < Grape::Entity
+ include DiffHelper
+ include RequestAwareEntity
+
+ expose :real_size
+ expose :size
+
+ expose :branch_name do |diffs|
+ merge_request&.source_branch
+ end
+
+ expose :target_branch_name do |diffs|
+ merge_request&.target_branch
+ end
+
+ expose :commit do |diffs|
+ options[:commit]
+ end
+
+ expose :merge_request_diff, using: MergeRequestDiffEntity do |diffs|
+ options[:merge_request_diff]
+ end
+
+ expose :start_version, using: MergeRequestDiffEntity do |diffs|
+ options[:start_version]
+ end
+
+ expose :latest_diff do |diffs|
+ options[:latest_diff]
+ end
+
+ expose :latest_version_path, if: -> (*) { merge_request } do |diffs|
+ diffs_project_merge_request_path(merge_request&.project, merge_request)
+ end
+
+ expose :added_lines do |diffs|
+ diffs.diff_files.sum(&:added_lines)
+ end
+
+ expose :removed_lines do |diffs|
+ diffs.diff_files.sum(&:removed_lines)
+ end
+
+ expose :render_overflow_warning do |diffs|
+ render_overflow_warning?(diffs.diff_files)
+ end
+
+ expose :email_patch_path, if: -> (*) { merge_request } do |diffs|
+ merge_request_path(merge_request, format: :patch)
+ end
+
+ expose :plain_diff_path, if: -> (*) { merge_request } do |diffs|
+ merge_request_path(merge_request, format: :diff)
+ end
+
+ expose :diff_files, using: DiffFileEntity
+
+ expose :merge_request_diffs, using: MergeRequestDiffEntity, if: -> (_, options) { options[:merge_request_diffs]&.any? } do |diffs|
+ options[:merge_request_diffs]
+ end
+
+ def merge_request
+ options[:merge_request]
+ end
+end
diff --git a/app/serializers/diffs_serializer.rb b/app/serializers/diffs_serializer.rb
new file mode 100644
index 00000000000..6771e10c5ac
--- /dev/null
+++ b/app/serializers/diffs_serializer.rb
@@ -0,0 +1,3 @@
+class DiffsSerializer < BaseSerializer
+ entity DiffsEntity
+end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index 718fb35e62d..63f28133a64 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -1,16 +1,31 @@
class DiscussionEntity < Grape::Entity
include RequestAwareEntity
+ include NotesHelper
expose :id, :reply_id
+ expose :position, if: -> (d, _) { d.diff_discussion? }
+ expose :line_code, if: -> (d, _) { d.diff_discussion? }
expose :expanded?, as: :expanded
+ expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? }
+ expose :project_id
expose :notes do |discussion, opts|
request.note_entity.represent(discussion.notes, opts)
end
+ expose :discussion_path do |discussion|
+ discussion_path(discussion)
+ end
+
expose :individual_note?, as: :individual_note
- expose :resolvable?, as: :resolvable
+ expose :resolvable do |discussion|
+ discussion.resolvable?
+ end
+
expose :resolved?, as: :resolved
+ expose :resolved_by_push?, as: :resolved_by_push
+ expose :resolved_by
+ expose :resolved_at
expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion|
resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id)
end
@@ -18,24 +33,17 @@ class DiscussionEntity < Grape::Entity
new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id)
end
- expose :diff_file, using: DiffFileEntity, if: -> (d, _) { defined? d.diff_file }
+ expose :diff_file, using: DiffFileEntity, if: -> (d, _) { d.diff_discussion? }
expose :diff_discussion?, as: :diff_discussion
- expose :truncated_diff_lines, if: -> (d, _) { (defined? d.diff_file) && d.diff_file.text? } do |discussion|
- options[:context].render_to_string(
- partial: "projects/diffs/line",
- collection: discussion.truncated_diff_lines,
- as: :line,
- locals: { diff_file: discussion.diff_file,
- discussion_expanded: true,
- plain: true },
- layout: false,
- formats: [:html]
- )
+ expose :truncated_diff_lines_path, if: -> (d, _) { !d.expanded? && !render_truncated_diff_lines? } do |discussion|
+ project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion)
end
- expose :image_diff_html, if: -> (d, _) { defined? d.diff_file } do |discussion|
+ expose :truncated_diff_lines, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) }
+
+ expose :image_diff_html, if: -> (d, _) { d.diff_discussion? && d.on_image? } do |discussion|
diff_file = discussion.diff_file
partial = diff_file.new_file? || diff_file.deleted_file? ? 'single_image_diff' : 'replaced_image_diff'
options[:context].render_to_string(
@@ -47,4 +55,17 @@ class DiscussionEntity < Grape::Entity
formats: [:html]
)
end
+
+ expose :for_commit?, as: :for_commit
+ expose :commit_id
+
+ private
+
+ def render_truncated_diff_lines?
+ options[:render_truncated_diff_lines]
+ end
+
+ def current_user
+ request.current_user
+ end
end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index e4aec977f01..1c06691026d 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -5,4 +5,8 @@ class MergeRequestBasicEntity < IssuableSidebarEntity
expose :state
expose :source_branch_exists?, as: :source_branch_exists
expose :rebase_in_progress?, as: :rebase_in_progress
+ expose :milestone, using: API::Entities::Milestone
+ expose :labels, using: LabelEntity
+ expose :assignee, using: API::Entities::UserBasic
+ expose :task_status, :task_status_short
end
diff --git a/app/serializers/merge_request_diff_entity.rb b/app/serializers/merge_request_diff_entity.rb
new file mode 100644
index 00000000000..32c761b45ac
--- /dev/null
+++ b/app/serializers/merge_request_diff_entity.rb
@@ -0,0 +1,46 @@
+class MergeRequestDiffEntity < Grape::Entity
+ include Gitlab::Routing
+ include GitHelper
+ include MergeRequestsHelper
+
+ expose :version_index do |merge_request_diff|
+ @merge_request_diffs = options[:merge_request_diffs]
+ diff = options[:merge_request_diff]
+
+ next unless diff.present?
+ next unless @merge_request_diffs.size > 1
+
+ version_index(merge_request_diff)
+ end
+
+ expose :created_at
+ expose :commits_count
+
+ expose :latest?, as: :latest
+
+ expose :short_commit_sha do |merge_request_diff|
+ short_sha(merge_request_diff.head_commit_sha)
+ end
+
+ expose :version_path do |merge_request_diff|
+ start_sha = options[:start_sha]
+ project = merge_request.target_project
+
+ next unless project
+
+ merge_request_version_path(project, merge_request, merge_request_diff, start_sha)
+ end
+
+ expose :compare_path do |merge_request_diff|
+ project = merge_request.target_project
+ diff = options[:merge_request_diff]
+
+ if project && diff
+ merge_request_version_path(project, merge_request, diff, merge_request_diff.head_commit_sha)
+ end
+ end
+
+ def merge_request
+ options[:merge_request]
+ end
+end
diff --git a/app/serializers/merge_request_user_entity.rb b/app/serializers/merge_request_user_entity.rb
new file mode 100644
index 00000000000..33fc7b724d5
--- /dev/null
+++ b/app/serializers/merge_request_user_entity.rb
@@ -0,0 +1,24 @@
+class MergeRequestUserEntity < UserEntity
+ include RequestAwareEntity
+ include BlobHelper
+ include TreeHelper
+
+ expose :can_fork do |user|
+ can?(user, :fork_project, request.project) if project
+ end
+
+ expose :can_create_merge_request do |user|
+ project && can?(user, :create_merge_request_in, project)
+ end
+
+ expose :fork_path, if: -> (*) { project } do |user|
+ params = edit_blob_fork_params("Edit")
+ project_forks_path(project, namespace_key: user.namespace.id, continue: params)
+ end
+
+ def project
+ return false unless request.respond_to?(:project) && request.project
+
+ request.project
+ end
+end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 141070aef45..5d72ebdd7fd 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -13,7 +13,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :squash
expose :target_branch
expose :target_project_id
- expose :allow_maintainer_to_push
+ expose :allow_collaboration
expose :should_be_rebased?, as: :should_be_rebased
expose :ff_only_enabled do |merge_request|
@@ -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/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb
index 130968a44c1..8ba9cac53c4 100644
--- a/app/serializers/pipeline_details_entity.rb
+++ b/app/serializers/pipeline_details_entity.rb
@@ -1,6 +1,6 @@
class PipelineDetailsEntity < PipelineEntity
expose :details do
- expose :legacy_stages, as: :stages, using: StageEntity
+ expose :ordered_stages, as: :stages, using: StageEntity
expose :artifacts, using: BuildArtifactEntity
expose :manual_actions, using: BuildActionEntity
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index 7181f8a6b04..17a022539bb 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -1,14 +1,11 @@
class PipelineSerializer < BaseSerializer
include WithPagination
-
- InvalidResourceError = Class.new(StandardError)
-
entity PipelineDetailsEntity
def represent(resource, opts = {})
if resource.is_a?(ActiveRecord::Relation)
-
resource = resource.preload([
+ :stages,
:retryable_builds,
:cancelable_statuses,
:trigger_requests,
@@ -20,10 +17,14 @@ class PipelineSerializer < BaseSerializer
end
if paginated?
- super(@paginator.paginate(resource), opts)
- else
- super(resource, opts)
+ resource = paginator.paginate(resource)
end
+
+ if opts.delete(:preload)
+ resource = Gitlab::Ci::Pipeline::Preloader.preload!(resource)
+ end
+
+ super(resource, opts)
end
def represent_status(resource)
@@ -36,7 +37,7 @@ class PipelineSerializer < BaseSerializer
def represent_stages(resource)
return {} unless resource.present?
- data = represent(resource, { only: [{ details: [:stages] }] })
+ data = represent(resource, { only: [{ details: [:stages] }], preload: true })
data.dig(:details, :stages) || []
end
end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index 8e8bda2f9df..47df7f9dcf9 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -7,16 +7,7 @@ class StatusEntity < Grape::Entity
expose :details_path
expose :favicon do |status|
- dir =
- if Gitlab::Utils.to_boolean(ENV['CANARY'])
- File.join('ci_favicons', 'canary')
- elsif Rails.env.development?
- File.join('ci_favicons', 'dev')
- else
- 'ci_favicons'
- end
-
- ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
+ Gitlab::Favicon.status_overlay(status.favicon)
end
expose :action, if: -> (status, _) { status.has_action? } do
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index e70445cfb67..7bcb8f49d0d 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -1,5 +1,7 @@
module ApplicationSettings
class UpdateService < ApplicationSettings::BaseService
+ attr_reader :params, :application_setting
+
def execute
update_terms(@params.delete(:terms))
diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb
index 35d45f25a71..94a434b95dd 100644
--- a/app/services/applications/create_service.rb
+++ b/app/services/applications/create_service.rb
@@ -2,11 +2,10 @@ module Applications
class CreateService
def initialize(current_user, params)
@current_user = current_user
- @params = params
- @ip_address = @params.delete(:ip_address)
+ @params = params.except(:ip_address)
end
- def execute(request = nil)
+ def execute(request)
Doorkeeper::Application.create(@params)
end
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/base_service.rb b/app/services/base_service.rb
index 6883ba36c71..3519b7c5e7d 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -3,7 +3,7 @@ class BaseService
attr_accessor :project, :current_user, :params
- def initialize(project, user, params = {})
+ def initialize(project, user = nil, params = {})
@project, @current_user, @params = project, user, params.dup
end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 5a961ac89e4..b1dbe73cdf7 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -3,13 +3,18 @@ module Boards
class ListService < Boards::BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
- issues = without_board_labels(issues) unless movable_list? || closed_list?
- issues = with_list_label(issues) if movable_list?
+ issues = filter(issues)
issues.order_by_position_and_priority
end
private
+ def filter(issues)
+ issues = without_board_labels(issues) unless list&.movable? || list&.closed?
+ issues = with_list_label(issues) if list&.label?
+ issues
+ end
+
def board
@board ||= parent.boards.find(params[:board_id])
end
@@ -20,18 +25,6 @@ module Boards
@list = board.lists.find(params[:id]) if params.key?(:id)
end
- def movable_list?
- return @movable_list if defined?(@movable_list)
-
- @movable_list = list.present? && list.movable?
- end
-
- def closed_list?
- return @closed_list if defined?(@closed_list)
-
- @closed_list = list.present? && list.closed?
- end
-
def filter_params
set_parent
set_state
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 3ceab209f3f..ee3112c7571 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -3,7 +3,7 @@ module Boards
class MoveService < Boards::BaseService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
- return false if issue_params.empty?
+ return false if issue_params(issue).empty?
update(issue)
end
@@ -28,10 +28,10 @@ module Boards
end
def update(issue)
- ::Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue)
+ ::Issues::UpdateService.new(issue.project, current_user, issue_params(issue)).execute(issue)
end
- def issue_params
+ def issue_params(issue)
attrs = {}
if move_between_lists?
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index 02f1c709374..6fd9885d4f3 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -1,16 +1,28 @@
module Boards
module Lists
class CreateService < Boards::BaseService
+ include Gitlab::Utils::StrongMemoize
+
def execute(board)
List.transaction do
- label = available_labels_for(board).find(params[:label_id])
+ target = target(board)
position = next_position(board)
- create_list(board, label, position)
+ create_list(board, type, target, position)
end
end
private
+ def type
+ :label
+ end
+
+ def target(board)
+ strong_memoize(:target) do
+ available_labels_for(board).find(params[:label_id])
+ end
+ end
+
def available_labels_for(board)
options = { include_ancestor_groups: true }
@@ -28,8 +40,8 @@ module Boards
max_position.nil? ? 0 : max_position.succ
end
- def create_list(board, label, position)
- board.lists.create(label: label, list_type: :label, position: position)
+ def create_list(board, type, target, position)
+ board.lists.create(type => target, list_type: type, position: position)
end
end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 925775aea0b..9bdbb2c0d99 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -25,14 +25,12 @@ module Ci
valid = true
- if Feature.enabled?('ci_job_request_with_tags_matcher')
- # pick builds that does not have other tags than runner's one
- builds = builds.matches_tag_ids(runner.tags.ids)
+ # pick builds that does not have other tags than runner's one
+ builds = builds.matches_tag_ids(runner.tags.ids)
- # pick builds that have at least one tag
- unless runner.run_untagged?
- builds = builds.with_any_tags
- end
+ # pick builds that have at least one tag
+ unless runner.run_untagged?
+ builds = builds.with_any_tags
end
builds.find do |build|
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index f96f2931508..4d0578becbe 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -17,7 +17,7 @@ module Commits
new_commit = create_commit!
success(result: new_commit)
- rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
diff --git a/app/services/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb
index 30be6accc32..f45436370c1 100644
--- a/app/services/concerns/exclusive_lease_guard.rb
+++ b/app/services/concerns/exclusive_lease_guard.rb
@@ -47,6 +47,6 @@ module ExclusiveLeaseGuard
end
def log_error(message, extra_args = {})
- logger.error(message)
+ Rails.logger.error(message)
end
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/create_branch_service.rb b/app/services/create_branch_service.rb
index 0ba6a0ac6b5..9b1a4d960e2 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -14,7 +14,7 @@ class CreateBranchService < BaseService
else
error('Invalid reference name')
end
- rescue Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 1f059c31944..e1499dcee64 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -16,7 +16,7 @@ class DeleteBranchService < BaseService
else
error('Failed to remove branch')
end
- rescue Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
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/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb
index 7eb89339a92..7e3edf21d54 100644
--- a/app/services/lfs/unlock_file_service.rb
+++ b/app/services/lfs/unlock_file_service.rb
@@ -24,7 +24,7 @@ module Lfs
success(lock: lock, http_status: :ok)
elsif forced
- error(_('You must have master access to force delete a lock'), 403)
+ error(_('You must have maintainer access to force delete a lock'), 403)
else
error(_("%{lock_path} is locked by GitLab User %{lock_user_id}") % { lock_path: lock.path, lock_user_id: lock.user_id }, 403)
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 231ab76fde4..4c420b38258 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -38,8 +38,8 @@ module MergeRequests
def filter_params(merge_request)
super
- unless merge_request.can_allow_maintainer_to_push?(current_user)
- params.delete(:allow_maintainer_to_push)
+ unless merge_request.can_allow_collaboration?(current_user)
+ params.delete(:allow_collaboration)
end
end
diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
index cf687b71d16..3407b312700 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -41,7 +41,9 @@ module MergeRequests
end
def ref
- @ref || project.default_branch || 'master'
+ return @ref if project.repository.branch_exists?(@ref)
+
+ project.default_branch || 'master'
end
def merge_request
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/ff_merge_service.rb b/app/services/merge_requests/ff_merge_service.rb
index ba6853b835a..bffc09c34f0 100644
--- a/app/services/merge_requests/ff_merge_service.rb
+++ b/app/services/merge_requests/ff_merge_service.rb
@@ -13,7 +13,7 @@ module MergeRequests
source,
merge_request.target_branch,
merge_request: merge_request)
- rescue Gitlab::Git::HooksService::PreReceiveError => e
+ rescue Gitlab::Git::PreReceiveError => e
raise MergeError, e.message
rescue StandardError => e
raise MergeError, "Something went wrong during merge: #{e.message}"
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/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 126da891c78..3d587f97906 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -79,7 +79,7 @@ module MergeRequests
message = params[:commit_message] || merge_request.merge_commit_message
repository.merge(current_user, source, merge_request, message)
- rescue Gitlab::Git::HooksService::PreReceiveError => e
+ rescue Gitlab::Git::PreReceiveError => e
handle_merge_error(log_message: e.message)
raise MergeError, 'Something went wrong during merge pre-receive hook'
rescue => e
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/pages_service.rb b/app/services/pages_service.rb
deleted file mode 100644
index 446eeb34d3b..00000000000
--- a/app/services/pages_service.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-class PagesService
- attr_reader :data
-
- def initialize(data)
- @data = data
- end
-
- def execute
- return unless Settings.pages.enabled
- return unless data[:build_name] == 'pages'
- return unless data[:build_status] == 'success'
-
- PagesWorker.perform_async(:deploy, data[:build_id])
- end
-end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 346971138b1..aa60661f7f2 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -11,7 +11,7 @@ module Projects
order: { due_date: :asc, title: :asc }
}
- finder_params[:group_ids] = [@project.group.id] if @project.group
+ finder_params[:group_ids] = @project.group.self_and_ancestors_ids if @project.group
MilestonesFinder.new(finder_params).execute.select([:iid, :title])
end
diff --git a/app/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 d16ecdb7b9b..a02a9052fb2 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -46,6 +46,9 @@ module Projects
yield(@project) if block_given?
+ # If the block added errors, don't try to save the project
+ return @project if @project.errors.any?
+
@project.creator = current_user
if forked_from_project_id
@@ -63,6 +66,7 @@ module Projects
message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} "
fail(error: message)
rescue => e
+ @project.errors.add(:base, e.message) if @project
fail(error: e.message)
end
@@ -141,7 +145,6 @@ module Projects
Rails.logger.error(log_message)
if @project
- @project.errors.add(:base, message)
@project.mark_import_as_failed(message) if @project.persisted? && @project.import?
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index de0125ed0dd..02769e72229 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -135,6 +135,7 @@ module Projects
raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.')
end
+ log_destroy_event
trash_repositories!
# Rails attempts to load all related records into memory before
@@ -148,6 +149,10 @@ module Projects
end
end
+ def log_destroy_event
+ log_info("Attempting to destroy #{project.full_path} (#{project.id})")
+ end
+
##
# This method makes sure that we correctly remove registry tags
# for legacy image repository (when repository path equals project path).
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 00080717600..1781a01cbd4 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -17,6 +17,8 @@ module Projects
def execute
add_repository_to_project
+ download_lfs_objects
+
import_data
success
@@ -37,7 +39,7 @@ module Projects
# We should skip the repository for a GitHub import or GitLab project import,
# because these importers fetch the project repositories for us.
- return if has_importer? && importer_class.try(:imports_repository?)
+ return if importer_imports_repository?
if unknown_url?
# In this case, we only want to import issues, not a repository.
@@ -73,6 +75,27 @@ module Projects
end
end
+ def download_lfs_objects
+ # In this case, we only want to import issues
+ return if unknown_url?
+
+ # If it has its own repository importer, it has to implements its own lfs import download
+ return if importer_imports_repository?
+
+ return unless project.lfs_enabled?
+
+ oids_to_download = Projects::LfsPointers::LfsImportService.new(project).execute
+ download_service = Projects::LfsPointers::LfsDownloadService.new(project)
+
+ oids_to_download.each do |oid, link|
+ download_service.execute(oid, link)
+ end
+ rescue => e
+ # Right now, to avoid aborting the importing process, we silently fail
+ # if any exception raises.
+ Rails.logger.error("The Lfs import process failed. #{e.message}")
+ end
+
def import_data
return unless has_importer?
@@ -98,5 +121,9 @@ module Projects
def unknown_url?
project.import_url == Project::UNKNOWN_IMPORT_URL
end
+
+ def importer_imports_repository?
+ has_importer? && importer_class.try(:imports_repository?)
+ end
end
end
diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
new file mode 100644
index 00000000000..d9fb74b090e
--- /dev/null
+++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
@@ -0,0 +1,93 @@
+# This service lists the download link from a remote source based on the
+# oids provided
+module Projects
+ module LfsPointers
+ class LfsDownloadLinkListService < BaseService
+ DOWNLOAD_ACTION = 'download'.freeze
+
+ DownloadLinksError = Class.new(StandardError)
+ DownloadLinkNotFound = Class.new(StandardError)
+
+ attr_reader :remote_uri
+
+ def initialize(project, remote_uri: nil)
+ super(project)
+
+ @remote_uri = remote_uri
+ end
+
+ # This method accepts two parameters:
+ # - oids: hash of oids to query. The structure is { lfs_file_oid => lfs_file_size }
+ #
+ # Returns a hash with the structure { lfs_file_oids => download_link }
+ def execute(oids)
+ return {} unless project&.lfs_enabled? && remote_uri && oids.present?
+
+ get_download_links(oids)
+ end
+
+ private
+
+ def get_download_links(oids)
+ response = Gitlab::HTTP.post(remote_uri,
+ body: request_body(oids),
+ headers: headers)
+
+ raise DownloadLinksError, response.message unless response.success?
+
+ parse_response_links(response['objects'])
+ end
+
+ def parse_response_links(objects_response)
+ objects_response.each_with_object({}) do |entry, link_list|
+ begin
+ oid = entry['oid']
+ link = entry.dig('actions', DOWNLOAD_ACTION, 'href')
+
+ raise DownloadLinkNotFound unless link
+
+ link_list[oid] = add_credentials(link)
+ rescue DownloadLinkNotFound, URI::InvalidURIError
+ Rails.logger.error("Link for Lfs Object with oid #{oid} not found or invalid.")
+ end
+ end
+ end
+
+ def request_body(oids)
+ {
+ operation: DOWNLOAD_ACTION,
+ objects: oids.map { |oid, size| { oid: oid, size: size } }
+ }.to_json
+ end
+
+ def headers
+ {
+ 'Accept' => LfsRequest::CONTENT_TYPE,
+ 'Content-Type' => LfsRequest::CONTENT_TYPE
+ }.freeze
+ end
+
+ def add_credentials(link)
+ uri = URI.parse(link)
+
+ if should_add_credentials?(uri)
+ uri.user = remote_uri.user
+ uri.password = remote_uri.password
+ end
+
+ uri.to_s
+ end
+
+ # The download link can be a local url or an object storage url
+ # If the download link has the some host as the import url then
+ # we add the same credentials because we may need them
+ def should_add_credentials?(link_uri)
+ url_credentials? && link_uri.host == remote_uri.host
+ end
+
+ def url_credentials?
+ remote_uri.user.present? || remote_uri.password.present?
+ end
+ end
+ end
+end
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
new file mode 100644
index 00000000000..6ea43561d61
--- /dev/null
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -0,0 +1,58 @@
+# This service downloads and links lfs objects from a remote URL
+module Projects
+ module LfsPointers
+ class LfsDownloadService < BaseService
+ def execute(oid, url)
+ return unless project&.lfs_enabled? && oid.present? && url.present?
+
+ return if LfsObject.exists?(oid: oid)
+
+ sanitized_uri = Gitlab::UrlSanitizer.new(url)
+
+ with_tmp_file(oid) do |file|
+ size = download_and_save_file(file, sanitized_uri)
+ lfs_object = LfsObject.new(oid: oid, size: size, file: file)
+
+ project.all_lfs_objects << lfs_object
+ end
+ rescue StandardError => e
+ Rails.logger.error("LFS file with oid #{oid} could't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}")
+ end
+
+ private
+
+ def download_and_save_file(file, sanitized_uri)
+ IO.copy_stream(open(sanitized_uri.sanitized_url, headers(sanitized_uri)), file)
+ end
+
+ def headers(sanitized_uri)
+ {}.tap do |headers|
+ credentials = sanitized_uri.credentials
+
+ if credentials[:user].present? || credentials[:password].present?
+ # Using authentication headers in the request
+ headers[:http_basic_authentication] = [credentials[:user], credentials[:password]]
+ end
+ end
+ end
+
+ def with_tmp_file(oid)
+ create_tmp_storage_dir
+
+ File.open(File.join(tmp_storage_dir, oid), 'w') { |file| yield file }
+ end
+
+ def create_tmp_storage_dir
+ FileUtils.makedirs(tmp_storage_dir) unless Dir.exist?(tmp_storage_dir)
+ end
+
+ def tmp_storage_dir
+ @tmp_storage_dir ||= File.join(storage_dir, 'tmp', 'download')
+ end
+
+ def storage_dir
+ @storage_dir ||= Gitlab.config.lfs.storage_path
+ end
+ end
+ end
+end
diff --git a/app/services/projects/lfs_pointers/lfs_import_service.rb b/app/services/projects/lfs_pointers/lfs_import_service.rb
new file mode 100644
index 00000000000..b6b0dec142f
--- /dev/null
+++ b/app/services/projects/lfs_pointers/lfs_import_service.rb
@@ -0,0 +1,92 @@
+# This service manages the whole worflow of discovering the Lfs files in a
+# repository, linking them to the project and downloading (and linking) the non
+# existent ones.
+module Projects
+ module LfsPointers
+ class LfsImportService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ HEAD_REV = 'HEAD'.freeze
+ LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze
+ LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'.freeze
+
+ LfsImportError = Class.new(StandardError)
+
+ def execute
+ return {} unless project&.lfs_enabled?
+
+ if external_lfs_endpoint?
+ # If the endpoint host is different from the import_url it means
+ # that the repo is using a third party service for storing the LFS files.
+ # In this case, we have to disable lfs in the project
+ disable_lfs!
+
+ return {}
+ end
+
+ get_download_links
+ rescue LfsDownloadLinkListService::DownloadLinksError => e
+ raise LfsImportError, "The LFS objects download list couldn't be imported. Error: #{e.message}"
+ end
+
+ private
+
+ def external_lfs_endpoint?
+ lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host
+ end
+
+ def disable_lfs!
+ project.update(lfs_enabled: false)
+ end
+
+ def get_download_links
+ existent_lfs = LfsListService.new(project).execute
+ linked_oids = LfsLinkService.new(project).execute(existent_lfs.keys)
+
+ # Retrieving those oids not linked and which we need to download
+ not_linked_lfs = existent_lfs.except(*linked_oids)
+
+ LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(not_linked_lfs)
+ end
+
+ def lfsconfig_endpoint_uri
+ strong_memoize(:lfsconfig_endpoint_uri) do
+ # Retrieveing the blob data from the .lfsconfig file
+ data = project.repository.lfsconfig_for(HEAD_REV)
+ # Parsing the data to retrieve the url
+ parsed_data = data&.match(LFS_ENDPOINT_PATTERN)
+
+ if parsed_data
+ URI.parse(parsed_data[1]).tap do |endpoint|
+ endpoint.user ||= import_uri.user
+ endpoint.password ||= import_uri.password
+ end
+ end
+ end
+ rescue URI::InvalidURIError
+ raise LfsImportError, 'Invalid URL in .lfsconfig file'
+ end
+
+ def import_uri
+ @import_uri ||= URI.parse(project.import_url)
+ rescue URI::InvalidURIError
+ raise LfsImportError, 'Invalid project import URL'
+ end
+
+ def current_endpoint_uri
+ (lfsconfig_endpoint_uri || default_endpoint_uri)
+ end
+
+ # The import url must end with '.git' here we ensure it is
+ def default_endpoint_uri
+ @default_endpoint_uri ||= begin
+ import_uri.dup.tap do |uri|
+ path = uri.path.gsub(%r(/$), '')
+ path += '.git' unless path.ends_with?('.git')
+ uri.path = path + LFS_BATCH_API_ENDPOINT
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb
new file mode 100644
index 00000000000..d20bdf86c58
--- /dev/null
+++ b/app/services/projects/lfs_pointers/lfs_link_service.rb
@@ -0,0 +1,29 @@
+# Given a list of oids, this services links the existent Lfs Objects to the project
+module Projects
+ module LfsPointers
+ class LfsLinkService < BaseService
+ # Accept an array of oids to link
+ #
+ # Returns a hash with the same structure with oids linked
+ def execute(oids)
+ return {} unless project&.lfs_enabled?
+
+ # Search and link existing LFS Object
+ link_existing_lfs_objects(oids)
+ end
+
+ private
+
+ def link_existing_lfs_objects(oids)
+ existent_lfs_objects = LfsObject.where(oid: oids)
+
+ return [] unless existent_lfs_objects.any?
+
+ not_linked_lfs_objects = existent_lfs_objects.where.not(id: project.all_lfs_objects)
+ project.all_lfs_objects << not_linked_lfs_objects
+
+ existent_lfs_objects.pluck(:oid)
+ end
+ end
+ end
+end
diff --git a/app/services/projects/lfs_pointers/lfs_list_service.rb b/app/services/projects/lfs_pointers/lfs_list_service.rb
new file mode 100644
index 00000000000..b770982cbc0
--- /dev/null
+++ b/app/services/projects/lfs_pointers/lfs_list_service.rb
@@ -0,0 +1,19 @@
+# This service list all existent Lfs objects in a repository
+module Projects
+ module LfsPointers
+ class LfsListService < BaseService
+ REV = 'HEAD'.freeze
+
+ # Retrieve all lfs blob pointers and returns a hash
+ # with the structure { lfs_file_oid => lfs_file_size }
+ def execute
+ return {} unless project&.lfs_enabled?
+
+ Gitlab::Git::LfsChanges.new(project.repository, REV)
+ .all_pointers
+ .map! { |blob| [blob.lfs_oid, blob.lfs_size] }
+ .to_h
+ end
+ end
+ end
+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/projects/update_service.rb b/app/services/projects/update_service.rb
index 679f4a9cb62..d8250cd8102 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -17,6 +17,11 @@ module Projects
ensure_wiki_exists if enabling_wiki?
+ yield if block_given?
+
+ # If the block added errors, don't try to save the project
+ return validation_failed! if project.errors.any?
+
if project.update_attributes(params.except(:default_branch))
if project.previous_changes.include?('path')
project.rename_repo
@@ -28,21 +33,25 @@ module Projects
success
else
- model_errors = project.errors.full_messages.to_sentence
- error_message = model_errors.presence || 'Project could not be updated!'
-
- error(error_message)
+ validation_failed!
end
end
def run_auto_devops_pipeline?
- return false if project.repository.gitlab_ci_yml || !project.auto_devops.previous_changes.include?('enabled')
+ return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled')
project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?)
end
private
+ def validation_failed!
+ model_errors = project.errors.full_messages.to_sentence
+ error_message = model_errors.presence || 'Project could not be updated!'
+
+ error(error_message)
+ end
+
def renaming_project_with_container_registry_tags?
new_path = params[:path]
@@ -53,8 +62,8 @@ module Projects
def changing_default_branch?
new_branch = params[:default_branch]
- project.repository.exists? &&
- new_branch && new_branch != project.default_branch
+ new_branch && project.repository.exists? &&
+ new_branch != project.default_branch
end
def enabling_wiki?
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 0215994b1a7..9ac8fdb4cff 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -561,6 +561,17 @@ module QuickActions
end
end
+ desc 'Make issue confidential.'
+ explanation do
+ 'Makes this issue confidential'
+ end
+ condition do
+ issuable.is_a?(Issue) && current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
+ end
+ command :confidential do
+ @updates[:confidential] = true
+ end
+
def extract_users(params)
return [] if params.nil?
diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb
index cc76d0df3a1..3cc88d77ba1 100644
--- a/app/services/tags/create_service.rb
+++ b/app/services/tags/create_service.rb
@@ -13,7 +13,7 @@ module Tags
new_tag = repository.add_tag(current_user, tag_name, target, message)
rescue Gitlab::Git::Repository::TagExistsError
return error("Tag #{tag_name} already exists")
- rescue Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue Gitlab::Git::PreReceiveError => ex
return error(ex.message)
end
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index d3d46064729..b84b061e460 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -21,7 +21,7 @@ module Tags
else
error('Failed to remove tag')
end
- rescue Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
diff --git a/app/services/test_hooks/project_service.rb b/app/services/test_hooks/project_service.rb
index 01d5d774cd5..65183e84cce 100644
--- a/app/services/test_hooks/project_service.rb
+++ b/app/services/test_hooks/project_service.rb
@@ -1,11 +1,13 @@
module TestHooks
class ProjectService < TestHooks::BaseService
- private
+ attr_writer :project
def project
@project ||= hook.project
end
+ private
+
def push_events_data
throw(:validation_error, 'Ensure the project has at least one commit.') if project.empty_repo?
diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb
index 7d1ed768ee8..643f2ce1481 100644
--- a/app/services/validate_new_branch_service.rb
+++ b/app/services/validate_new_branch_service.rb
@@ -13,7 +13,7 @@ class ValidateNewBranchService < BaseService
end
success
- rescue Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
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/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb
new file mode 100644
index 00000000000..3639375d474
--- /dev/null
+++ b/app/uploaders/favicon_uploader.rb
@@ -0,0 +1,13 @@
+class FaviconUploader < AttachmentUploader
+ EXTENSION_WHITELIST = %w[png ico].freeze
+
+ def extension_whitelist
+ EXTENSION_WHITELIST
+ end
+
+ private
+
+ def filename_for_different_format(filename, format)
+ filename.chomp(File.extname(filename)) + ".#{format}"
+ end
+end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 133fdf6684d..36bc0a4575a 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -65,10 +65,10 @@ class FileUploader < GitlabUploader
SecureRandom.hex
end
- def upload_paths(filename)
+ def upload_paths(identifier)
[
- File.join(secret, filename),
- File.join(base_dir(Store::REMOTE), secret, filename)
+ File.join(secret, identifier),
+ File.join(base_dir(Store::REMOTE), secret, identifier)
]
end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index 5bdca26a584..b8ecfc4ee2b 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -10,8 +10,17 @@ module ObjectStorage
UnknownStoreError = Class.new(StandardError)
ObjectStorageUnavailable = Class.new(StandardError)
- DIRECT_UPLOAD_TIMEOUT = 4.hours
- DIRECT_UPLOAD_EXPIRE_OFFSET = 15.minutes
+ class ExclusiveLeaseTaken < StandardError
+ def initialize(lease_key)
+ @lease_key = lease_key
+ end
+
+ def message
+ *lease_key_group, _ = *@lease_key.split(":")
+ "Exclusive lease for #{lease_key_group.join(':')} is already taken."
+ end
+ end
+
TMP_UPLOAD_PATH = 'tmp/uploads'.freeze
module Store
@@ -24,18 +33,18 @@ module ObjectStorage
module RecordsUploads
extend ActiveSupport::Concern
- def prepended(base)
+ prepended do |base|
raise "#{base} must include ObjectStorage::Concern to use extensions." unless base < Concern
- base.include(RecordsUploads::Concern)
+ base.include(::RecordsUploads::Concern)
end
def retrieve_from_store!(identifier)
- paths = store_dirs.map { |store, path| File.join(path, identifier) }
+ paths = upload_paths(identifier)
unless current_upload_satisfies?(paths, model)
# the upload we already have isn't right, find the correct one
- self.upload = uploads.find_by(model: model, path: paths)
+ self.upload = model&.retrieve_upload(identifier, paths)
end
super
@@ -48,7 +57,7 @@ module ObjectStorage
end
def upload=(upload)
- return unless upload
+ return if upload.nil?
self.object_store = upload.store
super
@@ -64,6 +73,15 @@ module ObjectStorage
upload.id)
end
+ def exclusive_lease_key
+ # For FileUploaders, model may have many uploaders. In that case
+ # we want to use exclusive key per upload, not per model to allow
+ # parallel migration
+ key_object = upload || model
+
+ "object_storage_migrate:#{key_object.class}:#{key_object.id}"
+ end
+
private
def current_upload_satisfies?(paths, model)
@@ -157,9 +175,9 @@ module ObjectStorage
model_class.uploader_options.dig(mount_point, :mount_on) || mount_point
end
- def workhorse_authorize
+ def workhorse_authorize(has_length:, maximum_size: nil)
{
- RemoteObject: workhorse_remote_upload_options,
+ RemoteObject: workhorse_remote_upload_options(has_length: has_length, maximum_size: maximum_size),
TempPath: workhorse_local_upload_path
}.compact
end
@@ -168,23 +186,16 @@ module ObjectStorage
File.join(self.root, TMP_UPLOAD_PATH)
end
- def workhorse_remote_upload_options
+ def workhorse_remote_upload_options(has_length:, maximum_size: nil)
return unless self.object_store_enabled?
return unless self.direct_upload_enabled?
id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-')
upload_path = File.join(TMP_UPLOAD_PATH, id)
- connection = ::Fog::Storage.new(self.object_store_credentials)
- expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT + DIRECT_UPLOAD_EXPIRE_OFFSET
- options = { 'Content-Type' => 'application/octet-stream' }
+ direct_upload = ObjectStorage::DirectUpload.new(self.object_store_credentials, remote_store_path, upload_path,
+ has_length: has_length, maximum_size: maximum_size)
- {
- ID: id,
- Timeout: DIRECT_UPLOAD_TIMEOUT,
- GetURL: connection.get_object_url(remote_store_path, upload_path, expire_at),
- DeleteURL: connection.delete_object_url(remote_store_path, upload_path, expire_at),
- StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
- }
+ direct_upload.to_hash.merge(ID: id)
end
end
@@ -270,7 +281,7 @@ module ObjectStorage
end
def delete_migrated_file(migrated_file)
- migrated_file.delete if exists?
+ migrated_file.delete
end
def exists?
@@ -288,6 +299,13 @@ module ObjectStorage
}
end
+ # Returns all the possible paths for an upload.
+ # the `upload.path` is a lookup parameter, and it may change
+ # depending on the `store` param.
+ def upload_paths(identifier)
+ store_dirs.map { |store, path| File.join(path, identifier) }
+ end
+
def cache!(new_file = sanitized_file)
# We intercept ::UploadedFile which might be stored on remote storage
# We use that for "accelerated" uploads, where we store result on remote storage
@@ -307,6 +325,10 @@ module ObjectStorage
super
end
+ def exclusive_lease_key
+ "object_storage_migrate:#{model.class}:#{model.id}"
+ end
+
private
def schedule_background_upload?
@@ -373,17 +395,14 @@ module ObjectStorage
end
end
- def exclusive_lease_key
- "object_storage_migrate:#{model.class}:#{model.id}"
- end
-
def with_exclusive_lease
- uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
- raise 'exclusive lease already taken' unless uuid
+ lease_key = exclusive_lease_key
+ uuid = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.hour.to_i).try_obtain
+ raise ExclusiveLeaseTaken.new(lease_key) unless uuid
yield uuid
ensure
- Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
#
diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb
index 89c74a78835..301f4681fcd 100644
--- a/app/uploaders/records_uploads.rb
+++ b/app/uploaders/records_uploads.rb
@@ -22,7 +22,7 @@ module RecordsUploads
Upload.transaction do
uploads.where(path: upload_path).delete_all
- upload.destroy! if upload
+ upload.delete if upload
self.upload = build_upload.tap(&:save!)
end
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index fd446d31092..207928b61d0 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -1,6 +1,6 @@
# Extra methods for uploader
module UploaderHelper
- IMAGE_EXT = %w[png jpg jpeg gif bmp tiff].freeze
+ IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze
# We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
index 8648c4c75e3..6854fec582e 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -18,6 +18,13 @@
# This validator can also block urls pointing to localhost or the local network to
# protect against Server-side Request Forgery (SSRF), or check for the right port.
#
+# The available options are:
+# - protocols: Allowed protocols. Default: http and https
+# - allow_localhost: Allow urls pointing to localhost. Default: true
+# - allow_local_network: Allow urls pointing to private network addresses. Default: true
+# - ports: Allowed ports. Default: all.
+# - enforce_user: Validate user format. Default: false
+#
# Example:
# class User < ActiveRecord::Base
# validates :personal_url, url: { allow_localhost: false, allow_local_network: false}
@@ -35,7 +42,7 @@ class UrlValidator < ActiveModel::EachValidator
if value.present?
value.strip!
else
- record.errors.add(attribute, "must be a valid URL")
+ record.errors.add(attribute, 'must be a valid URL')
end
Gitlab::UrlBlocker.validate!(value, blocker_args)
@@ -51,7 +58,8 @@ class UrlValidator < ActiveModel::EachValidator
protocols: DEFAULT_PROTOCOLS,
ports: [],
allow_localhost: true,
- allow_local_network: true
+ allow_local_network: true,
+ enforce_user: false
}
end
@@ -64,7 +72,7 @@ class UrlValidator < ActiveModel::EachValidator
end
def blocker_args
- current_options.slice(:allow_localhost, :allow_local_network, :protocols, :ports).tap do |args|
+ current_options.slice(*default_options.keys).tap do |args|
if allow_setting_local_requests?
args[:allow_localhost] = args[:allow_local_network] = true
end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 5e08837255f..a0861870ba4 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -11,13 +11,32 @@
= image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
%hr
= f.hidden_field :header_logo_cache
= f.file_field :header_logo, class: ""
.hint
Maximum file size is 1MB. Pages are optimized for a 28px tall header logo
+ %fieldset.app_logo
+ %legend
+ Favicon:
+ .form-group.row
+ = f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label'
+ .col-sm-10
+ - if @appearance.favicon?
+ = image_tag @appearance.favicon_url, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
+ %hr
+ = f.hidden_field :favicon_cache
+ = f.file_field :favicon, class: ''
+ .hint
+ Maximum file size is 1MB. Image size must be 32x32px. Allowed image formats are #{favicon_extension_whitelist}.
+ %br
+ Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior.
+
%fieldset.sign-in
%legend
Sign in/Sign up pages:
@@ -38,7 +57,7 @@
= image_tag @appearance.logo_url, class: 'appearance-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
+ = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
%hr
= f.hidden_field :logo_cache
= f.file_field :logo, class: ""
diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml
index 6b9b2a17dd9..91993838fc8 100644
--- a/app/views/admin/application_settings/_abuse.html.haml
+++ b/app/views/admin/application_settings/_abuse.html.haml
@@ -2,11 +2,10 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- = f.label :admin_notification_email, 'Abuse reports notification email', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :admin_notification_email, class: 'form-control'
- .form-text.text-muted
- Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
+ .form-group
+ = f.label :admin_notification_email, 'Abuse reports notification email', class: 'label-light'
+ = f.text_field :admin_notification_email, class: 'form-control'
+ .form-text.text-muted
+ Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index 07f9ea0865b..f40939747f4 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -2,38 +2,32 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :gravatar_enabled, class: 'form-check-input'
- = f.label :gravatar_enabled, class: 'form-check-label' do
- Gravatar enabled
- .form-group.row
- = f.label :default_projects_limit, class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :default_projects_limit, class: 'form-control'
- .form-group.row
- = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :max_attachment_size, class: 'form-control'
- .form-group.row
- = f.label :session_expire_delay, 'Session duration (minutes)', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :session_expire_delay, class: 'form-control'
- %span.form-text.text-muted#session_expire_delay_help_block GitLab restart is required to apply changes
- .form-group.row
- = f.label :user_oauth_applications, 'User OAuth applications', class: 'col-form-label col-sm-2'
- .col-sm-10
- .form-check
- = f.check_box :user_oauth_applications, class: 'form-check-input'
- = f.label :user_oauth_applications, class: 'form-check-label' do
- Allow users to register any application to use GitLab as an OAuth provider
- .form-group.row
- = f.label :user_default_external, 'New users set to external', class: 'col-form-label col-sm-2'
- .col-sm-10
- .form-check
- = f.check_box :user_default_external, class: 'form-check-input'
- = f.label :user_default_external, class: 'form-check-label' do
- Newly registered users will by default be external
+ .form-group
+ .form-check
+ = f.check_box :gravatar_enabled, class: 'form-check-input'
+ = f.label :gravatar_enabled, class: 'form-check-label' do
+ Gravatar enabled
+ .form-group
+ = f.label :default_projects_limit, class: 'label-light'
+ = f.number_field :default_projects_limit, class: 'form-control'
+ .form-group
+ = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'label-light'
+ = f.number_field :max_attachment_size, class: 'form-control'
+ .form-group
+ = f.label :session_expire_delay, 'Session duration (minutes)', class: 'label-light'
+ = f.number_field :session_expire_delay, class: 'form-control'
+ %span.form-text.text-muted#session_expire_delay_help_block GitLab restart is required to apply changes
+ .form-group
+ = f.label :user_oauth_applications, 'User OAuth applications', class: 'label-light'
+ .form-check
+ = f.check_box :user_oauth_applications, class: 'form-check-input'
+ = f.label :user_oauth_applications, class: 'form-check-label' do
+ Allow users to register any application to use GitLab as an OAuth provider
+ .form-group
+ = f.label :user_default_external, 'New users set to external', class: 'label-light'
+ .form-check
+ = f.check_box :user_default_external, class: 'form-check-input'
+ = f.label :user_default_external, class: 'form-check-label' do
+ Newly registered users will by default be external
= f.submit 'Save changes', class: 'btn btn-success'
diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml
index fc5df02242a..fd8e695ed49 100644
--- a/app/views/admin/application_settings/_background_jobs.html.haml
+++ b/app/views/admin/application_settings/_background_jobs.html.haml
@@ -6,25 +6,22 @@
These settings require a
= link_to 'restart', help_page_path('administration/restart_gitlab')
to take effect.
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :sidekiq_throttling_enabled, class: 'form-check-input'
- = f.label :sidekiq_throttling_enabled, class: 'form-check-label' do
- Enable Sidekiq Job Throttling
- .form-text.text-muted
- Limit the amount of resources slow running jobs are assigned.
- .form-group.row
- = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
+ .form-group
+ .form-check
+ = f.check_box :sidekiq_throttling_enabled, class: 'form-check-input'
+ = f.label :sidekiq_throttling_enabled, class: 'form-check-label' do
+ Enable Sidekiq Job Throttling
.form-text.text-muted
- Choose which queues you wish to throttle.
- .form-group.row
- = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
- .form-text.text-muted
- The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
+ Limit the amount of resources slow running jobs are assigned.
+ .form-group
+ = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'label-light'
+ = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
+ .form-text.text-muted
+ Choose which queues you wish to throttle.
+ .form-group
+ = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'label-light'
+ = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
+ .form-text.text-muted
+ The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 233821818e6..7c16cafe13f 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -2,46 +2,40 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :auto_devops_enabled, class: 'form-check-input'
- = f.label :auto_devops_enabled, class: 'form-check-label' do
- Enabled Auto DevOps for projects by default
- .form-text.text-muted
- It will automatically build, test, and deploy applications based on a predefined CI/CD configuration
- = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md')
- .form-group.row
- = f.label :auto_devops_domain, class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com'
- .form-text.text-muted
- = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.")
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :shared_runners_enabled, class: 'form-check-input'
- = f.label :shared_runners_enabled, class: 'form-check-label' do
- Enable shared runners for new projects
- .form-group.row
- = f.label :shared_runners_text, class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_area :shared_runners_text, class: 'form-control', rows: 4
- .form-text.text-muted Markdown enabled
- .form-group.row
- = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :max_artifacts_size, class: 'form-control'
- .form-text.text-muted
- Set the maximum file size for each job's artifacts
- = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
- .form-group.row
- = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :default_artifacts_expire_in, class: 'form-control'
- .form-text.text-muted
- Set the default expiration time for each job's artifacts.
- 0 for unlimited.
- = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
+ .form-group
+ .form-check
+ = f.check_box :auto_devops_enabled, class: 'form-check-input'
+ = f.label :auto_devops_enabled, class: 'form-check-label' do
+ Enabled Auto DevOps for projects by default
+ .form-text.text-muted
+ It will automatically build, test, and deploy applications based on a predefined CI/CD configuration
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md')
+ .form-group
+ = f.label :auto_devops_domain, class: 'label-light'
+ = f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com'
+ .form-text.text-muted
+ = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.")
+ .form-group
+ .form-check
+ = f.check_box :shared_runners_enabled, class: 'form-check-input'
+ = f.label :shared_runners_enabled, class: 'form-check-label' do
+ Enable shared runners for new projects
+ .form-group
+ = f.label :shared_runners_text, class: 'label-light'
+ = f.text_area :shared_runners_text, class: 'form-control', rows: 4
+ .form-text.text-muted Markdown enabled
+ .form-group
+ = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'label-light'
+ = f.number_field :max_artifacts_size, class: 'form-control'
+ .form-text.text-muted
+ Set the maximum file size for each job's artifacts
+ = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
+ .form-group
+ = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'label-light'
+ = f.text_field :default_artifacts_expire_in, class: 'form-control'
+ .form-text.text-muted
+ Set the default expiration time for each job's artifacts.
+ 0 for unlimited.
+ = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index 01be5878a60..99e44ffa741 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -2,25 +2,23 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :email_author_in_body, class: 'form-check-input'
- = f.label :email_author_in_body, class: 'form-check-label' do
- Include author name in notification email body
- .form-text.text-muted
- Some email servers do not support overriding the email sender name.
- Enable this option to include the name of the author of the issue,
- merge request or comment in the email body instead.
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :html_emails_enabled, class: 'form-check-input'
- = f.label :html_emails_enabled, class: 'form-check-label' do
- Enable HTML emails
- .form-text.text-muted
- By default GitLab sends emails in HTML and plain text formats so mail
- clients can choose what format to use. Disable this option if you only
- want to send emails in plain text format.
+ .form-group
+ .form-check
+ = f.check_box :email_author_in_body, class: 'form-check-input'
+ = f.label :email_author_in_body, class: 'form-check-label' do
+ Include author name in notification email body
+ .form-text.text-muted
+ Some email servers do not support overriding the email sender name.
+ Enable this option to include the name of the author of the issue,
+ merge request or comment in the email body instead.
+ .form-group
+ .form-check
+ = f.check_box :html_emails_enabled, class: 'form-check-input'
+ = f.label :html_emails_enabled, class: 'form-check-label' do
+ Enable HTML emails
+ .form-text.text-muted
+ By default GitLab sends emails in HTML and plain text formats so mail
+ clients can choose what format to use. Disable this option if you only
+ want to send emails in plain text format.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml
index 859a1c6f45c..0b4001c0824 100644
--- a/app/views/admin/application_settings/_gitaly.html.haml
+++ b/app/views/admin/application_settings/_gitaly.html.haml
@@ -2,26 +2,23 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :gitaly_timeout_default, class: 'form-control'
- .form-text.text-muted
- Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
- for git fetch/push operations or Sidekiq jobs.
- .form-group.row
- = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :gitaly_timeout_fast, class: 'form-control'
- .form-text.text-muted
- Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
- If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
- can help maintain the stability of the GitLab instance.
- .form-group.row
- = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :gitaly_timeout_medium, class: 'form-control'
- .form-text.text-muted
- Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
+ .form-group
+ = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'label-light'
+ = f.number_field :gitaly_timeout_default, class: 'form-control'
+ .form-text.text-muted
+ Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
+ for git fetch/push operations or Sidekiq jobs.
+ .form-group
+ = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'label-light'
+ = f.number_field :gitaly_timeout_fast, class: 'form-control'
+ .form-text.text-muted
+ Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
+ If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
+ can help maintain the stability of the GitLab instance.
+ .form-group
+ = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'label-light'
+ = f.number_field :gitaly_timeout_medium, class: 'form-control'
+ .form-text.text-muted
+ Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml
index 1f6c52d8b1a..1f402fcb786 100644
--- a/app/views/admin/application_settings/_help_page.html.haml
+++ b/app/views/admin/application_settings/_help_page.html.haml
@@ -2,21 +2,18 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- = f.label :help_page_text, class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_area :help_page_text, class: 'form-control', rows: 4
- .form-text.text-muted Markdown enabled
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :help_page_hide_commercial_content, class: 'form-check-input'
- = f.label :help_page_hide_commercial_content, class: 'form-check-label' do
- Hide marketing-related entries from help
- .form-group.row
- = f.label :help_page_support_url, 'Support page URL', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block'
- %span.form-text.text-muted#support_help_block Alternate support URL for help page
+ .form-group
+ = f.label :help_page_text, class: 'label-light'
+ = f.text_area :help_page_text, class: 'form-control', rows: 4
+ .form-text.text-muted Markdown enabled
+ .form-group
+ .form-check
+ = f.check_box :help_page_hide_commercial_content, class: 'form-check-input'
+ = f.label :help_page_hide_commercial_content, class: 'form-check-label' do
+ Hide marketing-related entries from help
+ .form-group
+ = f.label :help_page_support_url, 'Support page URL', class: 'label-light'
+ = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block'
+ %span.form-text.text-muted#support_help_block Alternate support URL for help page
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_influx.html.haml b/app/views/admin/application_settings/_influx.html.haml
index b40a714ed8f..61e8e3199a9 100644
--- a/app/views/admin/application_settings/_influx.html.haml
+++ b/app/views/admin/application_settings/_influx.html.haml
@@ -8,61 +8,53 @@
= link_to 'restart', help_page_path('administration/restart_gitlab')
to take effect.
= link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction')
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :metrics_enabled, class: 'form-check-input'
- = f.label :metrics_enabled, class: 'form-check-label' do
- Enable InfluxDB Metrics
- .form-group.row
- = f.label :metrics_host, 'InfluxDB host', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
- .form-group.row
- = f.label :metrics_port, 'InfluxDB port', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
- .form-text.text-muted
- The UDP port to use for connecting to InfluxDB. InfluxDB requires that
- your server configuration specifies a database to store data in when
- sending messages to this port, without it metrics data will not be
- saved.
- .form-group.row
- = f.label :metrics_pool_size, 'Connection pool size', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_pool_size, class: 'form-control'
- .form-text.text-muted
- The amount of InfluxDB connections to open. Connections are opened
- lazily. Users using multi-threaded application servers should ensure
- enough connections are available (at minimum the amount of application
- server threads).
- .form-group.row
- = f.label :metrics_timeout, 'Connection timeout', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_timeout, class: 'form-control'
- .form-text.text-muted
- The amount of seconds after which an InfluxDB connection will time
- out.
- .form-group.row
- = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_method_call_threshold, class: 'form-control'
- .form-text.text-muted
- A method call is only tracked when it takes longer to complete than
- the given amount of milliseconds.
- .form-group.row
- = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_sample_interval, class: 'form-control'
- .form-text.text-muted
- The sampling interval in seconds. Sampled data includes memory usage,
- retained Ruby objects, file descriptors and so on.
- .form-group.row
- = f.label :metrics_packet_size, 'Metrics per packet', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_packet_size, class: 'form-control'
- .form-text.text-muted
- The amount of points to store in a single UDP packet. More points
- results in fewer but larger UDP packets being sent.
+ .form-group
+ .form-check
+ = f.check_box :metrics_enabled, class: 'form-check-input'
+ = f.label :metrics_enabled, class: 'form-check-label' do
+ Enable InfluxDB Metrics
+ .form-group
+ = f.label :metrics_host, 'InfluxDB host', class: 'label-light'
+ = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
+ .form-group
+ = f.label :metrics_port, 'InfluxDB port', class: 'label-light'
+ = f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
+ .form-text.text-muted
+ The UDP port to use for connecting to InfluxDB. InfluxDB requires that
+ your server configuration specifies a database to store data in when
+ sending messages to this port, without it metrics data will not be
+ saved.
+ .form-group
+ = f.label :metrics_pool_size, 'Connection pool size', class: 'label-light'
+ = f.number_field :metrics_pool_size, class: 'form-control'
+ .form-text.text-muted
+ The amount of InfluxDB connections to open. Connections are opened
+ lazily. Users using multi-threaded application servers should ensure
+ enough connections are available (at minimum the amount of application
+ server threads).
+ .form-group
+ = f.label :metrics_timeout, 'Connection timeout', class: 'label-light'
+ = f.number_field :metrics_timeout, class: 'form-control'
+ .form-text.text-muted
+ The amount of seconds after which an InfluxDB connection will time
+ out.
+ .form-group
+ = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'label-light'
+ = f.number_field :metrics_method_call_threshold, class: 'form-control'
+ .form-text.text-muted
+ A method call is only tracked when it takes longer to complete than
+ the given amount of milliseconds.
+ .form-group
+ = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'label-light'
+ = f.number_field :metrics_sample_interval, class: 'form-control'
+ .form-text.text-muted
+ The sampling interval in seconds. Sampled data includes memory usage,
+ retained Ruby objects, file descriptors and so on.
+ .form-group
+ = f.label :metrics_packet_size, 'Metrics per packet', class: 'label-light'
+ = f.number_field :metrics_packet_size, class: 'form-control'
+ .form-text.text-muted
+ The amount of points to store in a single UDP packet. More points
+ results in fewer but larger UDP packets being sent.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml
index 320dd52ffc2..73d570a5fee 100644
--- a/app/views/admin/application_settings/_ip_limits.html.haml
+++ b/app/views/admin/application_settings/_ip_limits.html.haml
@@ -2,53 +2,44 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :throttle_unauthenticated_enabled, class: 'form-check-input'
- = f.label :throttle_unauthenticated_enabled, class: 'form-check-label' do
- Enable unauthenticated request rate limit
- %span.form-text.text-muted
- Helps reduce request volume (e.g. from crawlers or abusive bots)
- .form-group.row
- = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
- .form-group.row
- = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :throttle_authenticated_api_enabled, class: 'form-check-input'
- = f.label :throttle_authenticated_api_enabled, class: 'form-check-label' do
- Enable authenticated API request rate limit
- %span.form-text.text-muted
- Helps reduce request volume (e.g. from crawlers or abusive bots)
- .form-group.row
- = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
- .form-group.row
- = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :throttle_authenticated_web_enabled, class: 'form-check-input'
- = f.label :throttle_authenticated_web_enabled, class: 'form-check-label' do
- Enable authenticated web request rate limit
- %span.form-text.text-muted
- Helps reduce request volume (e.g. from crawlers or abusive bots)
- .form-group.row
- = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
- .form-group.row
- = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
+ .form-group
+ .form-check
+ = f.check_box :throttle_unauthenticated_enabled, class: 'form-check-input'
+ = f.label :throttle_unauthenticated_enabled, class: 'form-check-label' do
+ Enable unauthenticated request rate limit
+ %span.form-text.text-muted
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'label-light'
+ = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'label-light'
+ = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
+ .form-group
+ .form-check
+ = f.check_box :throttle_authenticated_api_enabled, class: 'form-check-input'
+ = f.label :throttle_authenticated_api_enabled, class: 'form-check-label' do
+ Enable authenticated API request rate limit
+ %span.form-text.text-muted
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'label-light'
+ = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'label-light'
+ = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
+ .form-group
+ .form-check
+ = f.check_box :throttle_authenticated_web_enabled, class: 'form-check-input'
+ = f.label :throttle_authenticated_web_enabled, class: 'form-check-label' do
+ Enable authenticated web request rate limit
+ %span.form-text.text-muted
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'label-light'
+ = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'label-light'
+ = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml
index 341c7641fcc..ae60f68f5fe 100644
--- a/app/views/admin/application_settings/_koding.html.haml
+++ b/app/views/admin/application_settings/_koding.html.haml
@@ -2,23 +2,21 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :koding_enabled, class: 'form-check-input'
- = f.label :koding_enabled, class: 'form-check-label' do
- Enable Koding
- .form-text.text-muted
- Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
- .form-group.row
- = f.label :koding_url, 'Koding URL', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
- .form-text.text-muted
- Koding has integration enabled out of the box for the
- %strong gitlab
- team, and you need to provide that team's URL here. Learn more in the
- = succeed "." do
- = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+ .form-group
+ .form-check
+ = f.check_box :koding_enabled, class: 'form-check-input'
+ = f.label :koding_enabled, class: 'form-check-label' do
+ Enable Koding
+ .form-text.text-muted
+ Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
+ .form-group
+ = f.label :koding_url, 'Koding URL', class: 'label-light'
+ = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
+ .form-text.text-muted
+ Koding has integration enabled out of the box for the
+ %strong gitlab
+ team, and you need to provide that team's URL here. Learn more in the
+ = succeed "." do
+ = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml
index f5c1e126c70..a6e549cd1f0 100644
--- a/app/views/admin/application_settings/_logging.html.haml
+++ b/app/views/admin/application_settings/_logging.html.haml
@@ -2,35 +2,31 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :sentry_enabled, class: 'form-check-input'
- = f.label :sentry_enabled, class: 'form-check-label' do
- Enable Sentry
- .form-text.text-muted
- %p This setting requires a restart to take effect.
- Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
- %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
+ .form-group
+ .form-check
+ = f.check_box :sentry_enabled, class: 'form-check-input'
+ = f.label :sentry_enabled, class: 'form-check-label' do
+ Enable Sentry
+ .form-text.text-muted
+ %p This setting requires a restart to take effect.
+ Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
+ %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
- .form-group.row
- = f.label :sentry_dsn, 'Sentry DSN', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :sentry_dsn, class: 'form-control'
+ .form-group
+ = f.label :sentry_dsn, 'Sentry DSN', class: 'label-light'
+ = f.text_field :sentry_dsn, class: 'form-control'
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :clientside_sentry_enabled, class: 'form-check-input'
- = f.label :clientside_sentry_enabled, class: 'form-check-label' do
- Enable Clientside Sentry
- .form-text.text-muted
- Sentry can also be used for reporting and logging clientside exceptions.
- %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/
+ .form-group
+ .form-check
+ = f.check_box :clientside_sentry_enabled, class: 'form-check-input'
+ = f.label :clientside_sentry_enabled, class: 'form-check-label' do
+ Enable Clientside Sentry
+ .form-text.text-muted
+ Sentry can also be used for reporting and logging clientside exceptions.
+ %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/
- .form-group.row
- = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :clientside_sentry_dsn, class: 'form-control'
+ .form-group
+ = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'label-light'
+ = f.text_field :clientside_sentry_dsn, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index 5dadb7b814b..e046242bee0 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -2,11 +2,10 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :allow_local_requests_from_hooks_and_services, class: 'form-check-input'
- = f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do
- Allow requests to the local network from hooks and services
+ .form-group
+ .form-check
+ = f.check_box :allow_local_requests_from_hooks_and_services, class: 'form-check-input'
+ = f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do
+ Allow requests to the local network from hooks and services
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index f1889c3105f..f168ec62ffd 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -2,21 +2,19 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :max_pages_size, class: 'form-control'
- .form-text.text-muted 0 for unlimited
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :pages_domain_verification_enabled, class: 'form-check-input'
- = f.label :pages_domain_verification_enabled, class: 'form-check-label' do
- Require users to prove ownership of custom domains
- .form-text.text-muted
- Domain verification is an essential security measure for public GitLab
- sites. Users are required to demonstrate they control a domain before
- it is enabled
- = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
+ .form-group
+ = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'label-light'
+ = f.number_field :max_pages_size, class: 'form-control'
+ .form-text.text-muted 0 for unlimited
+ .form-group
+ .form-check
+ = f.check_box :pages_domain_verification_enabled, class: 'form-check-input'
+ = f.label :pages_domain_verification_enabled, class: 'form-check-label' do
+ Require users to prove ownership of custom domains
+ .form-text.text-muted
+ Domain verification is an essential security measure for public GitLab
+ sites. Users are required to demonstrate they control a domain before
+ it is enabled
+ = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index 57c22ce563f..ffa25af77ed 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -2,18 +2,17 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :authorized_keys_enabled, class: 'form-check-input'
- = f.label :authorized_keys_enabled, class: 'form-check-label' do
- Write to "authorized_keys" file
- .form-text.text-muted
- By default, we write to the "authorized_keys" file to support Git
- over SSH without additional configuration. GitLab can be optimized
- to authenticate SSH keys via the database file. Only uncheck this
- if you have configured your OpenSSH server to use the
- AuthorizedKeysCommand. Click on the help icon for more details.
- = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
+ .form-group
+ .form-check
+ = f.check_box :authorized_keys_enabled, class: 'form-check-input'
+ = f.label :authorized_keys_enabled, class: 'form-check-label' do
+ Write to "authorized_keys" file
+ .form-text.text-muted
+ By default, we write to the "authorized_keys" file to support Git
+ over SSH without additional configuration. GitLab can be optimized
+ to authenticate SSH keys via the database file. Only uncheck this
+ if you have configured your OpenSSH server to use the
+ AuthorizedKeysCommand. Click on the help icon for more details.
+ = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
index ed4de2234f7..ddbfcc6b77b 100644
--- a/app/views/admin/application_settings/_performance_bar.html.haml
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -2,15 +2,13 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :performance_bar_enabled, class: 'form-check-input'
- = f.label :performance_bar_enabled, class: 'form-check-label' do
- Enable the Performance Bar
- .form-group.row
- = f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :performance_bar_allowed_group_path, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
+ .form-group
+ .form-check
+ = f.check_box :performance_bar_enabled, class: 'form-check-input'
+ = f.label :performance_bar_enabled, class: 'form-check-label' do
+ Enable the Performance Bar
+ .form-group
+ = f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'label-light'
+ = f.text_field :performance_bar_allowed_group_path, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
index e0dc058762e..259f18b3b96 100644
--- a/app/views/admin/application_settings/_plantuml.html.haml
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -2,19 +2,17 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :plantuml_enabled, class: 'form-check-input'
- = f.label :plantuml_enabled, class: 'form-check-label' do
- Enable PlantUML
- .form-group.row
- = f.label :plantuml_url, 'PlantUML URL', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
- .form-text.text-muted
- Allow rendering of
- = link_to "PlantUML", "http://plantuml.com"
- diagrams in Asciidoc documents using an external PlantUML service.
+ .form-group
+ .form-check
+ = f.check_box :plantuml_enabled, class: 'form-check-input'
+ = f.label :plantuml_enabled, class: 'form-check-label' do
+ Enable PlantUML
+ .form-group
+ = f.label :plantuml_url, 'PlantUML URL', class: 'label-light'
+ = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
+ .form-text.text-muted
+ Allow rendering of
+ = link_to "PlantUML", "http://plantuml.com"
+ diagrams in Asciidoc documents using an external PlantUML service.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml
index d3c3656e96a..ad92b18b2c9 100644
--- a/app/views/admin/application_settings/_prometheus.html.haml
+++ b/app/views/admin/application_settings/_prometheus.html.haml
@@ -11,18 +11,17 @@
= link_to 'restart', help_page_path('administration/restart_gitlab')
to take effect.
= link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/index')
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :prometheus_metrics_enabled, class: 'form-check-input'
- = f.label :prometheus_metrics_enabled, class: 'form-check-label' do
- Enable Prometheus Metrics
- - unless Gitlab::Metrics.metrics_folder_present?
- .form-text.text-muted
- %strong.cred WARNING:
- Environment variable
- %code prometheus_multiproc_dir
- does not exist or is not pointing to a valid directory.
- = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory')
+ .form-group
+ .form-check
+ = f.check_box :prometheus_metrics_enabled, class: 'form-check-input'
+ = f.label :prometheus_metrics_enabled, class: 'form-check-label' do
+ Enable Prometheus Metrics
+ - unless Gitlab::Metrics.metrics_folder_present?
+ .form-text.text-muted
+ %strong.cred WARNING:
+ Environment variable
+ %code prometheus_multiproc_dir
+ does not exist or is not pointing to a valid directory.
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml
index 63a592cc2fd..120cf4909b2 100644
--- a/app/views/admin/application_settings/_realtime.html.haml
+++ b/app/views/admin/application_settings/_realtime.html.haml
@@ -2,18 +2,17 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :polling_interval_multiplier, class: 'form-control'
- .form-text.text-muted
- Change this value to influence how frequently the GitLab UI polls for updates.
- If you set the value to 2 all polling intervals are multiplied
- by 2, which means that polling happens half as frequently.
- The multiplier can also have a decimal value.
- The default value (1) is a reasonable choice for the majority of GitLab
- installations. Set to 0 to completely disable polling.
- = link_to icon('question-circle'), help_page_path('administration/polling')
+ .form-group
+ = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'label-light'
+ = f.text_field :polling_interval_multiplier, class: 'form-control'
+ .form-text.text-muted
+ Change this value to influence how frequently the GitLab UI polls for updates.
+ If you set the value to 2 all polling intervals are multiplied
+ by 2, which means that polling happens half as frequently.
+ The multiplier can also have a decimal value.
+ The default value (1) is a reasonable choice for the majority of GitLab
+ installations. Set to 0 to completely disable polling.
+ = link_to icon('question-circle'), help_page_path('administration/polling')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml
index 8524cbfe4d9..beac70482e5 100644
--- a/app/views/admin/application_settings/_registry.html.haml
+++ b/app/views/admin/application_settings/_registry.html.haml
@@ -2,9 +2,8 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :container_registry_token_expire_delay, class: 'form-control'
+ .form-group
+ = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'label-light'
+ = f.number_field :container_registry_token_expire_delay, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
index 1311f17ecda..57facc380eb 100644
--- a/app/views/admin/application_settings/_repository_check.html.haml
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -4,59 +4,53 @@
%fieldset
.sub-section
%h4 Repository checks
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :repository_checks_enabled, class: 'form-check-input'
- = f.label :repository_checks_enabled, class: 'form-check-label' do
- Enable Repository Checks
- .form-text.text-muted
- GitLab will periodically run
- %a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck'
- in all project and wiki repositories to look for silent disk corruption issues.
- .form-group.row
- .offset-sm-2.col-sm-10
- = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
+ .form-group
+ .form-check
+ = f.check_box :repository_checks_enabled, class: 'form-check-input'
+ = f.label :repository_checks_enabled, class: 'form-check-label' do
+ Enable Repository Checks
.form-text.text-muted
- If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
+ GitLab will periodically run
+ %a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck'
+ in all project and wiki repositories to look for silent disk corruption issues.
+ .form-group
+ = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
+ .form-text.text-muted
+ If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
.sub-section
%h4 Housekeeping
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :housekeeping_enabled, class: 'form-check-input'
- = f.label :housekeeping_enabled, class: 'form-check-label' do
- Enable automatic repository housekeeping (git repack, git gc)
- .form-text.text-muted
- If you keep automatic housekeeping disabled for a long time Git
- repository access on your GitLab server will become slower and your
- repositories will use more disk space. We recommend to always leave
- this enabled.
- .form-check
- = f.check_box :housekeeping_bitmaps_enabled, class: 'form-check-input'
- = f.label :housekeeping_bitmaps_enabled, class: 'form-check-label' do
- Enable Git pack file bitmap creation
- .form-text.text-muted
- Creating pack file bitmaps makes housekeeping take a little longer but
- bitmaps should accelerate 'git clone' performance.
- .form-group.row
- = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :housekeeping_incremental_repack_period, class: 'form-control'
+ .form-group
+ .form-check
+ = f.check_box :housekeeping_enabled, class: 'form-check-input'
+ = f.label :housekeeping_enabled, class: 'form-check-label' do
+ Enable automatic repository housekeeping (git repack, git gc)
.form-text.text-muted
- Number of Git pushes after which an incremental 'git repack' is run.
- .form-group.row
- = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :housekeeping_full_repack_period, class: 'form-control'
+ If you keep automatic housekeeping disabled for a long time Git
+ repository access on your GitLab server will become slower and your
+ repositories will use more disk space. We recommend to always leave
+ this enabled.
+ .form-check
+ = f.check_box :housekeeping_bitmaps_enabled, class: 'form-check-input'
+ = f.label :housekeeping_bitmaps_enabled, class: 'form-check-label' do
+ Enable Git pack file bitmap creation
.form-text.text-muted
- Number of Git pushes after which a full 'git repack' is run.
- .form-group.row
- = f.label :housekeeping_gc_period, 'Git GC period', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :housekeeping_gc_period, class: 'form-control'
- .form-text.text-muted
- Number of Git pushes after which 'git gc' is run.
+ Creating pack file bitmaps makes housekeeping take a little longer but
+ bitmaps should accelerate 'git clone' performance.
+ .form-group
+ = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'label-light'
+ = f.number_field :housekeeping_incremental_repack_period, class: 'form-control'
+ .form-text.text-muted
+ Number of Git pushes after which an incremental 'git repack' is run.
+ .form-group
+ = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'label-light'
+ = f.number_field :housekeeping_full_repack_period, class: 'form-control'
+ .form-text.text-muted
+ Number of Git pushes after which a full 'git repack' is run.
+ .form-group
+ = f.label :housekeeping_gc_period, 'Git GC period', class: 'label-light'
+ = f.number_field :housekeeping_gc_period, class: 'form-control'
+ .form-text.text-muted
+ Number of Git pushes after which 'git gc' is run.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
index 187c6c28bb1..beeb5169361 100644
--- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml
+++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
@@ -2,15 +2,14 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-4'
- .col-sm-8
- .form-check
- = f.check_box :mirror_available, class: 'form-check-input'
- = f.label :mirror_available, class: 'form-check-label' do
- Allow mirrors to be setup for projects
- %span.form-text.text-muted
- If disabled, only admins will be able to setup mirrors in projects.
- = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
+ .form-group
+ = f.label :mirror_available, 'Enable mirror configuration', class: 'label-light'
+ .form-check
+ = f.check_box :mirror_available, class: 'form-check-input'
+ = f.label :mirror_available, class: 'form-check-label' do
+ Allow mirrors to be setup for projects
+ %span.form-text.text-muted
+ If disabled, only admins will be able to setup mirrors in projects.
+ = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index 89d2c114b22..5a303666353 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -3,56 +3,49 @@
%fieldset
.sub-section
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :hashed_storage_enabled, class: 'form-check-input'
- = f.label :hashed_storage_enabled, class: 'form-check-label' do
- Create new projects using hashed storage paths
- .form-text.text-muted
- Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
- repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
- %em (EXPERIMENTAL)
- .form-group.row
- = f.label :repository_storages, 'Storage paths for new projects', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
- {include_hidden: false}, multiple: true, class: 'form-control'
+ .form-group
+ .form-check
+ = f.check_box :hashed_storage_enabled, class: 'form-check-input'
+ = f.label :hashed_storage_enabled, class: 'form-check-label' do
+ Create new projects using hashed storage paths
.form-text.text-muted
- Manage repository storage paths. Learn more in the
- = succeed "." do
- = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths")
+ Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
+ repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
+ %em (EXPERIMENTAL)
+ .form-group
+ = f.label :repository_storages, 'Storage paths for new projects', class: 'label-light'
+ = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
+ {include_hidden: false}, multiple: true, class: 'form-control'
+ .form-text.text-muted
+ Manage repository storage paths. Learn more in the
+ = succeed "." do
+ = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths")
.sub-section
%h4 Circuit breaker
- .form-group.row
- = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_check_interval, class: 'form-control'
- .form-text.text-muted
- = circuitbreaker_check_interval_help_text
- .form-group.row
- = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_access_retries, class: 'form-control'
- .form-text.text-muted
- = circuitbreaker_access_retries_help_text
- .form-group.row
- = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
- .form-text.text-muted
- = circuitbreaker_storage_timeout_help_text
- .form-group.row
- = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
- .form-text.text-muted
- = circuitbreaker_failure_count_help_text
- .form-group.row
- = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
- .form-text.text-muted
- = circuitbreaker_failure_reset_time_help_text
+ .form-group
+ = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'label-light'
+ = f.number_field :circuitbreaker_check_interval, class: 'form-control'
+ .form-text.text-muted
+ = circuitbreaker_check_interval_help_text
+ .form-group
+ = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'label-light'
+ = f.number_field :circuitbreaker_access_retries, class: 'form-control'
+ .form-text.text-muted
+ = circuitbreaker_access_retries_help_text
+ .form-group
+ = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'label-light'
+ = f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
+ .form-text.text-muted
+ = circuitbreaker_storage_timeout_help_text
+ .form-group
+ = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'label-light'
+ = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
+ .form-text.text-muted
+ = circuitbreaker_failure_count_help_text
+ .form-group
+ = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'label-light'
+ = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
+ .form-text.text-muted
+ = circuitbreaker_failure_reset_time_help_text
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 2ba26158162..69d1a43c511 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -2,59 +2,51 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :password_authentication_enabled_for_web, class: 'form-check-input'
- = f.label :password_authentication_enabled_for_web, class: 'form-check-label' do
- Password authentication enabled for web interface
- .form-text.text-muted
- When disabled, an external authentication provider must be used.
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :password_authentication_enabled_for_git, class: 'form-check-input'
- = f.label :password_authentication_enabled_for_git, class: 'form-check-label' do
- Password authentication enabled for Git over HTTP(S)
- .form-text.text-muted
- When disabled, a Personal Access Token
- - if Gitlab::Auth::LDAP::Config.enabled?
- or LDAP password
- must be used to authenticate.
+ .form-group
+ .form-check
+ = f.check_box :password_authentication_enabled_for_web, class: 'form-check-input'
+ = f.label :password_authentication_enabled_for_web, class: 'form-check-label' do
+ Password authentication enabled for web interface
+ .form-text.text-muted
+ When disabled, an external authentication provider must be used.
+ .form-group
+ .form-check
+ = f.check_box :password_authentication_enabled_for_git, class: 'form-check-input'
+ = f.label :password_authentication_enabled_for_git, class: 'form-check-label' do
+ Password authentication enabled for Git over HTTP(S)
+ .form-text.text-muted
+ When disabled, a Personal Access Token
+ - if Gitlab::Auth::LDAP::Config.enabled?
+ or LDAP password
+ must be used to authenticate.
- if omniauth_enabled? && button_based_providers.any?
- .form-group.row
- = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'col-form-label col-sm-2'
+ .form-group
+ = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'label-light'
= hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]'
- .col-sm-10
- .btn-group{ data: { toggle: 'buttons' } }
- - oauth_providers_checkboxes.each do |source|
- = source
- .form-group.row
- = f.label :two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2'
- .col-sm-10
- .form-check
- = f.check_box :require_two_factor_authentication, class: 'form-check-input'
- = f.label :require_two_factor_authentication, class: 'form-check-label' do
- Require all users to setup Two-factor authentication
- .form-group.row
- = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
- .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
- .form-group.row
- = f.label :home_page_url, 'Home page URL', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
- %span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page
- .form-group.row
- = f.label :after_sign_out_path, class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block'
- %span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out
- .form-group.row
- = f.label :sign_in_text, class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_area :sign_in_text, class: 'form-control', rows: 4
- .form-text.text-muted Markdown enabled
+ .btn-group{ data: { toggle: 'buttons' } }
+ - oauth_providers_checkboxes.each do |source|
+ = source
+ .form-group
+ = f.label :two_factor_authentication, 'Two-factor authentication', class: 'label-light'
+ .form-check
+ = f.check_box :require_two_factor_authentication, class: 'form-check-input'
+ = f.label :require_two_factor_authentication, class: 'form-check-label' do
+ Require all users to setup Two-factor authentication
+ .form-group
+ = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'label-light'
+ = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
+ .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
+ .form-group
+ = f.label :home_page_url, 'Home page URL', class: 'label-light'
+ = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
+ %span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page
+ .form-group
+ = f.label :after_sign_out_path, class: 'label-light'
+ = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block'
+ %span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out
+ .form-group
+ = f.label :sign_in_text, class: 'label-light'
+ = f.text_area :sign_in_text, class: 'form-control', rows: 4
+ .form-text.text-muted Markdown enabled
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml
index 279f96389e9..b9ba9128cc9 100644
--- a/app/views/admin/application_settings/_signup.html.haml
+++ b/app/views/admin/application_settings/_signup.html.haml
@@ -2,57 +2,49 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :signup_enabled, class: 'form-check-input'
- = f.label :signup_enabled, class: 'form-check-label' do
- Sign-up enabled
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :send_user_confirmation_email, class: 'form-check-input'
- = f.label :send_user_confirmation_email, class: 'form-check-label' do
- Send confirmation email on sign-up
- .form-group.row
- = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
- .form-text.text-muted ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
- .form-group.row
- = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'col-form-label col-sm-2'
- .col-sm-10
- .form-check
- = f.check_box :domain_blacklist_enabled, class: 'form-check-input'
- = f.label :domain_blacklist_enabled, class: 'form-check-label' do
- Enable domain blacklist for sign ups
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = radio_button_tag :blacklist_type, :file, class: 'form-check-input'
- = label_tag :blacklist_type_file, class: 'form-check-label' do
- .option-title
- Upload blacklist file
- .form-check
- = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?, class: 'form-check-input'
- = label_tag :blacklist_type_raw, class: 'form-check-label' do
- .option-title
- Enter blacklist manually
- .form-group.row.blacklist-file
- = f.label :domain_blacklist_file, 'Blacklist file', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf'
- .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
- .form-group.row.blacklist-raw
- = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
- .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
+ .form-group
+ .form-check
+ = f.check_box :signup_enabled, class: 'form-check-input'
+ = f.label :signup_enabled, class: 'form-check-label' do
+ Sign-up enabled
+ .form-group
+ .form-check
+ = f.check_box :send_user_confirmation_email, class: 'form-check-input'
+ = f.label :send_user_confirmation_email, class: 'form-check-label' do
+ Send confirmation email on sign-up
+ .form-group
+ = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'label-light'
+ = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
+ .form-text.text-muted ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
+ .form-group
+ = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'label-light'
+ .form-check
+ = f.check_box :domain_blacklist_enabled, class: 'form-check-input'
+ = f.label :domain_blacklist_enabled, class: 'form-check-label' do
+ Enable domain blacklist for sign ups
+ .form-group
+ .form-check
+ = radio_button_tag :blacklist_type, :file, false, class: 'form-check-input'
+ = label_tag :blacklist_type_file, class: 'form-check-label' do
+ .option-title
+ Upload blacklist file
+ .form-check
+ = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?, class: 'form-check-input'
+ = label_tag :blacklist_type_raw, class: 'form-check-label' do
+ .option-title
+ Enter blacklist manually
+ .form-group.blacklist-file
+ = f.label :domain_blacklist_file, 'Blacklist file', class: 'label-light'
+ = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf'
+ .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
+ .form-group.blacklist-raw
+ = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'label-light'
+ = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
+ .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
- .form-group.row
- = f.label :after_sign_up_text, class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_area :after_sign_up_text, class: 'form-control', rows: 4
- .form-text.text-muted Markdown enabled
+ .form-group
+ = f.label :after_sign_up_text, class: 'label-light'
+ = f.text_area :after_sign_up_text, class: 'form-control', rows: 4
+ .form-text.text-muted Markdown enabled
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
index fb38e4ae922..8f0dce962a9 100644
--- a/app/views/admin/application_settings/_spam.html.haml
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -2,64 +2,56 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :recaptcha_enabled, class: 'form-check-input'
- = f.label :recaptcha_enabled, class: 'form-check-label' do
- Enable reCAPTCHA
- %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts
-
- .form-group.row
- = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :recaptcha_site_key, class: 'form-control'
- .form-text.text-muted
- Generate site and private keys at
- %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
-
- .form-group.row
- = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :recaptcha_private_key, class: 'form-control'
-
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :akismet_enabled, class: 'form-check-input'
- = f.label :akismet_enabled, class: 'form-check-label' do
- Enable Akismet
- %span.form-text.text-muted#akismet_help_block Helps prevent bots from creating issues
-
- .form-group.row
- = f.label :akismet_api_key, 'Akismet API Key', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.text_field :akismet_api_key, class: 'form-control'
- .form-text.text-muted
- Generate API key at
- %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
-
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :unique_ips_limit_enabled, class: 'form-check-input'
- = f.label :unique_ips_limit_enabled, class: 'form-check-label' do
- Limit sign in from multiple ips
- %span.form-text.text-muted#unique_ip_help_block
- Helps prevent malicious users hide their activity
-
- .form-group.row
- = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :unique_ips_limit_per_user, class: 'form-control'
- .form-text.text-muted
- Maximum number of unique IPs per user
-
- .form-group.row
- = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :unique_ips_limit_time_window, class: 'form-control'
- .form-text.text-muted
- How many seconds an IP will be counted towards the limit
+ .form-group
+ .form-check
+ = f.check_box :recaptcha_enabled, class: 'form-check-input'
+ = f.label :recaptcha_enabled, class: 'form-check-label' do
+ Enable reCAPTCHA
+ %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts
+
+ .form-group
+ = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-light'
+ = f.text_field :recaptcha_site_key, class: 'form-control'
+ .form-text.text-muted
+ Generate site and private keys at
+ %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
+
+ .form-group
+ = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'label-light'
+ = f.text_field :recaptcha_private_key, class: 'form-control'
+
+ .form-group
+ .form-check
+ = f.check_box :akismet_enabled, class: 'form-check-input'
+ = f.label :akismet_enabled, class: 'form-check-label' do
+ Enable Akismet
+ %span.form-text.text-muted#akismet_help_block Helps prevent bots from creating issues
+
+ .form-group
+ = f.label :akismet_api_key, 'Akismet API Key', class: 'label-light'
+ = f.text_field :akismet_api_key, class: 'form-control'
+ .form-text.text-muted
+ Generate API key at
+ %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
+
+ .form-group
+ .form-check
+ = f.check_box :unique_ips_limit_enabled, class: 'form-check-input'
+ = f.label :unique_ips_limit_enabled, class: 'form-check-label' do
+ Limit sign in from multiple ips
+ %span.form-text.text-muted#unique_ip_help_block
+ Helps prevent malicious users hide their activity
+
+ .form-group
+ = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'label-light'
+ = f.number_field :unique_ips_limit_per_user, class: 'form-control'
+ .form-text.text-muted
+ Maximum number of unique IPs per user
+
+ .form-group
+ = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'label-light'
+ = f.number_field :unique_ips_limit_time_window, class: 'form-control'
+ .form-text.text-muted
+ How many seconds an IP will be counted towards the limit
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml
index ae02d07e556..543628ff0ee 100644
--- a/app/views/admin/application_settings/_terminal.html.haml
+++ b/app/views/admin/application_settings/_terminal.html.haml
@@ -2,12 +2,11 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- = f.label :terminal_max_session_time, 'Max session time', class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.number_field :terminal_max_session_time, class: 'form-control'
- .form-text.text-muted
- Maximum time for web terminal websocket connection (in seconds).
- 0 for unlimited.
+ .form-group
+ = f.label :terminal_max_session_time, 'Max session time', class: 'label-light'
+ = f.number_field :terminal_max_session_time, class: 'form-control'
+ .form-text.text-muted
+ Maximum time for web terminal websocket connection (in seconds).
+ 0 for unlimited.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml
index 257565ce193..d3dc8659d1b 100644
--- a/app/views/admin/application_settings/_terms.html.haml
+++ b/app/views/admin/application_settings/_terms.html.haml
@@ -1,22 +1,19 @@
-= form_for @application_setting, url: admin_application_settings_path do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .col-sm-12
- .form-check
- = f.check_box :enforce_terms, class: 'form-check-input'
- = f.label :enforce_terms, class: 'form-check-label' do
- = _("Require all users to accept Terms of Service when they access GitLab.")
- .form-text.text-muted
- = _("When enabled, users cannot use GitLab until the terms have been accepted.")
- .form-group.row
- .col-sm-12
- = f.label :terms do
- = _("Terms of Service Agreement")
- .col-sm-12
- = f.text_area :terms, class: 'form-control', rows: 8
+ .form-group
+ .form-check
+ = f.check_box :enforce_terms, class: 'form-check-input'
+ = f.label :enforce_terms, class: 'form-check-label' do
+ = _("Require all users to accept Terms of Service and Privacy Policy when they access GitLab.")
.form-text.text-muted
- = _("Markdown enabled")
+ = _("When enabled, users cannot use GitLab until the terms have been accepted.")
+ .form-group
+ = f.label :terms do
+ = _("Terms of Service Agreement and Privacy Policy")
+ = f.text_area :terms, class: 'form-control', rows: 8
+ .form-text.text-muted
+ = _("Markdown enabled")
= f.submit _("Save changes"), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index c110fd4d60d..49a3ee33a85 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -2,36 +2,34 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- = f.check_box :version_check_enabled, class: 'form-check-input'
- = f.label :version_check_enabled, class: 'form-check-label' do
- Enable version check
- .form-text.text-muted
- GitLab will inform you if a new version is available.
- = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
- about what information is shared with GitLab Inc.
- .form-group.row
- .offset-sm-2.col-sm-10
- - can_be_configured = @application_setting.usage_ping_can_be_configured?
- .form-check
- = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input'
- = f.label :usage_ping_enabled, class: 'form-check-label' do
- Enable usage ping
- .form-text.text-muted
- - if can_be_configured
- To help improve GitLab and its user experience, GitLab will
- periodically collect usage information.
- = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
- about what information is shared with GitLab Inc. Visit
- = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
- to see the JSON payload sent.
- - else
- The usage ping is disabled, and cannot be configured through this
- form. For more information, see the documentation on
- = succeed '.' do
- = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
+ .form-group
+ .form-check
+ = f.check_box :version_check_enabled, class: 'form-check-input'
+ = f.label :version_check_enabled, class: 'form-check-label' do
+ Enable version check
+ .form-text.text-muted
+ GitLab will inform you if a new version is available.
+ = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
+ about what information is shared with GitLab Inc.
+ .form-group
+ - can_be_configured = @application_setting.usage_ping_can_be_configured?
+ .form-check
+ = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input'
+ = f.label :usage_ping_enabled, class: 'form-check-label' do
+ Enable usage ping
+ .form-text.text-muted
+ - if can_be_configured
+ To help improve GitLab and its user experience, GitLab will
+ periodically collect usage information.
+ = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
+ about what information is shared with GitLab Inc. Visit
+ = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
+ to see the JSON payload sent.
+ - else
+ The usage ping is disabled, and cannot be configured through this
+ form. For more information, see the documentation on
+ = succeed '.' do
+ = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index 2b4d3bab54d..4cc3e6a7d03 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -2,66 +2,57 @@
= form_errors(@application_setting)
%fieldset
- .form-group.row
- = f.label :default_branch_protection, class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
- .form-group.row.visibility-level-setting
- = f.label :default_project_visibility, class: 'col-form-label col-sm-2'
- .col-sm-10
- = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
- .form-group.row.visibility-level-setting
- = f.label :default_snippet_visibility, class: 'col-form-label col-sm-2'
- .col-sm-10
- = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
- .form-group.row.visibility-level-setting
- = f.label :default_group_visibility, class: 'col-form-label col-sm-2'
- .col-sm-10
- = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
- .form-group.row
- = f.label :restricted_visibility_levels, class: 'col-form-label col-sm-2'
- .col-sm-10
- - checkbox_name = 'application_setting[restricted_visibility_levels][]'
- = hidden_field_tag(checkbox_name)
- - restricted_level_checkboxes('restricted-visibility-help', checkbox_name).each do |level|
- .form-check
- = level
- %span.form-text.text-muted#restricted-visibility-help
- Selected levels cannot be used by non-admin users for groups, projects or snippets.
- If the public level is restricted, user profiles are only visible to logged in users.
- .form-group.row
- = f.label :import_sources, class: 'col-form-label col-sm-2'
- .col-sm-10
- = hidden_field_tag 'application_setting[import_sources][]'
- - import_sources_checkboxes('import-sources-help').each do |source|
- .form-check= source
- %span.form-text.text-muted#import-sources-help
- Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
- = link_to "(?)", help_page_path("integration/github")
- , Bitbucket
- = link_to "(?)", help_page_path("integration/bitbucket")
- and GitLab.com
- = link_to "(?)", help_page_path("integration/gitlab")
-
- .form-group.row
- .offset-sm-2.col-sm-10
+ .form-group
+ = f.label :default_branch_protection, class: 'label-light'
+ = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
+ .form-group.visibility-level-setting
+ = f.label :default_project_visibility, class: 'label-light'
+ = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
+ .form-group.visibility-level-setting
+ = f.label :default_snippet_visibility, class: 'label-light'
+ = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
+ .form-group.visibility-level-setting
+ = f.label :default_group_visibility, class: 'label-light'
+ = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
+ .form-group
+ = f.label :restricted_visibility_levels, class: 'label-light'
+ - checkbox_name = 'application_setting[restricted_visibility_levels][]'
+ = hidden_field_tag(checkbox_name)
+ - restricted_level_checkboxes('restricted-visibility-help', checkbox_name, class: 'form-check-input').each do |level|
.form-check
- = f.check_box :project_export_enabled, class: 'form-check-input'
- = f.label :project_export_enabled, class: 'form-check-label' do
- Project export enabled
+ = level
+ %span.form-text.text-muted#restricted-visibility-help
+ Selected levels cannot be used by non-admin users for groups, projects or snippets.
+ If the public level is restricted, user profiles are only visible to logged in users.
+ .form-group
+ = f.label :import_sources, class: 'label-light'
+ = hidden_field_tag 'application_setting[import_sources][]'
+ - import_sources_checkboxes('import-sources-help', class: 'form-check-input').each do |source|
+ .form-check= source
+ %span.form-text.text-muted#import-sources-help
+ Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
+ = link_to "(?)", help_page_path("integration/github")
+ , Bitbucket
+ = link_to "(?)", help_page_path("integration/bitbucket")
+ and GitLab.com
+ = link_to "(?)", help_page_path("integration/gitlab")
+
+ .form-group
+ .form-check
+ = f.check_box :project_export_enabled, class: 'form-check-input'
+ = f.label :project_export_enabled, class: 'form-check-label' do
+ Project export enabled
- .form-group.row
- %label.col-form-label.col-sm-2 Enabled Git access protocols
- .col-sm-10
- = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
- %span.form-text.text-muted#clone-protocol-help
- Allow only the selected protocols to be used for Git access.
+ .form-group
+ %label.label-light Enabled Git access protocols
+ = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
+ %span.form-text.text-muted#clone-protocol-help
+ Allow only the selected protocols to be used for Git access.
- ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
- field_name = :"#{type}_key_restriction"
- .form-group.row
- = f.label field_name, "#{type.upcase} SSH keys", class: 'col-form-label col-sm-2'
- .col-sm-10
- = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
+ .form-group
+ = f.label field_name, "#{type.upcase} SSH keys", class: 'label-light'
+ = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index 3f440c76ee0..bd43504dd37 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -17,7 +17,7 @@
%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
- = _('Account and limit settings')
+ = _('Account and limit')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
@@ -50,11 +50,11 @@
%section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
- = _('Terms of Service')
+ = _('Terms of Service and Privacy Policy')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = _('Include a Terms of Service agreement that all users must accept.')
+ = _('Include a Terms of Service agreement and Privacy Policy that all users must accept.')
.settings-content
= render 'terms'
@@ -169,7 +169,7 @@
.settings-content
= render 'logging'
-%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) }
+%section.qa-repository-storage-settings.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Repository storage')
@@ -317,10 +317,13 @@
%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
- = _('Repository mirror settings')
+ = _('Repository mirror')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('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/gitaly_servers/index.html.haml b/app/views/admin/gitaly_servers/index.html.haml
index d0cf5761726..9b24f411a75 100644
--- a/app/views/admin/gitaly_servers/index.html.haml
+++ b/app/views/admin/gitaly_servers/index.html.haml
@@ -6,10 +6,10 @@
- if @gitaly_servers.any?
.table-holder
%table.table.responsive-table
- %thead.d-none.d-sm-none.d-md-block
+ %thead
%tr
%th= _("Storage")
- %th= n_("Gitaly|Address")
+ %th= s_("Gitaly|Address")
%th= _("Server version")
%th= _("Git version")
%th= _("Up to date")
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index dc4dccc9e0d..c8008771236 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -2,6 +2,9 @@
= form_errors(@group)
= render 'shared/group_form', f: f
+ = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
+ = render_if_exists 'admin/namespace_plan', f: f
+
.form-group.row.group-description-holder
= f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2'
.col-sm-10
@@ -15,6 +18,8 @@
= render 'groups/group_admin_settings', f: f
+ = render_if_exists 'namespaces/shared_runners_minutes_settings', group: @group, form: f
+
- if @group.new_record?
.form-group.row
.offset-sm-2.col-sm-10
@@ -28,3 +33,5 @@
.form-actions
= f.submit 'Save changes', class: "btn btn-save"
= link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel"
+
+= render_if_exists 'ldap_group_links/ldap_syncrhonizations', group: @group
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index e7c70a6f187..3f96988c203 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -1,3 +1,4 @@
+- group = local_assigns.fetch(:group)
- css_class = 'no-description' if group.description.blank?
%li.group-row{ class: css_class }
@@ -8,6 +9,8 @@
%span.badge.badge-pill
= storage_counter(group.storage_size)
+ = render_if_exists 'admin/namespace_plan_badge', namespace: group
+
%span
= icon('bookmark')
= number_with_delimiter(group.projects.count)
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 6d75ccd5add..a40f98ad24f 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -40,6 +40,8 @@
%strong
= @group.created_at.to_s(:medium)
+ = render_if_exists 'admin/namespace_plan_info', namespace: @group
+
%li
%span.light Storage:
%strong= storage_counter(@group.storage_size)
@@ -58,6 +60,10 @@
= group_lfs_status(@group)
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ = render_if_exists 'namespaces/shared_runner_status', namespace: @group
+
+ = render_if_exists 'ldap_group_links/ldap_group_links_show', group: @group
+
.card
.card-header
%h3.card-title
@@ -104,7 +110,7 @@
= form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
- = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all)
+ = users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all)
.prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
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/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 0a22a142858..ccba1c461fc 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -116,8 +116,8 @@
.card-body
= form_for @project, url: transfer_admin_project_path(@project), method: :put do |f|
.form-group.row
- = f.label :new_namespace_id, "Namespace", class: 'col-form-label col-sm-2'
- .col-sm-10
+ = f.label :new_namespace_id, "Namespace", class: 'col-form-label col-sm-3'
+ .col-sm-9
.dropdown
= dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select
@@ -127,7 +127,7 @@
= dropdown_loading
.form-group.row
- .offset-sm-2.col-sm-10
+ .offset-sm-3.col-sm-9
= f.submit 'Transfer', class: 'btn btn-primary'
.card.repository-check
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index f38aeb151df..8dfd176f1b7 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -67,7 +67,7 @@
%th Projects
%th Jobs
%th Tags
- %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc'))
+ %th= link_to 'Last contact', admin_runners_path(safe_params.slice(:search).merge(sort: 'contacted_asc'))
%th
- @runners.each do |runner|
diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml
index 144dceacbdd..993006e8745 100644
--- a/app/views/admin/services/_form.html.haml
+++ b/app/views/admin/services/_form.html.haml
@@ -7,5 +7,4 @@
= render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block
- .form-actions
- = form.submit 'Save', class: 'btn btn-save'
+ = form.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index 35a331283ab..04acc5f8423 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -1,26 +1,26 @@
%fieldset
%legend Access
- .form-group
- = f.label :projects_limit, class: 'col-form-label'
+ .form-group.row
+ = f.label :projects_limit, class: 'col-form-label col-sm-2'
.col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control'
- .form-group
- = f.label :can_create_group, class: 'col-form-label'
+ .form-group.row
+ = f.label :can_create_group, class: 'col-form-label col-sm-2'
.col-sm-10= f.check_box :can_create_group
- .form-group
- = f.label :access_level, class: 'col-form-label'
+ .form-group.row
+ = f.label :access_level, class: 'col-form-label col-sm-2'
.col-sm-10
- editing_current_user = (current_user == @user)
= f.radio_button :access_level, :regular, disabled: editing_current_user
- = label_tag :regular do
+ = label_tag :regular, class: 'font-weight-bold' do
Regular
%p.light
Regular users have access to their groups and projects
= f.radio_button :access_level, :admin, disabled: editing_current_user
- = label_tag :admin do
+ = label_tag :admin, class: 'font-weight-bold' do
Admin
%p.light
Administrators have access to all groups, projects and users and can manage all features in this installation
@@ -28,8 +28,8 @@
%p.light
You cannot remove your own admin rights.
- .form-group
- = f.label :external, class: 'col-form-label'
+ .form-group.row
+ = f.label :external, class: 'col-form-label col-sm-2'
.col-sm-10
= f.check_box :external do
External
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 010cb2ac354..58be07fc83e 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -56,7 +56,7 @@
= f.label :linkedin, class: 'col-form-label col-sm-2'
.col-sm-10= f.text_field :linkedin, class: 'form-control'
.form-group.row
- = f.label :twitter, class: 'col-form-label'
+ = f.label :twitter, class: 'col-form-label col-sm-2'
.col-sm-10= f.text_field :twitter, class: 'form-control'
.form-group.row
= f.label :website_url, 'Website', class: 'col-form-label col-sm-2'
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 5e176b61d68..b2163ee85fa 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -38,7 +38,7 @@
%li.divider
- if user.can_be_removed?
%li
- %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal',
+ %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
delete_user_url: admin_user_path(user),
block_user_url: block_admin_user_path(user),
@@ -47,7 +47,7 @@
= s_('AdminUsers|Delete user')
%li
- %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal',
+ %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
delete_user_url: admin_user_path(user, hard_delete: true),
block_user_url: block_admin_user_path(user),
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 4b3c52af16a..8ca9fb4512e 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -12,9 +12,9 @@
- if can?(current_user, :award_emoji, awardable)
.award-menu-holder.js-award-holder
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
- 'aria-label': 'Add reaction',
+ 'aria-label': _('Add reaction'),
class: ("js-user-authored" if user_authored),
- data: { title: 'Add reaction', placement: "bottom" } }
+ data: { title: _('Add reaction'), placement: "bottom" } }
%span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley')
%span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile')
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 571eb28f195..6ee55836dd2 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -43,5 +43,6 @@
%span.toggle-icon
= 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')
+ = render_if_exists 'ci/variables/environment_scope', form_field: form_field, variable: variable
%button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
= icon('minus-circle')
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/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 2554b2688bb..ee7369f54a9 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -22,6 +22,13 @@
= f.label :password
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
%p.gl-field-hint Minimum length is #{@minimum_password_length} characters
+ - if Gitlab::CurrentSettings.current_application_settings.enforce_terms?
+ .form-group
+ = check_box_tag :terms_opt_in, '1', false, required: true
+ = label_tag :terms_opt_in do
+ - terms_link = link_to s_("I accept the|Terms of Service and Privacy Policy"), terms_path, target: "_blank"
+ - accept_terms_label = _("I accept the %{terms_link}") % { terms_link: terms_link }
+ = accept_terms_label.html_safe
%div
- if Gitlab::Recaptcha.enabled?
= recaptcha_tags
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/email_rejection_mailer/rejection.html.haml b/app/views/email_rejection_mailer/rejection.html.haml
index 7f7d841fe21..c4ae7befe4e 100644
--- a/app/views/email_rejection_mailer/rejection.html.haml
+++ b/app/views/email_rejection_mailer/rejection.html.haml
@@ -2,3 +2,4 @@
Unfortunately, your email message to GitLab could not be processed.
= markdown @reason
+= render_if_exists 'shared/additional_email_text'
diff --git a/app/views/email_rejection_mailer/rejection.text.haml b/app/views/email_rejection_mailer/rejection.text.haml
index af518b5b583..0e13b2a6473 100644
--- a/app/views/email_rejection_mailer/rejection.text.haml
+++ b/app/views/email_rejection_mailer/rejection.text.haml
@@ -1,3 +1,4 @@
Unfortunately, your email message to GitLab could not be processed.
\
= @reason
+= render_if_exists 'shared/additional_email_text'
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index 227c7884915..8ae29b9d337 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -1,4 +1,4 @@
-- message = local_assigns.fetch(:message)
+- message = local_assigns.fetch(:message, nil)
- content_for(:title, 'Access Denied')
= image_tag('illustrations/error-403.svg', alt: '403', lazy: false)
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 845f4046d0d..6abb56ba6d2 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,6 +1,6 @@
- if current_user
.dropdown
- %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
+ %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' }
= icon('globe')
%span.light Visibility:
- if params[:visibility_level].present?
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index fd6e7111f38..577c63503a8 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -1,4 +1,4 @@
-.nav-block
+.nav-block.activities
.controls
= link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
%i.fa.fa-rss
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 6a0321bcd2b..13d584f5f1d 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -25,9 +25,10 @@
%span.badge= @members.total_count
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
- %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
- = icon("search")
+ .position-relative.append-right-8
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = icon("search")
- if can_manage_members
= render 'shared/members/filter_2fa_dropdown'
= render 'shared/members/sort_dropdown'
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/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index ac7e12fcd0b..db7eaff6658 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,21 +1,32 @@
-- page_title 'Labels'
-
+- @no_container = true
+- page_title "Labels"
+- can_admin_label = can?(current_user, :admin_label, @group)
+- hide_class = ''
+- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
- issuables = ['issues', 'merge requests']
-.top-area.adjust
- .nav-text
- = _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence }
+- if can_admin_label
+ - content_for(:header_content) do
+ .nav-controls
+ = link_to _('New label'), new_group_label_path(@group), class: "btn btn-new"
+
+- if @labels.exists?
+ #promote-label-modal
+ %div{ class: container_class }
+ .top-area.adjust
+ .nav-text
+ = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence }
- .nav-controls
- - if can?(current_user, :admin_label, @group)
- = link_to "New label", new_group_label_path(@group), class: "btn btn-new"
+ .labels-container.prepend-top-5
+ .other-labels
+ - if can_admin_label
+ %h5{ class: ('hide' if hide) } Labels
+ %ul.content-list.manage-labels-list.js-other-labels
+ = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false }
+ = paginate @labels, theme: 'gitlab'
+- else
+ = render 'shared/empty_states/labels'
-.labels
- .other-labels
- - if @labels.present?
- %ul.content-list.manage-labels-list.js-other-labels
- = render partial: 'shared/label', subject: @group, collection: @labels, as: :label
- = paginate @labels, theme: 'gitlab'
- - else
- .nothing-here-block
- = _("No labels created yet.")
+%template#js-badge-item-template
+ %li.label-link-item.js-priority-badge.inline.prepend-left-10
+ .label-badge.label-badge-blue= _('Prioritized label')
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/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 383d955d71f..647948c7dff 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -7,7 +7,7 @@
.settings-header
%h4
= _('Variables')
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand')
%p.append-bottom-0
@@ -18,7 +18,7 @@
%section.settings#runners-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
- = _('Runners settings')
+ = _('Runners')
%button.btn.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand')
%p
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 8b0ef3cd87a..5a88619f769 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -18,7 +18,7 @@
- if can_create_subgroups
.btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } }
%input.btn.btn-success.dropdown-primary.js-new-group-child{ type: "button", value: new_project_label, data: { action: "new-project" } }
- %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown" } }
+ %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } }
= icon("caret-down", class: "dropdown-btn-icon")
%ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } }
%li.droplab-item-selected{ role: "button", data: { value: "new-project", text: new_project_label } }
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 29db29235c1..c23fe0b5c49 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -18,71 +18,71 @@
%th Global Shortcuts
%tr
%td.shortcut
- .key s
+ %kbd s
%td Focus Search
%tr
%td.shortcut
- .key f
+ %kbd f
%td Focus Filter
- if performance_bar_enabled?
%tr
%td.shortcut
- .key p b
+ %kbd p b
%td Show/hide the Performance Bar
%tr
%td.shortcut
- .key ?
+ %kbd ?
%td Show/hide this dialog
%tr
%td.shortcut
- if browser.platform.mac?
- .key &#8984; shift p
+ %kbd &#8984; shift p
- else
- .key ctrl shift p
+ %kbd ctrl shift p
%td Toggle Markdown preview
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-up
%td Edit last comment (when focused on an empty textarea)
%tr
%td.shortcut
- .key shift t
+ %kbd shift t
%td
Go to todos
%tr
%td.shortcut
- .key shift a
+ %kbd shift a
%td
Go to the activity feed
%tr
%td.shortcut
- .key shift p
+ %kbd shift p
%td
Go to projects
%tr
%td.shortcut
- .key shift i
+ %kbd shift i
%td
Go to issues
%tr
%td.shortcut
- .key shift m
+ %kbd shift m
%td
Go to merge requests
%tr
%td.shortcut
- .key shift g
+ %kbd shift g
%td
Go to groups
%tr
%td.shortcut
- .key shift l
+ %kbd shift l
%td
Go to milestones
%tr
%td.shortcut
- .key shift s
+ %kbd shift s
%td
Go to snippets
%tbody
@@ -91,21 +91,21 @@
%th Finding Project File
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-up
%td Move selection up
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-down
%td Move selection down
%tr
%td.shortcut
- .key enter
+ %kbd enter
%td Open Selection
%tr
%td.shortcut
- .key esc
+ %kbd esc
%td Go back
.col-lg-4
%table.shortcut-mappings
@@ -115,95 +115,95 @@
%th Project
%tr
%td.shortcut
- .key g
- .key p
+ %kbd g
+ %kbd p
%td
Go to the project's overview page
%tr
%td.shortcut
- .key g
- .key v
+ %kbd g
+ %kbd v
%td
Go to the project's activity feed
%tr
%td.shortcut
- .key g
- .key f
+ %kbd g
+ %kbd f
%td
Go to files
%tr
%td.shortcut
- .key g
- .key c
+ %kbd g
+ %kbd c
%td
Go to commits
%tr
%td.shortcut
- .key g
- .key j
+ %kbd g
+ %kbd j
%td
Go to jobs
%tr
%td.shortcut
- .key g
- .key n
+ %kbd g
+ %kbd n
%td
Go to network graph
%tr
%td.shortcut
- .key g
- .key d
+ %kbd g
+ %kbd d
%td
Go to repository charts
%tr
%td.shortcut
- .key g
- .key i
+ %kbd g
+ %kbd i
%td
Go to issues
%tr
%td.shortcut
- .key g
- .key b
+ %kbd g
+ %kbd b
%td
Go to issue boards
%tr
%td.shortcut
- .key g
- .key m
+ %kbd g
+ %kbd m
%td
Go to merge requests
%tr
%td.shortcut
- .key g
- .key e
+ %kbd g
+ %kbd e
%td
Go to environments
%tr
%td.shortcut
- .key g
- .key k
+ %kbd g
+ %kbd k
%td
Go to kubernetes
%tr
%td.shortcut
- .key g
- .key s
+ %kbd g
+ %kbd s
%td
Go to snippets
%tr
%td.shortcut
- .key g
- .key w
+ %kbd g
+ %kbd w
%td
Go to wiki
%tr
%td.shortcut
- .key t
+ %kbd t
%td Go to finding file
%tr
%td.shortcut
- .key i
+ %kbd i
%td New issue
%tbody
@@ -212,17 +212,17 @@
%th Project Files browsing
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-up
%td Move selection up
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-down
%td Move selection down
%tr
%td.shortcut
- .key enter
+ %kbd enter
%td Open Selection
%tbody
%tr
@@ -230,7 +230,7 @@
%th Project File
%tr
%td.shortcut
- .key y
+ %kbd y
%td Go to file permalink
%tbody
%tr
@@ -239,115 +239,115 @@
%tr
%td.shortcut
- if browser.platform.mac?
- .key &#8984; p
+ %kbd &#8984; p
- else
- .key ctrl p
+ %kbd ctrl p
%td Go to file
.col-lg-4
%table.shortcut-mappings
- %tbody.hidden-shortcut.network{ style: 'display:none' }
+ %tbody.hidden-shortcut{ style: 'display:none' }
%tr
%th
%th Network Graph
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-left
\/
- .key h
+ %kbd h
%td Scroll left
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-right
\/
- .key l
+ %kbd l
%td Scroll right
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-up
\/
- .key k
+ %kbd k
%td Scroll up
%tr
%td.shortcut
- .key
+ %kbd
%i.fa.fa-arrow-down
\/
- .key j
+ %kbd j
%td Scroll down
%tr
%td.shortcut
- .key
+ %kbd
shift
%i.fa.fa-arrow-up
\/
- .key
+ %kbd
shift k
%td Scroll to top
%tr
%td.shortcut
- .key
+ %kbd
shift
%i.fa.fa-arrow-down
\/
- .key
+ %kbd
shift j
%td Scroll to bottom
- %tbody.hidden-shortcut.issues{ style: 'display:none' }
+ %tbody.hidden-shortcut{ style: 'display:none' }
%tr
%th
%th Issues
%tr
%td.shortcut
- .key a
+ %kbd a
%td Change assignee
%tr
%td.shortcut
- .key m
+ %kbd m
%td Change milestone
%tr
%td.shortcut
- .key r
+ %kbd r
%td Reply (quoting selected text)
%tr
%td.shortcut
- .key e
+ %kbd e
%td Edit issue
%tr
%td.shortcut
- .key l
+ %kbd l
%td Change Label
- %tbody.hidden-shortcut.merge_requests{ style: 'display:none' }
+ %tbody.hidden-shortcut{ style: 'display:none' }
%tr
%th
%th Merge Requests
%tr
%td.shortcut
- .key a
+ %kbd a
%td Change assignee
%tr
%td.shortcut
- .key m
+ %kbd m
%td Change milestone
%tr
%td.shortcut
- .key r
+ %kbd r
%td Reply (quoting selected text)
%tr
%td.shortcut
- .key e
+ %kbd e
%td Edit merge request
%tr
%td.shortcut
- .key l
+ %kbd l
%td Change Label
- %tbody.hidden-shortcut.wiki{ style: 'display:none' }
+ %tbody.hidden-shortcut{ style: 'display:none' }
%tr
%th
%th Wiki pages
%tr
%td.shortcut
- .key e
+ %kbd e
%td Edit wiki page
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index de8369ed7b9..b32b602ceb3 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -443,8 +443,6 @@
.col-md-6
.alert.alert-success
= lorem
- .alert.alert-primary
- = lorem
.alert.alert-info
= lorem
.col-md-6
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
index eb9790c7903..581576a8a3d 100644
--- a/app/views/import/gitea/new.html.haml
+++ b/app/views/import/gitea/new.html.haml
@@ -12,11 +12,11 @@
= form_tag personal_access_token_import_gitea_path do
.form-group.row
- = label_tag :gitea_host_url, 'Gitea Host URL', class: 'col-form-label col-sm-8'
+ = label_tag :gitea_host_url, 'Gitea Host URL', class: 'col-form-label col-sm-2'
.col-sm-4
= text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control'
.form-group.row
- = label_tag :personal_access_token, 'Personal Access Token', class: 'col-form-label col-sm-8'
+ = label_tag :personal_access_token, 'Personal Access Token', class: 'col-form-label col-sm-2'
.col-sm-4
= text_field_tag :personal_access_token, nil, class: 'form-control'
.form-actions
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index c63cf2b31cb..b9ebb1a39d9 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -19,7 +19,7 @@
= form_tag personal_access_token_import_github_path, method: :post, class: 'form-inline' do
.form-group
- = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('Personal Access Token'), size: 40
+ = text_field_tag :personal_access_token, '', class: 'form-control append-right-8', placeholder: _('Personal Access Token'), size: 40
= submit_tag _('List your GitHub repositories'), class: 'btn btn-success'
- unless github_import_configured?
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 2d059e78490..cc672a5ea7c 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -20,7 +20,7 @@
- else
.input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
- .input-group-text
+ .input-group-text.border-0
#{user_url(current_user.username)}/
= hidden_field_tag :namespace_id, value: current_user.namespace_id
.form-group.col-12.col-sm-6.project-path
@@ -37,6 +37,6 @@
.form-group
= file_field_tag :file, class: ''
.row
- .form-actions
+ .form-actions.col-sm-12
= submit_tag 'Import project', class: 'btn btn-create'
= link_to 'Cancel', new_project_path, class: 'btn btn-cancel'
diff --git a/app/views/kaminari/gitlab/_first_page.html.haml b/app/views/kaminari/gitlab/_first_page.html.haml
index 369165da02a..3b7d4a1c578 100644
--- a/app/views/kaminari/gitlab/_first_page.html.haml
+++ b/app/views/kaminari/gitlab/_first_page.html.haml
@@ -5,5 +5,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li.first.page-item
+%li.page-item.js-first-button
= link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote, class: 'page-link'
diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml
index 6eec30212d1..849f92fdc95 100644
--- a/app/views/kaminari/gitlab/_gap.html.haml
+++ b/app/views/kaminari/gitlab/_gap.html.haml
@@ -4,5 +4,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li.page-item.disabled
+%li.page-item.disabled.d-none.d-md-block
= link_to raw(t 'views.pagination.truncate'), '#', class: 'page-link'
diff --git a/app/views/kaminari/gitlab/_last_page.html.haml b/app/views/kaminari/gitlab/_last_page.html.haml
index 8b49db58281..7836e17f877 100644
--- a/app/views/kaminari/gitlab/_last_page.html.haml
+++ b/app/views/kaminari/gitlab/_last_page.html.haml
@@ -5,5 +5,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li.last.page-item
+%li.page-item.js-last-button
= link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote, class: 'page-link'}
diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml
index 05f151555ad..a7fa1a21a6c 100644
--- a/app/views/kaminari/gitlab/_next_page.html.haml
+++ b/app/views/kaminari/gitlab/_next_page.html.haml
@@ -8,5 +8,5 @@
- page_url = current_page.last? ? '#' : url
-%li.page-item{ class: ('disabled' if current_page.last?) }
+%li.page-item.js-next-button{ class: ('disabled' if current_page.last?) }
= link_to raw(t 'views.pagination.next'), page_url, rel: 'next', remote: remote, class: 'page-link'
diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml
index 8a40e13a537..d0dc1784540 100644
--- a/app/views/kaminari/gitlab/_page.html.haml
+++ b/app/views/kaminari/gitlab/_page.html.haml
@@ -6,5 +6,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li.page-item.js-pagination-page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?)] }
+%li.page-item.js-pagination-page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?), ('d-none d-md-block' if !page.current?) ] }
= link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil, class: 'page-link' }
diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml
index a6435deb4bf..ac9e274dbc7 100644
--- a/app/views/kaminari/gitlab/_paginator.html.haml
+++ b/app/views/kaminari/gitlab/_paginator.html.haml
@@ -6,7 +6,7 @@
-# remote: data-remote
-# paginator: the paginator that renders the pagination tags inside
= paginator.render do
- .gl-pagination
+ .gl-pagination.prepend-top-default
%ul.pagination.justify-content-center
- unless current_page.first?
= first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages
diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml
index f4a11a449b7..12b0e106a62 100644
--- a/app/views/kaminari/gitlab/_prev_page.html.haml
+++ b/app/views/kaminari/gitlab/_prev_page.html.haml
@@ -8,5 +8,5 @@
- page_url = current_page.first? ? '#' : url
-%li.page-item{ class: ('disabled' if current_page.first?) }
+%li.page-item.js-previous-button{ class: ('disabled' if current_page.first?) }
= link_to raw(t 'views.pagination.previous'), page_url, rel: 'prev', remote: remote, class: 'page-link'
diff --git a/app/views/kaminari/gitlab/_without_count.html.haml b/app/views/kaminari/gitlab/_without_count.html.haml
index 1425a809052..f780400ebcb 100644
--- a/app/views/kaminari/gitlab/_without_count.html.haml
+++ b/app/views/kaminari/gitlab/_without_count.html.haml
@@ -1,5 +1,5 @@
-.gl-pagination
- %ul.pagination.clearfix
+.gl-pagination.prepend-top-default
+ %ul.pagination.justify-content-center
- if previous_path
%li.page-item.prev
= link_to(t('views.pagination.previous'), previous_path, rel: 'prev', class: 'page-link')
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 02bdfe9aa3c..9253a0652da 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -25,7 +25,7 @@
%title= page_title(site_name)
%meta{ name: "description", content: page_description }
- = favicon_link_tag favicon, id: 'favicon'
+ = favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png'
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 82ca7252424..81f35615555 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html.devise-layout-html{ class: system_message_class }
= render "layouts/head"
- %body.ui_indigo.login-page.application.navless{ data: { page: body_data_page } }
+ %body.ui-indigo.login-page.application.navless{ data: { page: body_data_page } }
.page-wrap
= render "layouts/header/empty"
.login-page-broadcast
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index adf90cb7667..52805e0da73 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: "en", class: system_message_class }
= render "layouts/head"
- %body.ui_indigo.login-page.application.navless
+ %body.ui-indigo.login-page.application.navless
= render "layouts/header/empty"
= render "layouts/broadcast"
.container.navless-container
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 24b6c490a5a..a74ea246eaf 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -17,6 +17,11 @@
= link_to _("Help"), help_path
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
%li.divider
+ %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
- 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 1bca837a311..5cec443e969 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -61,7 +61,7 @@
- if header_link?(:sign_in)
%li.nav-item
%div
- = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'nav-link btn btn-sign-in'
+ = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
%button.navbar-toggler.d-block.d-sm-none{ type: 'button' }
%span.sr-only Toggle navigation
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index db8137cc248..d35df706036 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -1,5 +1,5 @@
%li.header-new.dropdown
- = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
+ = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do
= sprite_icon('plus-square', size: 16)
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index fdb07ce6fc5..33416bf76d7 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
@@ -278,7 +284,7 @@
= _('Snippets')
- if project_nav_tab? :settings
- = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show]) do
+ = nav_link(path: sidebar_settings_paths) do
= link_to edit_project_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
= sprite_icon('settings')
@@ -288,7 +294,7 @@
%ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project)
- if can_edit
- = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(path: sidebar_settings_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to edit_project_path(@project) do
%strong.fly-out-top-item-name
= _('Settings')
@@ -326,6 +332,8 @@
%span
= _('Pages')
+ = render_if_exists 'projects/sidebar/settings_audit_events'
+
- else
= nav_link(controller: :project_members) do
= link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index a8964b19ba1..977eb350365 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -14,9 +14,9 @@
%div{ class: "#{container_class} limit-container-width" }
.content{ id: "content-body" }
- .panel.panel-default
- .panel-heading
- .title
+ .card
+ .card-header
+ .card-title
= brand_header_logo
- logo_text = brand_header_logo_type
- if logo_text.present?
diff --git a/app/views/notify/merge_request_unmergeable_email.html.haml b/app/views/notify/merge_request_unmergeable_email.html.haml
index 578fa1fbce7..7ec0c1ef390 100644
--- a/app/views/notify/merge_request_unmergeable_email.html.haml
+++ b/app/views/notify/merge_request_unmergeable_email.html.haml
@@ -1,6 +1,2 @@
%p
- Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} can no longer be merged due to the following #{'reason'.pluralize(@reasons.count)}:
-
- %ul
- - @reasons.each do |reason|
- %li= reason
+ Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} can no longer be merged due to conflict.
diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml
index e4f9f1bf5e7..dcdd6db69d6 100644
--- a/app/views/notify/merge_request_unmergeable_email.text.haml
+++ b/app/views/notify/merge_request_unmergeable_email.text.haml
@@ -1,7 +1,4 @@
-Merge Request #{@merge_request.to_reference} can no longer be merged due to the following #{'reason'.pluralize(@reasons.count)}:
-
-- @reasons.each do |reason|
- * #{reason}
+Merge Request #{@merge_request.to_reference} can no longer be merged due to conflict.
Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 0a9adc6f243..dd6a84e503d 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -9,6 +9,8 @@
%p
Assignee: #{@merge_request.assignee_name}
+= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request
+
- if @merge_request.description
%div
= markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb
index 7d98400e6fe..d5b8f8d764f 100644
--- a/app/views/notify/new_merge_request_email.text.erb
+++ b/app/views/notify/new_merge_request_email.text.erb
@@ -5,6 +5,6 @@ New Merge Request <%= @merge_request.to_reference %>
<%= merge_path_description(@merge_request, 'to') %>
Author: <%= @merge_request.author_name %>
Assignee: <%= @merge_request.assignee_name %>
+<%= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request %>
<%= @merge_request.description %>
-
diff --git a/app/views/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/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index ce312943154..8f1078bd41d 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -4,18 +4,12 @@
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-4.application-theme
%h4.prepend-top-0
- s_('Preferences|Navigation theme')
+ = s_('Preferences|Navigation theme')
%p Customize the appearance of the application header and navigation sidebar.
.col-lg-8.application-theme
- Gitlab::Themes.each do |theme|
= label_tag do
- .preview{ class: theme.name.downcase }
- .preview-row
- .quadrant.one
- .quadrant.two
- .preview-row
- .quadrant.three
- .quadrant.four
+ .preview{ class: theme.css_class }
= f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id
= theme.name
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/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 075badb9e56..89940512bc6 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -42,6 +42,10 @@
.project-clone-holder
= render "shared/clone_panel"
+ - if show_xcode_link?(@project)
+ .project-action-button.project-xcode.inline
+ = render "projects/buttons/xcode_link"
+
- if current_user
- if can?(current_user, :download_code, @project)
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index cfbd0459e3e..6f957533287 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -16,7 +16,7 @@
- else
.input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
- .input-group-text
+ .input-group-text.border-0
#{user_url(current_user.username)}/
= f.hidden_field :namespace_id, value: current_user.namespace_id
.form-group.project-path.col-sm-6
diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml
index 9d27f51926e..d08807b5135 100644
--- a/app/views/projects/_project_templates.html.haml
+++ b/app/views/projects/_project_templates.html.haml
@@ -10,16 +10,18 @@
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' } Preview
.project-fields-form
- .form-group
- %label.label-light
- Template
- .input-group.template-input-group
- .input-group-prepend
- .input-group-text
- .selected-icon
- - Gitlab::ProjectTemplate.all.each do |template|
- = custom_icon(template.logo)
- .selected-template
- %button.btn.btn-default.change-template{ type: "button" } Change template
+ .row
+ .form-group.col-sm-12
+ %label.label-light
+ Template
+ .input-group.template-input-group
+ .input-group-prepend
+ .input-group-text
+ .selected-icon
+ - Gitlab::ProjectTemplate.all.each do |template|
+ = custom_icon(template.logo)
+ .selected-template
+ .input-group-append
+ %button.btn.btn-default.change-template{ type: "button" } Change template
= render 'new_project_fields', f: f, project_name_id: "template-project-name"
diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml
index 8bffd1396ae..15ec58289e3 100644
--- a/app/views/projects/_stat_anchor_list.html.haml
+++ b/app/views/projects/_stat_anchor_list.html.haml
@@ -5,4 +5,4 @@
- anchors.each do |anchor|
%li.nav-item
= link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do
- %span.stat-text= anchor.label
+ .stat-text= anchor.label
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index 7ff7466e561..87b165e581a 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -9,10 +9,10 @@
.tree-holder
.nav-block
%ul.breadcrumb.repo-breadcrumb
- %li
+ %li.breadcrumb-item
= link_to 'Artifacts', browse_project_job_artifacts_path(@project, @build)
- path_breadcrumbs do |title, path|
- %li
+ %li.breadcrumb-item
= link_to truncate(title, length: 40), browse_project_job_artifacts_path(@project, @build, path)
.tree-controls
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 1b150ec3e5c..0a0b3ce1d6f 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -11,6 +11,7 @@
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }<
+ = render_if_exists 'projects/blob/header_file_locks_link'
= edit_blob_button
= ide_edit_button
- if current_user
@@ -18,3 +19,4 @@
= delete_blob_link
= render 'projects/fork_suggestion'
+= render_if_exists 'projects/blob/header_file_locks', project: @project, path: @path
diff --git a/app/views/projects/blob/viewers/_download.html.haml b/app/views/projects/blob/viewers/_download.html.haml
index f9b1da05a00..fda4b9c92cd 100644
--- a/app/views/projects/blob/viewers/_download.html.haml
+++ b/app/views/projects/blob/viewers/_download.html.haml
@@ -1,5 +1,5 @@
.file-content.blob_file.blob-no-preview
- .center.render-error.vertical-center
+ .center.render-error
= link_to blob_raw_path do
%h1.light
= sprite_icon('download')
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index f641d7bc51a..88f9b7dfc9f 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -72,7 +72,7 @@
- else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
disabled: true,
- title: s_('Branches|Only a project master or owner can delete a protected branch') }
+ title: s_('Branches|Only a project maintainer or owner can delete a protected branch') }
= icon("trash-o")
- else
= link_to project_branch_path(@project, branch.name),
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index c75093c4c24..f7551434d47 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -3,7 +3,7 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- archive_prefix = "#{project.path}-#{ref.tr('/', '-')}"
.project-action-button.dropdown.inline>
- %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') }
+ %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static' }
= sprite_icon('download')
= icon("caret-down")
%span.sr-only= _('Select Archive Format')
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 84245d72f4a..8b9c52f0802 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -8,7 +8,7 @@
- if show_menu
.project-action-button.dropdown.inline
- %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') }
+ %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...'), 'data-display' => 'static' }
= icon('plus')
= icon("caret-down")
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
diff --git a/app/views/projects/buttons/_xcode_link.html.haml b/app/views/projects/buttons/_xcode_link.html.haml
new file mode 100644
index 00000000000..a8b32fb0ef5
--- /dev/null
+++ b/app/views/projects/buttons/_xcode_link.html.haml
@@ -0,0 +1,2 @@
+%a.btn.btn-default{ href: xcode_uri_to_repo(@project) }
+ = _("Open in Xcode")
diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml
index e9bdc54364b..243e8cd9ba0 100644
--- a/app/views/projects/clusters/_advanced_settings.html.haml
+++ b/app/views/projects/clusters/_advanced_settings.html.haml
@@ -7,8 +7,8 @@
- link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
- .card.form-group
- %label.text-danger
+ .sub-section.form-group
+ %h4.text-danger
= s_('ClusterIntegration|Remove Kubernetes cluster integration')
%p
= s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.")
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/_sidebar.html.haml b/app/views/projects/clusters/_sidebar.html.haml
index 73cd7c50922..3d10348212f 100644
--- a/app/views/projects/clusters/_sidebar.html.haml
+++ b/app/views/projects/clusters/_sidebar.html.haml
@@ -1,7 +1,9 @@
+- clusters_help_url = help_page_path('user/project/clusters/index.md')
+- help_link_start = "<a href=\"%{url}\" target=\"_blank\" rel=\"noopener noreferrer\">".html_safe
+- help_link_end = '</a>'.html_safe
%h4.prepend-top-0
= s_('ClusterIntegration|Kubernetes cluster integration')
%p
= s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
%p
- - link = link_to(_('Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Learn more about %{link_to_documentation}').html_safe % { link_to_documentation: link }
+ = s_('ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: clusters_help_url }, help_link_end: help_link_end }
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index ca7a6d5a886..b8e40b0a38b 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -1,4 +1,10 @@
= javascript_include_tag 'https://apis.google.com/js/api.js'
+- external_link_icon = icon('external-link')
+- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones'
+- machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types'
+- pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype'
+- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
+- help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon }
%p
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
@@ -7,15 +13,15 @@
= form_for @cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster)
.form-group
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group
- = field.label :environment_scope, s_('ClusterIntegration|Environment scope')
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light'
= field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
= field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
.form-group
- = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
+ = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'), class: 'label-light'
.js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
= provider_gcp_field.hidden_field :gcp_project_id
.dropdown
@@ -26,8 +32,7 @@
%span.form-text.text-muted &nbsp;
.form-group
- = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
- = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone'), class: 'label-light'
.js-gcp-zone-dropdown-entry-point
= provider_gcp_field.hidden_field :zone
.dropdown
@@ -35,13 +40,15 @@
%span.dropdown-toggle-text
= _('Select project to choose zone')
= icon('chevron-down')
+ %p.form-text.text-muted
+ = s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end }
.form-group
- = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
+ = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes'), class: 'label-light'
= provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
.form-group
- = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
+ = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-light'
.js-gcp-machine-type-dropdown-entry-point
= provider_gcp_field.hidden_field :machine_type
.dropdown
@@ -49,6 +56,8 @@
%span.dropdown-toggle-text
= _('Select project and zone to choose machine type')
= icon('chevron-down')
+ %p.form-text.text-muted
+ = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
.form-group
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml
index 55a42ac4847..96c7a648676 100644
--- a/app/views/projects/clusters/gcp/login.html.haml
+++ b/app/views/projects/clusters/gcp/login.html.haml
@@ -16,5 +16,6 @@
= _('or')
= link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer')
- else
- - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer')
- = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
+ .settings-message.text-center
+ - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer')
+ = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
index 2e92524ce8f..d45ae6ec91f 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -1,27 +1,28 @@
= form_for @cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster)
.form-group
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
- .form-group
- = field.label :environment_scope, s_('ClusterIntegration|Environment scope')
- = field.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|
.form-group
- = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
+ = 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')
.form-group
- = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-light'
= platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
.form-group
- = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-light'
= platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off'
.form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-light'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml
index 77d7a055474..4d117f435dc 100644
--- a/app/views/projects/clusters/user/_show.html.haml
+++ b/app/views/projects/clusters/user/_show.html.haml
@@ -1,20 +1,20 @@
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
- = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
+ = 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')
.form-group
- = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-light'
= platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
.form-group
- = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-light'
.input-group
= platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off'
%span.input-group-append.clipboard-addon
@@ -23,7 +23,7 @@
= s_('ClusterIntegration|Show')
.form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-light'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 30605927fd1..3d97e93c9e9 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -20,18 +20,18 @@
%span{ "aria-hidden": true } &times;
.modal-body
- if description
- %p.append-bottom-20= description
+ %p= description
= form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "js-#{type}-form js-requires-input" do
- .form-group.row.branch
- = label_tag 'start_branch', branch_label, class: 'col-form-label col-sm-2'
- .col-sm-10
- = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch'
- = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } })
+ .form-group.branch
+ = label_tag 'start_branch', branch_label, class: 'label-light'
- - if can?(current_user, :push_code, @project)
- = render 'shared/new_merge_request_checkbox'
- - else
- = hidden_field_tag 'create_merge_request', 1, id: nil
+ = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch'
+ = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } })
+
+ - if can?(current_user, :push_code, @project)
+ = render 'shared/new_merge_request_checkbox'
+ - else
+ = hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions
= submit_tag label, class: 'btn btn-create'
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml
index a91e31afc2b..0b8e5105bc0 100644
--- a/app/views/projects/commit/branches.html.haml
+++ b/app/views/projects/commit/branches.html.haml
@@ -6,7 +6,7 @@
- if @branches.any? || @tags.any? || @tags_limit_exceeded
%span
- = link_to "#", class: "js-details-expand label label-gray ref-name" do
+ = link_to "#", class: "js-details-expand badge badge-gray ref-name" do
= sprite_icon('ellipsis_h', size: 12, css_class: 'vertical-align-middle')
%span.js-details-content.hide
= commit_branches_links(@project, @branches)
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 12b27eb9b66..90e55fd0fb0 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -34,7 +34,8 @@
.d-block.d-sm-none
= render_commit_status(commit, ref: ref)
- if commit.description?
- %button.text-expander.d-none.d-sm-inline-block.js-toggle-button{ type: "button" } ...
+ %button.text-expander.js-toggle-button
+ = sprite_icon('ellipsis_h', size: 12)
.commiter
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 6af57d3ab26..fb1ea471dec 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -1,5 +1,5 @@
- expanded = Rails.env.test?
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.qa-deploy-keys-settings.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Deploy Keys
diff --git a/app/views/projects/deploy_tokens/_index.html.haml b/app/views/projects/deploy_tokens/_index.html.haml
index 50e5950ced4..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' }
@@ -10,9 +10,8 @@
.settings-content
- if @new_deploy_token.persisted?
= render 'projects/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
- - else
- %h5.prepend-top-0
- = s_('DeployTokens|Add a deploy token')
- = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens
- %hr
+ %h5.prepend-top-0
+ = s_('DeployTokens|Add a deploy token')
+ = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens
+ %hr
= render 'projects/deploy_tokens/table', project: @project, active_tokens: @deploy_tokens
diff --git a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
index 1e715681e59..5dd9ffba074 100644
--- a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
+++ b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
@@ -1,14 +1,18 @@
-.created-deploy-token-container
- %h5.prepend-top-0
- = s_('DeployTokens|Your New Deploy Token')
+.created-deploy-token-container.info-well
+ .well-segment
+ %h5.prepend-top-0
+ = s_('DeployTokens|Your New Deploy Token')
- .form-group
- = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
- = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left')
- %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.")
+ .form-group
+ .input-group
+ = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
+ .input-group-append
+ = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left')
+ %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.")
- .form-group
- = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
- = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left')
- %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.")
-%hr
+ .form-group
+ .input-group
+ = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus'
+ .input-group-append
+ = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left')
+ %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.")
diff --git a/app/views/projects/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/diffs/_collapsed.html.haml b/app/views/projects/diffs/_collapsed.html.haml
index 5762f4d86d7..9bd1255fe00 100644
--- a/app/views/projects/diffs/_collapsed.html.haml
+++ b/app/views/projects/diffs/_collapsed.html.haml
@@ -2,4 +2,4 @@
- url = url_for(safe_params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
.nothing-here-block.diff-collapsed{ data: { diff_for_path: url } }
This diff is collapsed.
- %a.click-to-expand Click to expand it.
+ %button.click-to-expand.btn.btn-link Click to expand it.
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 77ba422e87e..c2d900cbcf7 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -4,10 +4,10 @@
- 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 settings
+ General project
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
@@ -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,10 +82,10 @@
= render_if_exists 'projects/issues_settings'
- %section.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 settings
+ Merge request
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
@@ -101,10 +101,10 @@
= render 'export', project: @project
- %section.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 settings
+ Advanced
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index bf6fc8af12d..d47dc3d8143 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,3 +1,4 @@
+- @content_class = "limit-container-width" unless fluid_layout
- @no_container = true
- breadcrumb_title _("Details")
@@ -6,7 +7,7 @@
= render "home_panel"
.project-empty-note-panel
- %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] }
+ %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
.prepend-top-20
%h4
= _('The repository for this project is empty')
@@ -36,7 +37,7 @@
= render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
- if can?(current_user, :push_code, @project)
- %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] }
+ %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
.prepend-top-20
.empty_wrapper
%h3#repo-command-line-instructions.page-title-empty
@@ -44,14 +45,14 @@
.git-empty
%fieldset
%h5 Git global setup
- %pre.card.bg-light
+ %pre.bg-light
:preserve
git config --global user.name "#{h git_user_name}"
git config --global user.email "#{h git_user_email}"
%fieldset
%h5 Create a new repository
- %pre.card.bg-light
+ %pre.bg-light
:preserve
git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
cd #{h @project.path}
@@ -64,7 +65,7 @@
%fieldset
%h5 Existing folder
- %pre.card.bg-light
+ %pre.bg-light
:preserve
cd existing_folder
git init
@@ -77,7 +78,7 @@
%fieldset
%h5 Existing Git repository
- %pre.card.bg-light
+ %pre.bg-light
:preserve
cd existing_repo
git remote rename origin old-origin
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/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/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 76438fae663..a678cb6f058 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -18,7 +18,7 @@
%button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } }
= value
- %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
+ %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' }, display: 'static' } }
= icon('caret-down')
.droplab-dropdown
@@ -40,7 +40,7 @@
%label{ for: 'new-branch-name' }
= _('Branch name')
%input#new-branch-name.js-branch-name.form-control{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
- %span.js-branch-message.form-text.text-muted
+ %span.js-branch-message.form-text
.form-group
%label{ for: 'source-name' }
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index ec9a04c0eab..1f33bb3a129 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -86,9 +86,7 @@
%button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_down')
- %pre.build-trace#build-trace
- %code.bash.js-build-output
- .build-loader-animation.js-build-refresh
+ = render 'shared/builds/build_output'
- else
= render "empty_states"
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 9c78bade254..fb5b0fc15c9 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,40 +1,44 @@
- @no_container = true
- page_title "Labels"
-- hide_class = ''
- can_admin_label = can?(current_user, :admin_label, @project)
+- hide_class = ''
+
+- if can_admin_label
+ - content_for(:header_content) do
+ .nav-controls
+ = link_to _('New label'), new_project_label_path(@project), class: "btn btn-new"
- if @labels.exists? || @prioritized_labels.exists?
#promote-label-modal
%div{ class: container_class }
.top-area.adjust
.nav-text
- Labels can be applied to issues and merge requests.
+ = _('Labels can be applied to issues and merge requests.')
- if can_admin_label
- Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
+ = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.')
- - if can_admin_label
- .nav-controls
- = link_to new_project_label_path(@project), class: "btn btn-new" do
- New label
-
- .labels
+ .labels-container.prepend-top-5
- if can_admin_label
-# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels{ class: ('hide' if hide) }
- %h5 Prioritized Labels
- %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) }
- #js-priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" }
+ %h5.prepend-top-10= _('Prioritized Labels')
+ .content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) }
+ #js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" }
= render 'shared/empty_states/priority_labels'
- if @prioritized_labels.present?
- = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label
+ = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label, locals: { force_priority: true }
- if @labels.present?
.other-labels
- if can_admin_label
- %h5{ class: ('hide' if hide) } Other Labels
- %ul.content-list.manage-labels-list.js-other-labels
+ %h5{ class: ('hide' if hide) }= _('Other Labels')
+ .content-list.manage-labels-list.js-other-labels
= render partial: 'shared/label', subject: @project, collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- else
= render 'shared/empty_states/labels'
+
+%template#js-badge-item-template
+ %li.label-link-item.js-priority-badge.inline.prepend-left-10
+ .label-badge.label-badge-blue= _('Prioritized label')
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 ebcd99f2a9b..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.active
- = 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/diffs/_version_controls.html.haml b/app/views/projects/merge_requests/diffs/_version_controls.html.haml
index 1c26f0405d2..52bf584d550 100644
--- a/app/views/projects/merge_requests/diffs/_version_controls.html.haml
+++ b/app/views/projects/merge_requests/diffs/_version_controls.html.haml
@@ -3,7 +3,7 @@
.mr-version-menus-container.content-block
Changes between
%span.dropdown.inline.mr-version-dropdown
- %a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} }
+ %a.dropdown-toggle.btn.btn-default{ data: { toggle: :dropdown, display: 'static' } }
%span
- if @merge_request_diff.latest?
latest version
@@ -36,7 +36,7 @@
- if @merge_request_diff.base_commit_sha
and
%span.dropdown.inline.mr-version-compare-dropdown
- %a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} }
+ %a.btn.btn-default.dropdown-toggle{ data: { toggle: :dropdown, display: 'static' } }
- if @start_version
version #{version_index(@start_version)}
- else
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 01e38ffee20..b23baa22d8b 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -32,45 +32,27 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- .nav-links.scrolling-tabs.nav.nav-tabs
- %ul.merge-request-tabs.nav-tabs.nav
- %li.notes-tab
- = tab_link_for @merge_request, :show, force_link: @commit.present? do
- Discussion
- %span.badge.badge-pill= @merge_request.related_notes.user.count
- - if @merge_request.source_project
- %li.commits-tab
- = tab_link_for @merge_request, :commits do
- Commits
- %span.badge.badge-pill= @commits_count
- - if @pipelines.any?
- %li.pipelines-tab
- = tab_link_for @merge_request, :pipelines do
- Pipelines
- %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
- %li.diffs-tab
- = tab_link_for @merge_request, :diffs do
- Changes
- %span.badge.badge-pill= @merge_request.diff_size
+ %ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs
+ %li.notes-tab
+ = tab_link_for @merge_request, :show, force_link: @commit.present? do
+ Discussion
+ %span.badge.badge-pill= @merge_request.related_notes.user.count
+ - if @merge_request.source_project
+ %li.commits-tab
+ = tab_link_for @merge_request, :commits do
+ Commits
+ %span.badge.badge-pill= @commits_count
+ - if @pipelines.any?
+ %li.pipelines-tab
+ = tab_link_for @merge_request, :pipelines do
+ Pipelines
+ %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
+ %li.diffs-tab
+ = tab_link_for @merge_request, :diffs do
+ 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
@@ -78,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/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index b478fbbb15e..f7b04c436a6 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -69,6 +69,8 @@
.wiki
= markdown_field(@milestone, :description)
+ = render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project
+
- if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero?
.alert.alert-success.prepend-top-default
%span Assign some issues to this milestone.
diff --git a/app/views/projects/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/new.html.haml b/app/views/projects/new.html.haml
index 35a09f06bfa..5bb1bfb7059 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -29,16 +29,16 @@
.col-lg-9.js-toggle-container
%ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' }
- %li{ class: active_when(active_tab == 'blank'), role: 'presentation' }
- %a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' }
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.d-none.d-sm-block Blank project
%span.d-block.d-sm-none Blank
- %li{ class: active_when(active_tab == 'template'), role: 'presentation' }
- %a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' }
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.d-none.d-sm-block Create from template
%span.d-block.d-sm-none Template
- %li{ class: active_when(active_tab == 'import'), role: 'presentation' }
- %a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.d-none.d-sm-block Import project
%span.d-block.d-sm-none Import
diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml
index 4ada19a1368..9b77c4e3494 100644
--- a/app/views/projects/pages/_destroy.haml
+++ b/app/views/projects/pages/_destroy.haml
@@ -5,7 +5,7 @@
.errors-holder
.card-body
%p
- Removing the pages will prevent from exposing them to outside world.
+ Removing pages will prevent them from being exposed to the outside world.
.form-actions
= link_to 'Remove pages', project_pages_path(@project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove"
- else
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index aa53fc3ea28..04131a90a57 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -29,7 +29,7 @@
= link_to @commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha js-details-short"
= link_to("#", class: "js-details-expand d-none d-sm-none d-md-inline") do
%span.text-expander
- \...
+ = sprite_icon('ellipsis_h', size: 12)
%span.js-details-content.hide
= link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full"
= clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard")
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 118391aac64..951f80b378d 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,7 +1,7 @@
.tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
%li.js-pipeline-tab-link
- = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
+ = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
= _("Pipeline")
%li.js-builds-tab-link
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
@@ -43,12 +43,36 @@
= render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage
- if @pipeline.failed_builds.present?
- #js-tab-failures.build-failures.tab-pane
- - @pipeline.failed_builds.each_with_index do |build, index|
- .build-state
- %span.ci-status-icon-failed= custom_icon('icon_status_failed')
- %span.stage
- = build.stage.titleize
- %span.build-name
- = link_to build.name, pipeline_job_url(pipeline, build)
- %pre.build-log= build_summary(build, skip: index >= 10)
+ #js-tab-failures.build-failures.tab-pane.build-page
+ %table.table.responsive-table.ci-table.responsive-table-sm-rounded
+ %thead
+ %th.table-th-transparent
+ %th.table-th-transparent= _("Name")
+ %th.table-th-transparent= _("Stage")
+ %th.table-th-transparent= _("Failure")
+
+ %tbody
+ - @pipeline.failed_builds.each_with_index do |build, index|
+ - job = build.present(current_user: current_user)
+ %tr.build-state.responsive-table-border-start
+ %td.responsive-table-cell.ci-status-icon-failed{ data: { column: "Status"} }
+ .d-none.d-md-block.build-icon
+ = custom_icon("icon_status_#{build.status}")
+ .d-md-none.build-badge
+ = render "ci/status/badge", link: false, status: job.detailed_status(current_user)
+ %td.responsive-table-cell.build-name{ data: { column: _("Name")} }
+ = link_to build.name, pipeline_job_url(pipeline, build)
+ %td.responsive-table-cell.build-stage{ data: { column: _("Stage")} }
+ = build.stage.titleize
+ %td.responsive-table-cell.build-failure{ data: { column: _("Failure")} }
+ = build.present.callout_failure_message
+ %td.responsive-table-cell.build-actions
+ = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
+ = icon('repeat')
+ %tr.build-trace-row.responsive-table-border-end
+ %td
+ %td.responsive-table-cell.build-trace-container{ colspan: 4 }
+ %pre.build-trace.build-trace-rounded
+ %code.bash.js-build-output
+ = build_summary(build)
+
diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml
index 128f52ff648..3f05e06b0c6 100644
--- a/app/views/projects/project_members/_groups.html.haml
+++ b/app/views/projects/project_members/_groups.html.haml
@@ -3,5 +3,5 @@
Groups with access to
%strong= @project.name
%span.badge.badge-pill= group_links.size
- %ul.content-list
+ %ul.content-list.members-list
= render partial: 'shared/members/group', collection: group_links, as: :group_link
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index a30870a241c..0c5a187f208 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -9,9 +9,10 @@
%span.badge.badge-pill= members.total_count
= form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
- %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
- = icon("search")
+ .position-relative
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = icon("search")
= render 'shared/members/sort_dropdown'
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: members, as: :member
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index a56023e98cd..9716322f8a1 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -12,17 +12,17 @@
- else
%p
Members can be added by project
- %i Masters
+ %i Maintainers
or
%i Owners
.light
- if can?(current_user, :admin_project_member, @project)
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
- %li.active{ role: 'presentation' }
- %a{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member
+ %li.nav-tab{ role: 'presentation' }
+ %a.nav-link.active{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member
- if @project.allowed_to_share_with_group?
- %li{ role: 'presentation' }
- %a{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group
+ %li.nav-tab{ role: 'presentation' }
+ %a.nav-link{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group
.tab-content.gitlab-tab-content
.tab-pane.active{ id: 'add-member-pane', role: 'tabpanel' }
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index a2cd7752fc4..9a06eca89bb 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -1,7 +1,7 @@
.protected-branches-list.js-protected-branches-list.qa-protected-branches-list
- if @protected_branches.empty?
- .card-header
- %h3.card-title
+ .card-header.bg-white
+ %h3.card-title.mb-0
Protected branch (#{@protected_branches_count})
%p.settings-message.text-center
There are currently no protected branches, protect a branch with the form above.
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index fd5c1aa342a..4f1c6c92484 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = Rails.env.test?
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.qa-protected-branches-settings.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Protected Branches
@@ -12,8 +12,8 @@
%p
By default, protected branches are designed to:
%ul
- %li prevent their creation, if not already created, from everybody except Masters
- %li prevent pushes from everybody except Masters
+ %li prevent their creation, if not already created, from everybody except Maintainers
+ %li prevent pushes from everybody except Maintainers
%li prevent <strong>anyone</strong> from force pushing to the branch
%li prevent <strong>anyone</strong> from deleting the branch
%p Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches")} and #{link_to "project permissions", help_page_path("user/permissions")}.
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index c33723d8072..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
@@ -12,7 +12,7 @@
%p
By default, protected tags are designed to:
%ul
- %li Prevent tag creation by everybody except Masters
+ %li Prevent tag creation by everybody except Maintainers
%li Prevent <strong>anyone</strong> from updating the tag
%li Prevent <strong>anyone</strong> from deleting the tag
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index d07bb661615..506bf54b3f8 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -8,6 +8,8 @@
row.find("td.tree-time-ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
row.find("td.tree-commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
+ = render_if_exists 'projects/refs/logs_tree_lock_label', lock_label: content_data[:lock_label]
+
- if @more_log_url
:plain
if($('#tree-slider').length) {
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index b4787032966..d6f758608a0 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -13,7 +13,7 @@
= form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name), html: { class: 'common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
- = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
+ = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here…"
= render 'shared/notes/hints'
.error-alert
.prepend-top-default
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
index dfed0553f84..86de71c732b 100644
--- a/app/views/projects/runners/_group_runners.html.haml
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -26,9 +26,9 @@
- if can?(current_user, :admin_pipeline, @project.group)
- group_link = link_to _('Group CI/CD settings'), group_settings_ci_cd_path(@project.group)
- = _('Group masters can register group runners in the %{link}').html_safe % { link: group_link }
+ = _('Group maintainers can register group runners in the %{link}').html_safe % { link: group_link }
- else
- = _('Ask your group master to setup a group Runner.')
+ = _('Ask your group maintainer to setup a group Runner.')
- else
%h4.underlined-title
diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml
index acbab8b85c9..16e48814578 100644
--- a/app/views/projects/services/_index.html.haml
+++ b/app/views/projects/services/_index.html.haml
@@ -8,7 +8,7 @@
%colgroup
%col
%col
- %col.d-none.d-sm-block
+ %col
%col{ width: "120" }
%thead
%tr
diff --git a/app/views/projects/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 62bef77be97..f51dd581d29 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,18 +1,19 @@
- enabled = Gitlab.config.mattermost.enabled
-.card
- %p
- This service allows users to perform common operations on this
- project by entering slash commands in Mattermost.
- = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
- View documentation
- = icon('external-link')
- %p.inline
- See list of available commands in Mattermost after setting up this service,
- by entering
- %kbd.inline /&lt;trigger&gt; help
- - unless enabled || @service.template?
- = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
+.info-well
+ .well-segment
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Mattermost.
+ = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
+ View documentation
+ = sprite_icon('external-link', size: 16)
+ %p.inline
+ See list of available commands in Mattermost after setting up this service,
+ by entering
+ %kbd.inline /&lt;trigger&gt; help
+ - unless enabled || @service.template?
+ = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
- if enabled && !@service.template?
= render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
diff --git a/app/views/projects/services/prometheus/_configuration_banner.html.haml b/app/views/projects/services/prometheus/_configuration_banner.html.haml
index 2cc2a6b2b5b..898b55e4b39 100644
--- a/app/views/projects/services/prometheus/_configuration_banner.html.haml
+++ b/app/views/projects/services/prometheus/_configuration_banner.html.haml
@@ -2,7 +2,7 @@
= s_('PrometheusService|Auto configuration')
- if service.manual_configuration?
- .well
+ .info-well
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
- else
.container-fluid
diff --git a/app/views/projects/services/prometheus/_help.html.haml b/app/views/projects/services/prometheus/_help.html.haml
index 15e7362c2ba..35d655e4b32 100644
--- a/app/views/projects/services/prometheus/_help.html.haml
+++ b/app/views/projects/services/prometheus/_help.html.haml
@@ -5,5 +5,5 @@
= s_('PrometheusService|Manual configuration')
- unless @service.editable?
- .card
+ .info-well
= s_('PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters')
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 44f58ad05e5..f25d2ecdfb1 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,99 +1,100 @@
- pretty_name = defined?(@project) ? @project.full_name : 'namespace / path'
- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
-.card
- %p
- This service allows users to perform common operations on this
- project by entering slash commands in Slack.
- = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
- View documentation
- = icon('external-link')
- %p.inline
- See list of available commands in Slack after setting up this service,
- by entering
- %kbd.inline /&lt;command&gt; help
- - unless @service.template?
- %p To setup this service:
- %ul.list-unstyled.indent-list
- %li
- 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')
- in your Slack team with these options:
+.info-well
+ .well-segment
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Slack.
+ = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
+ View documentation
+ = sprite_icon('external-link', size: 16)
+ %p.inline
+ See list of available commands in Slack after setting up this service,
+ by entering
+ %kbd.inline /&lt;command&gt; help
+ - unless @service.template?
+ %p To setup this service:
+ %ul.list-unstyled.indent-list
+ %li
+ 1.
+ = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = sprite_icon('external-link', size: 16)
+ in your Slack team with these options:
- %hr
+ %hr
- .help-form
- .form-group
- = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block
- %p Fill in the word that works best for your team.
- %p
- Suggestions:
- %code= 'gitlab'
- %code= @project.path # Path contains no spaces, but dashes
- %code= @project.full_path
+ .help-form
+ .form-group
+ = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block
+ %p Fill in the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.full_path
- .form-group
- = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
- = text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
- .input-group-append
- = clipboard_button(target: '#url', class: 'input-group-text')
+ .form-group
+ = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#url', class: 'input-group-text')
- .form-group
- = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block POST
+ .form-group
+ = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block POST
- .form-group
- = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
- = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
- .input-group-append
- = clipboard_button(target: '#customize_name', class: 'input-group-text')
+ .form-group
+ = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#customize_name', class: 'input-group-text')
- .form-group
- = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block
- = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
- = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
+ .form-group
+ = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block
+ = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
+ = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
- .form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block Show this command in the autocomplete list
+ .form-group
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block Show this command in the autocomplete list
- .form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
- = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
- .input-group-append
- = clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
+ .form-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
- .form-group
- = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
- = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
- .input-group-append
- = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
+ .form-group
+ = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
- .form-group
- = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
- = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly'
- .input-group-append
- = clipboard_button(target: '#descriptive_label', class: 'input-group-text')
+ .form-group
+ = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#descriptive_label', class: 'input-group-text')
- %hr
+ %hr
- %ul.list-unstyled.indent-list
- %li
- 2. Paste the
- %strong Token
- into the field below
- %li
- 3. Select the
- %strong Active
- checkbox, press
- %strong Save changes
- and start using GitLab inside Slack!
+ %ul.list-unstyled.indent-list
+ %li
+ 2. Paste the
+ %strong Token
+ into the field below
+ %li
+ 3. Select the
+ %strong Active
+ checkbox, press
+ %strong Save changes
+ and start using GitLab inside Slack!
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 bbabb98dafe..31c2616d283 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -1,43 +1,66 @@
-.row.prepend-top-default
+.row
.col-lg-12
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
- %fieldset.builds-feature
+ %fieldset.builds-feature.js-auto-devops-settings
.form-group
- message = auto_devops_warning_message(@project)
- ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe
- if message
- %p.settings-message.text-center
+ %p.auto-devops-warning-message.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
- .form-check
- = form.radio_button :enabled, 'true', class: 'form-check-input'
- = form.label :enabled_true, class: 'form-check-label' do
- %strong= s_('CICD|Enable Auto DevOps')
- %br
- = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
+ .card.auto-devops-card
+ .card-body
+ .form-check
+ = form.radio_button :enabled, 'true', class: 'form-check-input js-toggle-extra-settings'
+ = form.label :enabled_true, class: 'form-check-label' do
+ %strong= s_('CICD|Enable Auto DevOps')
+ .form-text.text-muted
+ = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
- .form-check
- = form.radio_button :enabled, 'false', class: 'form-check-input'
- = form.label :enabled_false, class: 'form-check-label' do
- %strong= s_('CICD|Disable Auto DevOps')
- %br
- = 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 }
+ .card.auto-devops-card
+ .card-body
+ .form-check
+ = form.radio_button :enabled, '', class: 'form-check-input js-toggle-extra-settings'
+ = form.label :enabled_, class: 'form-check-label' do
+ %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
+ .form-text.text-muted
+ = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
- .form-check
- = form.radio_button :enabled, '', class: 'form-check-input'
- = form.label :enabled_, class: 'form-check-label' do
- %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
- %br
- = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
+ .card.auto-devops-card.js-extra-settings{ class: form.object&.enabled == false ? 'hidden' : nil }
+ .card-body.bg-light
+ = form.label :domain do
+ %strong= _('Domain')
+ = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
+ .form-text.text-muted
+ = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
+ - if cluster_ingress_ip = cluster_ingress_ip(@project)
+ = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe }
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank'
- = form.label :domain, class:"prepend-top-10" do
- = _('Domain')
- = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
- .form-text.text-muted
- = s_('CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages.')
- - if cluster_ingress_ip = cluster_ingress_ip(@project)
- = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe }
- = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank'
+ %label.prepend-top-10
+ %strong= s_('CICD|Deployment strategy')
+ %p.settings-message.text-center
+ = s_('CICD|Deployment strategy needs a domain name to work correctly.')
+ .form-check
+ = form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input'
+ = form.label :deploy_strategy_continuous, class: 'form-check-label' do
+ %strong= s_('CICD|Continuous deployment to production')
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-deploy'), target: '_blank'
+ .form-check
+ = form.radio_button :deploy_strategy, 'manual', class: 'form-check-input'
+ = form.label :deploy_strategy_manual, class: 'form-check-label' do
+ %strong= s_('CICD|Automatic deployment to staging, manual deployment to production')
+ = link_to icon('question-circle'), help_page_path('ci/environments.md', anchor: 'manually-deploying-to-environments'), target: '_blank'
- = f.submit 'Save changes', class: "btn btn-success prepend-top-15"
+ .card.auto-devops-card
+ .card-body
+ .form-check
+ = form.radio_button :enabled, 'false', class: 'form-check-input js-toggle-extra-settings', data: { hide_extra_settings: true }
+ = form.label :enabled_false, class: 'form-check-label' do
+ %strong= s_('CICD|Disable Auto DevOps')
+ .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"
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index e93b240a007..5025460a2d0 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 hidden js-secret-value", placeholder: 'xEeFCaDAB89'
- %p.form-text.text-muted The secure token used by the Runner to checkout the project
+ = 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")
%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) -
@@ -120,7 +118,7 @@
JaCoCo (Java/Kotlin)
%code Total.*?([0-9]{1,3})%
- = 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 ed17bd4f7dc..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,15 +8,15 @@
%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
.settings-header
%h4
- General pipelines settings
+ = _("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'
-%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
+%section.qa-autodevops-settings.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= s_('CICD|Auto DevOps')
@@ -28,38 +28,36 @@
.settings-content
= render 'autodevops_form'
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.qa-runners-settings.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
- Runners settings
+ = _("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'
-%section.settings.no-animate{ class: ('expanded' if expanded) }
+%section.qa-variables-settings.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Variables')
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
+ = 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/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index b596748ca5f..da822ac5675 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -36,7 +36,7 @@
= label_tag :release_description, s_('TagsPage|Release notes'), class: 'col-form-label col-sm-2'
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
- = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here...'), current_text: @release_description
+ = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here…'), current_text: @release_description
= render 'shared/notes/hints'
.form-text.text-muted
= s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.')
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 4fc1a284693..9d196075bf1 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -6,7 +6,7 @@
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- if on_top_of_branch?
- - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
+ - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown', 'data-boundary': 'window' }
- else
- addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index bcceb69954a..26fe1de31fe 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -25,7 +25,7 @@
.col-sm-12= f.label :content, class: 'control-label-full-width'
.col-sm-12
= render layout: 'projects/md_preview', locals: { url: project_wiki_preview_markdown_path(@project, @page.slug) } do
- = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here...")
+ = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here…")
= render 'shared/notes/hints'
.clearfix
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 35c7dc2984a..d80d2957466 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,4 +1,4 @@
-- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
+- @content_class = "limit-container-width" unless fluid_layout
- page_title _("Edit"), @page.title.capitalize, _("Wiki")
= wiki_page_errors(@error)
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 909987d8090..8c2cbd495a0 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,4 +1,4 @@
-- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
+- @content_class = "limit-container-width" unless fluid_layout
- page_title s_("WikiClone|Git Access"), _("Wiki")
.wiki-page-header.has-sidebar-toggle
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index ff72c8bb75d..a08973c7f32 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,4 +1,4 @@
-- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
+- @content_class = "limit-container-width" unless fluid_layout
- breadcrumb_title @page.title.capitalize
- wiki_breadcrumb_dropdown_links(@page.slug)
- page_title @page.title.capitalize, _("Wiki")
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index de473c23d66..fdcd126e7a3 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,13 +1,5 @@
-- file_name, blob = blob
-.blob-result
- .file-holder
- .js-file-title.file-title
- - ref = @search_results.repository_ref
- - blob_link = project_blob_path(@project, tree_join(ref, file_name))
- = link_to blob_link do
- %i.fa.fa-file
- %strong
- = file_name
- - if blob
- .file-content.code.term
- = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
+- project = find_project_for_result_blob(blob)
+- file_name, blob = parse_search_result(blob)
+- blob_link = project_blob_path(project, tree_join(blob.ref, file_name))
+
+= render partial: 'search/results/blob_data', locals: { blob: blob, project: project, file_name: file_name, blob_link: blob_link }
diff --git a/app/views/search/results/_blob_data.html.haml b/app/views/search/results/_blob_data.html.haml
new file mode 100644
index 00000000000..143e9f91ca3
--- /dev/null
+++ b/app/views/search/results/_blob_data.html.haml
@@ -0,0 +1,10 @@
+.blob-result
+ .file-holder
+ .js-file-title.file-title
+ = link_to blob_link do
+ %i.fa.fa-file
+ %strong
+ = search_blob_title(project, file_name)
+ - if blob.data
+ .file-content.code.term
+ = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline
diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml
index f34eaf89027..ed5a3badf11 100644
--- a/app/views/search/results/_commit.html.haml
+++ b/app/views/search/results/_commit.html.haml
@@ -1 +1 @@
-= render 'projects/commits/commit', project: @project, commit: commit, ref: nil
+= render 'projects/commits/commit', project: commit.project, commit: commit, ref: nil, show_project_name: @project.nil?
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index 16a0e432d62..4346217c230 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -1,10 +1,5 @@
-- wiki_blob = parse_search_result(wiki_blob)
-.blob-result
- .file-holder
- .js-file-title.file-title
- = link_to project_wiki_path(@project, wiki_blob.basename) do
- %i.fa.fa-file
- %strong
- = wiki_blob.basename
- .file-content.code.term
- = render 'shared/file_highlight', blob: wiki_blob, first_line_number: wiki_blob.startline
+- project = find_project_for_result_blob(wiki_blob)
+- file_name, wiki_blob = parse_search_result(wiki_blob)
+- wiki_blob_link = project_wiki_path(project, wiki_blob.basename)
+
+= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: file_name, blob_link: wiki_blob_link }
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 5fc02ba3160..3655c2a1d42 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -11,7 +11,7 @@
%span
= default_clone_protocol.upcase
= icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown
+ %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown
%li
= ssh_clone_button(project)
%li
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
index 0c8d90d92f5..b89045e726a 100644
--- a/app/views/shared/_field.html.haml
+++ b/app/views/shared/_field.html.haml
@@ -9,11 +9,11 @@
- help = field[:help]
- disabled = disable_fields_service?(@service)
-.form-group
+.form-group.row
- if type == "password" && value.present?
- = form.label name, "Enter new #{title.downcase}", class: "col-form-label"
+ = form.label name, "Enter new #{title.downcase}", class: "col-form-label col-sm-2"
- else
- = form.label name, title, class: "col-form-label"
+ = form.label name, title, class: "col-form-label col-sm-2"
.col-sm-10
- if type == 'text'
= form.text_field name, class: "form-control", placeholder: placeholder, required: required, disabled: disabled
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index ba5b65a209d..e93925b5ef9 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -1,93 +1,72 @@
- label_css_id = dom_id(label)
- status = label_subscription_status(label, @project).inquiry if current_user
- subject = local_assigns[:subject]
+- use_label_priority = local_assigns.fetch(:use_label_priority, false)
+- force_priority = local_assigns.fetch(:force_priority, use_label_priority ? label.priority.present? : false)
- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
+- tooltip_title = label_status_tooltip(label, status) if status
%li.label-list-item{ id: label_css_id, data: { id: label.id } }
- = render "shared/label_row", label: label
-
- .d-inline-block.d-sm-none.dropdown
- %button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } }
- Options
- = icon('caret-down')
- .dropdown-menu.dropdown-menu-right
- %ul
- - if show_label_merge_requests_link
- %li
- = link_to_label(label, subject: subject, type: :merge_request) do
- View merge requests
- - if show_label_issues_link
- %li
- = link_to_label(label, subject: subject) do
- View open issues
- - if current_user
- %li.label-subscription
- - if can_subscribe_to_label_in_different_levels?(label)
- %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } }
- %span Unsubscribe
- %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } }
- %span Subscribe at project level
- %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } }
- %span Subscribe at group level
- - else
- %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_path } }
- %span= label_subscription_toggle_button_text(label, @project)
-
- - if can?(current_user, :admin_label, label)
- %li
- = link_to 'Edit', edit_label_path(label)
- %li
- = link_to 'Delete',
- destroy_label_path(label),
- title: 'Delete',
- method: :delete,
- data: {confirm: 'Remove this label? Are you sure?'},
- class: 'text-danger'
-
- .float-right.d-none.d-sm-none.d-md-block
+ = render "shared/label_row", label: label, subject: subject, force_priority: force_priority
+ %ul.label-actions-list
+ - if @project
+ %li.inline
+ .label-badge.label-badge-gray= label.model_name.human.capitalize
+ - if can?(current_user, :admin_label, @project)
+ %li.inline.js-toggle-priority{ data: { url: remove_priority_project_label_path(@project, label),
+ dom_id: dom_id(label), type: label.type } }
+ %button.label-action.add-priority.btn.btn-transparent.has-tooltip{ title: _('Prioritize'), type: 'button', data: { placement: 'top' }, aria_label: _('Prioritize label') }
+ = 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)
- - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
- %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
- disabled: true,
- type: 'button',
- data: { url: promote_project_label_path(label.project, label),
- label_title: label.title,
- label_color: label.color,
- label_text_color: label.text_color,
- group_name: label.project.group.name,
- target: '#promote-label-modal',
- container: 'body',
- toggle: 'modal' } }
- = sprite_icon('level-up')
- = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
- %span.sr-only Edit
- = sprite_icon('pencil')
- %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
- = link_to "#", title: "Delete", class: 'btn btn-transparent btn-action remove-row', data: { toggle: "tooltip" } do
- %span.sr-only Delete
- = sprite_icon('remove')
+ %li.inline
+ = link_to edit_label_path(label), class: 'btn btn-transparent label-action edit', aria_label: 'Edit label' do
+ = sprite_icon('pencil')
+ %li.inline
+ .dropdown
+ %button{ type: 'button', class: 'btn btn-transparent js-label-options-dropdown label-action', data: { toggle: 'dropdown' }, aria_label: _('Label actions dropdown') }
+ = sprite_icon('ellipsis_v')
+ .dropdown-menu.dropdown-open-left
+ %ul
+ - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
+ %li
+ %button.js-promote-project-label-button.btn.btn-transparent.btn-action{ disabled: true, type: 'button',
+ data: { url: promote_project_label_path(label.project, label),
+ label_title: label.title,
+ label_color: label.color,
+ label_text_color: label.text_color,
+ group_name: label.project.group.name,
+ target: '#promote-label-modal',
+ container: 'body',
+ toggle: 'modal' } }
+ = _('Promote to group label')
+ - 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
- .label-subscription.inline
+ %li.inline.label-subscription
- if can_subscribe_to_label_in_different_levels?(label)
- %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } }
- %span Unsubscribe
- = icon('spinner spin', class: 'label-subscribe-button-loading')
-
+ %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
+ %span= _('Unsubscribe')
.dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
- %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span Subscribe
- = icon('chevron-down')
- %ul.dropdown-menu
- %li
- %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } }
- Project level
- %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } }
- Group level
+ %button.label-subscribe-button.btn.btn-default{ data: { toggle: 'dropdown' } }
+ %span
+ = _('Subscribe')
+ = sprite_icon('chevron-down')
+ .dropdown-menu.dropdown-open-left
+ %ul
+ %li
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_project_label_path(@project, label) } }
+ %span= _('Subscribe at project level')
+ %li
+ %button.js-subscribe-button.js-group-level.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } }
+ %span= _('Subscribe at group level')
- else
- %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_path } }
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
%span= label_subscription_toggle_button_text(label, @project)
- = icon('spinner spin', class: 'label-subscribe-button-loading')
= render 'shared/delete_label_modal', label: label
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index f1c1ca9b2c9..0ae3ab8f090 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -1,30 +1,23 @@
- subject = local_assigns[:subject]
+- force_priority = local_assigns.fetch(:force_priority, false)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
-%span.label-row
- - if can?(current_user, :admin_label, @project)
- .draggable-handler
- = icon('bars')
- .js-toggle-priority.toggle-priority{ data: { url: remove_priority_project_label_path(@project, label),
- dom_id: dom_id(label), type: label.type } }
- %button.add-priority.btn.has-tooltip{ title: 'Prioritize', type: 'button', :'data-placement' => 'top' }
- = icon('star-o')
- %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', type: 'button', :'data-placement' => 'top' }
- = icon('star')
- %span.label-name
- = link_to_label(label, subject: @project, tooltip: false)
- - if defined?(@project) && @project.group.present?
- %span.label-type
- = label.model_name.human.titleize
-
- %span.label-description
+.label-name
+ = link_to_label(label, subject: @project, tooltip: false)
+.label-description
+ .append-right-default.prepend-left-default
- if label.description.present?
- .description-text
+ .description-text.append-bottom-10
= markdown_field(label, :description)
- .d-none.d-sm-none.d-md-block
+ %ul.label-links
- if show_label_issues_link
- = link_to_label(label, subject: subject) { 'Issues' }
+ %li.label-link-item.inline
+ = link_to_label(label, subject: subject) { 'Issues' }
- if show_label_merge_requests_link
&middot;
- = link_to_label(label, subject: subject, type: :merge_request) { 'Merge requests' }
+ %li.label-link-item.inline
+ = link_to_label(label, subject: subject, type: :merge_request) { _('Merge requests') }
+ - if force_priority
+ %li.label-link-item.js-priority-badge.inline.prepend-left-10
+ .label-badge.label-badge-blue= _('Prioritized 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/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml
index 165109b6b70..24c0dfe247f 100644
--- a/app/views/shared/_new_merge_request_checkbox.html.haml
+++ b/app/views/shared/_new_merge_request_checkbox.html.haml
@@ -1,4 +1,4 @@
-.form-check
+.form-check.prepend-top-8
- nonce = SecureRandom.hex
= check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request form-check-input', id: "create_merge_request-#{nonce}"
= label_tag "create_merge_request-#{nonce}", class: 'form-check-label' do
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/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 0ebf365c7bd..6fa61c15493 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -3,8 +3,9 @@
- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true)
= render "projects/services/#{@service.to_param}/help", subject: subject
- elsif @service.help.present?
- .card
- = markdown @service.help
+ .info-well
+ .well-segment
+ = markdown @service.help
.service-settings
- if @service.show_active_box?
@@ -15,25 +16,24 @@
- if @service.configurable_events.present?
.form-group.row
- = form.label :url, "Trigger", class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right Trigger
.col-sm-10
- @service.configurable_events.each do |event|
- %div
- = form.check_box service_event_field_name(event), class: 'float-left'
- .prepend-left-20
- = form.label service_event_field_name(event), class: 'list-label' do
+ .form-group
+ .form-check
+ = form.check_box service_event_field_name(event), class: 'form-check-input'
+ = form.label service_event_field_name(event), class: 'form-check-label' do
%strong
= event.humanize
- - field = @service.event_field(event)
+ - field = @service.event_field(event)
- - if field
- %p
+ - if field
= form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
- %p.light
- = @service.class.event_description(event)
+ %p.text-muted
+ = @service.class.event_description(event)
- @service.global_fields.each do |field|
- type = field[:type]
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index d67409ffe14..01ce1225b8d 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -1,11 +1,11 @@
- with_label = local_assigns.fetch(:with_label, true)
-.form-group.visibility-level-setting
+.form-group.row.visibility-level-setting
- if with_label
= f.label :visibility_level, class: 'col-form-label col-sm-2' do
Visibility Level
= link_to icon('question-circle'), help_page_path("public_access/public_access")
- %div{ :class => ("col-sm-10" if with_label) }
+ %div{ :class => (with_label ? "col-sm-10" : "col-sm-12") }
- if can_change_visibility_level
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
- else
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 496b94ec953..a88d8f61fb4 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -3,8 +3,8 @@
- @no_breadcrumb_container = true
- @no_container = true
- @content_class = "issue-boards-content"
-- breadcrumb_title "Issue Board"
-- page_title "Boards"
+- breadcrumb_title _("Issue Board")
+- page_title _("Boards")
- content_for :page_specific_javascripts do
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index 67476a3f573..03e008f5fa0 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -1,4 +1,4 @@
-.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded }',
+.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }',
":data-id" => "list.id" }
.board-inner
%header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" }
@@ -7,10 +7,18 @@
":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded }",
"aria-hidden": "true" }
+ %a.user-avatar-link.js-no-trigger{ "v-if": "list.type === \"assignee\"", ":href": "list.assignee.path" }
+ -# haml-lint:disable AltText
+ %img.avatar.s20.has-tooltip{ height: "20", width: "20", ":src": "list.assignee.avatar", ":alt": "list.assignee.name" }
+
%span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"",
- ":title" => '(list.label ? list.label.description : "")', data: { container: "body" } }
+ ":title" => '((list.label && list.label.description) || list.title || "")', data: { container: "body" } }
{{ list.title }}
+ %span.board-title-sub-text.prepend-left-5.has-tooltip{ "v-if": "list.type === \"assignee\"",
+ ":title" => '(list.assignee && list.assignee.username || "")' }
+ @{{ list.assignee.username }}
+
%span.has-tooltip{ "v-if": "list.type === \"label\"",
":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" },
@@ -22,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",
@@ -47,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 a112a9f1f7e..607e7f471c9 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -1,15 +1,15 @@
.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" }
- %span.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
+ .badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
{{ label.title }}
- if can_admin_issue?
.selectbox
@@ -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/builds/_build_output.html.haml b/app/views/shared/builds/_build_output.html.haml
new file mode 100644
index 00000000000..0e18128a8f1
--- /dev/null
+++ b/app/views/shared/builds/_build_output.html.haml
@@ -0,0 +1,3 @@
+%pre.build-trace#build-trace
+ %code.bash.js-build-output
+ .build-loader-animation.js-build-refresh
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/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml
index fabb1f39a34..f1a41074c28 100644
--- a/app/views/shared/empty_states/_wikis.html.haml
+++ b/app/views/shared/empty_states/_wikis.html.haml
@@ -8,7 +8,7 @@
%h4
= s_('WikiEmpty|The wiki lets you write documentation for your project')
%p.text-left
- = s_("WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, it's principles, how to use it, and so on.")
+ = s_("WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on.")
= create_link
- elsif can?(current_user, :read_issue, @project)
diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml
index e5dfa7dbf71..25df2fe5cd6 100644
--- a/app/views/shared/form_elements/_description.html.haml
+++ b/app/views/shared/form_elements/_description.html.haml
@@ -16,7 +16,7 @@
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'projects/zen', f: form, attr: :description,
classes: 'note-textarea qa-issuable-form-description',
- placeholder: "Write a comment or drag your files here...",
+ placeholder: "Write a comment or drag your files here…",
supports_quick_actions: supports_quick_actions
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
.clearfix
diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
new file mode 100644
index 00000000000..23b2e1b91e5
--- /dev/null
+++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
@@ -0,0 +1,8 @@
+.dropdown.prepend-left-10#js-add-list
+ %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
+ Add list
+ .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
+ = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
+ - if can?(current_user, :admin_label, board.parent)
+ = render partial: "shared/issuable/label_page_create"
+ = dropdown_loading
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index 9f4021802df..55edaa7eda4 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -1,6 +1,7 @@
+- show_close = local_assigns.fetch(:show_close, true)
- subject = @project || @group
.dropdown-page-two.dropdown-new-label
- = dropdown_title(create_label_title(subject), options: { back: true })
+ = dropdown_title(create_label_title(subject), options: { back: true, close: show_close })
= dropdown_content do
.dropdown-labels-error.js-label-error
%input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') }
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index 2bd922bca2b..aa4a5f0e0d3 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -1,15 +1,18 @@
- title = local_assigns.fetch(:title, _('Assign labels'))
+- content_title = local_assigns.fetch(:content_title, _('Create lists from labels. Issues with that label appear in that list.'))
+- show_title = local_assigns.fetch(:show_title, true)
- show_create = local_assigns.fetch(:show_create, true)
- show_footer = local_assigns.fetch(:show_footer, true)
- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search')
- show_boards_content = local_assigns.fetch(:show_boards_content, false)
- subject = @project || @group
.dropdown-page-one
- = dropdown_title(title)
+ - if show_title
+ = dropdown_title(title)
- if show_boards_content
.issue-board-dropdown-content
%p
- = _('Create lists from labels. Issues with that label appear in that list.')
+ = content_title
= dropdown_filter(filter_placeholder)
= dropdown_content
- if current_board_parent && show_footer
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 955b8866c2c..37625a4a163 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -1,6 +1,8 @@
- project = @target_project || @project
- extra_class = extra_class || ''
- show_menu_above = show_menu_above || false
+- selected = local_assigns.fetch(:selected, nil)
+
- selected_text = selected.try(:title) || params[:milestone_title]
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone")
- if selected.present? || params[:milestone_title].present?
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 644f7c4dd28..ef9ea2194ee 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -104,14 +104,7 @@
.filter-dropdown-container
- if type == :boards
- if can?(current_user, :admin_list, board.parent)
- .dropdown.prepend-left-10#js-add-list
- %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
- Add list
- .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
- = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- - if can?(current_user, :admin_label, board.parent)
- = render partial: "shared/issuable/label_page_create"
- = dropdown_loading
+ = render_if_exists '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) } }
- elsif type != :boards_modal
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 9e50e888b35..0ca35ea1298 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -43,7 +43,7 @@
.selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
- = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: project_milestones_path(@project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true, default_no: true, selected: (issuable.milestone.name if issuable.milestone), null_default: true }})
+ = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: project_milestones_path(@project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true, default_no: true, selected: (issuable.milestone.name if issuable.milestone), null_default: true, display: 'static' }})
- if issuable.has_attribute?(:time_estimate)
#issuable-time-tracker.block
// Fallback while content is loading
@@ -77,7 +77,7 @@
.selectbox.hide-collapsed
= f.hidden_field :due_date, value: issuable.due_date.try(:strftime, 'yy-mm-dd')
.dropdown
- %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } }
+ %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), display: 'static' } }
%span.dropdown-toggle-text
= _('Due date')
= icon('chevron-down', 'aria-hidden': 'true')
@@ -109,7 +109,7 @@
- selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project) } }
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project), display: 'static' } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index e1cde527ad7..8a13c7a3b83 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -37,7 +37,7 @@
- issuable.assignees.each do |assignee|
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
- - options = { toggle_class: 'js-user-search js-author-search', title: _('Assign to'), filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: _('Search users'), data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }
+ - options = { toggle_class: 'js-user-search js-author-search', title: _('Assign to'), filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: _('Search users'), data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true, display: 'static' } }
- title = _('Select assignee')
- if issuable.is_a?(Issue)
@@ -50,7 +50,7 @@
- data[:multi_select] = true
- data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- - data['max-select'] = dropdown_options[:data][:'max-select']
+ - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
= dropdown_tag(title, options: options)
diff --git a/app/views/shared/issuable/form/_contribution.html.haml b/app/views/shared/issuable/form/_contribution.html.haml
index b34549240e0..bc9a1edc39c 100644
--- a/app/views/shared/issuable/form/_contribution.html.haml
+++ b/app/views/shared/issuable/form/_contribution.html.haml
@@ -7,14 +7,14 @@
%hr
-.form-group
- .col-form-label
+.form-group.row
+ %label.col-form-label.col-sm-2
= _('Contribution')
.col-sm-10
- .form-check
- = form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user), class: 'form-check-input'
- = form.label :allow_maintainer_to_push, class: 'form-check-label' do
- = _('Allow edits from maintainers.')
- = link_to 'About this feature', help_page_path('user/project/merge_requests/maintainer_access')
+ .form-check.prepend-top-5
+ = form.check_box :allow_collaboration, disabled: !issuable.can_allow_collaboration?(current_user), class: 'form-check-input'
+ = form.label :allow_collaboration, class: 'form-check-label' do
+ = _('Allow commits from members who can merge to the target branch.')
+ = link_to 'About this feature', help_page_path('user/project/merge_requests/allow_collaboration')
.form-text.text-muted
- = allow_maintainer_push_unavailable_reason(issuable)
+ = allow_collaboration_unavailable_reason(issuable)
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index 1e27253aaeb..bd87bb38e77 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -15,17 +15,20 @@
- else
= render "shared/issuable/form/metadata_merge_request_assignee", issuable: issuable, form: form, has_due_date: has_due_date
.form-group.row.issue-milestone
- = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-2"}"
- .col-10{ class: ("col-md-8" if has_due_date) }
+ = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
+ .col-sm-10{ class: ("col-md-8" if has_due_date) }
.issuable-form-select-holder
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
.form-group.row
- has_labels = @labels && @labels.any?
- = form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-2"}"
+ = form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
= form.hidden_field :label_ids, multiple: true, value: ''
- .col-10{ class: "#{"col-md-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
+ .col-sm-10{ class: "#{"col-md-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
.issuable-form-select-holder
- = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
+ = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
+
+ = render_if_exists "shared/issuable/form/weight", issuable: issuable, form: form
+
- if has_due_date
.col-lg-6
.form-group.row
diff --git a/app/views/shared/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/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 67b8843a27f..d0b492b43f3 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -5,16 +5,16 @@
%li.member.group_member{ id: dom_id }
%span.list-item-name
= group_icon(group, class: "avatar s40", alt: '')
- %strong
- = link_to group.full_name, group_path(group)
- .cgray
- Given access #{time_ago_with_tooltip(group_link.created_at)}
- - if group_link.expires?
- ·
- %span{ class: ('text-warning' if group_link.expires_soon?) }
- Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
+ .user-info
+ = link_to group.full_name, group_path(group), class: 'member'
+ .cgray
+ Given access #{time_ago_with_tooltip(group_link.created_at)}
+ - if group_link.expires?
+ ·
+ %span{ class: ('text-warning' if group_link.expires_soon?) }
+ Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls
- = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form' do
+ = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group row append-right-5' do
= hidden_field_tag "group_link[group_access]", group_link.group_access
.member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
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/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml
index 8923e5602a4..71a5b94e958 100644
--- a/app/views/shared/notes/_edit_form.html.haml
+++ b/app/views/shared/notes/_edit_form.html.haml
@@ -3,7 +3,7 @@
= hidden_field_tag :target_id, '', class: 'js-form-target-id'
= hidden_field_tag :target_type, '', class: 'js-form-target-type'
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do
- = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
+ = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here…"
= render 'shared/notes/hints'
.note-form-actions.clearfix
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index 71c0d740bc8..6b2715b47a7 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -29,7 +29,7 @@
= render 'projects/zen', f: f,
attr: :note,
classes: 'note-textarea js-note-text',
- placeholder: "Write a comment or drag your files here...",
+ placeholder: "Write a comment or drag your files here…",
supports_quick_actions: supports_quick_actions,
supports_autocomplete: supports_autocomplete
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
@@ -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 ca6e3602f05..d4e8f30e458 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -36,8 +36,6 @@
= note.author.to_reference
%span.note-headline-light
%span.note-headline-meta
- - unless note.system
- commented
- if note.system
%span.system-note-message
= markdown_field(note, :note)
@@ -61,7 +59,7 @@
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
- .system-note-commit-list-toggler
+ .system-note-commit-list-toggler.hide
Toggle commit list
%i.fa.fa-angle-down
- if note.attachment.url
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index b98d5339d2d..e0832fd9136 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -1,14 +1,13 @@
- issuable = @issue || @merge_request
- discussion_locked = issuable&.discussion_locked?
-- unless has_vue_discussions_cookie?
- %ul#notes-list.notes.main-notes-list.timeline
- = render "shared/notes/notes"
+%ul#notes-list.notes.main-notes-list.timeline
+ = render "shared/notes/notes"
= render 'shared/notes/edit_form', project: @project
- if can_create_note?
- %ul.notes.notes-form.timeline{ :class => ('hidden' if has_vue_discussions_cookie?) }
+ %ul.notes.notes-form.timeline
%li.timeline-entry
.timeline-entry-inner
.flash-container.timeline-content
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index e99d8d0973f..09ddf732ada 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -6,14 +6,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
= icon("caret-down")
diff --git a/app/views/shared/projects/_edit_information.html.haml b/app/views/shared/projects/_edit_information.html.haml
index ec9dc8f62c2..9230e045a81 100644
--- a/app/views/shared/projects/_edit_information.html.haml
+++ b/app/views/shared/projects/_edit_information.html.haml
@@ -1,6 +1,6 @@
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
- - if @project.branch_allows_maintainer_push?(current_user, selected_branch)
+ - if @project.branch_allows_collaboration?(current_user, selected_branch)
= commit_in_single_accessible_branch
- else
= commit_in_fork_help
diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml
index e50b7fa68dd..96527fcb4f2 100644
--- a/app/views/shared/runners/show.html.haml
+++ b/app/views/shared/runners/show.html.haml
@@ -21,17 +21,17 @@
%th Value
%tr
%td Active
- %td= @runner.active? ? _('Yes') : _('No')
+ %td= @runner.active? ? 'Yes' : 'No'
%tr
%td Protected
- %td= @runner.ref_protected? ? _('Yes') : _('No')
+ %td= @runner.active? ? _('Yes') : _('No')
%tr
- %td= _('Can run untagged jobs')
- %td= @runner.run_untagged? ? _('Yes') : _('No')
+ %td Can run untagged jobs
+ %td= @runner.run_untagged? ? 'Yes' : 'No'
- unless @runner.group_type?
%tr
- %td= _('Locked to this project')
- %td= @runner.locked? ? _('Yes') : _('No')
+ %td Locked to this project
+ %td= @runner.locked? ? 'Yes' : 'No'
%tr
%td Tags
%td
@@ -60,7 +60,7 @@
%td Description
%td= @runner.description
%tr
- %td= _('Maximum job timeout')
+ %td Maximum job timeout
%td= @runner.maximum_timeout_human_readable
%tr
%td Last contact
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index ae437dd16d6..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
- %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/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml
index e0fe551cf36..a869cf9cdee 100644
--- a/app/views/users/terms/index.html.haml
+++ b/app/views/users/terms/index.html.haml
@@ -1,13 +1,18 @@
- redirect_params = { redirect: @redirect } if @redirect
-.panel-content.rendered-terms
+.card-body.rendered-terms
= markdown_field(@term, :terms)
-.row-content-block.footer-block.clearfix
- - if can?(current_user, :accept_terms, @term)
- .float-right
- = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do
- = _('Accept terms')
- - if can?(current_user, :decline_terms, @term)
- .float-right
- = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do
- = _('Decline and sign out')
+- if current_user
+ .card-footer.footer-block.clearfix
+ - if can?(current_user, :accept_terms, @term)
+ .float-right
+ = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do
+ = _('Accept terms')
+ - else
+ .pull-right
+ = link_to root_path, class: 'btn btn-success prepend-left-8' do
+ = _('Continue')
+ - if can?(current_user, :decline_terms, @term)
+ .float-right
+ = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do
+ = _('Decline and sign out')
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 93e57512edb..b8b854853b7 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -11,14 +11,16 @@
- 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
- cronjob:stuck_import_jobs
- cronjob:stuck_merge_jobs
+- 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
@@ -30,12 +32,14 @@
- github_importer:github_import_import_diff_note
- github_importer:github_import_import_issue
- github_importer:github_import_import_note
+- github_importer:github_import_import_lfs_object
- github_importer:github_import_import_pull_request
- github_importer:github_import_refresh_import_jid
- github_importer:github_import_stage_finish_import
- github_importer:github_import_stage_import_base_data
- github_importer:github_import_stage_import_issues_and_diff_notes
- github_importer:github_import_stage_import_notes
+- github_importer:github_import_stage_import_lfs_objects
- github_importer:github_import_stage_import_pull_requests
- github_importer:github_import_stage_import_repository
@@ -68,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
@@ -115,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..9169f21af2a 100644
--- a/app/workers/archive_trace_worker.rb
+++ b/app/workers/archive_trace_worker.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ArchiveTraceWorker
include ApplicationWorker
include PipelineBackgroundQueue
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
new file mode 100644
index 00000000000..7016edde698
--- /dev/null
+++ b/app/workers/ci/archive_traces_cron_worker.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Ci
+ class ArchiveTracesCronWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ def perform
+ # Archive stale live traces which still resides in redis or database
+ # This could happen when ArchiveTraceWorker sidekiq jobs were lost by receiving SIGKILL
+ # More details in https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
+ Ci::Build.finished.with_live_trace.find_each(batch_size: 100) do |build|
+ begin
+ build.trace.archive!
+ rescue => e
+ failed_archive_counter.increment
+ Rails.logger.error "Failed to archive stale live trace. id: #{build.id} message: #{e.message}"
+ end
+ end
+ end
+
+ private
+
+ def failed_archive_counter
+ @failed_archive_counter ||= Gitlab::Metrics.counter(:job_trace_archive_failed_total, "Counter of failed attempts of traces archiving")
+ end
+ end
+end
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 be4203bc7ad..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
@@ -6,12 +8,6 @@ class GitGarbageCollectWorker
# Timeout set to 24h
LEASE_TIMEOUT = 86400
- GITALY_MIGRATED_TASKS = {
- gc: :garbage_collect,
- full_repack: :repack_full,
- incremental_repack: :repack_incremental
- }.freeze
-
def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil)
project = Project.find(project_id)
active_uuid = get_lease_uuid(lease_key)
@@ -27,21 +23,7 @@ class GitGarbageCollectWorker
end
task = task.to_sym
- cmd = command(task)
-
- gitaly_migrate(GITALY_MIGRATED_TASKS[task]) do |is_enabled|
- if is_enabled
- gitaly_call(task, project.repository.raw_repository)
- else
- repo_path = project.repository.path_to_repo
- description = "'#{cmd.join(' ')}' in #{repo_path}"
- Gitlab::GitLogger.info(description)
-
- output, status = Gitlab::Popen.popen(cmd, repo_path)
-
- Gitlab::GitLogger.error("#{description} failed:\n#{output}") unless status.zero?
- end
- end
+ gitaly_call(task, project.repository.raw_repository)
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
@@ -82,21 +64,12 @@ class GitGarbageCollectWorker
when :incremental_repack
client.repack_incremental
end
- end
-
- def command(task)
- case task
- when :gc
- git(write_bitmaps: bitmaps_enabled?) + %w[gc]
- when :full_repack
- git(write_bitmaps: bitmaps_enabled?) + %w[repack -A -d --pack-kept-objects]
- when :incremental_repack
- # Normal git repack fails when bitmaps are enabled. It is impossible to
- # create a bitmap here anyway.
- git(write_bitmaps: false) + %w[repack -d]
- else
- raise "Invalid gc task: #{task.inspect}"
- end
+ rescue GRPC::NotFound => e
+ Gitlab::GitLogger.error("#{method} failed:\nRepository not found")
+ raise Gitlab::Git::Repository::NoRepository.new(e)
+ rescue GRPC::BadStatus => e
+ Gitlab::GitLogger.error("#{method} failed:\n#{e}")
+ raise Gitlab::Git::CommandError.new(e)
end
def flush_ref_caches(project)
@@ -108,19 +81,4 @@ class GitGarbageCollectWorker
def bitmaps_enabled?
Gitlab::CurrentSettings.housekeeping_bitmaps_enabled
end
-
- def git(write_bitmaps:)
- config_value = write_bitmaps ? 'true' : 'false'
- %W[git -c repack.writeBitmaps=#{config_value}]
- end
-
- def gitaly_migrate(method, &block)
- Gitlab::GitalyClient.migrate(method, &block)
- rescue GRPC::NotFound => e
- Gitlab::GitLogger.error("#{method} failed:\nRepository not found")
- raise Gitlab::Git::Repository::NoRepository.new(e)
- rescue GRPC::BadStatus => e
- Gitlab::GitLogger.error("#{method} failed:\n#{e}")
- raise Gitlab::Git::CommandError.new(e)
- end
end
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index 8d708e15a66..be0b6c180b0 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -21,6 +21,7 @@ module Gitlab
STAGES = {
issues_and_diff_notes: Stage::ImportIssuesAndDiffNotesWorker,
notes: Stage::ImportNotesWorker,
+ lfs_objects: Stage::ImportLfsObjectsWorker,
finish: Stage::FinishImportWorker
}.freeze
diff --git a/app/workers/gitlab/github_import/import_lfs_object_worker.rb b/app/workers/gitlab/github_import/import_lfs_object_worker.rb
new file mode 100644
index 00000000000..520c5cb091a
--- /dev/null
+++ b/app/workers/gitlab/github_import/import_lfs_object_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class ImportLfsObjectWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::LfsObject
+ end
+
+ def importer_class
+ Importer::LfsObjectImporter
+ end
+
+ def counter_name
+ :github_importer_imported_lfs_objects
+ end
+
+ def counter_description
+ 'The number of imported GitHub Lfs Objects'
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
new file mode 100644
index 00000000000..29257603a9d
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class ImportLfsObjectsWorker
+ include ApplicationWorker
+ include GithubImport::Queue
+ include StageMethods
+
+ def perform(project_id)
+ return unless (project = find_project(project_id))
+
+ import(project)
+ end
+
+ # project - An instance of Project.
+ def import(project)
+ waiter = Importer::LfsObjectsImporter
+ .new(project, nil)
+ .execute
+
+ AdvanceStageWorker.perform_async(
+ project.id,
+ { waiter.key => waiter.jobs_remaining },
+ :finish
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
index 5f4678a595f..ccf0013180d 100644
--- a/app/workers/gitlab/github_import/stage/import_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
@@ -18,7 +18,7 @@ module Gitlab
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :finish
+ :lfs_objects
)
end
end
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..abe86066fb4 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
# Worker for updating any project specific caches.
class ProjectCacheWorker
include ApplicationWorker
+ include ExclusiveLeaseGuard
LEASE_TIMEOUT = 15.minutes.to_i
@@ -11,30 +14,30 @@ class ProjectCacheWorker
# statistics - An Array containing columns from ProjectStatistics to
# refresh, if empty all columns will be refreshed
def perform(project_id, files = [], statistics = [])
- project = Project.find_by(id: project_id)
-
- return unless project && project.repository.exists?
+ @project = Project.find_by(id: project_id)
+ return unless @project&.repository&.exists?
- update_statistics(project, statistics.map(&:to_sym))
+ update_statistics(statistics)
- project.repository.refresh_method_caches(files.map(&:to_sym))
+ @project.repository.refresh_method_caches(files.map(&:to_sym))
- project.cleanup
+ @project.cleanup
end
- def update_statistics(project, statistics = [])
- return unless try_obtain_lease_for(project.id, :update_statistics)
-
- Rails.logger.info("Updating statistics for project #{project.id}")
+ private
- project.statistics.refresh!(only: statistics)
+ def update_statistics(statistics = [])
+ try_obtain_lease do
+ Rails.logger.info("Updating statistics for project #{@project.id}")
+ @project.statistics.refresh!(only: statistics.to_a.map(&:to_sym))
+ end
end
- private
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
- def try_obtain_lease_for(project_id, section)
- Gitlab::ExclusiveLease
- .new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT)
- .try_obtain
+ def lease_key
+ "project_cache_worker:#{@project.id}:update_statistics"
end
end
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 08b1c3a7d7a..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
@@ -8,27 +10,12 @@ class RepositoryForkWorker
target_project_id = args.shift
target_project = Project.find(target_project_id)
- # By v10.8, we should've drained the queue of all jobs using the old arguments.
- # We can remove the else clause if we're no longer logging the message in that clause.
- # See https://gitlab.com/gitlab-org/gitaly/issues/1110
- if args.empty?
- source_project = target_project.forked_from_project
- unless source_project
- return target_project.mark_import_as_failed('Source project cannot be found.')
- end
-
- fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
- else
- Rails.logger.info("Project #{target_project.id} is being forked using old-style arguments.")
-
- source_repository_storage_path, source_disk_path = *args
-
- source_repository_storage_name = Gitlab.config.repositories.storages.find do |_, info|
- info.legacy_disk_path == source_repository_storage_path
- end&.first || raise("no shard found for path '#{source_repository_storage_path}'")
-
- fork_repository(target_project, source_repository_storage_name, source_disk_path)
+ source_project = target_project.forked_from_project
+ unless source_project
+ return target_project.mark_import_as_failed('Source project cannot be found.')
end
+
+ fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
end
private
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 f92421a667d..fa76fbac55c 100644
--- a/app/workers/storage_migrator_worker.rb
+++ b/app/workers/storage_migrator_worker.rb
@@ -1,29 +1,10 @@
+# frozen_string_literal: true
+
class StorageMigratorWorker
include ApplicationWorker
- BATCH_SIZE = 100
-
def perform(start, finish)
- projects = build_relation(start, finish)
-
- projects.with_route.find_each(batch_size: BATCH_SIZE) do |project|
- Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..."
-
- begin
- project.migrate_to_hashed_storage!
- rescue => err
- Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}")
- end
- end
- end
-
- def build_relation(start, finish)
- relation = Project
- table = Project.arel_table
-
- relation = relation.where(table[:id].gteq(start)) if start
- relation = relation.where(table[:id].lteq(finish)) if finish
-
- relation
+ migrator = Gitlab::HashedStorage::Migrator.new
+ migrator.bulk_migrate(start, finish)
end
end
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