summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorMatija Čupić <matteeyah@gmail.com>2017-12-21 18:30:34 +0100
committerMatija Čupić <matteeyah@gmail.com>2017-12-21 18:30:34 +0100
commit305bce8d246d2c6e88b5f22439c0ce0833eba1a3 (patch)
treee043cb4041c121957610f81d6a65790e91f84fb9 /app
parent614c0e0bf9c404ba43f835166183a2f1883071d1 (diff)
parentb8d79cc479200ff714f89dc43a3bbec18af3c5b5 (diff)
downloadgitlab-ce-305bce8d246d2c6e88b5f22439c0ce0833eba1a3.tar.gz
Merge branch 'master' into 39957-redirect-to-gpc-page-if-users-try-to-create-a-cluster-but-the-account-is-not-enabled
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/activities.js10
-rw-r--r--app/assets/javascripts/admin.js114
-rw-r--r--app/assets/javascripts/api.js14
-rw-r--r--app/assets/javascripts/aside.js24
-rw-r--r--app/assets/javascripts/behaviors/secret_values.js42
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/commit/image_file.js1
-rw-r--r--app/assets/javascripts/commits.js5
-rw-r--r--app/assets/javascripts/compare.js3
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue3
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js4
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js3
-rw-r--r--app/assets/javascripts/dispatcher.js52
-rw-r--r--app/assets/javascripts/docs/docs_bundle.js13
-rw-r--r--app/assets/javascripts/fly_out_nav.js17
-rw-r--r--app/assets/javascripts/gl_dropdown.js24
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js42
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js57
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue14
-rw-r--r--app/assets/javascripts/helpers/user_feature_helper.js7
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue66
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue (renamed from app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue)0
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue (renamed from app/assets/javascripts/repo/components/commit_sidebar/list_item.vue)0
-rw-r--r--app/assets/javascripts/ide/components/ide.vue73
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue75
-rw-r--r--app/assets/javascripts/ide/components/ide_project_branches_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_project_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_repo_tree.vue66
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue62
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue71
-rw-r--r--app/assets/javascripts/ide/components/new_branch_form.vue (renamed from app/assets/javascripts/repo/components/new_branch_form.vue)2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue101
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue (renamed from app/assets/javascripts/repo/components/new_dropdown/modal.vue)24
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue (renamed from app/assets/javascripts/repo/components/new_dropdown/upload.vue)35
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue (renamed from app/assets/javascripts/repo/components/repo_commit_section.vue)61
-rw-r--r--app/assets/javascripts/ide/components/repo_edit_button.vue (renamed from app/assets/javascripts/repo/components/repo_edit_button.vue)6
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue (renamed from app/assets/javascripts/repo/components/repo_editor.vue)42
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue (renamed from app/assets/javascripts/repo/components/repo_file.vue)74
-rw-r--r--app/assets/javascripts/ide/components/repo_file_buttons.vue (renamed from app/assets/javascripts/repo/components/repo_file_buttons.vue)0
-rw-r--r--app/assets/javascripts/ide/components/repo_loading_file.vue (renamed from app/assets/javascripts/repo/components/repo_loading_file.vue)8
-rw-r--r--app/assets/javascripts/ide/components/repo_prev_directory.vue (renamed from app/assets/javascripts/repo/components/repo_prev_directory.vue)8
-rw-r--r--app/assets/javascripts/ide/components/repo_preview.vue (renamed from app/assets/javascripts/repo/components/repo_preview.vue)2
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue (renamed from app/assets/javascripts/repo/components/repo_tab.vue)6
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue (renamed from app/assets/javascripts/repo/components/repo_tabs.vue)0
-rw-r--r--app/assets/javascripts/ide/ide_router.js101
-rw-r--r--app/assets/javascripts/ide/index.js55
-rw-r--r--app/assets/javascripts/ide/lib/common/disposable.js (renamed from app/assets/javascripts/repo/lib/common/disposable.js)0
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js (renamed from app/assets/javascripts/repo/lib/common/model.js)8
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js (renamed from app/assets/javascripts/repo/lib/common/model_manager.js)0
-rw-r--r--app/assets/javascripts/ide/lib/decorations/controller.js (renamed from app/assets/javascripts/repo/lib/decorations/controller.js)0
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js (renamed from app/assets/javascripts/repo/lib/diff/controller.js)0
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff.js (renamed from app/assets/javascripts/repo/lib/diff/diff.js)0
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff_worker.js (renamed from app/assets/javascripts/repo/lib/diff/diff_worker.js)0
-rw-r--r--app/assets/javascripts/ide/lib/editor.js (renamed from app/assets/javascripts/repo/lib/editor.js)30
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js (renamed from app/assets/javascripts/repo/lib/editor_options.js)0
-rw-r--r--app/assets/javascripts/ide/monaco_loader.js (renamed from app/assets/javascripts/repo/monaco_loader.js)0
-rw-r--r--app/assets/javascripts/ide/services/index.js (renamed from app/assets/javascripts/repo/services/index.js)7
-rw-r--r--app/assets/javascripts/ide/stores/actions.js179
-rw-r--r--app/assets/javascripts/ide/stores/actions/branch.js43
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js (renamed from app/assets/javascripts/repo/stores/actions/file.js)41
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js25
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js188
-rw-r--r--app/assets/javascripts/ide/stores/getters.js19
-rw-r--r--app/assets/javascripts/ide/stores/index.js (renamed from app/assets/javascripts/repo/stores/index.js)0
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js (renamed from app/assets/javascripts/repo/stores/mutation_types.js)19
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js (renamed from app/assets/javascripts/repo/stores/mutations.js)18
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js28
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js (renamed from app/assets/javascripts/repo/stores/mutations/file.js)20
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js23
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js (renamed from app/assets/javascripts/repo/stores/mutations/tree.js)9
-rw-r--r--app/assets/javascripts/ide/stores/state.js (renamed from app/assets/javascripts/repo/stores/state.js)20
-rw-r--r--app/assets/javascripts/ide/stores/utils.js (renamed from app/assets/javascripts/repo/stores/utils.js)64
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js4
-rw-r--r--app/assets/javascripts/init_notes.js6
-rw-r--r--app/assets/javascripts/issue.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue12
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue2
-rw-r--r--app/assets/javascripts/issue_show/index.js7
-rw-r--r--app/assets/javascripts/job.js3
-rw-r--r--app/assets/javascripts/layout_nav.js70
-rw-r--r--app/assets/javascripts/lib/utils/cache.js4
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js228
-rw-r--r--app/assets/javascripts/lib/utils/tick_formats.js39
-rw-r--r--app/assets/javascripts/line_highlighter.js2
-rw-r--r--app/assets/javascripts/locale/index.js15
-rw-r--r--app/assets/javascripts/main.js27
-rw-r--r--app/assets/javascripts/merge_request.js263
-rw-r--r--app/assets/javascripts/merge_request_tabs.js648
-rw-r--r--app/assets/javascripts/milestone_select.js3
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue24
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js43
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js21
-rw-r--r--app/assets/javascripts/new_commit_form.js7
-rw-r--r--app/assets/javascripts/notes.js11
-rw-r--r--app/assets/javascripts/notifications_dropdown.js48
-rw-r--r--app/assets/javascripts/notifications_form.js93
-rw-r--r--app/assets/javascripts/pager.js134
-rw-r--r--app/assets/javascripts/pages/users/show/index.js3
-rw-r--r--app/assets/javascripts/preview_markdown.js6
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue8
-rw-r--r--app/assets/javascripts/project_variables.js39
-rw-r--r--app/assets/javascripts/repo/components/commit_sidebar/list.vue89
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/index.vue89
-rw-r--r--app/assets/javascripts/repo/components/repo.vue63
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue85
-rw-r--r--app/assets/javascripts/repo/index.js106
-rw-r--r--app/assets/javascripts/repo/stores/actions.js146
-rw-r--r--app/assets/javascripts/repo/stores/actions/branch.js20
-rw-r--r--app/assets/javascripts/repo/stores/actions/tree.js163
-rw-r--r--app/assets/javascripts/repo/stores/getters.js40
-rw-r--r--app/assets/javascripts/repo/stores/mutations/branch.js9
-rw-r--r--app/assets/javascripts/right_sidebar.js438
-rw-r--r--app/assets/javascripts/shortcuts.js10
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js4
-rw-r--r--app/assets/javascripts/users/activity_calendar.js14
-rw-r--r--app/assets/javascripts/users/user_tabs.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js17
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/memory_graph.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/modal.vue (renamed from app/assets/javascripts/vue_shared/components/popup_dialog.vue)4
-rw-r--r--app/assets/javascripts/vue_shared/components/project_avatar/image.vue103
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue (renamed from app/assets/javascripts/vue_shared/components/recaptcha_dialog.vue)12
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue36
-rw-r--r--app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js (renamed from app/assets/javascripts/vue_shared/mixins/recaptcha_dialog_implementor.js)4
-rw-r--r--app/assets/javascripts/vue_shared/mixins/timeago.js6
-rw-r--r--app/assets/stylesheets/framework/contextual-sidebar.scss10
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss19
-rw-r--r--app/assets/stylesheets/framework/files.scss5
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss6
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss31
-rw-r--r--app/assets/stylesheets/framework/mobile.scss17
-rw-r--r--app/assets/stylesheets/framework/modal.scss13
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss5
-rw-r--r--app/assets/stylesheets/framework/toggle.scss64
-rw-r--r--app/assets/stylesheets/framework/variables.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss20
-rw-r--r--app/assets/stylesheets/pages/repo.scss252
-rw-r--r--app/assets/stylesheets/pages/search.scss14
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss3
-rw-r--r--app/controllers/autocomplete_controller.rb4
-rw-r--r--app/controllers/concerns/boards_responses.rb4
-rw-r--r--app/controllers/concerns/creates_commit.rb26
-rw-r--r--app/controllers/concerns/group_tree.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb15
-rw-r--r--app/controllers/concerns/issuable_collections.rb15
-rw-r--r--app/controllers/concerns/issues_action.rb2
-rw-r--r--app/controllers/concerns/merge_requests_action.rb2
-rw-r--r--app/controllers/concerns/milestone_actions.rb8
-rw-r--r--app/controllers/concerns/notes_actions.rb32
-rw-r--r--app/controllers/concerns/oauth_applications.rb2
-rw-r--r--app/controllers/concerns/preview_markdown.rb2
-rw-r--r--app/controllers/concerns/renders_commits.rb2
-rw-r--r--app/controllers/concerns/renders_notes.rb2
-rw-r--r--app/controllers/concerns/service_params.rb2
-rw-r--r--app/controllers/concerns/snippets_actions.rb2
-rw-r--r--app/controllers/concerns/spammable_actions.rb7
-rw-r--r--app/controllers/concerns/toggle_subscription_action.rb2
-rw-r--r--app/controllers/concerns/with_performance_bar.rb1
-rw-r--r--app/controllers/ide_controller.rb6
-rw-r--r--app/controllers/projects/blob_controller.rb5
-rw-r--r--app/controllers/projects/clusters/gcp_controller.rb1
-rw-r--r--app/controllers/projects/clusters/user_controller.rb1
-rw-r--r--app/controllers/projects/clusters_controller.rb2
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb5
-rw-r--r--app/controllers/projects/merge_requests_controller.rb8
-rw-r--r--app/controllers/projects/notes_controller.rb2
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb31
-rw-r--r--app/controllers/projects/pipelines_controller.rb2
-rw-r--r--app/controllers/projects/tree_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/blob_helper.rb43
-rw-r--r--app/helpers/clusters_helper.rb5
-rw-r--r--app/helpers/form_helper.rb2
-rw-r--r--app/helpers/gitlab_routing_helper.rb5
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/helpers/issues_helper.rb2
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/sorting_helper.rb10
-rw-r--r--app/mailers/emails/notes.rb10
-rw-r--r--app/mailers/notify.rb16
-rw-r--r--app/models/blob.rb12
-rw-r--r--app/models/blob_viewer/dependency_manager.rb13
-rw-r--r--app/models/blob_viewer/package_json.rb18
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/pipeline.rb17
-rw-r--r--app/models/commit.rb34
-rw-r--r--app/models/concerns/blocks_json_serialization.rb16
-rw-r--r--app/models/concerns/mentionable.rb10
-rw-r--r--app/models/concerns/milestoneish.rb8
-rw-r--r--app/models/concerns/noteable.rb2
-rw-r--r--app/models/concerns/participable.rb12
-rw-r--r--app/models/concerns/relative_positioning.rb8
-rw-r--r--app/models/concerns/resolvable_discussion.rb14
-rw-r--r--app/models/concerns/routable.rb10
-rw-r--r--app/models/concerns/spammable.rb5
-rw-r--r--app/models/concerns/taskable.rb2
-rw-r--r--app/models/concerns/time_trackable.rb20
-rw-r--r--app/models/diff_discussion.rb15
-rw-r--r--app/models/identity.rb14
-rw-r--r--app/models/merge_request.rb16
-rw-r--r--app/models/note.rb10
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/repository.rb49
-rw-r--r--app/models/user.rb3
-rw-r--r--app/models/user_synced_attributes_metadata.rb10
-rw-r--r--app/policies/ci/pipeline_policy.rb16
-rw-r--r--app/policies/ci/pipeline_schedule_policy.rb10
-rw-r--r--app/serializers/concerns/with_pagination.rb2
-rw-r--r--app/serializers/issuable_entity.rb8
-rw-r--r--app/serializers/issuable_sidebar_entity.rb6
-rw-r--r--app/serializers/issue_entity.rb8
-rw-r--r--app/serializers/merge_request_serializer.rb6
-rw-r--r--app/serializers/merge_request_widget_entity.rb (renamed from app/serializers/merge_request_entity.rb)5
-rw-r--r--app/services/ci/create_pipeline_service.rb2
-rw-r--r--app/services/concerns/issues/resolve_discussions.rb16
-rw-r--r--app/services/files/base_service.rb14
-rw-r--r--app/services/files/delete_service.rb10
-rw-r--r--app/services/files/multi_service.rb13
-rw-r--r--app/services/files/update_service.rb21
-rw-r--r--app/services/issuable/destroy_service.rb6
-rw-r--r--app/services/notes/destroy_service.rb4
-rw-r--r--app/services/projects/unlink_fork_service.rb2
-rw-r--r--app/services/quick_actions/interpret_service.rb4
-rw-r--r--app/services/spam_check_service.rb4
-rw-r--r--app/services/todo_service.rb16
-rw-r--r--app/views/admin/groups/index.html.haml18
-rw-r--r--app/views/admin/system_info/show.html.haml6
-rw-r--r--app/views/admin/users/show.html.haml1
-rw-r--r--app/views/ci/variables/_index.html.haml6
-rw-r--r--app/views/ci/variables/_table.html.haml6
-rw-r--r--app/views/events/event/_push.html.haml3
-rw-r--r--app/views/help/index.html.haml10
-rw-r--r--app/views/ide/index.html.haml12
-rw-r--r--app/views/layouts/nav_only.html.haml13
-rw-r--r--app/views/notify/pipeline_success_email.html.haml2
-rw-r--r--app/views/notify/pipeline_success_email.text.erb4
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/projects/_md_preview.html.haml2
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml2
-rw-r--r--app/views/projects/blob/_header.html.haml1
-rw-r--r--app/views/projects/blob/_header_content.html.haml3
-rw-r--r--app/views/projects/blob/show.html.haml15
-rw-r--r--app/views/projects/blob/viewers/_dependency_manager.html.haml2
-rw-r--r--app/views/projects/clusters/_cluster.html.haml7
-rw-r--r--app/views/projects/clusters/_enabled.html.haml6
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml3
-rw-r--r--app/views/projects/clusters/gcp/_show.html.haml5
-rw-r--r--app/views/projects/clusters/user/_form.html.haml3
-rw-r--r--app/views/projects/clusters/user/_show.html.haml4
-rw-r--r--app/views/projects/commits/_commit.html.haml4
-rw-r--r--app/views/projects/issues/show.html.haml11
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml4
-rw-r--r--app/views/projects/pipelines/_info.html.haml50
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml2
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml8
-rw-r--r--app/views/projects/tree/_blob_item.html.haml3
-rw-r--r--app/views/projects/tree/_old_tree_content.html.haml24
-rw-r--r--app/views/projects/tree/_old_tree_header.html.haml64
-rw-r--r--app/views/projects/tree/_tree_content.html.haml29
-rw-r--r--app/views/projects/tree/_tree_header.html.haml78
-rw-r--r--app/views/projects/tree/show.html.haml7
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/views/shared/_show_aside.html.haml2
-rw-r--r--app/views/shared/boards/components/_sidebar.html.haml3
-rw-r--r--app/views/shared/empty_states/_issues.html.haml13
-rw-r--r--app/views/shared/groups/_dropdown.html.haml5
-rw-r--r--app/views/shared/issuable/_assignees.html.haml18
-rw-r--r--app/views/shared/repo/_repo.html.haml13
-rw-r--r--app/workers/all_queues.yml99
-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/concerns/application_worker.rb24
-rw-r--r--app/workers/concerns/cluster_queue.rb2
-rw-r--r--app/workers/concerns/cronjob_queue.rb3
-rw-r--r--app/workers/concerns/gitlab/github_import/queue.rb4
-rw-r--r--app/workers/concerns/new_issuable.rb8
-rw-r--r--app/workers/concerns/pipeline_queue.rb10
-rw-r--r--app/workers/concerns/project_import_options.rb23
-rw-r--r--app/workers/concerns/project_start_import.rb1
-rw-r--r--app/workers/concerns/repository_check_queue.rb4
-rw-r--r--app/workers/create_pipeline_worker.rb2
-rw-r--r--app/workers/expire_job_cache_worker.rb2
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/advance_stage_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_process_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/repository_fork_worker.rb22
-rw-r--r--app/workers/repository_import_worker.rb19
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb22
-rw-r--r--app/workers/stage_update_worker.rb2
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb7
-rw-r--r--app/workers/update_head_pipeline_for_merge_request_worker.rb16
311 files changed, 4604 insertions, 2971 deletions
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index 5d060165f4b..6a0662ba903 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -1,9 +1,10 @@
/* eslint-disable no-param-reassign, class-methods-use-this */
-/* global Pager */
import Cookies from 'js-cookie';
+import Pager from './pager';
+import { localTimeAgo } from './lib/utils/datetime_utility';
-class Activities {
+export default class Activities {
constructor() {
Pager.init(20, true, false, data => data, this.updateTooltips);
@@ -15,7 +16,7 @@ class Activities {
}
updateTooltips() {
- gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
+ localTimeAgo($('.js-timeago', '.content_list'));
}
reloadActivities() {
@@ -33,6 +34,3 @@ class Activities {
$sender.closest('li').toggleClass('active');
}
}
-
-window.gl = window.gl || {};
-window.gl.Activities = Activities;
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index b0b72c40f25..c1f7fa2aced 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,63 +1,59 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */
import { refreshCurrentPage } from './lib/utils/url_utility';
-window.Admin = (function() {
- function Admin() {
- var modal, showBlacklistType;
- $('input#user_force_random_password').on('change', function(elem) {
- var elems;
- elems = $('#user_password, #user_password_confirmation');
- if ($(this).attr('checked')) {
- return elems.val('').attr('disabled', true);
- } else {
- return elems.removeAttr('disabled');
- }
- });
- $('body').on('click', '.js-toggle-colors-link', function(e) {
- e.preventDefault();
- return $('.js-toggle-colors-container').toggle();
- });
- $('.log-tabs a').click(function(e) {
- e.preventDefault();
- return $(this).tab('show');
- });
- $('.log-bottom').click(function(e) {
- var visible_log;
- e.preventDefault();
- visible_log = $(".file-content:visible");
- return visible_log.animate({
- scrollTop: visible_log.find('ol').height()
- }, "fast");
- });
- modal = $('.change-owner-holder');
- $('.change-owner-link').bind("click", function(e) {
- e.preventDefault();
- $(this).hide();
- return modal.show();
- });
- $('.change-owner-cancel-link').bind("click", function(e) {
- e.preventDefault();
- modal.hide();
- return $('.change-owner-link').show();
- });
- $('li.project_member').bind('ajax:success', function() {
- return refreshCurrentPage();
- });
- $('li.group_member').bind('ajax:success', function() {
- return refreshCurrentPage();
- });
- showBlacklistType = function() {
- if ($("input[name='blacklist_type']:checked").val() === 'file') {
- $('.blacklist-file').show();
- return $('.blacklist-raw').hide();
- } else {
- $('.blacklist-file').hide();
- return $('.blacklist-raw').show();
- }
- };
- $("input[name='blacklist_type']").click(showBlacklistType);
- showBlacklistType();
+function showBlacklistType() {
+ if ($('input[name="blacklist_type"]:checked').val() === 'file') {
+ $('.blacklist-file').show();
+ $('.blacklist-raw').hide();
+ } else {
+ $('.blacklist-file').hide();
+ $('.blacklist-raw').show();
}
+}
- return Admin;
-})();
+export default function adminInit() {
+ const modal = $('.change-owner-holder');
+
+ $('input#user_force_random_password').on('change', function randomPasswordClick() {
+ const $elems = $('#user_password, #user_password_confirmation');
+ if ($(this).attr('checked')) {
+ $elems.val('').attr('disabled', true);
+ } else {
+ $elems.removeAttr('disabled');
+ }
+ });
+
+ $('body').on('click', '.js-toggle-colors-link', (e) => {
+ e.preventDefault();
+ $('.js-toggle-colors-container').toggle();
+ });
+
+ $('.log-tabs a').on('click', function logTabsClick(e) {
+ e.preventDefault();
+ $(this).tab('show');
+ });
+
+ $('.log-bottom').on('click', (e) => {
+ e.preventDefault();
+ const $visibleLog = $('.file-content:visible');
+ $visibleLog.animate({
+ scrollTop: $visibleLog.find('ol').height(),
+ }, 'fast');
+ });
+
+ $('.change-owner-link').on('click', function changeOwnerLinkClick(e) {
+ e.preventDefault();
+ $(this).hide();
+ modal.show();
+ });
+
+ $('.change-owner-cancel-link').on('click', (e) => {
+ e.preventDefault();
+ modal.hide();
+ $('.change-owner-link').show();
+ });
+
+ $('li.project_member, li.group_member').on('ajax:success', refreshCurrentPage);
+
+ $("input[name='blacklist_type']").on('click', showBlacklistType);
+ showBlacklistType();
+}
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index d963101028a..21d8c790e90 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import axios from './lib/utils/axios_utils';
const Api = {
groupsPath: '/api/:version/groups.json',
@@ -6,6 +7,7 @@ const Api = {
namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json',
+ projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
groupLabelsPath: '/groups/:namespace_path/labels',
licensePath: '/api/:version/templates/licenses/:key',
@@ -76,6 +78,14 @@ const Api = {
.done(projects => callback(projects));
},
+ // Return single project
+ project(projectPath) {
+ const url = Api.buildUrl(Api.projectPath)
+ .replace(':id', encodeURIComponent(projectPath));
+
+ return axios.get(url);
+ },
+
newLabel(namespacePath, projectPath, data, callback) {
let url;
@@ -115,7 +125,7 @@ const Api = {
commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath)
- .replace(':id', id);
+ .replace(':id', encodeURIComponent(id));
return this.wrapAjaxCall({
url,
type: 'POST',
@@ -127,7 +137,7 @@ const Api = {
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
- .replace(':id', id)
+ .replace(':id', encodeURIComponent(id))
.replace(':branch', branch);
return this.wrapAjaxCall({
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
deleted file mode 100644
index 88756884d16..00000000000
--- a/app/assets/javascripts/aside.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */
-
-window.Aside = (function() {
- function Aside() {
- $(document).off("click", "a.show-aside");
- $(document).on("click", 'a.show-aside', function(e) {
- var btn, icon;
- e.preventDefault();
- btn = $(e.currentTarget);
- icon = btn.find('i');
- if (icon.hasClass('fa-angle-left')) {
- btn.parent().find('section').hide();
- btn.parent().find('aside').fadeIn();
- return icon.removeClass('fa-angle-left').addClass('fa-angle-right');
- } else {
- btn.parent().find('aside').hide();
- btn.parent().find('section').fadeIn();
- return icon.removeClass('fa-angle-right').addClass('fa-angle-left');
- }
- });
- }
-
- return Aside;
-})();
diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js
new file mode 100644
index 00000000000..1cf0b960eb0
--- /dev/null
+++ b/app/assets/javascripts/behaviors/secret_values.js
@@ -0,0 +1,42 @@
+import { n__ } from '../locale';
+import { convertPermissionToBoolean } from '../lib/utils/common_utils';
+
+export default class SecretValues {
+ constructor(container) {
+ this.container = container;
+ }
+
+ init() {
+ this.values = this.container.querySelectorAll('.js-secret-value');
+ this.placeholders = this.container.querySelectorAll('.js-secret-value-placeholder');
+ this.revealButton = this.container.querySelector('.js-secret-value-reveal-button');
+
+ this.revealText = n__('Reveal value', 'Reveal values', this.values.length);
+ this.hideText = n__('Hide value', 'Hide values', this.values.length);
+
+ const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus);
+ this.updateDom(isRevealed);
+
+ this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this));
+ }
+
+ onRevealButtonClicked() {
+ const previousIsRevealed = convertPermissionToBoolean(
+ this.revealButton.dataset.secretRevealStatus,
+ );
+ this.updateDom(!previousIsRevealed);
+ }
+
+ updateDom(isRevealed) {
+ this.values.forEach((value) => {
+ value.classList.toggle('hide', !isRevealed);
+ });
+
+ this.placeholders.forEach((placeholder) => {
+ placeholder.classList.toggle('hide', isRevealed);
+ });
+
+ this.revealButton.textContent = isRevealed ? this.hideText : this.revealText;
+ this.revealButton.dataset.secretRevealStatus = isRevealed;
+ }
+}
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index faa76da964f..616de2347e1 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -1,9 +1,9 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* global MilestoneSelect */
-/* global Sidebar */
import Vue from 'vue';
import Flash from '../../flash';
+import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub';
import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
import assignees from '../../sidebar/components/assignees/assignees';
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 5662802525e..b6a0ece7907 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -176,6 +176,7 @@ export default class ImageFile {
left: dragTrackWidth
});
+ $frameAdded.css('opacity', 1);
framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
_this.initDraggable($dragger, framePadding, function(e, left) {
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 9b952ea7b60..3a03cbf6b90 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,9 +1,10 @@
/* eslint-disable func-names, wrap-iife, consistent-return,
no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars,
prefer-template, object-shorthand, prefer-arrow-callback */
-/* global Pager */
import { pluralize } from './lib/utils/text_utility';
+import { localTimeAgo } from './lib/utils/datetime_utility';
+import Pager from './pager';
export default (function () {
const CommitsList = {};
@@ -91,7 +92,7 @@ export default (function () {
$commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
}
- gl.utils.localTimeAgo($processedData.find('.js-timeago'));
+ localTimeAgo($processedData.find('.js-timeago'));
return processedData;
};
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index 0ce467a3bd4..144caf1d278 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -1,4 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
+import { localTimeAgo } from './lib/utils/datetime_utility';
export default class Compare {
constructor(opts) {
@@ -81,7 +82,7 @@ export default class Compare {
loading.hide();
$target.html(html);
var className = '.' + $target[0].className.replace(' ', '.');
- gl.utils.localTimeAgo($('.js-timeago', className));
+ localTimeAgo($('.js-timeago', className));
}
});
}
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index b41d464475f..2a05c6f001e 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -1,5 +1,6 @@
<script>
import actionBtn from './action_btn.vue';
+ import { getTimeago } from '../../lib/utils/datetime_utility';
export default {
props: {
@@ -21,7 +22,7 @@
},
computed: {
timeagoDate() {
- return gl.utils.getTimeago().format(this.deployKey.created_at);
+ return getTimeago().format(this.deployKey.created_at);
},
editDeployKeyPath() {
return `${this.endpoint}/${this.deployKey.id}/edit`;
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 06ce84d7599..300b02da663 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -1,8 +1,8 @@
/* global CommentsStore */
-/* global notes */
import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg';
+import Notes from '../../notes';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const DiffNoteAvatars = Vue.extend({
@@ -129,7 +129,7 @@ const DiffNoteAvatars = Vue.extend({
},
methods: {
clickedAvatar(e) {
- notes.onAddDiffNote(e);
+ Notes.instance.onAddDiffNote(e);
// Toggle the active state of the toggle all button
this.toggleDiscussionsToggleState();
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js
index dc43e4b2cc7..1b8a9af9390 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js
+++ b/app/assets/javascripts/diff_notes/models/discussion.js
@@ -2,6 +2,7 @@
/* global NoteModel */
import Vue from 'vue';
+import { localTimeAgo } from '../../lib/utils/datetime_utility';
class DiscussionModel {
constructor (discussionId) {
@@ -71,7 +72,7 @@ class DiscussionModel {
$(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html);
}
- gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`));
+ localTimeAgo($('.js-timeago', `${discussionSelector}`));
} else {
$discussionHeadline.remove();
}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 12402fd645f..118437b82a3 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -7,21 +7,21 @@ import IssuableForm from './issuable_form';
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
import NewBranchForm from './new_branch_form';
-/* global NotificationsForm */
-/* global NotificationsDropdown */
+import NotificationsForm from './notifications_form';
+import notificationsDropdown from './notifications_dropdown';
import groupAvatar from './group_avatar';
import GroupLabelSubscription from './group_label_subscription';
-/* global LineHighlighter */
+import LineHighlighter from './line_highlighter';
import BuildArtifacts from './build_artifacts';
import CILintEditor from './ci_lint_editor';
import groupsSelect from './groups_select';
import Search from './search';
-/* global Admin */
+import initAdmin from './admin';
import NamespaceSelect from './namespace_select';
import NewCommitForm from './new_commit_form';
import Project from './project';
import projectAvatar from './project_avatar';
-/* global MergeRequest */
+import MergeRequest from './merge_request';
import Compare from './compare';
import initCompareAutocomplete from './compare_autocomplete';
import ProjectFindFile from './project_find_file';
@@ -29,12 +29,13 @@ import ProjectNew from './project_new';
import projectImport from './project_import';
import Labels from './labels';
import LabelManager from './label_manager';
-/* global Sidebar */
+import Sidebar from './right_sidebar';
import IssuableTemplateSelectors from './templates/issuable_template_selectors';
import Flash from './flash';
import CommitsList from './commits';
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
+import SecretValues from './behaviors/secret_values';
import DeleteModal from './branches/branches_delete_modal';
import Group from './group';
import GroupsList from './groups_list';
@@ -72,7 +73,6 @@ import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar';
import initProjectVisibilitySelector from './project_visibility';
import GpgBadges from './gpg_badges';
-import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown';
import NewGroupChild from './groups/new_group_child';
import AbuseReports from './abuse_reports';
@@ -90,8 +90,8 @@ import memberExpirationDate from './member_expiration_date';
import DueDateSelectors from './due_date_select';
import Diff from './diff';
import ProjectLabelSubscription from './project_label_subscription';
-import ProjectVariables from './project_variables';
import SearchAutocomplete from './search_autocomplete';
+import Activities from './activities';
(function() {
var Dispatcher;
@@ -110,6 +110,8 @@ import SearchAutocomplete from './search_autocomplete';
return false;
}
+ const fail = () => Flash('Error loading dynamic module');
+
path = page.split(':');
shortcut_handler = null;
@@ -334,7 +336,7 @@ import SearchAutocomplete from './search_autocomplete';
shortcut_handler = new ShortcutsIssuable(true);
break;
case 'dashboard:activity':
- new gl.Activities();
+ new Activities();
break;
case 'projects:commit:show':
new Diff();
@@ -355,7 +357,7 @@ import SearchAutocomplete from './search_autocomplete';
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
break;
case 'projects:activity':
- new gl.Activities();
+ new Activities();
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:commits:show':
@@ -373,7 +375,7 @@ import SearchAutocomplete from './search_autocomplete';
if ($('#tree-slider').length) new TreeView();
if ($('.blob-viewer').length) new BlobViewer();
- if ($('.project-show-activity').length) new gl.Activities();
+ if ($('.project-show-activity').length) new Activities();
$('#tree-slider').waitForImages(function() {
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
@@ -407,13 +409,13 @@ import SearchAutocomplete from './search_autocomplete';
});
break;
case 'groups:activity':
- new gl.Activities();
+ new Activities();
break;
case 'groups:show':
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
- new NotificationsDropdown();
+ notificationsDropdown();
new ProjectsList();
if (newGroupChildWrapper) {
@@ -446,9 +448,6 @@ import SearchAutocomplete from './search_autocomplete';
break;
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
-
- if (UserFeatureHelper.isNewRepoEnabled()) break;
-
new TreeView();
new BlobViewer();
new NewCommitForm($('.js-create-dir-form'));
@@ -467,7 +466,6 @@ import SearchAutocomplete from './search_autocomplete';
shortcut_handler = true;
break;
case 'projects:blob:show':
- if (UserFeatureHelper.isNewRepoEnabled()) break;
new BlobViewer();
initBlob();
break;
@@ -526,15 +524,25 @@ import SearchAutocomplete from './search_autocomplete';
case 'projects:settings:ci_cd:show':
// Initialize expandable settings panels
initSettingsPanels();
+
+ const runnerToken = document.querySelector('.js-secret-runner-token');
+ if (runnerToken) {
+ const runnerTokenSecretValue = new SecretValues(runnerToken);
+ runnerTokenSecretValue.init();
+ }
case 'groups:settings:ci_cd:show':
- new ProjectVariables();
+ const secretVariableTable = document.querySelector('.js-secret-variable-table');
+ if (secretVariableTable) {
+ const secretVariableTableValues = new SecretValues(secretVariableTable);
+ secretVariableTableValues.init();
+ }
break;
case 'ci:lints:create':
case 'ci:lints:show':
new CILintEditor();
break;
case 'users:show':
- new UserCallout();
+ import('./pages/users/show').then(m => m.default()).catch(fail);
break;
case 'admin:conversational_development_index:show':
new UserCallout();
@@ -584,7 +592,7 @@ import SearchAutocomplete from './search_autocomplete';
// needed in rspec
gl.u2fAuthenticate = u2fAuthenticate;
case 'admin':
- new Admin();
+ initAdmin();
switch (path[1]) {
case 'broadcast_messages':
initBroadcastMessagesForm();
@@ -616,7 +624,7 @@ import SearchAutocomplete from './search_autocomplete';
break;
case 'profiles':
new NotificationsForm();
- new NotificationsDropdown();
+ notificationsDropdown();
break;
case 'projects':
new Project();
@@ -639,7 +647,7 @@ import SearchAutocomplete from './search_autocomplete';
case 'show':
new Star();
new ProjectNew();
- new NotificationsDropdown();
+ notificationsDropdown();
break;
case 'wikis':
new Wikis();
diff --git a/app/assets/javascripts/docs/docs_bundle.js b/app/assets/javascripts/docs/docs_bundle.js
new file mode 100644
index 00000000000..a32bd6d0fc7
--- /dev/null
+++ b/app/assets/javascripts/docs/docs_bundle.js
@@ -0,0 +1,13 @@
+import Mousetrap from 'mousetrap';
+
+function addMousetrapClick(el, key) {
+ el.addEventListener('click', () => Mousetrap.trigger(key));
+}
+
+function domContentLoaded() {
+ addMousetrapClick(document.querySelector('.js-trigger-shortcut'), '?');
+ addMousetrapClick(document.querySelector('.js-trigger-search-bar'), 's');
+}
+
+document.addEventListener('DOMContentLoaded', domContentLoaded);
+
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 6110d961609..abb04d77f8f 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -161,13 +161,16 @@ export default () => {
const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
- sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
- clearTimeout(timeoutId);
-
- timeoutId = setTimeout(() => {
- if (currentOpenMenu) hideMenu(currentOpenMenu);
- }, getHideSubItemsInterval());
- });
+ const topItems = sidebar.querySelector('.sidebar-top-level-items');
+ if (topItems) {
+ sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
+ clearTimeout(timeoutId);
+
+ timeoutId = setTimeout(() => {
+ if (currentOpenMenu) hideMenu(currentOpenMenu);
+ }, getHideSubItemsInterval());
+ });
+ }
headerHeight = document.querySelector('.nav-sidebar').offsetTop;
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index cf4a70e321e..64f258aed64 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -300,7 +300,7 @@ GitLabDropdown = (function() {
return function(data) {
_this.fullData = data;
_this.parseData(_this.fullData);
- _this.focusTextInput(true);
+ _this.focusTextInput();
if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
@@ -790,24 +790,16 @@ GitLabDropdown = (function() {
return [selectedObject, isMarking];
};
- GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) {
+ GitLabDropdown.prototype.focusTextInput = function() {
if (this.options.filterable) {
- this.dropdown.one('transitionend', () => {
- const initialScrollTop = $(window).scrollTop();
+ const initialScrollTop = $(window).scrollTop();
- if (this.dropdown.is('.open')) {
- this.filterInput.focus();
- }
-
- if ($(window).scrollTop() < initialScrollTop) {
- $(window).scrollTop(initialScrollTop);
- }
- });
+ if (this.dropdown.is('.open')) {
+ this.filterInput.focus();
+ }
- if (triggerFocus) {
- // This triggers after a ajax request
- // in case of slow requests, the dropdown transition could already be finished
- this.dropdown.trigger('transitionend');
+ if ($(window).scrollTop() < initialScrollTop) {
+ $(window).scrollTop(initialScrollTop);
}
}
};
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index e7232ca3712..151a4ce012c 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,13 +1,14 @@
/* 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 */
import _ from 'underscore';
-import d3 from 'd3';
import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
import ContributorsStatGraphUtil from './stat_graph_contributors_util';
-import { n__ } from '../locale';
+import { n__, s__, createDateTimeFormat, sprintf } from '../locale';
export default (function() {
- function ContributorsStatGraph() {}
+ function ContributorsStatGraph() {
+ this.dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
+ }
ContributorsStatGraph.prototype.init = function(log) {
var author_commits, total_commits;
@@ -83,9 +84,12 @@ export default (function() {
return _.each(author_commits, (function(_this) {
return function(d) {
_this.redraw_author_commit_info(d);
- $(_this.authors[d.author_name].list_item).appendTo("ol");
- _this.authors[d.author_name].set_data(d.dates);
- return _this.authors[d.author_name].redraw();
+ if (_this.authors[d.author_name] != null) {
+ $(_this.authors[d.author_name].list_item).appendTo("ol");
+ _this.authors[d.author_name].set_data(d.dates);
+ return _this.authors[d.author_name].redraw();
+ }
+ return '';
};
})(this));
};
@@ -95,18 +99,26 @@ export default (function() {
};
ContributorsStatGraph.prototype.change_date_header = function() {
- var print, print_date_format, x_domain;
- x_domain = ContributorsGraph.prototype.x_domain;
- print_date_format = d3.time.format("%B %e %Y");
- print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
- return $("#date_header").text(print);
+ const x_domain = ContributorsGraph.prototype.x_domain;
+ const formattedDateRange = sprintf(
+ s__('ContributorsPage|%{startDate} – %{endDate}'),
+ {
+ startDate: this.dateFormat.format(new Date(x_domain[0])),
+ endDate: this.dateFormat.format(new Date(x_domain[1])),
+ },
+ );
+ return $('#date_header').text(formattedDateRange);
};
ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
- var author_commit_info, author_list_item;
- author_list_item = $(this.authors[author.author_name].list_item);
- author_commit_info = this.format_author_commit_info(author);
- return author_list_item.find("span").html(author_commit_info);
+ var author_commit_info, author_list_item, $author;
+ $author = this.authors[author.author_name];
+ if ($author != null) {
+ author_list_item = $(this.authors[author.author_name].list_item);
+ author_commit_info = this.format_author_commit_info(author);
+ return author_list_item.find("span").html(author_commit_info);
+ }
+ return '';
};
return ContributorsStatGraph;
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index f64b4638485..9a4012232a0 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,6 +1,15 @@
/* eslint-disable func-names, space-before-function-paren, no-var, 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 */
import _ from 'underscore';
-import d3 from 'd3';
+import { extent, max } from 'd3-array';
+import { select, event as d3Event } from 'd3-selection';
+import { scaleTime, scaleLinear } from 'd3-scale';
+import { axisLeft, axisBottom } from 'd3-axis';
+import { area } from 'd3-shape';
+import { brushX } from 'd3-brush';
+import { timeParse } from 'd3-time-format';
+import { dateTickFormat } from '../lib/utils/tick_formats';
+
+const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse };
const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
const hasProp = {}.hasOwnProperty;
@@ -70,8 +79,8 @@ export const ContributorsGraph = (function() {
};
ContributorsGraph.prototype.create_scale = function(width, height) {
- this.x = d3.time.scale().range([0, width]).clamp(true);
- return this.y = d3.scale.linear().range([height, 0]).nice();
+ this.x = d3.scaleTime().range([0, width]).clamp(true);
+ return this.y = d3.scaleLinear().range([height, 0]).nice();
};
ContributorsGraph.prototype.draw_x_axis = function() {
@@ -93,9 +102,12 @@ export const ContributorsMasterGraph = (function(superClass) {
extend(ContributorsMasterGraph, superClass);
function ContributorsMasterGraph(data1) {
+ const $parentElement = $('#contributors-master');
+ const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
+
this.data = data1;
this.update_content = this.update_content.bind(this);
- this.width = $('.content').width() - 70;
+ this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right);
this.height = 200;
this.x = null;
this.y = null;
@@ -120,7 +132,7 @@ export const ContributorsMasterGraph = (function(superClass) {
ContributorsMasterGraph.prototype.parse_dates = function(data) {
var parseDate;
- parseDate = d3.time.format("%Y-%m-%d").parse;
+ parseDate = d3.timeParse("%Y-%m-%d");
return data.forEach(function(d) {
return d.date = parseDate(d.date);
});
@@ -131,8 +143,10 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.create_axes = function() {
- this.x_axis = d3.svg.axis().scale(this.x).orient("bottom");
- return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+ this.x_axis = d3.axisBottom()
+ .scale(this.x)
+ .tickFormat(dateTickFormat);
+ return this.y_axis = d3.axisLeft().scale(this.y).ticks(5);
};
ContributorsMasterGraph.prototype.create_svg = function() {
@@ -140,16 +154,16 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.create_area = function(x, y) {
- return this.area = d3.svg.area().x(function(d) {
+ return this.area = d3.area().x(function(d) {
return x(d.date);
}).y0(this.height).y1(function(d) {
d.commits = d.commits || d.additions || d.deletions;
return y(d.commits);
- }).interpolate("basis");
+ });
};
ContributorsMasterGraph.prototype.create_brush = function() {
- return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content);
+ return this.brush = d3.brushX(this.x).extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]).on("end", this.update_content);
};
ContributorsMasterGraph.prototype.draw_path = function(data) {
@@ -161,7 +175,12 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.update_content = function() {
- ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent());
+ // d3Event.selection replaces the function brush.empty() calls
+ if (d3Event.selection != null) {
+ ContributorsGraph.set_x_domain(d3Event.selection.map(this.x.invert));
+ } else {
+ ContributorsGraph.set_x_domain(this.x_max_domain);
+ }
return $("#brush_change").trigger('change');
};
@@ -219,14 +238,17 @@ export const ContributorsAuthorGraph = (function(superClass) {
};
ContributorsAuthorGraph.prototype.create_axes = function() {
- this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8);
- return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+ this.x_axis = d3.axisBottom()
+ .scale(this.x)
+ .ticks(8)
+ .tickFormat(dateTickFormat);
+ return this.y_axis = d3.axisLeft().scale(this.y).ticks(5);
};
ContributorsAuthorGraph.prototype.create_area = function(x, y) {
- return this.area = d3.svg.area().x(function(d) {
+ return this.area = d3.area().x(function(d) {
var parseDate;
- parseDate = d3.time.format("%Y-%m-%d").parse;
+ parseDate = d3.timeParse("%Y-%m-%d");
return x(parseDate(d));
}).y0(this.height).y1((function(_this) {
return function(d) {
@@ -236,11 +258,12 @@ export const ContributorsAuthorGraph = (function(superClass) {
return y(0);
}
};
- })(this)).interpolate("basis");
+ })(this));
};
ContributorsAuthorGraph.prototype.create_svg = function() {
- this.list_item = d3.selectAll(".person")[0].pop();
+ var persons = document.querySelectorAll('.person');
+ this.list_item = persons[persons.length - 1];
return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
};
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index 09cb79c1afd..58ba5aff7cf 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -1,7 +1,7 @@
<script>
import { s__ } from '../../locale';
import tooltip from '../../vue_shared/directives/tooltip';
-import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
+import modal from '../../vue_shared/components/modal.vue';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
import Icon from '../../vue_shared/components/icon.vue';
@@ -9,7 +9,7 @@ import Icon from '../../vue_shared/components/icon.vue';
export default {
components: {
Icon,
- PopupDialog,
+ modal,
},
directives: {
tooltip,
@@ -27,7 +27,7 @@ export default {
},
data() {
return {
- dialogStatus: false,
+ modalStatus: false,
};
},
computed: {
@@ -43,10 +43,10 @@ export default {
},
methods: {
onLeaveGroup() {
- this.dialogStatus = true;
+ this.modalStatus = true;
},
leaveGroup(leaveConfirmed) {
- this.dialogStatus = false;
+ this.modalStatus = false;
if (leaveConfirmed) {
eventHub.$emit('leaveGroup', this.group, this.parentGroup);
}
@@ -82,8 +82,8 @@ export default {
class="fa fa-sign-out"
aria-hidden="true"/>
</a>
- <popup-dialog
- v-show="dialogStatus"
+ <modal
+ v-show="modalStatus"
:primary-button-label="__('Leave')"
kind="warning"
:title="__('Are you sure?')"
diff --git a/app/assets/javascripts/helpers/user_feature_helper.js b/app/assets/javascripts/helpers/user_feature_helper.js
deleted file mode 100644
index 638118a5204..00000000000
--- a/app/assets/javascripts/helpers/user_feature_helper.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import Cookies from 'js-cookie';
-
-export default {
- isNewRepoEnabled() {
- return Cookies.get('new_repo') === 'true';
- },
-};
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
new file mode 100644
index 00000000000..704dff981df
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -0,0 +1,66 @@
+<script>
+ import { mapState } from 'vuex';
+ import icon from '../../../vue_shared/components/icon.vue';
+ import listItem from './list_item.vue';
+ import listCollapsed from './list_collapsed.vue';
+
+ export default {
+ components: {
+ icon,
+ listItem,
+ listCollapsed,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ fileList: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'currentProjectId',
+ 'currentBranchId',
+ 'rightPanelCollapsed',
+ ]),
+ },
+ methods: {
+ toggleCollapsed() {
+ this.$emit('toggleCollapsed');
+ },
+ },
+
+ };
+</script>
+
+<template>
+ <div class="multi-file-commit-list">
+ <list-collapsed
+ v-if="rightPanelCollapsed"
+ />
+ <template v-else>
+ <ul
+ v-if="fileList.length"
+ class="list-unstyled append-bottom-0"
+ >
+ <li
+ v-for="file in fileList"
+ :key="file.key"
+ >
+ <list-item
+ :file="file"
+ />
+ </li>
+ </ul>
+ <div
+ v-else
+ class="help-block prepend-top-0"
+ >
+ No changes
+ </div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 6a0262f271b..6a0262f271b 100644
--- a/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 742f746e02f..742f746e02f 100644
--- a/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
new file mode 100644
index 00000000000..7f29a355eca
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -0,0 +1,73 @@
+<script>
+import { mapState, mapGetters } from 'vuex';
+import ideSidebar from './ide_side_bar.vue';
+import ideContextbar from './ide_context_bar.vue';
+import repoTabs from './repo_tabs.vue';
+import repoFileButtons from './repo_file_buttons.vue';
+import ideStatusBar from './ide_status_bar.vue';
+import repoPreview from './repo_preview.vue';
+import repoEditor from './repo_editor.vue';
+
+export default {
+ computed: {
+ ...mapState([
+ 'currentBlobView',
+ 'selectedFile',
+ ]),
+ ...mapGetters([
+ 'changedFiles',
+ 'activeFile',
+ ]),
+ },
+ components: {
+ ideSidebar,
+ ideContextbar,
+ repoTabs,
+ repoFileButtons,
+ ideStatusBar,
+ repoEditor,
+ repoPreview,
+ },
+ mounted() {
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = (e) => {
+ if (!this.changedFiles.length) return undefined;
+
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-view"
+ >
+ <ide-sidebar/>
+ <div
+ class="multi-file-edit-pane"
+ >
+ <template
+ v-if="activeFile">
+ <repo-tabs/>
+ <component
+ class="multi-file-edit-pane-content"
+ :is="currentBlobView"
+ />
+ <repo-file-buttons/>
+ <ide-status-bar
+ :file="selectedFile"/>
+ </template>
+ <template
+ v-else>
+ <div class="ide-empty-state">
+ <h2 class="clgray">Welcome to the GitLab IDE</h2>
+ </div>
+ </template>
+ </div>
+ <ide-contextbar/>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
new file mode 100644
index 00000000000..5a08718e386
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_context_bar.vue
@@ -0,0 +1,75 @@
+<script>
+import { mapGetters, mapState, mapActions } from 'vuex';
+import repoCommitSection from './repo_commit_section.vue';
+import icon from '../../vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ repoCommitSection,
+ icon,
+ },
+ computed: {
+ ...mapState([
+ 'rightPanelCollapsed',
+ ]),
+ ...mapGetters([
+ 'changedFiles',
+ ]),
+ currentIcon() {
+ return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'setPanelCollapsedStatus',
+ ]),
+ toggleCollapsed() {
+ this.setPanelCollapsedStatus({
+ side: 'right',
+ collapsed: !this.rightPanelCollapsed,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel"
+ :class="{
+ 'is-collapsed': rightPanelCollapsed,
+ }"
+ >
+ <div
+ class="multi-file-commit-panel-section">
+ <header
+ class="multi-file-commit-panel-header"
+ :class="{
+ 'is-collapsed': rightPanelCollapsed,
+ }"
+ >
+ <div
+ class="multi-file-commit-panel-header-title"
+ v-if="!rightPanelCollapsed">
+ <icon
+ name="list-bulleted"
+ :size="18"
+ />
+ Staged
+ </div>
+ <button
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ @click="toggleCollapsed"
+ >
+ <icon
+ :name="currentIcon"
+ :size="18"
+ />
+ </button>
+ </header>
+ <repo-commit-section
+ class=""/>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
new file mode 100644
index 00000000000..bd3a521ff43
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
@@ -0,0 +1,47 @@
+<script>
+import repoTree from './ide_repo_tree.vue';
+import icon from '../../vue_shared/components/icon.vue';
+import newDropdown from './new_dropdown/index.vue';
+
+export default {
+ components: {
+ repoTree,
+ icon,
+ newDropdown,
+ },
+ props: {
+ projectId: {
+ type: String,
+ required: true,
+ },
+ branch: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="branch-container">
+ <div class="branch-header">
+ <div class="branch-header-title">
+ <icon
+ name="branch"
+ :size="12">
+ </icon>
+ {{ branch.name }}
+ </div>
+ <div class="branch-header-btns">
+ <new-dropdown
+ :project-id="projectId"
+ :branch="branch.name"
+ path=""/>
+ </div>
+ </div>
+ <div>
+ <repo-tree
+ :treeId="branch.treeId"/>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue
new file mode 100644
index 00000000000..61daba6d176
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_project_tree.vue
@@ -0,0 +1,47 @@
+<script>
+import branchesTree from './ide_project_branches_tree.vue';
+import projectAvatarImage from '../../vue_shared/components/project_avatar/image.vue';
+
+export default {
+ components: {
+ branchesTree,
+ projectAvatarImage,
+ },
+ props: {
+ project: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="projects-sidebar">
+ <div class="context-header">
+ <a
+ :title="project.name"
+ :href="project.web_url">
+ <div class="avatar-container s40 project-avatar">
+ <project-avatar-image
+ class="avatar-container project-avatar"
+ :link-href="project.path"
+ :img-src="project.avatar_url"
+ :img-alt="project.name"
+ :img-size="40"
+ />
+ </div>
+ <div class="sidebar-context-title">
+ {{ project.name }}
+ </div>
+ </a>
+ </div>
+ <div class="multi-file-commit-panel-inner-scroll">
+ <branches-tree
+ v-for="(branch, index) in project.branches"
+ :key="branch.name"
+ :project-id="project.path_with_namespace"
+ :branch="branch"/>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue
new file mode 100644
index 00000000000..b6b089e6b25
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue
@@ -0,0 +1,66 @@
+<script>
+import { mapState } from 'vuex';
+import RepoPreviousDirectory from './repo_prev_directory.vue';
+import RepoFile from './repo_file.vue';
+import RepoLoadingFile from './repo_loading_file.vue';
+import { treeList } from '../stores/utils';
+
+export default {
+ components: {
+ 'repo-previous-directory': RepoPreviousDirectory,
+ 'repo-file': RepoFile,
+ 'repo-loading-file': RepoLoadingFile,
+ },
+ props: {
+ treeId: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'loading',
+ 'isRoot',
+ ]),
+ ...mapState({
+ projectName(state) {
+ return state.project.name;
+ },
+ }),
+ fetchedList() {
+ return treeList(this.$store.state, this.treeId);
+ },
+ hasPreviousDirectory() {
+ return !this.isRoot && this.fetchedList.length;
+ },
+ showLoading() {
+ return this.loading;
+ },
+ },
+};
+</script>
+
+<template>
+<div>
+ <div class="ide-file-list">
+ <table class="table">
+ <tbody
+ v-if="treeId">
+ <repo-previous-directory
+ v-if="hasPreviousDirectory"
+ />
+ <repo-loading-file
+ v-if="showLoading"
+ v-for="n in 5"
+ :key="n"
+ />
+ <repo-file
+ v-for="file in fetchedList"
+ :key="file.key"
+ :file="file"
+ />
+ </tbody>
+ </table>
+ </div>
+</div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
new file mode 100644
index 00000000000..535398d98c2
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -0,0 +1,62 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import projectTree from './ide_project_tree.vue';
+import icon from '../../vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ projectTree,
+ icon,
+ },
+ computed: {
+ ...mapState([
+ 'projects',
+ 'leftPanelCollapsed',
+ ]),
+ currentIcon() {
+ return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'setPanelCollapsedStatus',
+ ]),
+ toggleCollapsed() {
+ this.setPanelCollapsedStatus({
+ side: 'left',
+ collapsed: !this.leftPanelCollapsed,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel"
+ :class="{
+ 'is-collapsed': leftPanelCollapsed,
+ }"
+ >
+ <div class="multi-file-commit-panel-inner">
+ <project-tree
+ v-for="(project, index) in projects"
+ :key="project.id"
+ :project="project"/>
+ </div>
+ <button
+ type="button"
+ class="btn btn-transparent left-collapse-btn"
+ @click="toggleCollapsed"
+ >
+ <icon
+ :name="currentIcon"
+ :size="18"
+ />
+ <span
+ v-if="!leftPanelCollapsed"
+ class="collapse-text"
+ >Collapse sidebar</span>
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
new file mode 100644
index 00000000000..a24abadd936
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -0,0 +1,71 @@
+<script>
+import { mapState } from 'vuex';
+import icon from '../../vue_shared/components/icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import timeAgoMixin from '../../vue_shared/mixins/timeago';
+
+export default {
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [
+ timeAgoMixin,
+ ],
+ computed: {
+ ...mapState([
+ 'selectedFile',
+ ]),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-status-bar">
+ <div>
+ <icon
+ name="branch"
+ :size="12">
+ </icon>
+ {{ selectedFile.branchId }}
+ </div>
+ <div>
+ <div
+ v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
+ Last commit:
+ <a
+ v-tooltip
+ :title="selectedFile.lastCommit.message"
+ :href="selectedFile.lastCommit.url">
+ {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
+ {{ selectedFile.lastCommit.author }}
+ </a>
+ </div>
+ </div>
+ <div
+ class="text-right">
+ {{ selectedFile.name }}
+ </div>
+ <div
+ class="text-right">
+ {{ selectedFile.eol }}
+ </div>
+ <div
+ class="text-right">
+ {{ file.editorRow }}:{{ file.editorColumn }}
+ </div>
+ <div
+ class="text-right">
+ {{ selectedFile.fileLanguage }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue
index ba7090e4a9d..2119d373d31 100644
--- a/app/assets/javascripts/repo/components/new_branch_form.vue
+++ b/app/assets/javascripts/ide/components/new_branch_form.vue
@@ -44,7 +44,7 @@
this.branchName = '';
if (this.dropdownText) {
- this.dropdownText.textContent = this.currentBranch;
+ this.dropdownText.textContent = this.currentBranchId;
}
this.toggleDropdown();
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
new file mode 100644
index 00000000000..6e67e99a70f
--- /dev/null
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -0,0 +1,101 @@
+<script>
+ import newModal from './modal.vue';
+ import upload from './upload.vue';
+ import icon from '../../../vue_shared/components/icon.vue';
+
+ export default {
+ props: {
+ branch: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ parent: {
+ type: Object,
+ default: null,
+ },
+ },
+ components: {
+ icon,
+ newModal,
+ upload,
+ },
+ data() {
+ return {
+ openModal: false,
+ modalType: '',
+ };
+ },
+ methods: {
+ createNewItem(type) {
+ this.modalType = type;
+ this.toggleModalOpen();
+ },
+ toggleModalOpen() {
+ this.openModal = !this.openModal;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="repo-new-btn pull-right">
+ <div class="dropdown">
+ <button
+ type="button"
+ class="btn btn-sm btn-default dropdown-toggle add-to-tree"
+ data-toggle="dropdown"
+ aria-label="Create new file or directory"
+ >
+ <icon
+ name="plus"
+ :size="12"
+ css-classes="pull-left"
+ />
+ <icon
+ name="arrow-down"
+ :size="12"
+ css-classes="pull-left"
+ />
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right">
+ <li>
+ <a
+ href="#"
+ role="button"
+ @click.prevent="createNewItem('blob')"
+ >
+ {{ __('New file') }}
+ </a>
+ </li>
+ <li>
+ <upload
+ :branch-id="branch"
+ :path="path"
+ :parent="parent"
+ />
+ </li>
+ <li>
+ <a
+ href="#"
+ role="button"
+ @click.prevent="createNewItem('tree')"
+ >
+ {{ __('New directory') }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ <new-modal
+ v-if="openModal"
+ :type="modalType"
+ :branch-id="branch"
+ :path="path"
+ :parent="parent"
+ @toggle="toggleModalOpen"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index ac1f613bb71..a0650d37690 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,10 +1,18 @@
<script>
- import { mapActions } from 'vuex';
+ import { mapActions, mapState } from 'vuex';
import { __ } from '../../../locale';
- import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
+ import modal from '../../../vue_shared/components/modal.vue';
export default {
props: {
+ branchId: {
+ type: String,
+ required: true,
+ },
+ parent: {
+ type: Object,
+ default: null,
+ },
type: {
type: String,
required: true,
@@ -20,7 +28,7 @@
};
},
components: {
- popupDialog,
+ modal,
},
methods: {
...mapActions([
@@ -28,6 +36,9 @@
]),
createEntryInStore() {
this.createTempEntry({
+ projectId: this.currentProjectId,
+ branchId: this.branchId,
+ parent: this.parent,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type,
});
@@ -39,6 +50,9 @@
},
},
computed: {
+ ...mapState([
+ 'currentProjectId',
+ ]),
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');
@@ -68,7 +82,7 @@
</script>
<template>
- <popup-dialog
+ <modal
:title="modalTitle"
:primary-button-label="buttonLabel"
kind="success"
@@ -94,5 +108,5 @@
</div>
</fieldset>
</form>
- </popup-dialog>
+ </modal>
</template>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 14ad32f4ae0..2a2f2a241fc 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -1,12 +1,22 @@
<script>
- import { mapActions } from 'vuex';
+ import { mapActions, mapState } from 'vuex';
export default {
props: {
- path: {
+ branchId: {
type: String,
required: true,
},
+ parent: {
+ type: Object,
+ default: null,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'trees',
+ 'currentProjectId',
+ ]),
},
methods: {
...mapActions([
@@ -22,6 +32,9 @@
this.createTempEntry({
name,
+ projectId: this.currentProjectId,
+ branchId: this.branchId,
+ parent: this.parent,
type: 'blob',
content: result,
base64: !isText,
@@ -42,6 +55,9 @@
openFile() {
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
},
+ startFileUpload() {
+ this.$refs.fileUpload.click();
+ },
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
@@ -53,16 +69,19 @@
</script>
<template>
- <label
- role="button"
- class="menu-item"
- >
- {{ __('Upload file') }}
+ <div>
+ <a
+ href="#"
+ role="button"
+ @click.prevent="startFileUpload"
+ >
+ {{ __('Upload file') }}
+ </a>
<input
id="file-upload"
type="file"
class="hidden"
ref="fileUpload"
/>
- </label>
+ </div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index d3344d0c8dc..470db2c9650 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -2,12 +2,12 @@
import { mapGetters, mapState, mapActions } from 'vuex';
import tooltip from '../../vue_shared/directives/tooltip';
import icon from '../../vue_shared/components/icon.vue';
-import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
+import modal from '../../vue_shared/components/modal.vue';
import commitFilesList from './commit_sidebar/list.vue';
export default {
components: {
- PopupDialog,
+ modal,
icon,
commitFilesList,
},
@@ -16,16 +16,17 @@ export default {
},
data() {
return {
- showNewBranchDialog: false,
+ showNewBranchModal: false,
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
- collapsed: true,
};
},
computed: {
...mapState([
- 'currentBranch',
+ 'currentProjectId',
+ 'currentBranchId',
+ 'rightPanelCollapsed',
]),
...mapGetters([
'changedFiles',
@@ -42,12 +43,13 @@ export default {
'checkCommitStatus',
'commitChanges',
'getTreeData',
+ 'setPanelCollapsedStatus',
]),
makeCommit(newBranch = false) {
const createNewBranch = newBranch || this.startNewMR;
const payload = {
- branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
+ branch: createNewBranch ? `${this.currentBranchId}-${new Date().getTime().toString()}` : this.currentBranchId,
commit_message: this.commitMessage,
actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
@@ -55,16 +57,21 @@ export default {
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
})),
- start_branch: createNewBranch ? this.currentBranch : undefined,
+ start_branch: createNewBranch ? this.currentBranchId : undefined,
};
- this.showNewBranchDialog = false;
+ this.showNewBranchModal = false;
this.submitCommitsLoading = true;
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.submitCommitsLoading = false;
- this.getTreeData();
+ this.$store.dispatch('getTreeData', {
+ projectId: this.currentProjectId,
+ branch: this.currentBranchId,
+ endpoint: `/tree/${this.currentBranchId}`,
+ force: true,
+ });
})
.catch(() => {
this.submitCommitsLoading = false;
@@ -76,7 +83,7 @@ export default {
this.checkCommitStatus()
.then((branchChanged) => {
if (branchChanged) {
- this.showNewBranchDialog = true;
+ this.showNewBranchModal = true;
} else {
this.makeCommit();
}
@@ -86,50 +93,36 @@ export default {
});
},
toggleCollapsed() {
- this.collapsed = !this.collapsed;
+ this.setPanelCollapsedStatus({
+ side: 'right',
+ collapsed: !this.rightPanelCollapsed,
+ });
},
},
};
</script>
<template>
-<div
- class="multi-file-commit-panel"
- :class="{
- 'is-collapsed': collapsed,
- }"
->
- <popup-dialog
- v-if="showNewBranchDialog"
+<div class="multi-file-commit-panel-section">
+ <modal
+ v-if="showNewBranchModal"
:primary-button-label="__('Create new branch')"
kind="primary"
:title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
- @toggle="showNewBranchDialog = false"
+ @toggle="showNewBranchModal = false"
@submit="makeCommit(true)"
/>
- <button
- v-if="collapsed"
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn is-collapsed prepend-top-10 append-bottom-10"
- @click="toggleCollapsed"
- >
- <i
- aria-hidden="true"
- class="fa fa-angle-double-left"
- >
- </i>
- </button>
<commit-files-list
title="Staged"
:file-list="changedFiles"
- :collapsed="collapsed"
+ :collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed"
/>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent="tryCommit"
- v-if="!collapsed"
+ v-if="!rightPanelCollapsed"
>
<div class="multi-file-commit-fieldset">
<textarea
diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue
index 6c1bb4b8566..37bd9003e96 100644
--- a/app/assets/javascripts/repo/components/repo_edit_button.vue
+++ b/app/assets/javascripts/ide/components/repo_edit_button.vue
@@ -1,10 +1,10 @@
<script>
import { mapGetters, mapActions, mapState } from 'vuex';
-import popupDialog from '../../vue_shared/components/popup_dialog.vue';
+import modal from '../../vue_shared/components/modal.vue';
export default {
components: {
- popupDialog,
+ modal,
},
computed: {
...mapState([
@@ -43,7 +43,7 @@ export default {
{{buttonLabel}}
</span>
</button>
- <popup-dialog
+ <modal
v-if="discardPopupOpen"
class="text-left"
:primary-button-label="__('Discard changes')"
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f37cbd1e961..221be4b9074 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,6 +1,6 @@
<script>
/* global monaco */
-import { mapGetters, mapActions } from 'vuex';
+import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '../../flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
@@ -24,6 +24,9 @@ export default {
...mapActions([
'getRawFileData',
'changeFileContent',
+ 'setFileLanguage',
+ 'setEditorPosition',
+ 'setFileEOL',
]),
initMonaco() {
if (this.shouldHideEditor) return;
@@ -43,12 +46,36 @@ export default {
const model = this.editor.createModel(this.activeFile);
this.editor.attachModel(model);
+
model.onChange((m) => {
this.changeFileContent({
file: this.activeFile,
content: m.getValue(),
});
});
+
+ // Handle Cursor Position
+ this.editor.onPositionChange((instance, e) => {
+ this.setEditorPosition({
+ editorRow: e.position.lineNumber,
+ editorColumn: e.position.column,
+ });
+ });
+
+ this.editor.setPosition({
+ lineNumber: this.activeFile.editorRow,
+ column: this.activeFile.editorColumn,
+ });
+
+ // Handle File Language
+ this.setFileLanguage({
+ fileLanguage: model.language,
+ });
+
+ // Get File eol
+ this.setFileEOL({
+ eol: model.eol,
+ });
},
},
watch: {
@@ -57,12 +84,22 @@ export default {
this.initMonaco();
}
},
+ leftPanelCollapsed() {
+ this.editor.updateDimensions();
+ },
+ rightPanelCollapsed() {
+ this.editor.updateDimensions();
+ },
},
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
+ ...mapState([
+ 'leftPanelCollapsed',
+ 'rightPanelCollapsed',
+ ]),
shouldHideEditor() {
return this.activeFile.binary && !this.activeFile.raw;
},
@@ -76,13 +113,14 @@ export default {
class="blob-viewer-container blob-editor-container"
>
<div
- v-show="shouldHideEditor"
+ v-if="shouldHideEditor"
v-html="activeFile.html"
>
</div>
<div
v-show="!shouldHideEditor"
ref="editor"
+ class="multi-file-editor-holder"
>
</div>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 75787ad6103..09ca11531b1 100644
--- a/app/assets/javascripts/repo/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -1,7 +1,8 @@
<script>
- import { mapActions, mapGetters } from 'vuex';
+ import { mapState } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
+ import newDropdown from './new_dropdown/index.vue';
export default {
mixins: [
@@ -9,20 +10,22 @@
],
components: {
skeletonLoadingContainer,
+ newDropdown,
},
props: {
file: {
type: Object,
required: true,
},
+ showExtraColumns: {
+ type: Boolean,
+ default: false,
+ },
},
computed: {
- ...mapGetters([
- 'isCollapsed',
+ ...mapState([
+ 'leftPanelCollapsed',
]),
- isSubmodule() {
- return this.file.type === 'submodule';
- },
fileIcon() {
return {
'fa-spinner fa-spin': this.file.loading,
@@ -30,6 +33,12 @@
'fa-folder-open': !this.file.loading && this.file.opened,
};
},
+ isSubmodule() {
+ return this.file.type === 'submodule';
+ },
+ isTree() {
+ return this.file.type === 'tree';
+ },
levelIndentation() {
return {
marginLeft: `${this.file.level * 16}px`,
@@ -39,13 +48,39 @@
return this.file.id.substr(0, 8);
},
submoduleColSpan() {
- return !this.isCollapsed && this.isSubmodule ? 3 : 1;
+ return !this.leftPanelCollapsed && this.isSubmodule ? 3 : 1;
+ },
+ fileClass() {
+ if (this.file.type === 'blob') {
+ if (this.file.active) {
+ return 'file-open file-active';
+ }
+ return this.file.opened ? 'file-open' : '';
+ }
+ return '';
+ },
+ changedClass() {
+ return {
+ 'fa-circle unsaved-icon': this.file.changed || this.file.tempFile,
+ };
},
},
methods: {
- ...mapActions([
- 'clickedTreeRow',
- ]),
+ clickFile(row) {
+ // Manual Action if a tree is selected/opened
+ if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
+ this.$store.dispatch('toggleTreeOpen', {
+ endpoint: this.file.url,
+ tree: this.file,
+ });
+ }
+ this.$router.push(`/project${row.url}`);
+ },
+ },
+ updated() {
+ if (this.file.type === 'blob' && this.file.active) {
+ this.$el.scrollIntoView();
+ }
},
};
</script>
@@ -53,7 +88,8 @@
<template>
<tr
class="file"
- @click.prevent="clickedTreeRow(file)">
+ :class="fileClass"
+ @click="clickFile(file)">
<td
class="multi-file-table-name"
:colspan="submoduleColSpan"
@@ -66,11 +102,23 @@
>
</i>
<a
- :href="file.url"
class="repo-file-name"
>
{{ file.name }}
</a>
+ <new-dropdown
+ v-if="isTree"
+ :project-id="file.projectId"
+ :branch="file.branchId"
+ :path="file.path"
+ :parent="file"/>
+ <i
+ class="fa"
+ v-if="changedClass"
+ :class="changedClass"
+ aria-hidden="true"
+ >
+ </i>
<template v-if="isSubmodule && file.id">
@
<span class="commit-sha">
@@ -84,7 +132,7 @@
</template>
</td>
- <template v-if="!isCollapsed && !isSubmodule">
+ <template v-if="showExtraColumns && !isSubmodule">
<td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
<a
v-if="file.lastCommit.message"
diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue
index 34f0d51819a..34f0d51819a 100644
--- a/app/assets/javascripts/repo/components/repo_file_buttons.vue
+++ b/app/assets/javascripts/ide/components/repo_file_buttons.vue
diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue
index 8fa637d771f..7eb840c7608 100644
--- a/app/assets/javascripts/repo/components/repo_loading_file.vue
+++ b/app/assets/javascripts/ide/components/repo_loading_file.vue
@@ -1,5 +1,5 @@
<script>
- import { mapGetters } from 'vuex';
+ import { mapState } from 'vuex';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default {
@@ -7,8 +7,8 @@
skeletonLoadingContainer,
},
computed: {
- ...mapGetters([
- 'isCollapsed',
+ ...mapState([
+ 'leftPanelCollapsed',
]),
},
};
@@ -24,7 +24,7 @@
:small="true"
/>
</td>
- <template v-if="!isCollapsed">
+ <template v-if="!leftPanelCollapsed">
<td
class="hidden-sm hidden-xs">
<skeleton-loading-container
diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/ide/components/repo_prev_directory.vue
index a2b305bbd05..7cd359ea4ed 100644
--- a/app/assets/javascripts/repo/components/repo_prev_directory.vue
+++ b/app/assets/javascripts/ide/components/repo_prev_directory.vue
@@ -1,16 +1,14 @@
<script>
- import { mapGetters, mapState, mapActions } from 'vuex';
+ import { mapState, mapActions } from 'vuex';
export default {
computed: {
...mapState([
'parentTreeUrl',
- ]),
- ...mapGetters([
- 'isCollapsed',
+ 'leftPanelCollapsed',
]),
colSpanCondition() {
- return this.isCollapsed ? undefined : 3;
+ return this.leftPanelCollapsed ? undefined : 3;
},
},
methods: {
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue
index 425c55fafb5..3d1e0297bd5 100644
--- a/app/assets/javascripts/repo/components/repo_preview.vue
+++ b/app/assets/javascripts/ide/components/repo_preview.vue
@@ -1,6 +1,6 @@
<script>
-/* global LineHighlighter */
import { mapGetters } from 'vuex';
+import LineHighlighter from '../../line_highlighter';
import syntaxHighlight from '../../syntax_highlight';
export default {
diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index fb29a60df66..5bd63ac9ec5 100644
--- a/app/assets/javascripts/repo/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -27,16 +27,18 @@ export default {
methods: {
...mapActions([
- 'setFileActive',
'closeFile',
]),
+ clickFile(tab) {
+ this.$router.push(`/project${tab.url}`);
+ },
},
};
</script>
<template>
<li
- @click="setFileActive(tab)"
+ @click="clickFile(tab)"
>
<button
type="button"
diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index ab0bef4f0ac..ab0bef4f0ac 100644
--- a/app/assets/javascripts/repo/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
new file mode 100644
index 00000000000..a9cbf8e370f
--- /dev/null
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -0,0 +1,101 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import store from './stores';
+import flash from '../flash';
+import {
+ getTreeEntry,
+} from './stores/utils';
+
+Vue.use(VueRouter);
+
+/**
+ * Routes below /-/ide/:
+
+/project/h5bp/html5-boilerplate/blob/master
+/project/h5bp/html5-boilerplate/blob/master/app/js/test.js
+
+/project/h5bp/html5-boilerplate/mr/123
+/project/h5bp/html5-boilerplate/mr/123/app/js/test.js
+
+/workspace/123
+/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch
+/workspace/project/h5bp/html5-boilerplate/mr/123
+
+/ = /workspace
+
+/settings
+*/
+
+// Unfortunately Vue Router doesn't work without at least a fake component
+// If you do only data handling
+const EmptyRouterComponent = {
+ render(createElement) {
+ return createElement('div');
+ },
+};
+
+const router = new VueRouter({
+ mode: 'history',
+ base: `${gon.relative_url_root}/-/ide/`,
+ routes: [
+ {
+ path: '/project/:namespace/:project',
+ component: EmptyRouterComponent,
+ children: [
+ {
+ path: ':targetmode/:branch/*',
+ component: EmptyRouterComponent,
+ },
+ {
+ path: 'mr/:mrid',
+ component: EmptyRouterComponent,
+ },
+ ],
+ },
+ ],
+});
+
+router.beforeEach((to, from, next) => {
+ if (to.params.namespace && to.params.project) {
+ store.dispatch('getProjectData', {
+ namespace: to.params.namespace,
+ projectId: to.params.project,
+ })
+ .then(() => {
+ const fullProjectId = `${to.params.namespace}/${to.params.project}`;
+
+ if (to.params.branch) {
+ store.dispatch('getBranchData', {
+ projectId: fullProjectId,
+ branchId: to.params.branch,
+ });
+
+ store.dispatch('getTreeData', {
+ projectId: fullProjectId,
+ branch: to.params.branch,
+ endpoint: `/tree/${to.params.branch}`,
+ })
+ .then(() => {
+ if (to.params[0]) {
+ const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]);
+ if (treeEntry) {
+ store.dispatch('handleTreeEntryAction', treeEntry);
+ }
+ }
+ })
+ .catch((e) => {
+ flash('Error while loading the branch files. Please try again.');
+ throw e;
+ });
+ }
+ })
+ .catch((e) => {
+ flash('Error while loading the project data. Please try again.');
+ throw e;
+ });
+ }
+
+ next();
+});
+
+export default router;
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
new file mode 100644
index 00000000000..a96bd339f51
--- /dev/null
+++ b/app/assets/javascripts/ide/index.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+import { mapActions } from 'vuex';
+import { convertPermissionToBoolean } from '../lib/utils/common_utils';
+import ide from './components/ide.vue';
+
+import store from './stores';
+import router from './ide_router';
+import Translate from '../vue_shared/translate';
+import ContextualSidebar from '../contextual_sidebar';
+
+function initIde(el) {
+ if (!el) return null;
+
+ return new Vue({
+ el,
+ store,
+ router,
+ components: {
+ ide,
+ },
+ methods: {
+ ...mapActions([
+ 'setInitialData',
+ ]),
+ },
+ created() {
+ const data = el.dataset;
+
+ this.setInitialData({
+ endpoints: {
+ rootEndpoint: data.url,
+ newMergeRequestUrl: data.newMergeRequestUrl,
+ rootUrl: data.rootUrl,
+ },
+ canCommit: convertPermissionToBoolean(data.canCommit),
+ onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
+ path: data.currentPath,
+ isRoot: convertPermissionToBoolean(data.root),
+ isInitialRoot: convertPermissionToBoolean(data.root),
+ });
+ },
+ render(createElement) {
+ return createElement('ide');
+ },
+ });
+}
+
+const ideElement = document.getElementById('ide');
+
+Vue.use(Translate);
+
+initIde(ideElement);
+
+const contextualSidebar = new ContextualSidebar();
+contextualSidebar.bindEvents();
diff --git a/app/assets/javascripts/repo/lib/common/disposable.js b/app/assets/javascripts/ide/lib/common/disposable.js
index 84b29bdb600..84b29bdb600 100644
--- a/app/assets/javascripts/repo/lib/common/disposable.js
+++ b/app/assets/javascripts/ide/lib/common/disposable.js
diff --git a/app/assets/javascripts/repo/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 23c4811e6c0..14d9fe4771e 100644
--- a/app/assets/javascripts/repo/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -28,6 +28,14 @@ export default class Model {
return this.model.uri.toString();
}
+ get language() {
+ return this.model.getModeId();
+ }
+
+ get eol() {
+ return this.model.getEOL() === '\n' ? 'LF' : 'CRLF';
+ }
+
get path() {
return this.file.path;
}
diff --git a/app/assets/javascripts/repo/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index fd462252795..fd462252795 100644
--- a/app/assets/javascripts/repo/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
diff --git a/app/assets/javascripts/repo/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js
index 0954b7973c4..0954b7973c4 100644
--- a/app/assets/javascripts/repo/lib/decorations/controller.js
+++ b/app/assets/javascripts/ide/lib/decorations/controller.js
diff --git a/app/assets/javascripts/repo/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index dc0b1c95e59..dc0b1c95e59 100644
--- a/app/assets/javascripts/repo/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
diff --git a/app/assets/javascripts/repo/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js
index 0e37f5c4704..0e37f5c4704 100644
--- a/app/assets/javascripts/repo/lib/diff/diff.js
+++ b/app/assets/javascripts/ide/lib/diff/diff.js
diff --git a/app/assets/javascripts/repo/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js
index e74c4046330..e74c4046330 100644
--- a/app/assets/javascripts/repo/lib/diff/diff_worker.js
+++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js
diff --git a/app/assets/javascripts/repo/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index db499444402..51e202b9348 100644
--- a/app/assets/javascripts/repo/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -22,6 +22,11 @@ export default class Editor {
this.modelManager = new ModelManager(this.monaco),
this.decorationsController = new DecorationsController(this),
);
+
+ this.debouncedUpdate = _.debounce(() => {
+ this.updateDimensions();
+ }, 200);
+ window.addEventListener('resize', this.debouncedUpdate, false);
}
createInstance(domElement) {
@@ -32,6 +37,9 @@ export default class Editor {
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
+ minimap: {
+ enabled: false,
+ },
}),
this.dirtyDiffController = new DirtyDiffController(
this.modelManager, this.decorationsController,
@@ -70,10 +78,32 @@ export default class Editor {
dispose() {
this.disposable.dispose();
+ window.removeEventListener('resize', this.debouncedUpdate);
// dispose main monaco instance
if (this.instance) {
this.instance = null;
}
}
+
+ updateDimensions() {
+ this.instance.layout();
+ }
+
+ setPosition({ lineNumber, column }) {
+ this.instance.revealPositionInCenter({
+ lineNumber,
+ column,
+ });
+ this.instance.setPosition({
+ lineNumber,
+ column,
+ });
+ }
+
+ onPositionChange(cb) {
+ this.disposable.add(
+ this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
+ );
+ }
}
diff --git a/app/assets/javascripts/repo/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index 701affc466e..701affc466e 100644
--- a/app/assets/javascripts/repo/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
diff --git a/app/assets/javascripts/repo/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js
index af83a1ec0b4..af83a1ec0b4 100644
--- a/app/assets/javascripts/repo/monaco_loader.js
+++ b/app/assets/javascripts/ide/monaco_loader.js
diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/ide/services/index.js
index 994d325e991..1fb24e93f2e 100644
--- a/app/assets/javascripts/repo/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -23,8 +23,11 @@ export default {
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text());
},
- getBranchData(projectId, currentBranch) {
- return Api.branchSingle(projectId, currentBranch);
+ getProjectData(namespace, project) {
+ return Api.project(`${namespace}/${project}`);
+ },
+ getBranchData(projectId, currentBranchId) {
+ return Api.branchSingle(projectId, currentBranchId);
},
createBranch(projectId, payload) {
const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
new file mode 100644
index 00000000000..c01046c8c76
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -0,0 +1,179 @@
+import Vue from 'vue';
+import { visitUrl } from '../../lib/utils/url_utility';
+import flash from '../../flash';
+import service from '../services';
+import * as types from './mutation_types';
+
+export const redirectToUrl = (_, url) => visitUrl(url);
+
+export const setInitialData = ({ commit }, data) =>
+ commit(types.SET_INITIAL_DATA, data);
+
+export const closeDiscardPopup = ({ commit }) =>
+ commit(types.TOGGLE_DISCARD_POPUP, false);
+
+export const discardAllChanges = ({ commit, getters, dispatch }) => {
+ const changedFiles = getters.changedFiles;
+
+ changedFiles.forEach((file) => {
+ commit(types.DISCARD_FILE_CHANGES, file);
+
+ if (file.tempFile) {
+ dispatch('closeFile', { file, force: true });
+ }
+ });
+};
+
+export const closeAllFiles = ({ state, dispatch }) => {
+ state.openFiles.forEach(file => dispatch('closeFile', { file }));
+};
+
+export const toggleEditMode = (
+ { state, commit, getters, dispatch },
+ force = false,
+) => {
+ const changedFiles = getters.changedFiles;
+
+ if (changedFiles.length && !force) {
+ commit(types.TOGGLE_DISCARD_POPUP, true);
+ } else {
+ commit(types.TOGGLE_EDIT_MODE);
+ commit(types.TOGGLE_DISCARD_POPUP, false);
+ dispatch('toggleBlobView');
+
+ if (!state.editMode) {
+ dispatch('discardAllChanges');
+ }
+ }
+};
+
+export const toggleBlobView = ({ commit, state }) => {
+ if (state.editMode) {
+ commit(types.SET_EDIT_MODE);
+ } else {
+ commit(types.SET_PREVIEW_MODE);
+ }
+};
+
+export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
+ if (side === 'left') {
+ commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
+ } else {
+ commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
+ }
+};
+
+export const checkCommitStatus = ({ state }) =>
+ service
+ .getBranchData(state.currentProjectId, state.currentBranchId)
+ .then((data) => {
+ const { id } = data.commit;
+ const selectedBranch =
+ state.projects[state.currentProjectId].branches[state.currentBranchId];
+
+ if (selectedBranch.workingReference !== id) {
+ return true;
+ }
+
+ return false;
+ })
+ .catch(() => flash('Error checking branch data. Please try again.'));
+
+export const commitChanges = (
+ { commit, state, dispatch, getters },
+ { payload, newMr },
+) =>
+ service
+ .commit(state.currentProjectId, payload)
+ .then((data) => {
+ const { branch } = payload;
+ if (!data.short_id) {
+ flash(data.message);
+ return;
+ }
+
+ const selectedProject = state.projects[state.currentProjectId];
+ const lastCommit = {
+ commit_path: `${selectedProject.web_url}/commit/${data.id}`,
+ commit: {
+ message: data.message,
+ authored_date: data.committed_date,
+ },
+ };
+
+ flash(
+ `Your changes have been committed. Commit ${data.short_id} with ${
+ data.stats.additions
+ } additions, ${data.stats.deletions} deletions.`,
+ 'notice',
+ );
+
+ if (newMr) {
+ dispatch(
+ 'redirectToUrl',
+ `${
+ selectedProject.web_url
+ }/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
+ );
+ } else {
+ commit(types.SET_BRANCH_WORKING_REFERENCE, {
+ projectId: state.currentProjectId,
+ branchId: state.currentBranchId,
+ reference: data.id,
+ });
+
+ getters.changedFiles.forEach((entry) => {
+ commit(types.SET_LAST_COMMIT_DATA, {
+ entry,
+ lastCommit,
+ });
+ });
+
+ dispatch('discardAllChanges');
+ dispatch('closeAllFiles');
+
+ window.scrollTo(0, 0);
+ }
+ })
+ .catch(() => flash('Error committing changes. Please try again.'));
+
+export const createTempEntry = (
+ { state, dispatch },
+ { projectId, branchId, parent, name, type, content = '', base64 = false },
+) => {
+ const selectedParent = parent || state.trees[`${projectId}/${branchId}`];
+ if (type === 'tree') {
+ dispatch('createTempTree', {
+ projectId,
+ branchId,
+ parent: selectedParent,
+ name,
+ });
+ } else if (type === 'blob') {
+ dispatch('createTempFile', {
+ projectId,
+ branchId,
+ parent: selectedParent,
+ name,
+ base64,
+ content,
+ });
+ }
+};
+
+export const scrollToTab = () => {
+ Vue.nextTick(() => {
+ const tabs = document.getElementById('tabs');
+
+ if (tabs) {
+ const tabEl = tabs.querySelector('.active .repo-tab');
+
+ tabEl.focus();
+ }
+ });
+};
+
+export * from './actions/tree';
+export * from './actions/file';
+export * from './actions/project';
+export * from './actions/branch';
diff --git a/app/assets/javascripts/ide/stores/actions/branch.js b/app/assets/javascripts/ide/stores/actions/branch.js
new file mode 100644
index 00000000000..32bdf7fec22
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/branch.js
@@ -0,0 +1,43 @@
+import service from '../../services';
+import flash from '../../../flash';
+import * as types from '../mutation_types';
+
+export const getBranchData = (
+ { commit, state, dispatch },
+ { projectId, branchId, force = false } = {},
+) => new Promise((resolve, reject) => {
+ if ((typeof state.projects[`${projectId}`] === 'undefined' ||
+ !state.projects[`${projectId}`].branches[branchId])
+ || force) {
+ service.getBranchData(`${projectId}`, branchId)
+ .then((data) => {
+ const { id } = data.commit;
+ commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
+ commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading branch data. Please try again.');
+ reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
+ });
+ } else {
+ resolve(state.projects[`${projectId}`].branches[branchId]);
+ }
+});
+
+export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
+ state.currentProjectId,
+ {
+ branch,
+ ref: state.currentBranchId,
+ },
+)
+.then(res => res.json())
+.then((data) => {
+ const branchName = data.name;
+ const url = location.href.replace(state.currentBranchId, branchName);
+
+ if (this.$router) this.$router.push(url);
+
+ commit(types.SET_CURRENT_BRANCH, branchName);
+});
diff --git a/app/assets/javascripts/repo/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 5bae4fa826a..0f27d5bf1c3 100644
--- a/app/assets/javascripts/repo/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -2,9 +2,9 @@ import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash';
import service from '../../services';
import * as types from '../mutation_types';
+import router from '../../ide_router';
import {
findEntry,
- pushState,
setPageTitle,
createTemp,
findIndexOfFile,
@@ -25,7 +25,7 @@ export const closeFile = ({ commit, state, dispatch }, { file, force = false })
dispatch('setFileActive', nextFileToOpen);
} else if (!state.openFiles.length) {
- pushState(file.parentTreeUrl);
+ router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
}
dispatch('getLastCommitData');
@@ -45,6 +45,9 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
// reset hash for line highlighting
location.hash = '';
+
+ commit(types.SET_CURRENT_PROJECT, file.projectId);
+ commit(types.SET_CURRENT_BRANCH, file.branchId);
};
export const getFileData = ({ state, commit, dispatch }, file) => {
@@ -63,8 +66,6 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
commit(types.TOGGLE_LOADING, file);
-
- pushState(file.url);
})
.catch(() => {
commit(types.TOGGLE_LOADING, file);
@@ -82,21 +83,39 @@ export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
};
-export const createTempFile = ({ state, commit, dispatch }, { tree, name, content = '', base64 = '' }) => {
+export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
+ commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
+};
+
+export const setFileEOL = ({ state, commit }, { eol }) => {
+ commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
+};
+
+export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
+ commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
+};
+
+export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
+ const path = parent.path !== undefined ? parent.path : '';
+ // We need to do the replacement otherwise the web_url + file.url duplicate
+ const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`;
const file = createTemp({
- name: name.replace(`${state.path}/`, ''),
- path: tree.path,
+ projectId,
+ branchId,
+ name: name.replace(`${path}/`, ''),
+ path,
type: 'blob',
- level: tree.level !== undefined ? tree.level + 1 : 0,
+ level: parent.level !== undefined ? parent.level + 1 : 0,
changed: true,
content,
base64,
+ url: newUrl,
});
- if (findEntry(tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
+ if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
commit(types.CREATE_TMP_FILE, {
- parent: tree,
+ parent,
file,
});
commit(types.TOGGLE_FILE_OPEN, file);
@@ -106,5 +125,7 @@ export const createTempFile = ({ state, commit, dispatch }, { tree, name, conten
dispatch('toggleEditMode', true);
}
+ router.push(`/project${file.url}`);
+
return Promise.resolve(file);
};
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
new file mode 100644
index 00000000000..75e332090cb
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -0,0 +1,25 @@
+import service from '../../services';
+import flash from '../../../flash';
+import * as types from '../mutation_types';
+
+// eslint-disable-next-line import/prefer-default-export
+export const getProjectData = (
+ { commit, state, dispatch },
+ { namespace, projectId, force = false } = {},
+) => new Promise((resolve, reject) => {
+ if (!state.projects[`${namespace}/${projectId}`] || force) {
+ service.getProjectData(namespace, projectId)
+ .then(res => res.data)
+ .then((data) => {
+ commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
+ if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading project data. Please try again.');
+ reject(new Error(`Project not loaded ${namespace}/${projectId}`));
+ });
+ } else {
+ resolve(state.projects[`${namespace}/${projectId}`]);
+ }
+});
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
new file mode 100644
index 00000000000..25909400a75
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -0,0 +1,188 @@
+import { visitUrl } from '../../../lib/utils/url_utility';
+import { normalizeHeaders } from '../../../lib/utils/common_utils';
+import flash from '../../../flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+import router from '../../ide_router';
+import {
+ setPageTitle,
+ findEntry,
+ createTemp,
+ createOrMergeEntry,
+} from '../utils';
+
+export const getTreeData = (
+ { commit, state, dispatch },
+ { endpoint, tree = null, projectId, branch, force = false } = {},
+) => new Promise((resolve, reject) => {
+ // We already have the base tree so we resolve immediately
+ if (!tree && state.trees[`${projectId}/${branch}`] && !force) {
+ resolve();
+ } else {
+ if (tree) commit(types.TOGGLE_LOADING, tree);
+ const selectedProject = state.projects[projectId];
+ // We are merging the web_url that we got on the project info with the endpoint
+ // we got on the tree entry, as both contain the projectId, we replace it in the tree endpoint
+ const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, '');
+ if (completeEndpoint && (!tree || !tree.tempFile)) {
+ service.getTreeData(completeEndpoint)
+ .then((res) => {
+ const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
+
+ setPageTitle(pageTitle);
+
+ return res.json();
+ })
+ .then((data) => {
+ if (!state.isInitialRoot) {
+ commit(types.SET_ROOT, data.path === '/');
+ }
+
+ dispatch('updateDirectoryData', { data, tree, projectId, branch });
+ const selectedTree = tree || state.trees[`${projectId}/${branch}`];
+
+ commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
+ commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path });
+ if (tree) commit(types.TOGGLE_LOADING, selectedTree);
+
+ const prevLastCommitPath = selectedTree.lastCommitPath;
+ if (prevLastCommitPath !== null) {
+ dispatch('getLastCommitData', selectedTree);
+ }
+ resolve(data);
+ })
+ .catch((e) => {
+ flash('Error loading tree data. Please try again.');
+ if (tree) commit(types.TOGGLE_LOADING, tree);
+ reject(e);
+ });
+ } else {
+ resolve();
+ }
+ }
+});
+
+export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
+ if (tree.opened) {
+ // send empty data to clear the tree
+ const data = { trees: [], blobs: [], submodules: [] };
+
+ dispatch('updateDirectoryData', { data, tree, projectId: tree.projectId, branchId: tree.branchId });
+ } else {
+ dispatch('getTreeData', { endpoint, tree, projectId: tree.projectId, branch: tree.branchId });
+ }
+
+ commit(types.TOGGLE_TREE_OPEN, tree);
+};
+
+export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
+ if (row.type === 'tree') {
+ dispatch('toggleTreeOpen', {
+ endpoint: row.url,
+ tree: row,
+ });
+ } else if (row.type === 'submodule') {
+ commit(types.TOGGLE_LOADING, row);
+ visitUrl(row.url);
+ } else if (row.type === 'blob' && row.opened) {
+ dispatch('setFileActive', row);
+ } else {
+ dispatch('getFileData', row);
+ }
+};
+
+export const createTempTree = (
+ { state, commit, dispatch },
+ { projectId, branchId, parent, name },
+) => {
+ let selectedTree = parent;
+ const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
+
+ dirNames.forEach((dirName) => {
+ const foundEntry = findEntry(selectedTree.tree, 'tree', dirName);
+
+ if (!foundEntry) {
+ const path = selectedTree.path !== undefined ? selectedTree.path : '';
+ const tmpEntry = createTemp({
+ projectId,
+ branchId,
+ name: dirName,
+ path,
+ type: 'tree',
+ level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0,
+ tree: [],
+ url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`,
+ });
+
+ commit(types.CREATE_TMP_TREE, {
+ parent: selectedTree,
+ tmpEntry,
+ });
+ commit(types.TOGGLE_TREE_OPEN, tmpEntry);
+
+ router.push(`/project${tmpEntry.url}`);
+
+ selectedTree = tmpEntry;
+ } else {
+ selectedTree = foundEntry;
+ }
+ });
+};
+
+export const getLastCommitData = ({ state, commit, dispatch, getters }, 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.'));
+};
+
+export const updateDirectoryData = (
+ { commit, state },
+ { data, tree, projectId, branch },
+) => {
+ if (!tree) {
+ const existingTree = state.trees[`${projectId}/${branch}`];
+ if (!existingTree) {
+ commit(types.CREATE_TREE, { treePath: `${projectId}/${branch}` });
+ }
+ }
+
+ const selectedTree = tree || state.trees[`${projectId}/${branch}`];
+ const level = selectedTree.level !== undefined ? selectedTree.level + 1 : 0;
+ const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
+ const createEntry = (entry, type) => createOrMergeEntry({
+ tree: selectedTree,
+ projectId: `${projectId}`,
+ branchId: branch,
+ entry,
+ level,
+ type,
+ parentTreeUrl,
+ });
+
+ const formattedData = [
+ ...data.trees.map(t => createEntry(t, 'tree')),
+ ...data.submodules.map(m => createEntry(m, 'submodule')),
+ ...data.blobs.map(b => createEntry(b, 'blob')),
+ ];
+
+ commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
+};
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
new file mode 100644
index 00000000000..6b51ccff817
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -0,0 +1,19 @@
+export const changedFiles = state => state.openFiles.filter(file => file.changed);
+
+export const activeFile = state => state.openFiles.find(file => file.active) || null;
+
+export const activeFileExtension = (state) => {
+ const file = activeFile(state);
+ return file ? `.${file.path.split('.').pop()}` : '';
+};
+
+export const canEditFile = (state) => {
+ const currentActiveFile = activeFile(state);
+
+ return state.canCommit &&
+ (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
+};
+
+export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
+
+export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
diff --git a/app/assets/javascripts/repo/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index 6ac9bfd8189..6ac9bfd8189 100644
--- a/app/assets/javascripts/repo/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
diff --git a/app/assets/javascripts/repo/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index bc3390f1506..4e3c10972ba 100644
--- a/app/assets/javascripts/repo/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -1,16 +1,27 @@
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
-export const SET_COMMIT_REF = 'SET_COMMIT_REF';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT';
-export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
+export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
+export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
+
+// Project Mutation Types
+export const SET_PROJECT = 'SET_PROJECT';
+export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
+export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
+
+// Branch Mutation Types
+export const SET_BRANCH = 'SET_BRANCH';
+export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
+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 CREATE_TMP_TREE = 'CREATE_TMP_TREE';
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
+export const CREATE_TREE = 'CREATE_TREE';
// File mutation types
export const SET_FILE_DATA = 'SET_FILE_DATA';
@@ -18,6 +29,9 @@ export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
+export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
+export const SET_FILE_POSITION = 'SET_FILE_POSITION';
+export const SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
@@ -28,3 +42,4 @@ export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
+
diff --git a/app/assets/javascripts/repo/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index ae2ba5bedf7..2fed9019cb6 100644
--- a/app/assets/javascripts/repo/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,4 +1,5 @@
import * as types from './mutation_types';
+import projectMutations from './mutations/project';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
@@ -32,29 +33,32 @@ export default {
discardPopupOpen,
});
},
- [types.SET_COMMIT_REF](state, ref) {
- Object.assign(state, {
- currentRef: ref,
- });
- },
[types.SET_ROOT](state, isRoot) {
Object.assign(state, {
isRoot,
isInitialRoot: isRoot,
});
},
- [types.SET_PREVIOUS_URL](state, previousUrl) {
+ [types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
+ Object.assign(state, {
+ leftPanelCollapsed: collapsed,
+ });
+ },
+ [types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, {
- previousUrl,
+ rightPanelCollapsed: collapsed,
});
},
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
Object.assign(entry.lastCommit, {
+ id: lastCommit.commit.id,
url: lastCommit.commit_path,
message: lastCommit.commit.message,
+ author: lastCommit.commit.author_name,
updatedAt: lastCommit.commit.authored_date,
});
},
+ ...projectMutations,
...fileMutations,
...treeMutations,
...branchMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js
new file mode 100644
index 00000000000..04b9582c5bb
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/branch.js
@@ -0,0 +1,28 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_BRANCH](state, currentBranchId) {
+ Object.assign(state, {
+ currentBranchId,
+ });
+ },
+ [types.SET_BRANCH](state, { projectPath, branchName, branch }) {
+ // Add client side properties
+ Object.assign(branch, {
+ treeId: `${projectPath}/${branchName}`,
+ active: true,
+ workingReference: '',
+ });
+
+ Object.assign(state.projects[projectPath], {
+ branches: {
+ [branchName]: branch,
+ },
+ });
+ },
+ [types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
+ Object.assign(state.projects[projectId].branches[branchId], {
+ workingReference: reference,
+ });
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index f9ba80b9dc2..5f3655b0092 100644
--- a/app/assets/javascripts/repo/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -6,6 +6,10 @@ export default {
Object.assign(file, {
active,
});
+
+ Object.assign(state, {
+ selectedFile: file,
+ });
},
[types.TOGGLE_FILE_OPEN](state, file) {
Object.assign(file, {
@@ -42,6 +46,22 @@ export default {
changed,
});
},
+ [types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
+ Object.assign(file, {
+ fileLanguage,
+ });
+ },
+ [types.SET_FILE_EOL](state, { file, eol }) {
+ Object.assign(file, {
+ eol,
+ });
+ },
+ [types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
+ Object.assign(file, {
+ editorRow,
+ editorColumn,
+ });
+ },
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: '',
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
new file mode 100644
index 00000000000..2816562a919
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -0,0 +1,23 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_PROJECT](state, currentProjectId) {
+ Object.assign(state, {
+ currentProjectId,
+ });
+ },
+ [types.SET_PROJECT](state, { projectPath, project }) {
+ // Add client side properties
+ Object.assign(project, {
+ tree: [],
+ branches: {},
+ active: true,
+ });
+
+ Object.assign(state, {
+ projects: Object.assign({}, state.projects, {
+ [projectPath]: project,
+ }),
+ });
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
index 130221c9fda..4fe438ab465 100644
--- a/app/assets/javascripts/repo/stores/mutations/tree.js
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -6,6 +6,15 @@ export default {
opened: !tree.opened,
});
},
+ [types.CREATE_TREE](state, { treePath }) {
+ Object.assign(state, {
+ trees: Object.assign({}, state.trees, {
+ [treePath]: {
+ tree: [],
+ },
+ }),
+ });
+ },
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
Object.assign(tree, {
tree: data,
diff --git a/app/assets/javascripts/repo/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 0068834831e..539e382830f 100644
--- a/app/assets/javascripts/repo/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,10 +1,10 @@
export default () => ({
canCommit: false,
- currentBranch: '',
- currentBlobView: 'repo-preview',
- currentRef: '',
+ currentProjectId: '',
+ currentBranchId: '',
+ currentBlobView: 'repo-editor',
discardPopupOpen: false,
- editMode: false,
+ editMode: true,
endpoints: {},
isRoot: false,
isInitialRoot: false,
@@ -12,13 +12,11 @@ export default () => ({
loading: false,
onTopOfBranch: false,
openFiles: [],
+ selectedFile: null,
path: '',
- project: {
- id: 0,
- name: '',
- url: '',
- },
parentTreeUrl: '',
- previousUrl: '',
- tree: [],
+ trees: {},
+ projects: {},
+ leftPanelCollapsed: false,
+ rightPanelCollapsed: true,
});
diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index fae1f4439a9..29e3ab5d040 100644
--- a/app/assets/javascripts/repo/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -2,6 +2,8 @@ export const dataStructure = () => ({
id: '',
key: '',
type: '',
+ projectId: '',
+ branchId: '',
name: '',
url: '',
path: '',
@@ -15,9 +17,11 @@ export const dataStructure = () => ({
changed: false,
lastCommitPath: '',
lastCommit: {
+ id: '',
url: '',
message: '',
updatedAt: '',
+ author: '',
},
tree_url: '',
blamePath: '',
@@ -31,11 +35,17 @@ export const dataStructure = () => ({
parentTreeUrl: '',
renderError: false,
base64: false,
+ editorRow: 1,
+ editorColumn: 1,
+ fileLanguage: '',
+ eol: '',
});
export const decorateData = (entity) => {
const {
id,
+ projectId,
+ branchId,
type,
url,
name,
@@ -56,6 +66,8 @@ export const decorateData = (entity) => {
return {
...dataStructure(),
id,
+ projectId,
+ branchId,
key: `${name}-${type}-${id}`,
type,
name,
@@ -75,24 +87,51 @@ export const decorateData = (entity) => {
};
};
-export const findEntry = (state, type, name) => state.tree.find(
+/*
+ Takes the multi-dimensional tree and returns a flattened array.
+ This allows for the table to recursively render the table rows but keeps the data
+ structure nested to make it easier to add new files/directories.
+*/
+export const treeList = (state, treeId) => {
+ const baseTree = state.trees[treeId];
+ if (baseTree) {
+ const mapTree = arr => (!arr.tree || !arr.tree.length ?
+ [] : _.map(arr.tree, a => [a, mapTree(a)]));
+
+ return _.chain(baseTree.tree)
+ .map(arr => [arr, mapTree(arr)])
+ .flatten()
+ .value();
+ }
+ return [];
+};
+
+export const getTree = state => (namespace, projectId, branch) => state.trees[`${namespace}/${projectId}/${branch}`];
+
+export const getTreeEntry = (store, treeId, path) => {
+ const fileList = treeList(store.state, treeId);
+ return fileList ? fileList.find(file => file.path === path) : null;
+};
+
+export const findEntry = (tree, type, name) => tree.find(
f => f.type === type && f.name === name,
);
+
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => {
document.title = title;
};
-export const pushState = (url) => {
- history.pushState({ url }, '', url);
-};
-
-export const createTemp = ({ name, path, type, level, changed, content, base64 }) => {
+export const createTemp = ({
+ projectId, branchId, name, path, type, level, changed, content, base64, url,
+}) => {
const treePath = path ? `${path}/${name}` : name;
return decorateData({
id: new Date().getTime().toString(),
+ projectId,
+ branchId,
name,
type,
tempFile: true,
@@ -104,11 +143,18 @@ export const createTemp = ({ name, path, type, level, changed, content, base64 }
level,
base64,
renderError: base64,
+ url,
});
};
-export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => {
- const found = findEntry(tree, type, entry.name);
+export const createOrMergeEntry = ({ tree,
+ projectId,
+ branchId,
+ entry,
+ type,
+ parentTreeUrl,
+ level }) => {
+ const found = findEntry(tree.tree || tree, type, entry.name);
if (found) {
return Object.assign({}, found, {
@@ -120,6 +166,8 @@ export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level })
return decorateData({
...entry,
+ projectId,
+ branchId,
type,
parentTreeUrl,
level,
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index ada693afc46..5d4c1851fe5 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -2,7 +2,7 @@
/* global MilestoneSelect */
import LabelsSelect from './labels_select';
import IssuableContext from './issuable_context';
-/* global Sidebar */
+import Sidebar from './right_sidebar';
import DueDateSelectors from './due_date_select';
@@ -15,5 +15,5 @@ export default () => {
new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser);
new DueDateSelectors();
- window.sidebar = new Sidebar();
+ Sidebar.initialize();
};
diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js
index 3a8b4360cb6..882aedfcc76 100644
--- a/app/assets/javascripts/init_notes.js
+++ b/app/assets/javascripts/init_notes.js
@@ -1,4 +1,4 @@
-/* global Notes */
+import Notes from './notes';
export default () => {
const dataEl = document.querySelector('.js-notes-data');
@@ -10,5 +10,7 @@ export default () => {
autocomplete,
} = JSON.parse(dataEl.innerHTML);
- window.notes = new Notes(notesUrl, notesIds, now, diffView, autocomplete);
+ // 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);
};
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 91b5ef1c10a..411c820cc43 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -48,7 +48,7 @@ export default class Issue {
})
.fail(() => new Flash(issueFailMessage))
.done((data) => {
- const isClosedBadge = $('div.status-box-closed');
+ const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index fd1a50dd533..952f49d522e 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -9,7 +9,7 @@ import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
-import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor';
+import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
props: {
@@ -32,7 +32,7 @@ export default {
showInlineEditButton: {
type: Boolean,
required: false,
- default: false,
+ default: true,
},
showDeleteButton: {
type: Boolean,
@@ -152,7 +152,7 @@ export default {
},
mixins: [
- RecaptchaDialogImplementor,
+ recaptchaModalImplementor,
],
methods: {
@@ -197,7 +197,7 @@ export default {
});
},
- closeRecaptchaDialog() {
+ closeRecaptchaModal() {
this.store.setFormState({
updateLoading: false,
});
@@ -273,10 +273,10 @@ export default {
:enable-autocomplete="enableAutocomplete"
/>
- <recaptcha-dialog
+ <recaptcha-modal
v-show="showRecaptcha"
:html="recaptchaHTML"
- @close="closeRecaptchaDialog"
+ @close="closeRecaptchaModal"
/>
</div>
<div v-else>
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index feb73481422..c3f2bf130bb 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -1,12 +1,12 @@
<script>
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
- import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor';
+ import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
mixins: [
animateMixin,
- RecaptchaDialogImplementor,
+ recaptchaModalImplementor,
],
props: {
@@ -126,7 +126,7 @@
>
</textarea>
- <recaptcha-dialog
+ <recaptcha-modal
v-show="showRecaptcha"
:html="recaptchaHTML"
@close="closeRecaptcha"
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 52fe4ecd08b..4e577546551 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -53,7 +53,7 @@
<textarea
id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area"
- data-supports-quick-actionss="false"
+ data-supports-quick-actions="false"
aria-label="Description"
v-model="formState.description"
ref="textarea"
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index a363d06d950..b7e6eadd440 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -79,7 +79,7 @@
v-tooltip
v-if="showInlineEditButton && canUpdate"
type="button"
- class="btn btn-default btn-edit btn-svg"
+ class="btn btn-default btn-edit btn-svg js-issuable-edit"
v-html="pencilIcon"
title="Edit title and description"
data-placement="bottom"
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index 7b762496ba5..75dfdedcf1b 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import eventHub from './event_hub';
import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor';
@@ -7,12 +6,6 @@ document.addEventListener('DOMContentLoaded', () => {
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
const props = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
- $('.js-issuable-edit').on('click', (e) => {
- e.preventDefault();
-
- eventHub.$emit('open.form');
- });
-
return new Vue({
el: document.getElementById('js-issuable-app'),
components: {
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index 06b0e02a870..198a7823381 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -3,6 +3,7 @@ import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { bytesToKiB } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
+import { timeFor } from './lib/utils/datetime_utility';
export default class Job {
constructor(options) {
@@ -261,7 +262,7 @@ export default class Job {
if ($date.length) {
const date = $date.text();
return $date.text(
- gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '),
+ timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3'))),
);
}
}
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index a6f82b247e2..ab3cc29146a 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,59 +1,51 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
-import _ from 'underscore';
-import Cookies from 'js-cookie';
import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav';
-(function() {
- var hideEndFade;
+function hideEndFade($scrollingTabs) {
+ $scrollingTabs.each(function scrollTabsLoop() {
+ const $this = $(this);
+ $this.siblings('.fade-right').toggleClass('scrolling', $this.width() < $this.prop('scrollWidth'));
+ });
+}
- hideEndFade = function($scrollingTabs) {
- return $scrollingTabs.each(function() {
- var $this;
- $this = $(this);
- return $this.siblings('.fade-right').toggleClass('scrolling', $this.width() < $this.prop('scrollWidth'));
- });
- };
+export default function initLayoutNav() {
+ const contextualSidebar = new ContextualSidebar();
+ contextualSidebar.bindEvents();
+
+ initFlyOutNav();
$(document).on('init.scrolling-tabs', () => {
const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
$scrollingTabs.addClass('is-initialized');
- hideEndFade($scrollingTabs);
- $(window).off('resize.nav').on('resize.nav', function() {
- return hideEndFade($scrollingTabs);
- });
- $scrollingTabs.off('scroll').on('scroll', function(event) {
- var $this, currentPosition, maxPosition;
- $this = $(this);
- currentPosition = $this.scrollLeft();
- maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
+ $(window).on('resize.nav', () => {
+ hideEndFade($scrollingTabs);
+ }).trigger('resize.nav');
+
+ $scrollingTabs.on('scroll', function tabsScrollEvent() {
+ const $this = $(this);
+ const currentPosition = $this.scrollLeft();
+ const maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
+
$this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
- return $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
+ $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
});
- $scrollingTabs.each(function () {
- var $this = $(this);
- var scrollingTabWidth = $this.width();
- var $active = $this.find('.active');
- var activeWidth = $active.width();
+ $scrollingTabs.each(function scrollTabsEachLoop() {
+ const $this = $(this);
+ const scrollingTabWidth = $this.width();
+ const $active = $this.find('.active');
+ const activeWidth = $active.width();
if ($active.length) {
- var offset = $active.offset().left + activeWidth;
+ const offset = $active.offset().left + activeWidth;
if (offset > scrollingTabWidth - 30) {
- var scrollLeft = scrollingTabWidth / 2;
- scrollLeft = (offset - scrollLeft) - (activeWidth / 2);
+ const scrollLeft = (offset - (scrollingTabWidth / 2)) - (activeWidth / 2);
+
$this.scrollLeft(scrollLeft);
}
}
});
- });
-
- $(() => {
- const contextualSidebar = new ContextualSidebar();
- contextualSidebar.bindEvents();
-
- initFlyOutNav();
- });
-}).call(window);
+ }).trigger('init.scrolling-tabs');
+}
diff --git a/app/assets/javascripts/lib/utils/cache.js b/app/assets/javascripts/lib/utils/cache.js
index 3141f1eeafc..596bd1e388a 100644
--- a/app/assets/javascripts/lib/utils/cache.js
+++ b/app/assets/javascripts/lib/utils/cache.js
@@ -1,4 +1,4 @@
-class Cache {
+export default class Cache {
constructor() {
this.internalStorage = { };
}
@@ -15,5 +15,3 @@ class Cache {
delete this.internalStorage[key];
}
}
-
-export default Cache;
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 7a72509d234..9a61003ef30 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -1,3 +1,2 @@
-/* eslint-disable import/prefer-default-export */
export const BYTES_IN_KIB = 1024;
export const HIDDEN_CLASS = 'hidden';
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index d0578b230b1..1fa6715180e 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,132 +1,141 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */
-
import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format';
import { pluralize } from './text_utility';
-
import {
- lang,
+ languageCode,
s__,
} from '../../locale';
window.timeago = timeago;
window.dateFormat = dateFormat;
-(function() {
- (function(w) {
- var base;
- var timeagoInstance;
+/**
+ * Given a date object returns the day of the week in English
+ * @param {date} date
+ * @returns {String}
+ */
+export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()];
- if (w.gl == null) {
- w.gl = {};
- }
- if ((base = w.gl).utils == null) {
- base.utils = {};
- }
- w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+/**
+ * @example
+ * dateFormat('2017-12-05','mmm d, yyyy h:MMtt Z' ) -> "Dec 5, 2017 12:00am GMT+0000"
+ * @param {date} datetime
+ * @returns {String}
+ */
+export const formatDate = datetime => dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
- w.gl.utils.formatDate = function(datetime) {
- return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
- };
+/**
+ * Timeago uses underscores instead of dashes to separate language from country code.
+ *
+ * see https://github.com/hustcc/timeago.js/tree/v3.0.0/locales
+ */
+const timeagoLanguageCode = languageCode().replace(/-/g, '_');
- w.gl.utils.getDayName = function(date) {
- return this.days[date.getDay()];
+let timeagoInstance;
+
+/**
+ * Sets a timeago Instance
+ */
+export function getTimeago() {
+ if (!timeagoInstance) {
+ const localeRemaining = function getLocaleRemaining(number, index) {
+ return [
+ [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
+ [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|%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|%s days ago'), s__('Timeago|%s days remaining')],
+ [s__('Timeago|a 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|%s months ago'), s__('Timeago|%s months remaining')],
+ [s__('Timeago|a 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|in a while')],
+ [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|%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|%s days ago'), s__('Timeago|in %s days')],
+ [s__('Timeago|a 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|%s months ago'), s__('Timeago|in %s months')],
+ [s__('Timeago|a year ago'), s__('Timeago|in 1 year')],
+ [s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
+ ][index];
};
- w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
- $timeagoEls.each((i, el) => {
- if (setTimeago) {
- // Recreate with custom template
- $(el).tooltip({
- template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
- });
- }
+ timeago.register(timeagoLanguageCode, locale);
+ timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining);
+ timeagoInstance = timeago();
+ }
- el.classList.add('js-timeago-render');
- });
+ return timeagoInstance;
+}
- gl.utils.renderTimeago($timeagoEls);
- };
+/**
+ * For the given element, renders a timeago instance.
+ * @param {jQuery} $els
+ */
+export const renderTimeago = ($els) => {
+ const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
- w.gl.utils.getTimeago = function() {
- var locale;
-
- if (!timeagoInstance) {
- const localeRemaining = function(number, index) {
- return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
- [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|%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|%s days ago'), s__('Timeago|%s days remaining')],
- [s__('Timeago|a 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|%s months ago'), s__('Timeago|%s months remaining')],
- [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')],
- [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')]
- ][index];
- };
- locale = function(number, index) {
- return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
- [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|%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|%s days ago'), s__('Timeago|in %s days')],
- [s__('Timeago|a 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|%s months ago'), s__('Timeago|in %s months')],
- [s__('Timeago|a year ago'), s__('Timeago|in 1 year')],
- [s__('Timeago|%s years ago'), s__('Timeago|in %s years')]
- ][index];
- };
-
- timeago.register(lang, locale);
- timeago.register(`${lang}-remaining`, localeRemaining);
- timeagoInstance = timeago();
- }
-
- return timeagoInstance;
- };
+ // timeago.js sets timeouts internally for each timeago value to be updated in real time
+ getTimeago().render(timeagoEls, timeagoLanguageCode);
+};
- w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
- var timefor;
- if (!time) {
- return '';
- }
- if (new Date(time) < new Date()) {
- expiredLabel || (expiredLabel = s__('Timeago|Past due'));
- timefor = expiredLabel;
- } else {
- timefor = gl.utils.getTimeago().format(time, `${lang}-remaining`).trim();
- }
- return timefor;
- };
+/**
+ * For the given elements, sets a tooltip with a formatted date.
+ * @param {jQuery}
+ * @param {Boolean} setTimeago
+ */
+export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
+ $timeagoEls.each((i, el) => {
+ if (setTimeago) {
+ // Recreate with custom template
+ $(el).tooltip({
+ template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+ });
+ }
- w.gl.utils.renderTimeago = function($els) {
- const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
+ el.classList.add('js-timeago-render');
+ });
- // timeago.js sets timeouts internally for each timeago value to be updated in real time
- gl.utils.getTimeago().render(timeagoEls, lang);
- };
+ renderTimeago($timeagoEls);
+};
- w.gl.utils.getDayDifference = function(a, b) {
- var millisecondsPerDay = 1000 * 60 * 60 * 24;
- var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
- var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
+/**
+ * Returns remaining or passed time over the given time.
+ * @param {*} time
+ * @param {*} expiredLabel
+ */
+export const timeFor = (time, expiredLabel) => {
+ if (!time) {
+ return '';
+ }
+ if (new Date(time) < new Date()) {
+ return expiredLabel || s__('Timeago|Past due');
+ }
+ return getTimeago().format(time, `${timeagoLanguageCode}-remaining`).trim();
+};
- return Math.floor((date2 - date1) / millisecondsPerDay);
- };
- })(window);
-}).call(window);
+export const getDayDifference = (a, b) => {
+ const millisecondsPerDay = 1000 * 60 * 60 * 24;
+ const date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
+ const date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
+
+ return Math.floor((date2 - date1) / millisecondsPerDay);
+};
/**
* Port of ruby helper time_interval_in_words.
@@ -162,3 +171,10 @@ export function dateInWords(date, abbreviated = false) {
return `${monthName} ${date.getDate()}, ${year}`;
}
+
+window.gl = window.gl || {};
+window.gl.utils = {
+ ...(window.gl.utils || {}),
+ getTimeago,
+ localTimeAgo,
+};
diff --git a/app/assets/javascripts/lib/utils/tick_formats.js b/app/assets/javascripts/lib/utils/tick_formats.js
new file mode 100644
index 00000000000..0c10a85e336
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/tick_formats.js
@@ -0,0 +1,39 @@
+import { createDateTimeFormat } from '../../locale';
+
+let dateTimeFormats;
+
+export const initDateFormats = () => {
+ const dayFormat = createDateTimeFormat({ month: 'short', day: 'numeric' });
+ const monthFormat = createDateTimeFormat({ month: 'long' });
+ const yearFormat = createDateTimeFormat({ year: 'numeric' });
+
+ dateTimeFormats = {
+ dayFormat,
+ monthFormat,
+ yearFormat,
+ };
+};
+
+initDateFormats();
+
+/**
+ Formats a localized date in way that it can be used for d3.js axis.tickFormat().
+
+ That is, it displays
+ - 4-digit for first of January
+ - full month name for first of every month
+ - day and abbreviated month otherwise
+
+ see also https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#tickFormat
+ */
+export const dateTickFormat = (date) => {
+ if (date.getDate() !== 1) {
+ return dateTimeFormats.dayFormat.format(date);
+ }
+
+ if (date.getMonth() > 0) {
+ return dateTimeFormats.monthFormat.format(date);
+ }
+
+ return dateTimeFormats.yearFormat.format(date);
+};
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index a75d1a4b8d0..fbd381d8ff7 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -175,4 +175,4 @@ LineHighlighter.prototype.__setLocationHash__ = function(value) {
}, document.title, value);
};
-window.LineHighlighter = LineHighlighter;
+export default LineHighlighter;
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index 1003b9ba0af..2f4328b56e1 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -1,8 +1,7 @@
import Jed from 'jed';
import sprintf from './sprintf';
-const langAttribute = document.querySelector('html').getAttribute('lang');
-const lang = (langAttribute || 'en').replace(/-/g, '_');
+const languageCode = () => document.querySelector('html').getAttribute('lang') || 'en';
const locale = new Jed(window.translations || {});
delete window.translations;
@@ -47,9 +46,19 @@ const pgettext = (keyOrContext, key) => {
return translated[translated.length - 1];
};
-export { lang };
+/**
+ Creates an instance of Intl.DateTimeFormat for the current locale.
+
+ @param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
+ @returns {Intl.DateTimeFormat}
+*/
+const createDateTimeFormat =
+ formatOptions => Intl.DateTimeFormat(languageCode(), formatOptions);
+
+export { languageCode };
export { gettext as __ };
export { ngettext as n__ };
export { pgettext as s__ };
export { sprintf };
+export { createDateTimeFormat };
export default locale;
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 2db865f8abf..59bfa482bb0 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,6 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
/* global ConfirmDangerModal */
-/* global Aside */
import jQuery from 'jquery';
import _ from 'underscore';
@@ -28,42 +27,28 @@ import './commit/image_file';
// lib/utils
import { handleLocationHash } from './lib/utils/common_utils';
-import './lib/utils/datetime_utility';
+import { localTimeAgo, renderTimeago } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
// behaviors
import './behaviors/';
// everything else
-import './activities';
-import './admin';
-import './aside';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
import './confirm_danger_modal';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
-import './gl_field_error';
-import './gl_field_errors';
-import './gl_form';
import initTodoToggle from './header';
import initImporterStatus from './importer_status';
-import './layout_nav';
+import initLayoutNav from './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import initLogoAnimation from './logo';
-import './merge_request';
-import './merge_request_tabs';
import './milestone_select';
-import './notes';
-import './notifications_dropdown';
-import './notifications_form';
-import './pager';
import './preview_markdown';
-import './project_import';
import './projects_dropdown';
import './render_gfm';
-import './right_sidebar';
import initBreadcrumbs from './breadcrumb';
import './dispatcher';
@@ -104,6 +89,7 @@ $(function () {
var fitSidebarForSize;
initBreadcrumbs();
+ initLayoutNav();
initImporterStatus();
initTodoToggle();
initLogoAnimation();
@@ -186,7 +172,7 @@ $(function () {
return $(this).parents('form').submit();
// Form submitter
});
- gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
+ localTimeAgo($('abbr.timeago, .js-timeago'), true);
// Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
var buttons;
@@ -273,11 +259,8 @@ $(function () {
return fitSidebarForSize();
});
loadAwardsHandler();
- new Aside();
- gl.utils.renderTimeago();
-
- $(document).trigger('init.scrolling-tabs');
+ renderTimeago();
$('form.filter-form').on('submit', function (event) {
const link = document.createElement('a');
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index a9c08df4f93..cb3cdea8111 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,148 +1,143 @@
/* 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 */
-/* global MergeRequestTabs */
import 'vendor/jquery.waitforimages';
import TaskList from './task_list';
-import './merge_request_tabs';
+import MergeRequestTabs from './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
import { addDelimiter } from './lib/utils/text_utility';
-(function() {
- this.MergeRequest = (function() {
- function MergeRequest(opts) {
- // Initialize MergeRequest behavior
- //
- // Options:
- // action - String, current controller action
- //
- this.opts = opts != null ? opts : {};
- this.submitNoteForm = this.submitNoteForm.bind(this);
- this.$el = $('.merge-request');
- this.$('.show-all-commits').on('click', (function(_this) {
- return function() {
- return _this.showAllCommits();
- };
- })(this));
-
- this.initTabs();
- this.initMRBtnListeners();
- this.initCommitMessageListeners();
- this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
-
- if ($("a.btn-close").length) {
- this.taskList = new TaskList({
- dataType: 'merge_request',
- fieldName: 'description',
- selector: '.detail-page-description',
- onSuccess: (result) => {
- document.querySelector('#task_status').innerText = result.task_status;
- document.querySelector('#task_status_short').innerText = result.task_status_short;
- }
- });
- }
- }
-
- // Local jQuery finder
- MergeRequest.prototype.$ = function(selector) {
- return this.$el.find(selector);
+function MergeRequest(opts) {
+ // Initialize MergeRequest behavior
+ //
+ // Options:
+ // action - String, current controller action
+ //
+ this.opts = opts != null ? opts : {};
+ this.submitNoteForm = this.submitNoteForm.bind(this);
+ this.$el = $('.merge-request');
+ this.$('.show-all-commits').on('click', (function(_this) {
+ return function() {
+ return _this.showAllCommits();
};
-
- MergeRequest.prototype.initTabs = function() {
- if (window.mrTabs) {
- window.mrTabs.unbindEvents();
+ })(this));
+
+ this.initTabs();
+ this.initMRBtnListeners();
+ this.initCommitMessageListeners();
+ this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
+
+ if ($("a.btn-close").length) {
+ this.taskList = new TaskList({
+ dataType: 'merge_request',
+ fieldName: 'description',
+ selector: '.detail-page-description',
+ onSuccess: (result) => {
+ document.querySelector('#task_status').innerText = result.task_status;
+ document.querySelector('#task_status_short').innerText = result.task_status_short;
}
- window.mrTabs = new gl.MergeRequestTabs(this.opts);
- };
-
- MergeRequest.prototype.showAllCommits = function() {
- this.$('.first-commits').remove();
- return this.$('.all-commits').removeClass('hide');
- };
-
- MergeRequest.prototype.initMRBtnListeners = function() {
- var _this;
- _this = this;
- return $('a.btn-close, a.btn-reopen').on('click', function(e) {
- var $this, shouldSubmit;
- $this = $(this);
- shouldSubmit = $this.hasClass('btn-comment');
- if (shouldSubmit && $this.data('submitted')) {
- return;
- }
-
- if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable();
-
- if (shouldSubmit) {
- if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) {
- e.preventDefault();
- e.stopImmediatePropagation();
-
- _this.submitNoteForm($this.closest('form'), $this);
- }
- }
- });
- };
-
- MergeRequest.prototype.submitNoteForm = function(form, $button) {
- var noteText;
- noteText = form.find("textarea.js-note-text").val();
- if (noteText.trim().length > 0) {
- form.submit();
- $button.data('submitted', true);
- return $button.trigger('click');
- }
- };
-
- MergeRequest.prototype.initCommitMessageListeners = function() {
- $(document).on('click', 'a.js-with-description-link', function(e) {
- var textarea = $('textarea.js-commit-message');
- e.preventDefault();
+ });
+ }
+}
+
+// Local jQuery finder
+MergeRequest.prototype.$ = function(selector) {
+ return this.$el.find(selector);
+};
+
+MergeRequest.prototype.initTabs = function() {
+ if (window.mrTabs) {
+ window.mrTabs.unbindEvents();
+ }
+ window.mrTabs = new MergeRequestTabs(this.opts);
+};
+
+MergeRequest.prototype.showAllCommits = function() {
+ this.$('.first-commits').remove();
+ return this.$('.all-commits').removeClass('hide');
+};
+
+MergeRequest.prototype.initMRBtnListeners = function() {
+ var _this;
+ _this = this;
+ return $('a.btn-close, a.btn-reopen').on('click', function(e) {
+ var $this, shouldSubmit;
+ $this = $(this);
+ shouldSubmit = $this.hasClass('btn-comment');
+ if (shouldSubmit && $this.data('submitted')) {
+ return;
+ }
- textarea.val(textarea.data('messageWithDescription'));
- $('.js-with-description-hint').hide();
- $('.js-without-description-hint').show();
- });
+ if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable();
- $(document).on('click', 'a.js-without-description-link', function(e) {
- var textarea = $('textarea.js-commit-message');
+ if (shouldSubmit) {
+ if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) {
e.preventDefault();
+ e.stopImmediatePropagation();
- textarea.val(textarea.data('messageWithoutDescription'));
- $('.js-with-description-hint').show();
- $('.js-without-description-hint').hide();
- });
- };
-
- MergeRequest.prototype.updateStatusText = function(classToRemove, classToAdd, newStatusText) {
- $('.detail-page-header .status-box')
- .removeClass(classToRemove)
- .addClass(classToAdd)
- .find('span')
- .text(newStatusText);
- };
-
- MergeRequest.prototype.decreaseCounter = function(by = 1) {
- const $el = $('.nav-links .js-merge-counter');
- const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
-
- $el.text(addDelimiter(count));
- };
-
- MergeRequest.prototype.hideCloseButton = function() {
- const el = document.querySelector('.merge-request .js-issuable-actions');
- const closeDropdownItem = el.querySelector('li.close-item');
- if (closeDropdownItem) {
- closeDropdownItem.classList.add('hidden');
- // Selects the next dropdown item
- el.querySelector('li.report-item').click();
- } else {
- // No dropdown just hide the Close button
- el.querySelector('.btn-close').classList.add('hidden');
+ _this.submitNoteForm($this.closest('form'), $this);
}
- // Dropdown for mobile screen
- el.querySelector('li.js-close-item').classList.add('hidden');
- };
-
- return MergeRequest;
- })();
-}).call(window);
+ }
+ });
+};
+
+MergeRequest.prototype.submitNoteForm = function(form, $button) {
+ var noteText;
+ noteText = form.find("textarea.js-note-text").val();
+ if (noteText.trim().length > 0) {
+ form.submit();
+ $button.data('submitted', true);
+ return $button.trigger('click');
+ }
+};
+
+MergeRequest.prototype.initCommitMessageListeners = function() {
+ $(document).on('click', 'a.js-with-description-link', function(e) {
+ var textarea = $('textarea.js-commit-message');
+ e.preventDefault();
+
+ textarea.val(textarea.data('messageWithDescription'));
+ $('.js-with-description-hint').hide();
+ $('.js-without-description-hint').show();
+ });
+
+ $(document).on('click', 'a.js-without-description-link', function(e) {
+ var textarea = $('textarea.js-commit-message');
+ e.preventDefault();
+
+ textarea.val(textarea.data('messageWithoutDescription'));
+ $('.js-with-description-hint').show();
+ $('.js-without-description-hint').hide();
+ });
+};
+
+MergeRequest.prototype.updateStatusText = function(classToRemove, classToAdd, newStatusText) {
+ $('.detail-page-header .status-box')
+ .removeClass(classToRemove)
+ .addClass(classToAdd)
+ .find('span')
+ .text(newStatusText);
+};
+
+MergeRequest.prototype.decreaseCounter = function(by = 1) {
+ const $el = $('.nav-links .js-merge-counter');
+ const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
+
+ $el.text(addDelimiter(count));
+};
+
+MergeRequest.prototype.hideCloseButton = function() {
+ const el = document.querySelector('.merge-request .js-issuable-actions');
+ const closeDropdownItem = el.querySelector('li.close-item');
+ if (closeDropdownItem) {
+ closeDropdownItem.classList.add('hidden');
+ // Selects the next dropdown item
+ el.querySelector('li.report-item').click();
+ } else {
+ // No dropdown just hide the Close button
+ el.querySelector('.btn-close').classList.add('hidden');
+ }
+ // Dropdown for mobile screen
+ el.querySelector('li.js-close-item').classList.add('hidden');
+};
+
+export default MergeRequest;
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index a630daa6bdc..acfc62fe5cb 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,5 +1,4 @@
/* eslint-disable no-new, class-methods-use-this */
-/* global notes */
import Cookies from 'js-cookie';
import Flash from './flash';
@@ -14,7 +13,9 @@ import {
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';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -62,387 +63,382 @@ import syntaxHighlight from './syntax_highlight';
//
/* eslint-enable max-len */
-(() => {
- // Store the `location` object, allowing for easier stubbing in tests
- let location = window.location;
+// Store the `location` object, allowing for easier stubbing in tests
+let location = window.location;
- class MergeRequestTabs {
+export default class MergeRequestTabs {
- constructor({ action, setUrl, stubLocation } = {}) {
- const mergeRequestTabs = document.querySelector('.js-tabs-affix');
- const navbar = document.querySelector('.navbar-gitlab');
- const paddingTop = 16;
+ constructor({ action, setUrl, stubLocation } = {}) {
+ const mergeRequestTabs = document.querySelector('.js-tabs-affix');
+ const navbar = document.querySelector('.navbar-gitlab');
+ const paddingTop = 16;
- this.diffsLoaded = false;
- this.pipelinesLoaded = false;
- this.commitsLoaded = false;
- this.fixedLayoutPref = null;
+ this.diffsLoaded = false;
+ this.pipelinesLoaded = false;
+ this.commitsLoaded = false;
+ this.fixedLayoutPref = null;
- 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.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
+ 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.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
- if (mergeRequestTabs) {
- this.stickyTop += mergeRequestTabs.offsetHeight;
- }
-
- if (stubLocation) {
- location = stubLocation;
- }
+ if (mergeRequestTabs) {
+ this.stickyTop += mergeRequestTabs.offsetHeight;
+ }
- this.bindEvents();
- this.activateTab(action);
- this.initAffix();
+ if (stubLocation) {
+ location = stubLocation;
}
- bindEvents() {
- $(document)
- .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
- .on('click', '.js-show-tab', this.showTab);
+ this.bindEvents();
+ this.activateTab(action);
+ this.initAffix();
+ }
- $('.merge-request-tabs a[data-toggle="tab"]')
- .on('click', this.clickTab);
- }
+ bindEvents() {
+ $(document)
+ .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
+ .on('click', '.js-show-tab', this.showTab);
- // 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"]')
+ .on('click', this.clickTab);
+ }
- $('.merge-request-tabs a[data-toggle="tab"]')
- .off('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);
- destroyPipelinesView() {
- if (this.commitPipelinesTable) {
- this.commitPipelinesTable.$destroy();
- this.commitPipelinesTable = null;
+ $('.merge-request-tabs a[data-toggle="tab"]')
+ .off('click', this.clickTab);
+ }
- document.querySelector('#commit-pipeline-table-view').innerHTML = '';
- }
+ destroyPipelinesView() {
+ if (this.commitPipelinesTable) {
+ this.commitPipelinesTable.$destroy();
+ this.commitPipelinesTable = null;
+
+ document.querySelector('#commit-pipeline-table-view').innerHTML = '';
}
+ }
- showTab(e) {
+ showTab(e) {
+ e.preventDefault();
+ this.activateTab($(e.target).data('action'));
+ }
+
+ clickTab(e) {
+ if (e.currentTarget && isMetaClick(e)) {
+ const targetLink = e.currentTarget.getAttribute('href');
+ e.stopImmediatePropagation();
e.preventDefault();
- this.activateTab($(e.target).data('action'));
+ window.open(targetLink, '_blank');
}
+ }
- clickTab(e) {
- if (e.currentTarget && isMetaClick(e)) {
- const targetLink = e.currentTarget.getAttribute('href');
- e.stopImmediatePropagation();
- e.preventDefault();
- 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(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();
- }
- if (this.diffViewType() === 'parallel') {
- this.expandViewContainer();
- }
- this.destroyPipelinesView();
- } else if (action === 'pipelines') {
- this.resetViewContainer();
- this.mountPipelinesView();
- } else {
- if (bp.getBreakpointSize() !== 'xs') {
- this.expandView();
- }
- this.resetViewContainer();
- this.destroyPipelinesView();
-
- initDiscussionTab();
+ if (this.diffViewType() === 'parallel') {
+ this.expandViewContainer();
}
- if (this.setUrl) {
- this.setCurrentAction(action);
+ this.destroyPipelinesView();
+ } else if (action === 'pipelines') {
+ this.resetViewContainer();
+ this.mountPipelinesView();
+ } else {
+ if (bp.getBreakpointSize() !== 'xs') {
+ this.expandView();
}
+ this.resetViewContainer();
+ this.destroyPipelinesView();
+
+ initDiscussionTab();
+ }
+ if (this.setUrl) {
+ this.setCurrentAction(action);
}
+ }
- scrollToElement(container) {
- if (location.hash) {
- const offset = 0 - (
- $('.navbar-gitlab').outerHeight() +
- $('.js-tabs-affix').outerHeight()
- );
- const $el = $(`${container} ${location.hash}:not(.match)`);
- if ($el.length) {
- $.scrollTo($el[0], { offset });
- }
+ scrollToElement(container) {
+ if (location.hash) {
+ const offset = 0 - (
+ $('.navbar-gitlab').outerHeight() +
+ $('.js-tabs-affix').outerHeight()
+ );
+ const $el = $(`${container} ${location.hash}:not(.match)`);
+ if ($el.length) {
+ $.scrollTo($el[0], { offset });
}
}
+ }
- // 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');
+ // 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
+ // `MergeRequests#show` route.
+ //
+ // Examples:
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ // setCurrentAction('diffs')
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('show')
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('commits')
+ // location.pathname # => "/namespace/project/merge_requests/1/commits"
+ //
+ // Returns the new URL String
+ setCurrentAction(action) {
+ this.currentAction = action;
+
+ // Remove a trailing '/commits' '/diffs' '/pipelines'
+ let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, '');
+
+ // Append the new action if we're on a tab other than 'notes'
+ if (this.currentAction !== 'show' && this.currentAction !== 'new') {
+ newState += `/${this.currentAction}`;
}
- // 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
- // `MergeRequests#show` route.
- //
- // Examples:
- //
- // location.pathname # => "/namespace/project/merge_requests/1"
- // setCurrentAction('diffs')
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- //
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- // setCurrentAction('show')
- // location.pathname # => "/namespace/project/merge_requests/1"
- //
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- // setCurrentAction('commits')
- // location.pathname # => "/namespace/project/merge_requests/1/commits"
- //
- // Returns the new URL String
- setCurrentAction(action) {
- this.currentAction = action;
+ // Ensure parameters and hash come along for the ride
+ newState += location.search + location.hash;
- // Remove a trailing '/commits' '/diffs' '/pipelines'
- let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, '');
+ // TODO: Consider refactoring in light of turbolinks removal.
- // Append the new action if we're on a tab other than 'notes'
- if (this.currentAction !== 'show' && this.currentAction !== 'new') {
- newState += `/${this.currentAction}`;
- }
+ // Replace the current history state with the new one without breaking
+ // Turbolinks' history.
+ //
+ // See https://github.com/rails/turbolinks/issues/363
+ window.history.replaceState({
+ url: newState,
+ }, document.title, newState);
- // Ensure parameters and hash come along for the ride
- newState += location.search + location.hash;
+ return newState;
+ }
- // TODO: Consider refactoring in light of turbolinks removal.
+ loadCommits(source) {
+ if (this.commitsLoaded) {
+ return;
+ }
+ this.ajaxGet({
+ url: `${source}.json`,
+ success: (data) => {
+ document.querySelector('div#commits').innerHTML = data.html;
+ localTimeAgo($('.js-timeago', 'div#commits'));
+ this.commitsLoaded = true;
+ this.scrollToElement('#commits');
+ },
+ });
+ }
- // Replace the current history state with the new one without breaking
- // Turbolinks' history.
- //
- // See https://github.com/rails/turbolinks/issues/363
- window.history.replaceState({
- url: newState,
- }, document.title, newState);
+ mountPipelinesView() {
+ const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
+ const CommitPipelinesTable = gl.CommitPipelinesTable;
+ this.commitPipelinesTable = new CommitPipelinesTable({
+ propsData: {
+ endpoint: pipelineTableViewEl.dataset.endpoint,
+ helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
+ emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
+ errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
+ autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
+ },
+ }).$mount();
+
+ // $mount(el) replaces the el with the new rendered component. We need it in order to mount
+ // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
+ pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el);
+ }
- return newState;
+ loadDiff(source) {
+ if (this.diffsLoaded) {
+ document.dispatchEvent(new CustomEvent('scroll'));
+ return;
}
- loadCommits(source) {
- if (this.commitsLoaded) {
- return;
- }
- this.ajaxGet({
- url: `${source}.json`,
- success: (data) => {
- document.querySelector('div#commits').innerHTML = data.html;
- gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
- this.commitsLoaded = true;
- this.scrollToElement('#commits');
- },
- });
- }
+ // We extract pathname for the current Changes tab anchor href
+ // some pages like MergeRequestsController#new has query parameters on that anchor
+ const urlPathname = parseUrlPathname(source);
- mountPipelinesView() {
- const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- const CommitPipelinesTable = gl.CommitPipelinesTable;
- this.commitPipelinesTable = new CommitPipelinesTable({
- propsData: {
- endpoint: pipelineTableViewEl.dataset.endpoint,
- helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
- emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
- errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
- autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
- },
- }).$mount();
+ this.ajaxGet({
+ url: `${urlPathname}.json${location.search}`,
+ success: (data) => {
+ const $container = $('#diffs');
+ $container.html(data.html);
- // $mount(el) replaces the el with the new rendered component. We need it in order to mount
- // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
- pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el);
- }
+ initChangesDropdown(this.stickyTop);
- loadDiff(source) {
- if (this.diffsLoaded) {
- document.dispatchEvent(new CustomEvent('scroll'));
- return;
- }
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
+ }
+
+ localTimeAgo($('.js-timeago', 'div#diffs'));
+ syntaxHighlight($('#diffs .js-syntax-highlight'));
- // We extract pathname for the current Changes tab anchor href
- // some pages like MergeRequestsController#new has query parameters on that anchor
- const urlPathname = parseUrlPathname(source);
-
- this.ajaxGet({
- url: `${urlPathname}.json${location.search}`,
- success: (data) => {
- const $container = $('#diffs');
- $container.html(data.html);
-
- initChangesDropdown(this.stickyTop);
-
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- gl.diffNotesCompileComponents();
- }
-
- gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
- syntaxHighlight($('#diffs .js-syntax-highlight'));
-
- if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
- this.expandViewContainer();
- }
- this.diffsLoaded = true;
-
- new Diff();
- this.scrollToElement('#diffs');
-
- $('.diff-file').each((i, el) => {
- new BlobForkSuggestion({
- openButtons: $(el).find('.js-edit-blob-link-fork-toggler'),
- forkButtons: $(el).find('.js-fork-suggestion-button'),
- cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
- suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
- actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
- })
- .init();
+ if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
+ this.expandViewContainer();
+ }
+ this.diffsLoaded = true;
+
+ new Diff();
+ this.scrollToElement('#diffs');
+
+ $('.diff-file').each((i, el) => {
+ new BlobForkSuggestion({
+ openButtons: $(el).find('.js-edit-blob-link-fork-toggler'),
+ forkButtons: $(el).find('.js-fork-suggestion-button'),
+ cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
+ suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
+ actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
+ })
+ .init();
+ });
+
+ // Scroll any linked note into view
+ // Similar to `toggler_behavior` in the discussion tab
+ const hash = getLocationHash();
+ const anchor = hash && $container.find(`.note[id="${hash}"]`);
+ if (anchor && anchor.length > 0) {
+ const notesContent = anchor.closest('.notes_content');
+ const lineType = notesContent.hasClass('new') ? 'new' : 'old';
+ Notes.instance.toggleDiffNote({
+ target: anchor,
+ lineType,
+ forceShow: true,
});
+ anchor[0].scrollIntoView();
+ handleLocationHash();
+ // We have multiple elements on the page with `#note_xxx`
+ // (discussion and diff tabs) and `:target` only applies to the first
+ anchor.addClass('target');
+ }
+ },
+ });
+ }
- // Scroll any linked note into view
- // Similar to `toggler_behavior` in the discussion tab
- const hash = getLocationHash();
- const anchor = hash && $container.find(`.note[id="${hash}"]`);
- if (anchor && anchor.length > 0) {
- const notesContent = anchor.closest('.notes_content');
- const lineType = notesContent.hasClass('new') ? 'new' : 'old';
- notes.toggleDiffNote({
- target: anchor,
- lineType,
- forceShow: true,
- });
- anchor[0].scrollIntoView();
- handleLocationHash();
- // We have multiple elements on the page with `#note_xxx`
- // (discussion and diff tabs) and `:target` only applies to the first
- anchor.addClass('target');
- }
- },
- });
- }
+ // Show or hide the loading spinner
+ //
+ // status - Boolean, true to show, false to hide
+ toggleLoading(status) {
+ $('.mr-loading-status .loading').toggle(status);
+ }
- // Show or hide the loading spinner
- //
- // status - Boolean, true to show, false to hide
- toggleLoading(status) {
- $('.mr-loading-status .loading').toggle(status);
- }
+ ajaxGet(options) {
+ const defaults = {
+ beforeSend: () => this.toggleLoading(true),
+ error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
+ complete: () => this.toggleLoading(false),
+ dataType: 'json',
+ type: 'GET',
+ };
+ $.ajax($.extend({}, defaults, options));
+ }
- ajaxGet(options) {
- const defaults = {
- beforeSend: () => this.toggleLoading(true),
- error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
- complete: () => this.toggleLoading(false),
- dataType: 'json',
- type: 'GET',
- };
- $.ajax($.extend({}, defaults, options));
- }
+ diffViewType() {
+ return $('.inline-parallel-buttons a.active').data('view-type');
+ }
- diffViewType() {
- return $('.inline-parallel-buttons a.active').data('view-type');
- }
+ isDiffAction(action) {
+ return action === 'diffs' || action === 'new/diffs';
+ }
- isDiffAction(action) {
- return action === 'diffs' || action === 'new/diffs';
+ expandViewContainer() {
+ const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
+ if (this.fixedLayoutPref === null) {
+ this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
+ $wrapper.removeClass('container-limited');
+ }
- expandViewContainer() {
- const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
- if (this.fixedLayoutPref === null) {
- this.fixedLayoutPref = $wrapper.hasClass('container-limited');
- }
- $wrapper.removeClass('container-limited');
+ resetViewContainer() {
+ if (this.fixedLayoutPref !== null) {
+ $('.content-wrapper .container-fluid')
+ .toggleClass('container-limited', this.fixedLayoutPref);
}
+ }
- resetViewContainer() {
- if (this.fixedLayoutPref !== null) {
- $('.content-wrapper .container-fluid')
- .toggleClass('container-limited', this.fixedLayoutPref);
- }
- }
+ shrinkView() {
+ const $gutterIcon = $('.js-sidebar-toggle i:visible');
- shrinkView() {
- const $gutterIcon = $('.js-sidebar-toggle i:visible');
+ // Wait until listeners are set
+ setTimeout(() => {
+ // Only when sidebar is expanded
+ if ($gutterIcon.is('.fa-angle-double-right')) {
+ $gutterIcon.closest('a').trigger('click', [true]);
+ }
+ }, 0);
+ }
- // Wait until listeners are set
- setTimeout(() => {
- // Only when sidebar is expanded
- if ($gutterIcon.is('.fa-angle-double-right')) {
- $gutterIcon.closest('a').trigger('click', [true]);
- }
- }, 0);
+ // Expand the issuable sidebar unless the user explicitly collapsed it
+ expandView() {
+ if (Cookies.get('collapsed_gutter') === 'true') {
+ return;
}
+ const $gutterIcon = $('.js-sidebar-toggle i:visible');
- // Expand the issuable sidebar unless the user explicitly collapsed it
- expandView() {
- if (Cookies.get('collapsed_gutter') === 'true') {
- return;
+ // Wait until listeners are set
+ setTimeout(() => {
+ // Only when sidebar is collapsed
+ if ($gutterIcon.is('.fa-angle-double-left')) {
+ $gutterIcon.closest('a').trigger('click', [true]);
}
- const $gutterIcon = $('.js-sidebar-toggle i:visible');
+ }, 0);
+ }
- // Wait until listeners are set
- setTimeout(() => {
- // Only when sidebar is collapsed
- if ($gutterIcon.is('.fa-angle-double-left')) {
- $gutterIcon.closest('a').trigger('click', [true]);
- }
- }, 0);
- }
+ 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
+ if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return;
+
+ /**
+ 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 default back to Bootstraps affix
+ **/
+ 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: '' }));
- 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
- if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return;
-
- /**
- 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 default back to Bootstraps affix
- **/
- 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');
- }
+ // Fix bug when reloading the page already scrolling
+ if ($tabs.hasClass('affix')) {
+ $tabs.trigger('affix.bs.affix');
}
}
-
- window.gl = window.gl || {};
- window.gl.MergeRequestTabs = MergeRequestTabs;
-})();
+}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 74e5a4f1cea..2e5e818d61d 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -2,6 +2,7 @@
/* global Issuable */
/* global ListMilestone */
import _ from 'underscore';
+import { timeFor } from './lib/utils/datetime_utility';
(function() {
this.MilestoneSelect = (function() {
@@ -216,7 +217,7 @@ import _ from 'underscore';
$value.css('display', '');
if (data.milestone != null) {
data.milestone.full_path = _this.currentProject.full_path;
- data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
+ data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index cdae287658b..eede04a06cd 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -1,5 +1,8 @@
<script>
- import d3 from 'd3';
+ import { scaleLinear, scaleTime } from 'd3-scale';
+ import { axisLeft, axisBottom } from 'd3-axis';
+ import { max, extent } from 'd3-array';
+ import { select } from 'd3-selection';
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
@@ -7,10 +10,12 @@
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
- import { timeScaleFormat, bisectDate } from '../utils/date_time_formatters';
+ import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints';
+ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
+
export default {
props: {
graphData: {
@@ -156,25 +161,22 @@
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
}
- const axisXScale = d3.time.scale()
+ const axisXScale = d3.scaleTime()
.range([0, this.graphWidth - 70]);
- const axisYScale = d3.scale.linear()
+ const axisYScale = d3.scaleLinear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
axisXScale.domain(d3.extent(allValues, d => d.time));
axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
- const xAxis = d3.svg.axis()
+ const xAxis = d3.axisBottom()
.scale(axisXScale)
- .ticks(d3.time.minute, 60)
- .tickFormat(timeScaleFormat)
- .orient('bottom');
+ .tickFormat(timeScaleFormat);
- const yAxis = d3.svg.axis()
+ const yAxis = d3.axisLeft()
.scale(axisYScale)
- .ticks(measurements.yTicks)
- .orient('left');
+ .ticks(measurements.yTicks);
d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
index ad07a8465e2..48bdec1e030 100644
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
@@ -1,17 +1,32 @@
-import d3 from 'd3';
+import { timeFormat as time } from 'd3-time-format';
+import { timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear } from 'd3-time';
+import { bisector } from 'd3-array';
-export const dateFormat = d3.time.format('%b %-d, %Y');
-export const dateFormatWithName = d3.time.format('%a, %b %-d');
-export const timeFormat = d3.time.format('%-I:%M%p');
+const d3 = { time, bisector, timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear };
+
+export const dateFormat = d3.time('%b %-d, %Y');
+export const timeFormat = d3.time('%-I:%M%p');
+export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left;
-export const timeScaleFormat = d3.time.format.multi([
- ['.%L', d => d.getMilliseconds()],
- [':%S', d => d.getSeconds()],
- ['%-I:%M', d => d.getMinutes()],
- ['%-I %p', d => d.getHours()],
- ['%a %-d', d => d.getDay() && d.getDate() !== 1],
- ['%b %-d', d => d.getDate() !== 1],
- ['%B', d => d.getMonth()],
- ['%Y', () => true],
-]);
+export function timeScaleFormat(date) {
+ let formatFunction;
+ if (d3.timeSecond(date) < date) {
+ formatFunction = d3.time('.%L');
+ } else if (d3.timeMinute(date) < date) {
+ formatFunction = d3.time(':%S');
+ } else if (d3.timeHour(date) < date) {
+ formatFunction = d3.time('%-I:%M');
+ } else if (d3.timeDay(date) < date) {
+ formatFunction = d3.time('%-I %p');
+ } else if (d3.timeWeek(date) < date) {
+ formatFunction = d3.time('%a %d');
+ } else if (d3.timeMonth(date) < date) {
+ formatFunction = d3.time('%b %d');
+ } else if (d3.timeYear(date) < date) {
+ formatFunction = d3.time('%B');
+ } else {
+ formatFunction = d3.time('%Y');
+ }
+ return formatFunction(date);
+}
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index d21a265bd43..4ce3dad440c 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -1,5 +1,10 @@
-import d3 from 'd3';
import _ from 'underscore';
+import { scaleLinear, scaleTime } from 'd3-scale';
+import { line, area, curveLinear } from 'd3-shape';
+import { extent, max } from 'd3-array';
+import { timeMinute } from 'd3-time';
+
+const d3 = { scaleLinear, scaleTime, line, area, curveLinear, extent, max, timeMinute };
const defaultColorPalette = {
blue: ['#1f78d1', '#8fbce8'],
@@ -38,27 +43,27 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
let lineColor = '';
let areaColor = '';
- const timeSeriesScaleX = d3.time.scale()
+ const timeSeriesScaleX = d3.scaleTime()
.range([0, graphWidth - 70]);
- const timeSeriesScaleY = d3.scale.linear()
+ const timeSeriesScaleY = d3.scaleLinear()
.range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(xDom);
- timeSeriesScaleX.ticks(d3.time.minute, 60);
+ timeSeriesScaleX.ticks(d3.timeMinute, 60);
timeSeriesScaleY.domain(yDom);
const defined = d => !isNaN(d.value) && d.value != null;
- const lineFunction = d3.svg.line()
+ const lineFunction = d3.line()
.defined(defined)
- .interpolate('linear')
+ .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
.x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value));
- const areaFunction = d3.svg.area()
+ const areaFunction = d3.area()
.defined(defined)
- .interpolate('linear')
+ .curve(d3.curveLinear)
.x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset)
.y1(d => timeSeriesScaleY(d.value));
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 6e152497d20..a2f0a44863f 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -6,11 +6,12 @@ export default class NewCommitForm {
this.branchName = form.find('.js-branch-name');
this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request');
- this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
+ this.createMergeRequestContainer = form.find(
+ '.js-create-merge-request-container',
+ );
this.branchName.keyup(this.renderDestination);
this.renderDestination();
}
-
renderDestination() {
var different;
different = this.branchName.val() !== this.originalBranch.val();
@@ -23,6 +24,6 @@ export default class NewCommitForm {
this.createMergeRequestContainer.hide();
this.createMergeRequest.prop('checked', false);
}
- return this.wasDifferent = different;
+ return (this.wasDifferent = different);
}
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 2a570ac705e..a2b8e6f6495 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -25,6 +25,7 @@ import Autosave from './autosave';
import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
+import { localTimeAgo } from './lib/utils/datetime_utility';
window.autosize = Autosize;
@@ -36,6 +37,12 @@ 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) {
+ if (!this.instance) {
+ this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM);
+ }
+ }
+
constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
this.updateTargetButtons = this.updateTargetButtons.bind(this);
this.updateComment = this.updateComment.bind(this);
@@ -311,7 +318,7 @@ export default class Notes {
setupNewNote($note) {
// Update datetime format on the recent note
- gl.utils.localTimeAgo($note.find('.js-timeago'), false);
+ localTimeAgo($note.find('.js-timeago'), false);
this.collapseLongCommitList();
this.taskList.init();
@@ -463,7 +470,7 @@ export default class Notes {
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
}
- gl.utils.localTimeAgo($('.js-timeago'), false);
+ localTimeAgo($('.js-timeago'), false);
Notes.checkMergeRequestStatus();
return this.updateNotesCount(1);
}
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index f90ac2d9f71..9570d1c00aa 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,31 +1,25 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */
import Flash from './flash';
-(function() {
- this.NotificationsDropdown = (function() {
- function NotificationsDropdown() {
- $(document).off('click', '.update-notification').on('click', '.update-notification', function(e) {
- var form, label, notificationLevel;
- e.preventDefault();
- if ($(this).is('.is-active') && $(this).data('notification-level') === 'custom') {
- return;
- }
- notificationLevel = $(this).data('notification-level');
- label = $(this).data('notification-title');
- form = $(this).parents('.notification-form:first');
- form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner');
- form.find('#notification_setting_level').val(notificationLevel);
- return form.submit();
- });
- $(document).off('ajax:success', '.notification-form').on('ajax:success', '.notification-form', function(e, data) {
- if (data.saved) {
- return $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html);
- } else {
- return new Flash('Failed to save new settings', 'alert');
- }
- });
+export default function notificationsDropdown() {
+ $(document).on('click', '.update-notification', function updateNotificationCallback(e) {
+ e.preventDefault();
+ if ($(this).is('.is-active') && $(this).data('notification-level') === 'custom') {
+ return;
}
- return NotificationsDropdown;
- })();
-}).call(window);
+ const notificationLevel = $(this).data('notification-level');
+ const form = $(this).parents('.notification-form:first');
+
+ form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner');
+ form.find('#notification_setting_level').val(notificationLevel);
+ form.submit();
+ });
+
+ $(document).on('ajax:success', '.notification-form', (e, data) => {
+ if (data.saved) {
+ $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html);
+ } else {
+ Flash('Failed to save new settings', 'alert');
+ }
+ });
+}
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 2ab9c4fed2c..4534360d577 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -1,55 +1,50 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, newline-per-chained-call, comma-dangle, consistent-return, prefer-arrow-callback, max-len */
-(function() {
- this.NotificationsForm = (function() {
- function NotificationsForm() {
- this.toggleCheckbox = this.toggleCheckbox.bind(this);
- this.removeEventListeners();
- this.initEventListeners();
- }
+export default class NotificationsForm {
+ constructor() {
+ this.toggleCheckbox = this.toggleCheckbox.bind(this);
+ this.initEventListeners();
+ }
- NotificationsForm.prototype.removeEventListeners = function() {
- return $(document).off('change', '.js-custom-notification-event');
- };
+ initEventListeners() {
+ $(document).on('change', '.js-custom-notification-event', this.toggleCheckbox);
+ }
- NotificationsForm.prototype.initEventListeners = function() {
- return $(document).on('change', '.js-custom-notification-event', this.toggleCheckbox);
- };
+ toggleCheckbox(e) {
+ const $checkbox = $(e.currentTarget);
+ const $parent = $checkbox.closest('.checkbox');
- NotificationsForm.prototype.toggleCheckbox = function(e) {
- var $checkbox, $parent;
- $checkbox = $(e.currentTarget);
- $parent = $checkbox.closest('.checkbox');
- return this.saveEvent($checkbox, $parent);
- };
+ this.saveEvent($checkbox, $parent);
+ }
- NotificationsForm.prototype.showCheckboxLoadingSpinner = function($parent) {
- return $parent.addClass('is-loading').find('.custom-notification-event-loading').removeClass('fa-check').addClass('fa-spin fa-spinner').removeClass('is-done');
- };
+ // eslint-disable-next-line class-methods-use-this
+ showCheckboxLoadingSpinner($parent) {
+ $parent.addClass('is-loading')
+ .find('.custom-notification-event-loading')
+ .removeClass('fa-check')
+ .addClass('fa-spin fa-spinner')
+ .removeClass('is-done');
+ }
- NotificationsForm.prototype.saveEvent = function($checkbox, $parent) {
- var form;
- form = $parent.parents('form:first');
- return $.ajax({
- url: form.attr('action'),
- method: form.attr('method'),
- dataType: 'json',
- data: form.serialize(),
- beforeSend: (function(_this) {
- return function() {
- return _this.showCheckboxLoadingSpinner($parent);
- };
- })(this)
- }).done(function(data) {
- $checkbox.enable();
- if (data.saved) {
- $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
- return setTimeout(function() {
- return $parent.removeClass('is-loading').find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
- }, 2000);
- }
- });
- };
+ saveEvent($checkbox, $parent) {
+ const form = $parent.parents('form:first');
- return NotificationsForm;
- })();
-}).call(window);
+ return $.ajax({
+ url: form.attr('action'),
+ method: form.attr('method'),
+ dataType: 'json',
+ data: form.serialize(),
+ beforeSend: () => {
+ this.showCheckboxLoadingSpinner($parent);
+ },
+ }).done((data) => {
+ $checkbox.enable();
+ if (data.saved) {
+ $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
+ setTimeout(() => {
+ $parent.removeClass('is-loading')
+ .find('.custom-notification-event-loading')
+ .toggleClass('fa-spin fa-spinner fa-check is-done');
+ }, 2000);
+ }
+ });
+ }
+}
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
index 6792b984cc5..6552a88b606 100644
--- a/app/assets/javascripts/pager.js
+++ b/app/assets/javascripts/pager.js
@@ -1,78 +1,74 @@
import { getParameterByName } from '~/lib/utils/common_utils';
import { removeParams } from './lib/utils/url_utility';
-(() => {
- const ENDLESS_SCROLL_BOTTOM_PX = 400;
- const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
+const ENDLESS_SCROLL_BOTTOM_PX = 400;
+const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
- const Pager = {
- init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) {
- this.url = $('.content_list').data('href') || removeParams(['limit', 'offset']);
- this.limit = limit;
- this.offset = parseInt(getParameterByName('offset'), 10) || this.limit;
- this.disable = disable;
- this.prepareData = prepareData;
- this.callback = callback;
- this.loading = $('.loading').first();
- if (preload) {
- this.offset = 0;
- this.getOld();
- }
- this.initLoadMore();
- },
+export default {
+ init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) {
+ this.url = $('.content_list').data('href') || removeParams(['limit', 'offset']);
+ this.limit = limit;
+ this.offset = parseInt(getParameterByName('offset'), 10) || this.limit;
+ this.disable = disable;
+ this.prepareData = prepareData;
+ this.callback = callback;
+ this.loading = $('.loading').first();
+ if (preload) {
+ this.offset = 0;
+ this.getOld();
+ }
+ this.initLoadMore();
+ },
- getOld() {
- this.loading.show();
- $.ajax({
- type: 'GET',
- url: this.url,
- data: `limit=${this.limit}&offset=${this.offset}`,
- dataType: 'json',
- error: () => this.loading.hide(),
- success: (data) => {
- this.append(data.count, this.prepareData(data.html));
- this.callback();
+ getOld() {
+ this.loading.show();
+ $.ajax({
+ type: 'GET',
+ url: this.url,
+ data: `limit=${this.limit}&offset=${this.offset}`,
+ dataType: 'json',
+ error: () => this.loading.hide(),
+ success: (data) => {
+ this.append(data.count, this.prepareData(data.html));
+ this.callback();
- // keep loading until we've filled the viewport height
- if (!this.disable && !this.isScrollable()) {
- this.getOld();
- } else {
- this.loading.hide();
- }
- },
- });
- },
+ // keep loading until we've filled the viewport height
+ if (!this.disable && !this.isScrollable()) {
+ this.getOld();
+ } else {
+ this.loading.hide();
+ }
+ },
+ });
+ },
- append(count, html) {
- $('.content_list').append(html);
- if (count > 0) {
- this.offset += count;
- } else {
- this.disable = true;
- }
- },
+ append(count, html) {
+ $('.content_list').append(html);
+ if (count > 0) {
+ this.offset += count;
+ } else {
+ this.disable = true;
+ }
+ },
- isScrollable() {
- const $w = $(window);
- return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX;
- },
+ isScrollable() {
+ const $w = $(window);
+ return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX;
+ },
- initLoadMore() {
- $(document).unbind('scroll');
- $(document).endlessScroll({
- bottomPixels: ENDLESS_SCROLL_BOTTOM_PX,
- fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS,
- fireOnce: true,
- ceaseFire: () => this.disable === true,
- callback: () => {
- if (!this.loading.is(':visible')) {
- this.loading.show();
- this.getOld();
- }
- },
- });
- },
- };
-
- window.Pager = Pager;
-})();
+ initLoadMore() {
+ $(document).unbind('scroll');
+ $(document).endlessScroll({
+ bottomPixels: ENDLESS_SCROLL_BOTTOM_PX,
+ fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS,
+ fireOnce: true,
+ ceaseFire: () => this.disable === true,
+ callback: () => {
+ if (!this.loading.is(':visible')) {
+ this.loading.show();
+ this.getOld();
+ }
+ },
+ });
+ },
+};
diff --git a/app/assets/javascripts/pages/users/show/index.js b/app/assets/javascripts/pages/users/show/index.js
new file mode 100644
index 00000000000..f18f98b4e9a
--- /dev/null
+++ b/app/assets/javascripts/pages/users/show/index.js
@@ -0,0 +1,3 @@
+import UserCallout from '~/user_callout';
+
+export default () => new UserCallout();
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 141333b2b4d..ffaafb3ee9e 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -117,12 +117,10 @@
}());
markdownPreview = new window.MarkdownPreview();
-
previewButtonSelector = '.js-md-preview-button';
-
writeButtonSelector = '.js-md-write-button';
-
lastTextareaPreviewed = null;
+ const markdownToolbar = $('.md-header-toolbar');
$.fn.setupMarkdownPreview = function () {
var $form = $(this);
@@ -146,6 +144,7 @@
// toggle content
$form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show();
+ markdownToolbar.removeClass('active');
markdownPreview.showPreview($form);
});
@@ -167,6 +166,7 @@
$form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus();
$form.find('.md-preview-holder').hide();
+ markdownToolbar.addClass('active');
markdownPreview.hideReferencedCommands($form);
});
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 6348a2e331d..78be6b6e884 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -1,5 +1,5 @@
<script>
- import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
+ import modal from '../../../vue_shared/components/modal.vue';
import { __, s__, sprintf } from '../../../locale';
import csrf from '../../../lib/utils/csrf';
@@ -26,7 +26,7 @@
};
},
components: {
- popupDialog,
+ modal,
},
computed: {
csrfToken() {
@@ -89,7 +89,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
<template>
<div>
- <popup-dialog
+ <modal
v-if="isOpen"
:title="s__('Profiles|Delete your account?')"
:text="text"
@@ -134,7 +134,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
</form>
</template>
- </popup-dialog>
+ </modal>
<button
type="button"
diff --git a/app/assets/javascripts/project_variables.js b/app/assets/javascripts/project_variables.js
deleted file mode 100644
index 567c311f119..00000000000
--- a/app/assets/javascripts/project_variables.js
+++ /dev/null
@@ -1,39 +0,0 @@
-
-const HIDDEN_VALUE_TEXT = '******';
-
-export default class ProjectVariables {
- constructor() {
- this.$revealBtn = $('.js-btn-toggle-reveal-values');
- this.$revealBtn.on('click', this.toggleRevealState.bind(this));
- }
-
- toggleRevealState(e) {
- e.preventDefault();
-
- const oldStatus = this.$revealBtn.attr('data-status');
- let newStatus = 'hidden';
- let newAction = 'Reveal Values';
-
- if (oldStatus === 'hidden') {
- newStatus = 'revealed';
- newAction = 'Hide Values';
- }
-
- this.$revealBtn.attr('data-status', newStatus);
-
- const $variables = $('.variable-value');
-
- $variables.each((_, variable) => {
- const $variable = $(variable);
- let newText = HIDDEN_VALUE_TEXT;
-
- if (newStatus === 'revealed') {
- newText = $variable.attr('data-value');
- }
-
- $variable.text(newText);
- });
-
- this.$revealBtn.text(newAction);
- }
-}
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list.vue b/app/assets/javascripts/repo/components/commit_sidebar/list.vue
deleted file mode 100644
index fb862e7bf01..00000000000
--- a/app/assets/javascripts/repo/components/commit_sidebar/list.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-<script>
- import icon from '../../../vue_shared/components/icon.vue';
- import listItem from './list_item.vue';
- import listCollapsed from './list_collapsed.vue';
-
- export default {
- components: {
- icon,
- listItem,
- listCollapsed,
- },
- props: {
- title: {
- type: String,
- required: true,
- },
- fileList: {
- type: Array,
- required: true,
- },
- collapsed: {
- type: Boolean,
- required: true,
- },
- },
- methods: {
- toggleCollapsed() {
- this.$emit('toggleCollapsed');
- },
- },
- };
-</script>
-
-<template>
- <div class="multi-file-commit-panel-section">
- <header
- class="multi-file-commit-panel-header"
- :class="{
- 'is-collapsed': collapsed,
- }"
- >
- <icon
- name="list-bulleted"
- :size="18"
- css-classes="append-right-default"
- />
- <template v-if="!collapsed">
- {{ title }}
- <button
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- @click="toggleCollapsed"
- >
- <i
- aria-hidden="true"
- class="fa fa-angle-double-right"
- >
- </i>
- </button>
- </template>
- </header>
- <div class="multi-file-commit-list">
- <list-collapsed
- v-if="collapsed"
- />
- <template v-else>
- <ul
- v-if="fileList.length"
- class="list-unstyled append-bottom-0"
- >
- <li
- v-for="file in fileList"
- :key="file.key"
- >
- <list-item
- :file="file"
- />
- </li>
- </ul>
- <div
- v-else
- class="help-block prepend-top-0"
- >
- No changes
- </div>
- </template>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue
deleted file mode 100644
index 781404cf8ca..00000000000
--- a/app/assets/javascripts/repo/components/new_dropdown/index.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-<script>
- import { mapState } from 'vuex';
- import newModal from './modal.vue';
- import upload from './upload.vue';
- import icon from '../../../vue_shared/components/icon.vue';
-
- export default {
- components: {
- icon,
- newModal,
- upload,
- },
- data() {
- return {
- openModal: false,
- modalType: '',
- };
- },
- computed: {
- ...mapState([
- 'path',
- ]),
- },
- methods: {
- createNewItem(type) {
- this.modalType = type;
- this.toggleModalOpen();
- },
- toggleModalOpen() {
- this.openModal = !this.openModal;
- },
- },
- };
-</script>
-
-<template>
- <div>
- <ul class="breadcrumb repo-breadcrumb">
- <li class="dropdown">
- <button
- type="button"
- class="btn btn-default dropdown-toggle add-to-tree"
- data-toggle="dropdown"
- aria-label="Create new file or directory"
- >
- <icon
- name="plus"
- css-classes="pull-left"
- />
- <icon
- name="arrow-down"
- css-classes="pull-left"
- />
- </button>
- <ul class="dropdown-menu">
- <li>
- <a
- href="#"
- role="button"
- @click.prevent="createNewItem('blob')"
- >
- {{ __('New file') }}
- </a>
- </li>
- <li>
- <upload
- :path="path"
- />
- </li>
- <li>
- <a
- href="#"
- role="button"
- @click.prevent="createNewItem('tree')"
- >
- {{ __('New directory') }}
- </a>
- </li>
- </ul>
- </li>
- </ul>
- <new-modal
- v-if="openModal"
- :type="modalType"
- :path="path"
- @toggle="toggleModalOpen"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
deleted file mode 100644
index a00e1e9d809..00000000000
--- a/app/assets/javascripts/repo/components/repo.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import { mapState, mapGetters } from 'vuex';
-import RepoSidebar from './repo_sidebar.vue';
-import RepoCommitSection from './repo_commit_section.vue';
-import RepoTabs from './repo_tabs.vue';
-import RepoFileButtons from './repo_file_buttons.vue';
-import RepoPreview from './repo_preview.vue';
-import repoEditor from './repo_editor.vue';
-
-export default {
- computed: {
- ...mapState([
- 'currentBlobView',
- ]),
- ...mapGetters([
- 'isCollapsed',
- 'changedFiles',
- ]),
- },
- components: {
- RepoSidebar,
- RepoTabs,
- RepoFileButtons,
- repoEditor,
- RepoCommitSection,
- RepoPreview,
- },
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = (e) => {
- if (!this.changedFiles.length) return undefined;
-
- Object.assign(e, {
- returnValue,
- });
- return returnValue;
- };
- },
-};
-</script>
-
-<template>
- <div
- class="multi-file"
- :class="{
- 'is-collapsed': isCollapsed
- }"
- >
- <repo-sidebar/>
- <div
- v-if="isCollapsed"
- class="multi-file-edit-pane"
- >
- <repo-tabs />
- <component
- class="multi-file-edit-pane-content"
- :is="currentBlobView"
- />
- <repo-file-buttons />
- </div>
- <repo-commit-section />
- </div>
-</template>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
deleted file mode 100644
index 4ea21913129..00000000000
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
-import RepoPreviousDirectory from './repo_prev_directory.vue';
-import RepoFile from './repo_file.vue';
-import RepoLoadingFile from './repo_loading_file.vue';
-
-export default {
- components: {
- 'repo-previous-directory': RepoPreviousDirectory,
- 'repo-file': RepoFile,
- 'repo-loading-file': RepoLoadingFile,
- },
- created() {
- window.addEventListener('popstate', this.popHistoryState);
- },
- destroyed() {
- window.removeEventListener('popstate', this.popHistoryState);
- },
- mounted() {
- this.getTreeData();
- },
- computed: {
- ...mapState([
- 'loading',
- 'isRoot',
- ]),
- ...mapState({
- projectName(state) {
- return state.project.name;
- },
- }),
- ...mapGetters([
- 'treeList',
- 'isCollapsed',
- ]),
- },
- methods: {
- ...mapActions([
- 'getTreeData',
- 'popHistoryState',
- ]),
- },
-};
-</script>
-
-<template>
-<div class="ide-file-list">
- <table class="table">
- <thead>
- <tr>
- <th
- v-if="isCollapsed"
- >
- </th>
- <template v-else>
- <th class="name multi-file-table-name">
- Name
- </th>
- <th class="hidden-sm hidden-xs last-commit">
- Last commit
- </th>
- <th class="hidden-xs last-update text-right">
- Last update
- </th>
- </template>
- </tr>
- </thead>
- <tbody>
- <repo-previous-directory
- v-if="!isRoot && treeList.length"
- />
- <repo-loading-file
- v-if="!treeList.length && loading"
- v-for="n in 5"
- :key="n"
- />
- <repo-file
- v-for="file in treeList"
- :key="file.key"
- :file="file"
- />
- </tbody>
- </table>
-</div>
-</template>
diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js
deleted file mode 100644
index b6801af7fcb..00000000000
--- a/app/assets/javascripts/repo/index.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import Vue from 'vue';
-import { mapActions } from 'vuex';
-import { convertPermissionToBoolean } from '../lib/utils/common_utils';
-import Repo from './components/repo.vue';
-import RepoEditButton from './components/repo_edit_button.vue';
-import newBranchForm from './components/new_branch_form.vue';
-import newDropdown from './components/new_dropdown/index.vue';
-import store from './stores';
-import Translate from '../vue_shared/translate';
-
-function initRepo(el) {
- if (!el) return null;
-
- return new Vue({
- el,
- store,
- components: {
- repo: Repo,
- },
- methods: {
- ...mapActions([
- 'setInitialData',
- ]),
- },
- created() {
- const data = el.dataset;
-
- this.setInitialData({
- project: {
- id: data.projectId,
- name: data.projectName,
- url: data.projectUrl,
- },
- endpoints: {
- rootEndpoint: data.url,
- newMergeRequestUrl: data.newMergeRequestUrl,
- rootUrl: data.rootUrl,
- },
- canCommit: convertPermissionToBoolean(data.canCommit),
- onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
- currentRef: data.ref,
- path: data.currentPath,
- currentBranch: data.currentBranch,
- isRoot: convertPermissionToBoolean(data.root),
- isInitialRoot: convertPermissionToBoolean(data.root),
- });
- },
- render(createElement) {
- return createElement('repo');
- },
- });
-}
-
-function initRepoEditButton(el) {
- return new Vue({
- el,
- store,
- components: {
- repoEditButton: RepoEditButton,
- },
- render(createElement) {
- return createElement('repo-edit-button');
- },
- });
-}
-
-function initNewDropdown(el) {
- return new Vue({
- el,
- store,
- components: {
- newDropdown,
- },
- render(createElement) {
- return createElement('new-dropdown');
- },
- });
-}
-
-function initNewBranchForm() {
- const el = document.querySelector('.js-new-branch-dropdown');
-
- if (!el) return null;
-
- return new Vue({
- el,
- components: {
- newBranchForm,
- },
- store,
- render(createElement) {
- return createElement('new-branch-form');
- },
- });
-}
-
-const repo = document.getElementById('repo');
-const editButton = document.querySelector('.editable-mode');
-const newDropdownHolder = document.querySelector('.js-new-dropdown');
-
-Vue.use(Translate);
-
-initRepo(repo);
-initRepoEditButton(editButton);
-initNewBranchForm();
-initNewDropdown(newDropdownHolder);
diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js
deleted file mode 100644
index af5dcf054ef..00000000000
--- a/app/assets/javascripts/repo/stores/actions.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import Vue from 'vue';
-import { visitUrl } from '../../lib/utils/url_utility';
-import flash from '../../flash';
-import service from '../services';
-import * as types from './mutation_types';
-
-export const redirectToUrl = (_, url) => visitUrl(url);
-
-export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
-
-export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false);
-
-export const discardAllChanges = ({ commit, getters, dispatch }) => {
- const changedFiles = getters.changedFiles;
-
- changedFiles.forEach((file) => {
- commit(types.DISCARD_FILE_CHANGES, file);
-
- if (file.tempFile) {
- dispatch('closeFile', { file, force: true });
- }
- });
-};
-
-export const closeAllFiles = ({ state, dispatch }) => {
- state.openFiles.forEach(file => dispatch('closeFile', { file }));
-};
-
-export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => {
- const changedFiles = getters.changedFiles;
-
- if (changedFiles.length && !force) {
- commit(types.TOGGLE_DISCARD_POPUP, true);
- } else {
- commit(types.TOGGLE_EDIT_MODE);
- commit(types.TOGGLE_DISCARD_POPUP, false);
- dispatch('toggleBlobView');
-
- if (!state.editMode) {
- dispatch('discardAllChanges');
- }
- }
-};
-
-export const toggleBlobView = ({ commit, state }) => {
- if (state.editMode) {
- commit(types.SET_EDIT_MODE);
- } else {
- commit(types.SET_PREVIEW_MODE);
- }
-};
-
-export const checkCommitStatus = ({ state }) => service.getBranchData(
- state.project.id,
- state.currentBranch,
-)
- .then((data) => {
- const { id } = data.commit;
-
- if (state.currentRef !== id) {
- return true;
- }
-
- return false;
- })
- .catch(() => flash('Error checking branch data. Please try again.'));
-
-export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) =>
- service.commit(state.project.id, payload)
- .then((data) => {
- const { branch } = payload;
- if (!data.short_id) {
- flash(data.message);
- return;
- }
-
- const lastCommit = {
- commit_path: `${state.project.url}/commit/${data.id}`,
- commit: {
- message: data.message,
- authored_date: data.committed_date,
- },
- };
-
- flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
-
- if (newMr) {
- dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
- } else {
- commit(types.SET_COMMIT_REF, data.id);
-
- getters.changedFiles.forEach((entry) => {
- commit(types.SET_LAST_COMMIT_DATA, {
- entry,
- lastCommit,
- });
- });
-
- dispatch('discardAllChanges');
- dispatch('closeAllFiles');
- dispatch('toggleEditMode');
-
- window.scrollTo(0, 0);
- }
- })
- .catch(() => flash('Error committing changes. Please try again.'));
-
-export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => {
- if (type === 'tree') {
- dispatch('createTempTree', name);
- } else if (type === 'blob') {
- dispatch('createTempFile', {
- tree: state,
- name,
- base64,
- content,
- });
- }
-};
-
-export const popHistoryState = ({ state, dispatch, getters }) => {
- const treeList = getters.treeList;
- const tree = treeList.find(file => file.url === state.previousUrl);
-
- if (!tree) return;
-
- if (tree.type === 'tree') {
- dispatch('toggleTreeOpen', { endpoint: tree.url, tree });
- }
-};
-
-export const scrollToTab = () => {
- Vue.nextTick(() => {
- const tabs = document.getElementById('tabs');
-
- if (tabs) {
- const tabEl = tabs.querySelector('.active .repo-tab');
-
- tabEl.focus();
- }
- });
-};
-
-export * from './actions/tree';
-export * from './actions/file';
-export * from './actions/branch';
diff --git a/app/assets/javascripts/repo/stores/actions/branch.js b/app/assets/javascripts/repo/stores/actions/branch.js
deleted file mode 100644
index 61d9a5af3e3..00000000000
--- a/app/assets/javascripts/repo/stores/actions/branch.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import service from '../../services';
-import * as types from '../mutation_types';
-import { pushState } from '../utils';
-
-// eslint-disable-next-line import/prefer-default-export
-export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
- state.project.id,
- {
- branch,
- ref: state.currentBranch,
- },
-).then(res => res.json())
-.then((data) => {
- const branchName = data.name;
- const url = location.href.replace(state.currentBranch, branchName);
-
- pushState(url);
-
- commit(types.SET_CURRENT_BRANCH, branchName);
-});
diff --git a/app/assets/javascripts/repo/stores/actions/tree.js b/app/assets/javascripts/repo/stores/actions/tree.js
deleted file mode 100644
index 7c251e26bed..00000000000
--- a/app/assets/javascripts/repo/stores/actions/tree.js
+++ /dev/null
@@ -1,163 +0,0 @@
-import { visitUrl } from '../../../lib/utils/url_utility';
-import { normalizeHeaders } from '../../../lib/utils/common_utils';
-import flash from '../../../flash';
-import service from '../../services';
-import * as types from '../mutation_types';
-import {
- pushState,
- setPageTitle,
- findEntry,
- createTemp,
- createOrMergeEntry,
-} from '../utils';
-
-export const getTreeData = (
- { commit, state, dispatch },
- { endpoint = state.endpoints.rootEndpoint, tree = state } = {},
-) => {
- commit(types.TOGGLE_LOADING, tree);
-
- service.getTreeData(endpoint)
- .then((res) => {
- const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
- setPageTitle(pageTitle);
-
- return res.json();
- })
- .then((data) => {
- const prevLastCommitPath = tree.lastCommitPath;
- if (!state.isInitialRoot) {
- commit(types.SET_ROOT, data.path === '/');
- }
-
- dispatch('updateDirectoryData', { data, tree });
- commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
- commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path });
- commit(types.TOGGLE_LOADING, tree);
-
- if (prevLastCommitPath !== null) {
- dispatch('getLastCommitData', tree);
- }
-
- pushState(endpoint);
- })
- .catch(() => {
- flash('Error loading tree data. Please try again.');
- commit(types.TOGGLE_LOADING, tree);
- });
-};
-
-export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
- if (tree.opened) {
- // send empty data to clear the tree
- const data = { trees: [], blobs: [], submodules: [] };
-
- pushState(tree.parentTreeUrl);
-
- commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
- dispatch('updateDirectoryData', { data, tree });
- } else {
- commit(types.SET_PREVIOUS_URL, endpoint);
- dispatch('getTreeData', { endpoint, tree });
- }
-
- commit(types.TOGGLE_TREE_OPEN, tree);
-};
-
-export const clickedTreeRow = ({ commit, dispatch }, row) => {
- if (row.type === 'tree') {
- dispatch('toggleTreeOpen', {
- endpoint: row.url,
- tree: row,
- });
- } else if (row.type === 'submodule') {
- commit(types.TOGGLE_LOADING, row);
-
- visitUrl(row.url);
- } else if (row.type === 'blob' && row.opened) {
- dispatch('setFileActive', row);
- } else {
- dispatch('getFileData', row);
- }
-};
-
-export const createTempTree = ({ state, commit, dispatch }, name) => {
- let tree = state;
- const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
-
- dirNames.forEach((dirName) => {
- const foundEntry = findEntry(tree, 'tree', dirName);
-
- if (!foundEntry) {
- const tmpEntry = createTemp({
- name: dirName,
- path: tree.path,
- type: 'tree',
- level: tree.level !== undefined ? tree.level + 1 : 0,
- });
-
- commit(types.CREATE_TMP_TREE, {
- parent: tree,
- tmpEntry,
- });
- commit(types.TOGGLE_TREE_OPEN, tmpEntry);
-
- tree = tmpEntry;
- } else {
- tree = foundEntry;
- }
- });
-
- if (tree.tempFile) {
- dispatch('createTempFile', {
- tree,
- name: '.gitkeep',
- });
- }
-};
-
-export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
- if (tree.lastCommitPath === null || getters.isCollapsed) 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, lastCommit.type, lastCommit.file_name);
-
- if (entry) {
- commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
- }
- });
-
- dispatch('getLastCommitData', tree);
- })
- .catch(() => flash('Error fetching log data.'));
-};
-
-export const updateDirectoryData = ({ commit, state }, { data, tree }) => {
- const level = tree.level !== undefined ? tree.level + 1 : 0;
- const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
- const createEntry = (entry, type) => createOrMergeEntry({
- tree,
- entry,
- level,
- type,
- parentTreeUrl,
- });
-
- const formattedData = [
- ...data.trees.map(t => createEntry(t, 'tree')),
- ...data.submodules.map(m => createEntry(m, 'submodule')),
- ...data.blobs.map(b => createEntry(b, 'blob')),
- ];
-
- commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData });
-};
diff --git a/app/assets/javascripts/repo/stores/getters.js b/app/assets/javascripts/repo/stores/getters.js
deleted file mode 100644
index 5ce9f449905..00000000000
--- a/app/assets/javascripts/repo/stores/getters.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import _ from 'underscore';
-
-/*
- Takes the multi-dimensional tree and returns a flattened array.
- This allows for the table to recursively render the table rows but keeps the data
- structure nested to make it easier to add new files/directories.
-*/
-export const treeList = (state) => {
- const mapTree = arr => (!arr.tree.length ? [] : _.map(arr.tree, a => [a, mapTree(a)]));
-
- return _.chain(state.tree)
- .map(arr => [arr, mapTree(arr)])
- .flatten()
- .value();
-};
-
-export const changedFiles = state => state.openFiles.filter(file => file.changed);
-
-export const activeFile = state => state.openFiles.find(file => file.active);
-
-export const activeFileExtension = (state) => {
- const file = activeFile(state);
- return file ? `.${file.path.split('.').pop()}` : '';
-};
-
-export const isCollapsed = state => !!state.openFiles.length;
-
-export const canEditFile = (state) => {
- const currentActiveFile = activeFile(state);
- const openedFiles = state.openFiles;
-
- return state.canCommit &&
- state.onTopOfBranch &&
- openedFiles.length &&
- (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
-};
-
-export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
-
-export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
diff --git a/app/assets/javascripts/repo/stores/mutations/branch.js b/app/assets/javascripts/repo/stores/mutations/branch.js
deleted file mode 100644
index d8229e8a620..00000000000
--- a/app/assets/javascripts/repo/stores/mutations/branch.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import * as types from '../mutation_types';
-
-export default {
- [types.SET_CURRENT_BRANCH](state, currentBranch) {
- Object.assign(state, {
- currentBranch,
- });
- },
-};
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index ec85b8b6529..b830fcf7e80 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -3,226 +3,228 @@
import _ from 'underscore';
import Cookies from 'js-cookie';
-(function() {
- this.Sidebar = (function() {
- function Sidebar(currentUser) {
- this.toggleTodo = this.toggleTodo.bind(this);
- this.sidebar = $('aside');
-
- this.removeListeners();
- this.addEventListeners();
+function Sidebar(currentUser) {
+ this.toggleTodo = this.toggleTodo.bind(this);
+ this.sidebar = $('aside');
+
+ this.removeListeners();
+ this.addEventListeners();
+}
+
+Sidebar.initialize = function(currentUser) {
+ if (!this.instance) {
+ this.instance = new Sidebar(currentUser);
+ }
+};
+
+Sidebar.prototype.removeListeners = function () {
+ this.sidebar.off('click', '.sidebar-collapsed-icon');
+ this.sidebar.off('hidden.gl.dropdown');
+ $('.dropdown').off('loading.gl.dropdown');
+ $('.dropdown').off('loaded.gl.dropdown');
+ $(document).off('click', '.js-sidebar-toggle');
+};
+
+Sidebar.prototype.addEventListeners = function() {
+ const $document = $(document);
+
+ this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
+ this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
+ $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
+ $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
+
+ $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
+ return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
+};
+
+Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
+ var $allGutterToggleIcons, $this, $thisIcon;
+ e.preventDefault();
+ $this = $(this);
+ $thisIcon = $this.find('i');
+ $allGutterToggleIcons = $('.js-sidebar-toggle i');
+ if ($thisIcon.hasClass('fa-angle-double-right')) {
+ $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
+ $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ } else {
+ $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
+ $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+ $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+
+ if (gl.lazyLoader) gl.lazyLoader.loadCheck();
+ }
+ if (!triggered) {
+ Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
+ }
+};
+
+Sidebar.prototype.toggleTodo = function(e) {
+ var $btnText, $this, $todoLoading, ajaxType, url;
+ $this = $(e.currentTarget);
+ ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST';
+ if ($this.attr('data-delete-path')) {
+ url = "" + ($this.attr('data-delete-path'));
+ } else {
+ url = "" + ($this.data('url'));
+ }
+
+ $this.tooltip('hide');
+
+ return $.ajax({
+ url: url,
+ type: ajaxType,
+ dataType: 'json',
+ data: {
+ issuable_id: $this.data('issuable-id'),
+ issuable_type: $this.data('issuable-type')
+ },
+ beforeSend: (function(_this) {
+ return function() {
+ $('.js-issuable-todo').disable()
+ .addClass('is-loading');
+ };
+ })(this)
+ }).done((function(_this) {
+ return function(data) {
+ return _this.todoUpdateDone(data);
+ };
+ })(this));
+};
+
+Sidebar.prototype.todoUpdateDone = function(data) {
+ const deletePath = data.delete_path ? data.delete_path : null;
+ const attrPrefix = deletePath ? 'mark' : 'todo';
+ const $todoBtns = $('.js-issuable-todo');
+
+ $(document).trigger('todo:toggle', data.count);
+
+ $todoBtns.each((i, el) => {
+ const $el = $(el);
+ const $elText = $el.find('.js-issuable-todo-inner');
+
+ $el.removeClass('is-loading')
+ .enable()
+ .attr('aria-label', $el.data(`${attrPrefix}-text`))
+ .attr('data-delete-path', deletePath)
+ .attr('title', $el.data(`${attrPrefix}-text`));
+
+ if ($el.hasClass('has-tooltip')) {
+ $el.tooltip('fixTitle');
}
- Sidebar.prototype.removeListeners = function () {
- this.sidebar.off('click', '.sidebar-collapsed-icon');
- this.sidebar.off('hidden.gl.dropdown');
- $('.dropdown').off('loading.gl.dropdown');
- $('.dropdown').off('loaded.gl.dropdown');
- $(document).off('click', '.js-sidebar-toggle');
- };
-
- Sidebar.prototype.addEventListeners = function() {
- const $document = $(document);
-
- this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
- this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
- $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
- $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
-
- $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
- return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
- };
-
- Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
- var $allGutterToggleIcons, $this, $thisIcon;
- e.preventDefault();
- $this = $(this);
- $thisIcon = $this.find('i');
- $allGutterToggleIcons = $('.js-sidebar-toggle i');
- if ($thisIcon.hasClass('fa-angle-double-right')) {
- $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
- $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- } else {
- $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
- $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
- $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
-
- if (gl.lazyLoader) gl.lazyLoader.loadCheck();
- }
- if (!triggered) {
- Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
- }
- };
-
- Sidebar.prototype.toggleTodo = function(e) {
- var $btnText, $this, $todoLoading, ajaxType, url;
- $this = $(e.currentTarget);
- ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST';
- if ($this.attr('data-delete-path')) {
- url = "" + ($this.attr('data-delete-path'));
- } else {
- url = "" + ($this.data('url'));
- }
-
- $this.tooltip('hide');
-
- return $.ajax({
- url: url,
- type: ajaxType,
- dataType: 'json',
- data: {
- issuable_id: $this.data('issuable-id'),
- issuable_type: $this.data('issuable-type')
- },
- beforeSend: (function(_this) {
- return function() {
- $('.js-issuable-todo').disable()
- .addClass('is-loading');
- };
- })(this)
- }).done((function(_this) {
- return function(data) {
- return _this.todoUpdateDone(data);
- };
- })(this));
- };
-
- Sidebar.prototype.todoUpdateDone = function(data) {
- const deletePath = data.delete_path ? data.delete_path : null;
- const attrPrefix = deletePath ? 'mark' : 'todo';
- const $todoBtns = $('.js-issuable-todo');
-
- $(document).trigger('todo:toggle', data.count);
-
- $todoBtns.each((i, el) => {
- const $el = $(el);
- const $elText = $el.find('.js-issuable-todo-inner');
-
- $el.removeClass('is-loading')
- .enable()
- .attr('aria-label', $el.data(`${attrPrefix}-text`))
- .attr('data-delete-path', deletePath)
- .attr('title', $el.data(`${attrPrefix}-text`));
-
- if ($el.hasClass('has-tooltip')) {
- $el.tooltip('fixTitle');
- }
-
- if ($el.data(`${attrPrefix}-icon`)) {
- $elText.html($el.data(`${attrPrefix}-icon`));
- } else {
- $elText.text($el.data(`${attrPrefix}-text`));
- }
- });
- };
-
- Sidebar.prototype.sidebarDropdownLoading = function(e) {
- var $loading, $sidebarCollapsedIcon, i, img;
- $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
- img = $sidebarCollapsedIcon.find('img');
- i = $sidebarCollapsedIcon.find('i');
- $loading = $('<i class="fa fa-spinner fa-spin"></i>');
- if (img.length) {
- img.before($loading);
- return img.hide();
- } else if (i.length) {
- i.before($loading);
- return i.hide();
- }
- };
-
- Sidebar.prototype.sidebarDropdownLoaded = function(e) {
- var $sidebarCollapsedIcon, i, img;
- $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
- img = $sidebarCollapsedIcon.find('img');
- $sidebarCollapsedIcon.find('i.fa-spin').remove();
- i = $sidebarCollapsedIcon.find('i');
- if (img.length) {
- return img.show();
- } else {
- return i.show();
- }
- };
-
- Sidebar.prototype.sidebarCollapseClicked = function(e) {
- var $block, sidebar;
- if ($(e.currentTarget).hasClass('dont-change-state')) {
- return;
- }
- sidebar = e.data;
- e.preventDefault();
- $block = $(this).closest('.block');
- return sidebar.openDropdown($block);
- };
-
- Sidebar.prototype.openDropdown = function(blockOrName) {
- var $block;
- $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
- if (!this.isOpen()) {
- this.setCollapseAfterUpdate($block);
- this.toggleSidebar('open');
- }
-
- // Wait for the sidebar to trigger('click') open
- // so it doesn't cause our dropdown to close preemptively
- setTimeout(() => {
- $block.find('.js-sidebar-dropdown-toggle').trigger('click');
- });
- };
-
- Sidebar.prototype.setCollapseAfterUpdate = function($block) {
- $block.addClass('collapse-after-update');
- return $('.layout-page').addClass('with-overlay');
- };
-
- Sidebar.prototype.onSidebarDropdownHidden = function(e) {
- var $block, sidebar;
- sidebar = e.data;
- e.preventDefault();
- $block = $(e.target).closest('.block');
- return sidebar.sidebarDropdownHidden($block);
- };
-
- Sidebar.prototype.sidebarDropdownHidden = function($block) {
- if ($block.hasClass('collapse-after-update')) {
- $block.removeClass('collapse-after-update');
- $('.layout-page').removeClass('with-overlay');
- return this.toggleSidebar('hide');
- }
- };
-
- Sidebar.prototype.triggerOpenSidebar = function() {
- return this.sidebar.find('.js-sidebar-toggle').trigger('click');
- };
-
- Sidebar.prototype.toggleSidebar = function(action) {
- if (action == null) {
- action = 'toggle';
- }
- if (action === 'toggle') {
- this.triggerOpenSidebar();
- }
- if (action === 'open') {
- if (!this.isOpen()) {
- this.triggerOpenSidebar();
- }
- }
- if (action === 'hide') {
- if (this.isOpen()) {
- return this.triggerOpenSidebar();
- }
- }
- };
+ if ($el.data(`${attrPrefix}-icon`)) {
+ $elText.html($el.data(`${attrPrefix}-icon`));
+ } else {
+ $elText.text($el.data(`${attrPrefix}-text`));
+ }
+ });
+};
+
+Sidebar.prototype.sidebarDropdownLoading = function(e) {
+ var $loading, $sidebarCollapsedIcon, i, img;
+ $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
+ img = $sidebarCollapsedIcon.find('img');
+ i = $sidebarCollapsedIcon.find('i');
+ $loading = $('<i class="fa fa-spinner fa-spin"></i>');
+ if (img.length) {
+ img.before($loading);
+ return img.hide();
+ } else if (i.length) {
+ i.before($loading);
+ return i.hide();
+ }
+};
+
+Sidebar.prototype.sidebarDropdownLoaded = function(e) {
+ var $sidebarCollapsedIcon, i, img;
+ $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
+ img = $sidebarCollapsedIcon.find('img');
+ $sidebarCollapsedIcon.find('i.fa-spin').remove();
+ i = $sidebarCollapsedIcon.find('i');
+ if (img.length) {
+ return img.show();
+ } else {
+ return i.show();
+ }
+};
+
+Sidebar.prototype.sidebarCollapseClicked = function(e) {
+ var $block, sidebar;
+ if ($(e.currentTarget).hasClass('dont-change-state')) {
+ return;
+ }
+ sidebar = e.data;
+ e.preventDefault();
+ $block = $(this).closest('.block');
+ return sidebar.openDropdown($block);
+};
+
+Sidebar.prototype.openDropdown = function(blockOrName) {
+ var $block;
+ $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
+ if (!this.isOpen()) {
+ this.setCollapseAfterUpdate($block);
+ this.toggleSidebar('open');
+ }
+
+ // Wait for the sidebar to trigger('click') open
+ // so it doesn't cause our dropdown to close preemptively
+ setTimeout(() => {
+ $block.find('.js-sidebar-dropdown-toggle').trigger('click');
+ });
+};
+
+Sidebar.prototype.setCollapseAfterUpdate = function($block) {
+ $block.addClass('collapse-after-update');
+ return $('.layout-page').addClass('with-overlay');
+};
+
+Sidebar.prototype.onSidebarDropdownHidden = function(e) {
+ var $block, sidebar;
+ sidebar = e.data;
+ e.preventDefault();
+ $block = $(e.target).closest('.block');
+ return sidebar.sidebarDropdownHidden($block);
+};
+
+Sidebar.prototype.sidebarDropdownHidden = function($block) {
+ if ($block.hasClass('collapse-after-update')) {
+ $block.removeClass('collapse-after-update');
+ $('.layout-page').removeClass('with-overlay');
+ return this.toggleSidebar('hide');
+ }
+};
+
+Sidebar.prototype.triggerOpenSidebar = function() {
+ return this.sidebar.find('.js-sidebar-toggle').trigger('click');
+};
+
+Sidebar.prototype.toggleSidebar = function(action) {
+ if (action == null) {
+ action = 'toggle';
+ }
+ if (action === 'toggle') {
+ this.triggerOpenSidebar();
+ }
+ if (action === 'open') {
+ if (!this.isOpen()) {
+ this.triggerOpenSidebar();
+ }
+ }
+ if (action === 'hide') {
+ if (this.isOpen()) {
+ return this.triggerOpenSidebar();
+ }
+ }
+};
- Sidebar.prototype.isOpen = function() {
- return this.sidebar.is('.right-sidebar-expanded');
- };
+Sidebar.prototype.isOpen = function() {
+ return this.sidebar.is('.right-sidebar-expanded');
+};
- Sidebar.prototype.getBlock = function(name) {
- return this.sidebar.find(".block." + name);
- };
+Sidebar.prototype.getBlock = function(name) {
+ return this.sidebar.find(".block." + name);
+};
- return Sidebar;
- })();
-}).call(window);
+export default Sidebar;
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 130730b1700..d2f0d7410da 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -51,7 +51,10 @@ export default class Shortcuts {
}
onToggleHelp(e) {
- e.preventDefault();
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+
Shortcuts.toggleHelp(this.enabledHelp);
}
@@ -112,6 +115,9 @@ export default class Shortcuts {
static focusSearch(e) {
$('#search').focus();
- e.preventDefault();
+
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
}
}
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 305f97b010e..292e3d6a657 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,8 +1,8 @@
/* global Mousetrap */
-/* global sidebar */
import _ from 'underscore';
import 'mousetrap';
+import Sidebar from './right_sidebar';
import ShortcutsNavigation from './shortcuts_navigation';
import { CopyAsGFM } from './behaviors/copy_as_gfm';
@@ -69,7 +69,7 @@ export default class ShortcutsIssuable extends ShortcutsNavigation {
}
static openSidebarDropdown(name) {
- sidebar.openDropdown(name);
+ Sidebar.instance.openDropdown(name);
return false;
}
}
diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js
index 5e947769f8a..0581239d5a5 100644
--- a/app/assets/javascripts/users/activity_calendar.js
+++ b/app/assets/javascripts/users/activity_calendar.js
@@ -1,5 +1,9 @@
import _ from 'underscore';
-import d3 from 'd3';
+import { scaleLinear, scaleThreshold } from 'd3-scale';
+import { select } from 'd3-selection';
+import { getDayName, getDayDifference } from '../lib/utils/datetime_utility';
+
+const d3 = { select, scaleLinear, scaleThreshold };
const LOADING_HTML = `
<div class="text-center">
@@ -17,7 +21,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
function formatTooltipText({ date, count }) {
const dateObject = new Date(date);
- const dateDayName = gl.utils.getDayName(dateObject);
+ const dateDayName = getDayName(dateObject);
const dateText = dateObject.format('mmm d, yyyy');
let contribText = 'No contributions';
@@ -27,7 +31,7 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`;
}
-const initColorKey = () => d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]);
+const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]);
export default class ActivityCalendar {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) {
@@ -51,7 +55,7 @@ export default class ActivityCalendar {
const oneYearAgo = new Date(today);
oneYearAgo.setFullYear(today.getFullYear() - 1);
- const days = gl.utils.getDayDifference(oneYearAgo, today);
+ const days = getDayDifference(oneYearAgo, today);
for (let i = 0; i <= days; i += 1) {
const date = new Date(oneYearAgo);
@@ -204,7 +208,7 @@ export default class ActivityCalendar {
initColor() {
const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
- return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange);
+ return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange);
}
clickDay(stamp) {
diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js
index 1215b265e28..992baa9a1ef 100644
--- a/app/assets/javascripts/users/user_tabs.js
+++ b/app/assets/javascripts/users/user_tabs.js
@@ -1,4 +1,6 @@
+import Activities from '../activities';
import ActivityCalendar from './activity_calendar';
+import { localTimeAgo } from '../lib/utils/datetime_utility';
/**
* UserTabs
@@ -138,7 +140,7 @@ export default class UserTabs {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
- gl.utils.localTimeAgo($('.js-timeago', tabSelector));
+ localTimeAgo($('.js-timeago', tabSelector));
},
});
}
@@ -169,7 +171,7 @@ export default class UserTabs {
});
// eslint-disable-next-line no-new
- new gl.Activities();
+ new Activities();
this.loaded.activity = true;
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
index 32028a4a609..ee1a45cc754 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -1,4 +1,4 @@
-import '~/lib/utils/datetime_utility';
+import { getTimeago } from '~/lib/utils/datetime_utility';
import { visitUrl } from '../../lib/utils/url_utility';
import Flash from '../../flash';
import MemoryUsage from './mr_widget_memory_usage';
@@ -17,7 +17,7 @@ export default {
},
methods: {
formatDate(date) {
- return gl.utils.getTimeago().format(date);
+ return getTimeago().format(date);
},
hasExternalUrls(deployment = {}) {
return deployment.external_url && deployment.external_url_formatted;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
index 05c4a28be88..43b2d238f65 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
@@ -65,10 +65,12 @@ export default {
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
- <h4>
- Set by
- <mr-widget-author :author="mr.setToMWPSBy" />
- to be merged automatically when the pipeline succeeds
+ <h4 class="flex-container-block">
+ <span class="append-right-10">
+ Set by
+ <mr-widget-author :author="mr.setToMWPSBy" />
+ to be merged automatically when the pipeline succeeds
+ </span>
<a
v-if="mr.canCancelAutomaticMerge"
@click.prevent="cancelAutomaticMerge"
@@ -94,8 +96,13 @@ export default {
<p v-if="mr.shouldRemoveSourceBranch">
The source branch will be removed
</p>
- <p v-else>
- The source branch will not be removed
+ <p
+ v-else
+ class="flex-container-block"
+ >
+ <span class="append-right-10">
+ The source branch will not be removed
+ </span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 9cb3edead86..8a9129c385b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -62,7 +62,7 @@ export default {
return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
- return !!this.mr.relatedLinks;
+ return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState;
},
shouldRenderDeployments() {
return this.mr.deployments.length;
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index 99f5c305df5..5fa838baba3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -6,7 +6,7 @@ Vue.use(VueResource);
export default class MRWidgetService {
constructor(endpoints) {
this.mergeResource = Vue.resource(endpoints.mergePath);
- this.mergeCheckResource = Vue.resource(endpoints.statusPath);
+ this.mergeCheckResource = Vue.resource(`${endpoints.statusPath}?serializer=widget`);
this.cancelAutoMergeResource = Vue.resource(endpoints.cancelAutoMergePath);
this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index 7c15abfff10..2bace3311c8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -1,30 +1,32 @@
+import { stateKey } from './state_maps';
+
export default function deviseState(data) {
if (data.project_archived) {
- return 'archived';
+ return stateKey.archived;
} else if (data.branch_missing) {
- return 'missingBranch';
+ return stateKey.missingBranch;
} else if (!data.commits_count) {
- return 'nothingToMerge';
+ return stateKey.nothingToMerge;
} else if (this.mergeStatus === 'unchecked') {
- return 'checking';
+ return stateKey.checking;
} else if (data.has_conflicts) {
- return 'conflicts';
+ return stateKey.conflicts;
} else if (data.work_in_progress) {
- return 'workInProgress';
+ return stateKey.workInProgress;
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
- return 'pipelineFailed';
+ return stateKey.pipelineFailed;
} else if (this.hasMergeableDiscussionsState) {
- return 'unresolvedDiscussions';
+ return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) {
- return 'pipelineBlocked';
+ return stateKey.pipelineBlocked;
} else if (this.hasSHAChanged) {
- return 'shaMismatch';
+ return stateKey.shaMismatch;
} else if (this.mergeWhenPipelineSucceeds) {
- return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds';
+ return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds;
} else if (!this.canMerge) {
- return 'notAllowedToMerge';
+ return stateKey.notAllowedToMerge;
} else if (this.canBeMerged) {
- return 'readyToMerge';
+ return stateKey.readyToMerge;
}
return null;
}
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 c1f7e64f580..93d31a2a684 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
@@ -1,5 +1,7 @@
import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies';
+import { stateKey } from './state_maps';
+import { formatDate } from '../../lib/utils/datetime_utility';
export default class MergeRequestStore {
@@ -119,10 +121,14 @@ export default class MergeRequestStore {
}
}
+ get isNothingToMergeState() {
+ return this.state === stateKey.nothingToMerge;
+ }
+
static getEventObject(event) {
return {
author: MergeRequestStore.getAuthorObject(event),
- updatedAt: gl.utils.formatDate(MergeRequestStore.getEventUpdatedAtDate(event)),
+ updatedAt: formatDate(MergeRequestStore.getEventUpdatedAtDate(event)),
formattedUpdatedAt: MergeRequestStore.getEventDate(event),
};
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index 9074a064a6d..de980c175fb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -31,6 +31,23 @@ const statesToShowHelpWidget = [
'autoMergeFailed',
];
+export const stateKey = {
+ archived: 'archived',
+ missingBranch: 'missingBranch',
+ nothingToMerge: 'nothingToMerge',
+ checking: 'checking',
+ conflicts: 'conflicts',
+ workInProgress: 'workInProgress',
+ pipelineFailed: 'pipelineFailed',
+ unresolvedDiscussions: 'unresolvedDiscussions',
+ pipelineBlocked: 'pipelineBlocked',
+ shaMismatch: 'shaMismatch',
+ autoMergeFailed: 'autoMergeFailed',
+ mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds',
+ notAllowedToMerge: 'notAllowedToMerge',
+ readyToMerge: 'readyToMerge',
+};
+
export default {
stateToComponentMap,
statesToShowHelpWidget,
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 6c575d8eb49..36d2d1dc164 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -72,7 +72,9 @@
Preview
</a>
</li>
- <li class="md-header-toolbar">
+ <li
+ class="md-header-toolbar"
+ :class="{ active: !previewMarkdown }">
<toolbar-button
tag="**"
button-title="Add bold text"
diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.js b/app/assets/javascripts/vue_shared/components/memory_graph.js
index 643b77e04c7..f37ef1a5ca3 100644
--- a/app/assets/javascripts/vue_shared/components/memory_graph.js
+++ b/app/assets/javascripts/vue_shared/components/memory_graph.js
@@ -1,3 +1,5 @@
+import { getTimeago } from '../../lib/utils/datetime_utility';
+
export default {
name: 'MemoryGraph',
props: {
@@ -16,7 +18,7 @@ export default {
},
computed: {
getFormattedMedian() {
- const deployedSince = gl.utils.getTimeago().format(this.deploymentTime * 1000);
+ const deployedSince = getTimeago().format(this.deploymentTime * 1000);
return `Deployed ${deployedSince}`;
},
},
diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/modal.vue
index 6d15bbd84ba..55f466b7b41 100644
--- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue
+++ b/app/assets/javascripts/vue_shared/components/modal.vue
@@ -1,6 +1,6 @@
<script>
export default {
- name: 'popup-dialog',
+ name: 'modal',
props: {
title: {
@@ -75,7 +75,7 @@ export default {
<template>
<div class="modal-open">
<div
- class="modal popup-dialog"
+ class="modal show"
role="dialog"
tabindex="-1"
>
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
new file mode 100644
index 00000000000..dce23bd65f6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
@@ -0,0 +1,103 @@
+<script>
+
+/* This is a re-usable vue component for rendering a project avatar that
+ does not need to link to the project's profile. The image and an optional
+ tooltip can be configured by props passed to this component.
+
+ Sample configuration:
+
+ <project-avatar-image
+ :lazy="true"
+ :img-src="projectAvatarSrc"
+ :img-alt="tooltipText"
+ :tooltip-text="tooltipText"
+ tooltip-placement="top"
+ />
+
+*/
+
+import defaultAvatarUrl from 'images/no_avatar.png';
+import { placeholderImage } from '../../../lazy_loader';
+import tooltip from '../../directives/tooltip';
+
+export default {
+ name: 'ProjectAvatarImage',
+ props: {
+ lazy: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ imgSrc: {
+ type: String,
+ required: false,
+ default: defaultAvatarUrl,
+ },
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ imgAlt: {
+ type: String,
+ required: false,
+ default: 'project avatar',
+ },
+ size: {
+ type: Number,
+ required: false,
+ default: 20,
+ },
+ tooltipText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ },
+ directives: {
+ tooltip,
+ },
+ computed: {
+ // API response sends null when gravatar is disabled and
+ // we provide an empty string when we use it inside project avatar link.
+ // In both cases we should render the defaultAvatarUrl
+ sanitizedSource() {
+ return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
+ },
+ resultantSrcAttribute() {
+ return this.lazy ? placeholderImage : this.sanitizedSource;
+ },
+ tooltipContainer() {
+ return this.tooltipText ? 'body' : null;
+ },
+ avatarSizeClass() {
+ return `s${this.size}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <img
+ v-tooltip
+ class="avatar"
+ :class="{
+ lazy,
+ [avatarSizeClass]: true,
+ [cssClasses]: true
+ }"
+ :src="resultantSrcAttribute"
+ :width="size"
+ :height="size"
+ :alt="imgAlt"
+ :data-src="sanitizedSource"
+ :data-container="tooltipContainer"
+ :data-placement="tooltipPlacement"
+ :title="tooltipText"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_dialog.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index 3ec50f14eb4..8053c65d498 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_dialog.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -1,8 +1,8 @@
<script>
-import PopupDialog from './popup_dialog.vue';
+import modal from './modal.vue';
export default {
- name: 'recaptcha-dialog',
+ name: 'recaptcha-modal',
props: {
html: {
@@ -20,7 +20,7 @@ export default {
},
components: {
- PopupDialog,
+ modal,
},
methods: {
@@ -65,9 +65,9 @@ export default {
</script>
<template>
-<popup-dialog
+<modal
kind="warning"
- class="recaptcha-dialog js-recaptcha-dialog"
+ class="recaptcha-modal js-recaptcha-modal"
:hide-footer="true"
:title="__('Please solve the reCAPTCHA')"
@toggle="close"
@@ -81,5 +81,5 @@ export default {
v-html="html"
></div>
</div>
-</popup-dialog>
+</modal>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index ddc9ddbc3a3..4277d9281a0 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -1,6 +1,13 @@
<script>
+ import { s__ } from '../../locale';
+ import icon from './icon.vue';
import loadingIcon from './loading_icon.vue';
+ const ICON_ON = 'status_success_borderless';
+ const ICON_OFF = 'status_failed_borderless';
+ const LABEL_ON = s__('ToggleButton|Toggle Status: ON');
+ const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF');
+
export default {
props: {
name: {
@@ -22,19 +29,10 @@
required: false,
default: false,
},
- enabledText: {
- type: String,
- required: false,
- default: 'Enabled',
- },
- disabledText: {
- type: String,
- required: false,
- default: 'Disabled',
- },
},
components: {
+ icon,
loadingIcon,
},
@@ -43,6 +41,15 @@
event: 'change',
},
+ computed: {
+ toggleIcon() {
+ return this.value ? ICON_ON : ICON_OFF;
+ },
+ ariaLabel() {
+ return this.value ? LABEL_ON : LABEL_OFF;
+ },
+ },
+
methods: {
toggleFeature() {
if (!this.disabledInput) this.$emit('change', !this.value);
@@ -60,10 +67,8 @@
/>
<button
type="button"
- aria-label="Toggle"
class="project-feature-toggle"
- :data-enabled-text="enabledText"
- :data-disabled-text="disabledText"
+ :aria-label="ariaLabel"
:class="{
'is-checked': value,
'is-disabled': disabledInput,
@@ -72,6 +77,11 @@
@click="toggleFeature"
>
<loadingIcon class="loading-icon" />
+ <span class="toggle-icon">
+ <icon
+ css-classes="toggle-icon-svg"
+ :name="toggleIcon"/>
+ </span>
</button>
</label>
</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/recaptcha_dialog_implementor.js b/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js
index ef70f9432e3..ff1f565e79a 100644
--- a/app/assets/javascripts/vue_shared/mixins/recaptcha_dialog_implementor.js
+++ b/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js
@@ -1,4 +1,4 @@
-import RecaptchaDialog from '../components/recaptcha_dialog.vue';
+import recaptchaModal from '../components/recaptcha_modal.vue';
export default {
data() {
@@ -9,7 +9,7 @@ export default {
},
components: {
- RecaptchaDialog,
+ recaptchaModal,
},
methods: {
diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js
index 20f63ab663c..4e3b9d7b767 100644
--- a/app/assets/javascripts/vue_shared/mixins/timeago.js
+++ b/app/assets/javascripts/vue_shared/mixins/timeago.js
@@ -1,4 +1,4 @@
-import '../../lib/utils/datetime_utility';
+import { formatDate, getTimeago } from '../../lib/utils/datetime_utility';
/**
* Mixin with time ago methods used in some vue components
@@ -6,13 +6,13 @@ import '../../lib/utils/datetime_utility';
export default {
methods: {
timeFormated(time) {
- const timeago = gl.utils.getTimeago();
+ const timeago = getTimeago();
return timeago.format(time);
},
tooltipTitle(time) {
- return gl.utils.formatDate(time);
+ return formatDate(time);
},
},
};
diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss
index 26a2db99e0a..5da06b90113 100644
--- a/app/assets/stylesheets/framework/contextual-sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual-sidebar.scss
@@ -9,12 +9,6 @@
padding-left: $contextual-sidebar-width;
}
- // Override position: absolute
- .right-sidebar {
- position: fixed;
- height: calc(100% - #{$header-height});
- }
-
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
padding: 10px 0 15px;
}
@@ -29,7 +23,6 @@
.context-header {
position: relative;
margin-right: 2px;
- width: $contextual-sidebar-width;
a {
transition: padding $sidebar-transition-duration;
@@ -320,13 +313,14 @@
transition: width $sidebar-transition-duration;
position: fixed;
bottom: 0;
- padding: 16px;
+ padding: $gl-padding;
background-color: $gray-light;
border: 0;
border-top: 2px solid $border-color;
color: $gl-text-color-secondary;
display: flex;
align-items: center;
+ line-height: 1;
svg {
margin-right: 8px;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 478269f3fcf..bc907a390d8 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -16,27 +16,18 @@
@mixin set-visible {
transform: translateY(0);
- visibility: visible;
- opacity: 1;
- transition-duration: 100ms, 150ms, 25ms;
- transition-delay: 35ms, 50ms, 25ms;
+ display: block;
}
@mixin set-invisible {
transform: translateY(-10px);
- visibility: hidden;
- opacity: 0;
- transition-property: opacity, transform, visibility;
- transition-duration: 70ms, 250ms, 250ms;
- transition-timing-function: linear, $dropdown-animation-timing;
- transition-delay: 25ms, 50ms, 0ms;
+ display: none;
}
.open {
.dropdown-menu,
.dropdown-menu-nav {
@include set-visible;
- display: block;
min-height: 40px;
@media (max-width: $screen-xs-max) {
@@ -55,6 +46,11 @@
}
}
+// Get search dropdown to line up with other nav dropdowns
+.search-input-container .dropdown-menu {
+ margin-top: 11px;
+}
+
.dropdown-toggle {
padding: 6px 8px 6px 10px;
background-color: $white-light;
@@ -214,7 +210,6 @@
.dropdown-menu,
.dropdown-menu-nav {
@include set-invisible;
- display: block;
position: absolute;
width: auto;
top: 100%;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 609f33582e1..1588036aeae 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -396,3 +396,8 @@ span.idiff {
.file-fork-suggestion-note {
margin-right: 1.5em;
}
+
+.label-lfs {
+ color: $common-gray-light;
+ border: 1px solid $common-gray-light;
+}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 1537b0744cc..1d8bd26cf1a 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -24,10 +24,14 @@
font-size: $gl-font-size;
line-height: 25px;
- &.status-box-closed {
+ &.status-box-mr-closed {
background-color: $gl-danger;
}
+ &.status-box-issue-closed {
+ background-color: $gl-primary;
+ }
+
&.status-box-merged {
background-color: $gl-primary;
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 5389eb0a5f2..6b07ffdbd61 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -74,7 +74,7 @@
}
.md-header-tab {
- @media(max-width: $screen-xs-max) {
+ @media (max-width: $screen-xs-max) {
flex: 1;
width: 100%;
border-bottom: 1px solid $border-color;
@@ -82,16 +82,23 @@
}
}
-.md-header-toolbar {
- margin-left: auto;
+.nav-links {
+ li.md-header-toolbar {
+ margin-left: auto;
+ display: none;
- @media(max-width: $screen-xs-max) {
- flex: none;
- display: flex;
- justify-content: center;
- width: 100%;
- padding-top: $gl-padding-top;
- padding-bottom: $gl-padding-top;
+ &.active {
+ display: block;
+
+ @media (max-width: $screen-xs-max) {
+ flex: none;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ padding-top: $gl-padding-top;
+ padding-bottom: $gl-padding-top;
+ }
+ }
}
}
@@ -175,7 +182,7 @@
margin-left: $gl-padding;
margin-right: -5px;
- @media(max-width: $screen-xs-max) {
+ @media (max-width: $screen-xs-max) {
margin-left: 0;
margin-right: 0;
}
@@ -239,7 +246,7 @@
}
}
-@media(max-width: $screen-xs-max) {
+@media (max-width: $screen-xs-max) {
.atwho-view-ul {
width: 350px;
}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 600a1f53b58..a12f28efce6 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -111,21 +111,4 @@
aside:not(.right-sidebar) {
display: none;
}
-
- .show-aside {
- display: block !important;
- }
-}
-
-.show-aside {
- display: none;
- position: fixed;
- right: 0;
- top: 30%;
- padding: 5px 15px;
- background: $show-aside-bg;
- font-size: 20px;
- color: $show-aside-color;
- z-index: 100;
- box-shadow: 0 1px 2px $show-aside-shadow;
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index ce551e6b7ce..1be66d0ab21 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -44,11 +44,18 @@ body.modal-open {
}
}
-.modal.popup-dialog {
- display: block;
+.modal {
+ background-color: $black-transparent;
+ z-index: 2100;
+
+ @media (min-width: $screen-md-min) {
+ .modal-dialog {
+ margin: 30px auto;
+ }
+ }
}
-.recaptcha-dialog .recaptcha-form {
+.recaptcha-modal .recaptcha-form {
display: inline-block;
.recaptcha {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 0742c0a2a09..d61809cb0a4 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -90,11 +90,6 @@
.right-sidebar {
border-left: 1px solid $border-color;
height: calc(100% - #{$header-height});
-
- &.affix {
- position: fixed;
- top: $header-height;
- }
}
.with-performance-bar .right-sidebar.affix {
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
index 71765da3908..0cd83df218f 100644
--- a/app/assets/stylesheets/framework/toggle.scss
+++ b/app/assets/stylesheets/framework/toggle.scss
@@ -27,7 +27,7 @@
border: 0;
outline: 0;
display: block;
- width: 100px;
+ width: 50px;
height: 24px;
cursor: pointer;
user-select: none;
@@ -42,31 +42,31 @@
background: none;
}
- &::before {
- color: $feature-toggle-text-color;
- font-size: 12px;
- line-height: 24px;
- position: absolute;
- top: 0;
- left: 25px;
- right: 5px;
- text-align: center;
- overflow: hidden;
- text-overflow: ellipsis;
- animation: animate-disabled .2s ease-in;
- content: attr(data-disabled-text);
- }
-
- &::after {
+ .toggle-icon {
position: relative;
display: block;
- content: "";
- width: 22px;
- height: 18px;
left: 0;
border-radius: 9px;
background: $feature-toggle-color;
transition: all .2s ease;
+
+ &,
+ .toggle-icon-svg {
+ width: 18px;
+ height: 18px;
+ }
+
+ .toggle-icon-svg {
+ fill: $feature-toggle-color-disabled;
+ }
+
+ .toggle-status-checked {
+ display: none;
+ }
+
+ .toggle-status-unchecked {
+ display: inline;
+ }
}
.loading-icon {
@@ -77,11 +77,10 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
-
}
&.is-loading {
- &::before {
+ .toggle-icon {
display: none;
}
@@ -100,15 +99,20 @@
&.is-checked {
background: $feature-toggle-color-enabled;
- &::before {
- left: 5px;
- right: 25px;
- animation: animate-enabled .2s ease-in;
- content: attr(data-enabled-text);
- }
+ .toggle-icon {
+ left: calc(100% - 18px);
- &::after {
- left: calc(100% - 22px);
+ .toggle-icon-svg {
+ fill: $feature-toggle-color-enabled;
+ }
+
+ .toggle-status-checked {
+ display: inline;
+ }
+
+ .toggle-status-unchecked {
+ display: none;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 5de5403916f..1d6c7a5c472 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -219,6 +219,7 @@ $gl-input-padding: 10px;
$gl-vert-padding: 6px;
$gl-padding-top: 10px;
$gl-sidebar-padding: 22px;
+$gl-bar-padding: 3px;
/*
* Misc
@@ -245,9 +246,6 @@ $btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
$issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary;
-$show-aside-bg: #eee;
-$show-aside-color: #777;
-$show-aside-shadow: #ddd;
$group-path-color: #999;
$namespace-kind-color: #aaa;
$panel-heading-link-color: #777;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 8f8f11e3857..e1637618ab2 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -122,7 +122,7 @@
}
.right-sidebar {
- position: absolute;
+ position: fixed;
top: $header-height;
bottom: 0;
right: 0;
@@ -502,7 +502,7 @@
top: $header-height + $performance-bar-height;
.issuable-sidebar {
- height: calc(100% - #{$header-height} - #{$performance-bar-height});
+ height: calc(100% - #{$performance-bar-height});
}
}
@@ -610,11 +610,19 @@
}
.issuable-status-box {
- float: none;
- display: inline-block;
+ align-self: stretch;
+ display: flex;
+ justify-content: center;
+ align-items: center;
margin-top: 0;
- height: auto;
- align-self: center;
+ padding-left: 9px;
+ padding-right: 9px;
+
+ @media (min-width: $screen-sm-min) {
+ display: inline-block;
+ height: auto;
+ align-self: center;
+ }
}
.issuable-gutter-toggle {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 402412eae71..da3c2d7fa5d 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -1,16 +1,3 @@
-.modal.popup-dialog {
- display: block;
- background-color: $black-transparent;
- z-index: 2100;
-
- @media (min-width: $screen-md-min) {
- .modal-dialog {
- width: 600px;
- margin: 30px auto;
- }
- }
-}
-
.project-refs-form,
.project-refs-target-form {
display: inline-block;
@@ -35,9 +22,10 @@
}
}
-.multi-file {
+.ide-view {
display: flex;
- height: calc(100vh - 145px);
+ height: calc(100vh - #{$header-height});
+ color: $almost-black;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
@@ -48,12 +36,47 @@
}
}
+.with-performance-bar .ide-view {
+ height: calc(100vh - #{$header-height});
+}
+
.ide-file-list {
flex: 1;
- overflow: scroll;
.file {
cursor: pointer;
+
+ &.file-open {
+ background: $white-normal;
+ }
+
+ .repo-file-name {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .unsaved-icon {
+ color: $indigo-700;
+ float: right;
+ font-size: smaller;
+ line-height: 20px;
+ }
+
+ .repo-new-btn {
+ display: none;
+ margin-top: -4px;
+ margin-bottom: -4px;
+ }
+
+ &:hover {
+ .repo-new-btn {
+ display: block;
+ }
+
+ .unsaved-icon {
+ display: none;
+ }
+ }
}
a {
@@ -68,10 +91,9 @@
.multi-file-table-name,
.multi-file-table-col-commit-message {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+ overflow: visible;
max-width: 0;
+ padding: 6px 12px;
}
.multi-file-table-name {
@@ -79,6 +101,7 @@
}
.multi-file-table-col-commit-message {
+ white-space: nowrap;
width: 50%;
}
@@ -92,7 +115,7 @@
.multi-file-tabs {
display: flex;
- overflow: scroll;
+ overflow-x: auto;
background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark;
@@ -141,9 +164,38 @@
height: 0;
}
+.blob-editor-container {
+ flex: 1;
+ height: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ .vertical-center {
+ min-height: auto;
+ }
+}
+
+.multi-file-editor-holder {
+ height: 100%;
+}
+
.multi-file-editor-btn-group {
- padding: $grid-size;
+ padding: $gl-bar-padding $gl-padding;
border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ background: $white-light;
+}
+
+.ide-status-bar {
+ padding: $gl-bar-padding $gl-padding;
+ background: $white-light;
+ display: flex;
+ justify-content: space-between;
+
+ svg {
+ vertical-align: middle;
+ }
}
// Not great, but this is to deal with our current output
@@ -151,10 +203,6 @@
height: 100%;
overflow: scroll;
- .blob-viewer {
- height: 100%;
- }
-
.file-content.code {
display: flex;
@@ -175,18 +223,101 @@
}
}
+.file-content.blob-no-preview {
+ a {
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
.multi-file-commit-panel {
display: flex;
flex-direction: column;
height: 100%;
width: 290px;
- padding: $gl-padding;
+ padding: 0;
background-color: $gray-light;
border-left: 1px solid $white-dark;
+ .projects-sidebar {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .multi-file-commit-panel-inner {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ }
+
+ .multi-file-commit-panel-inner-scroll {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow: auto;
+ }
+
&.is-collapsed {
width: 60px;
- padding: 0;
+
+ .multi-file-commit-list {
+ padding-top: $gl-padding;
+ overflow: hidden;
+ }
+
+ .multi-file-context-bar-icon {
+ align-items: center;
+
+ svg {
+ float: none;
+ margin: 0;
+ }
+ }
+ }
+
+ .branch-container {
+ border-left: 4px solid $indigo-700;
+ margin-bottom: $gl-bar-padding;
+ }
+
+ .branch-header {
+ background: $white-dark;
+ display: flex;
+ }
+
+ .branch-header-title {
+ flex: 1;
+ padding: $grid-size $gl-padding;
+ color: $indigo-700;
+ font-weight: $gl-font-weight-bold;
+
+ svg {
+ vertical-align: middle;
+ }
+ }
+
+ .branch-header-btns {
+ padding: $gl-vert-padding $gl-padding;
+ }
+
+ .left-collapse-btn {
+ display: none;
+ background: $gray-light;
+ text-align: left;
+ border-top: 1px solid $white-dark;
+
+ svg {
+ vertical-align: middle;
+ }
+ }
+}
+
+.multi-file-context-bar-icon {
+ padding: 10px;
+
+ svg {
+ margin-right: 10px;
+ float: left;
}
}
@@ -199,9 +330,9 @@
.multi-file-commit-panel-header {
display: flex;
align-items: center;
- padding: 0 0 12px;
margin-bottom: 12px;
border-bottom: 1px solid $white-dark;
+ padding: $gl-btn-padding 0;
&.is-collapsed {
border-bottom: 1px solid $white-dark;
@@ -210,23 +341,33 @@
margin-left: auto;
margin-right: auto;
}
+
+ .multi-file-commit-panel-collapse-btn {
+ margin-right: auto;
+ margin-left: auto;
+ border-left: 0;
+ }
}
}
-.multi-file-commit-panel-collapse-btn {
- padding-top: 0;
- padding-bottom: 0;
- margin-left: auto;
- font-size: 20px;
+.multi-file-commit-panel-header-title {
+ display: flex;
+ flex: 1;
+ padding: $gl-btn-padding;
- &.is-collapsed {
- margin-right: auto;
+ svg {
+ margin-right: $gl-btn-padding;
}
}
+.multi-file-commit-panel-collapse-btn {
+ border-left: 1px solid $white-dark;
+}
+
.multi-file-commit-list {
flex: 1;
- overflow: scroll;
+ overflow: auto;
+ padding: $gl-padding;
}
.multi-file-commit-list-item {
@@ -257,7 +398,7 @@
}
.multi-file-commit-form {
- padding-top: 12px;
+ padding: $gl-padding;
border-top: 1px solid $white-dark;
}
@@ -308,3 +449,40 @@
}
}
}
+
+.ide-loading {
+ display: flex;
+ height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
+.ide-empty-state {
+ display: flex;
+ height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
+.repo-new-btn {
+ .dropdown-toggle svg {
+ margin-top: -2px;
+ margin-bottom: 2px;
+ }
+
+ .dropdown-menu {
+ left: auto;
+ right: 0;
+
+ label {
+ font-weight: $gl-font-weight-normal;
+ padding: 5px 8px;
+ margin-bottom: 0;
+ }
+ }
+}
+
+.ide-flash-container.flash-container {
+ margin-top: $header-height;
+ margin-bottom: 0;
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 49c8e546bf2..c9363188505 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -108,13 +108,6 @@ input[type="checkbox"]:hover {
// Custom dropdown positioning
.dropdown-menu {
- transition-property: opacity, transform;
- transition-duration: 250ms, 250ms;
- transition-delay: 0ms, 25ms;
- transition-timing-function: $dropdown-animation-timing;
- transform: translateY(0);
- opacity: 0;
- display: block;
left: -5px;
}
@@ -152,13 +145,6 @@ input[type="checkbox"]:hover {
background-color: $nav-badge-bg;
border-color: $border-color;
}
-
- .dropdown-menu {
- transition-duration: 100ms, 75ms;
- transition-delay: 75ms, 100ms;
- transform: translateY(7px);
- opacity: 1;
- }
}
&.has-value {
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index cede147d559..8e2c42c1bd3 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -10,7 +10,6 @@
}
.axis {
- fill: $stat-graph-axis-fill;
font-size: 10px;
}
@@ -54,9 +53,7 @@
}
.selection rect {
- fill: $stat-graph-selection-fill;
fill-opacity: 0.1;
- stroke: $stat-graph-selection-stroke;
stroke-width: 1px;
stroke-opacity: 0.4;
shape-rendering: crispedges;
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index cde1e284d2d..86bade49ec9 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -8,12 +8,12 @@ class AutocompleteController < ApplicationController
def users
@users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute
- render json: @users, only: [:name, :username, :id], methods: [:avatar_url]
+ render json: UserSerializer.new.represent(@users)
end
def user
@user = User.find(params[:id])
- render json: @user, only: [:name, :username, :id], methods: [:avatar_url]
+ render json: UserSerializer.new.represent(@user)
end
def projects
diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb
index 2c9c095a5d7..a145049dc7d 100644
--- a/app/controllers/concerns/boards_responses.rb
+++ b/app/controllers/concerns/boards_responses.rb
@@ -24,11 +24,11 @@ module BoardsResponses
end
def respond_with_boards
- respond_with(@boards)
+ respond_with(@boards) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def respond_with_board
- respond_with(@board)
+ respond_with(@board) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def respond_with(resource)
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 782f0be9c4a..6f4fdcdaa4f 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -1,6 +1,8 @@
module CreatesCommit
extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
if can?(current_user, :push_code, @project)
@project_to_commit_into = @project
@@ -45,6 +47,7 @@ module CreatesCommit
end
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def authorize_edit_tree!
return if can_collaborate_with_project?
@@ -77,6 +80,7 @@ module CreatesCommit
end
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def new_merge_request_path
project_new_merge_request_path(
@project_to_commit_into,
@@ -88,20 +92,28 @@ module CreatesCommit
}
)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def existing_merge_request_path
- project_merge_request_path(@project, @merge_request)
+ project_merge_request_path(@project, @merge_request) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def merge_request_exists?
- return @merge_request if defined?(@merge_request)
-
- @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
- .find_by(source_project_id: @project_to_commit_into, source_branch: @branch_name, target_branch: @start_branch)
+ strong_memoize(:merge_request) do
+ MergeRequestsFinder.new(current_user, project_id: @project.id)
+ .execute
+ .opened
+ .find_by(
+ source_project_id: @project_to_commit_into,
+ source_branch: @branch_name,
+ target_branch: @start_branch)
+ end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def different_project?
- @project_to_commit_into != @project
+ @project_to_commit_into != @project # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def create_merge_request?
@@ -109,6 +121,6 @@ module CreatesCommit
# as the target branch in the same project,
# we don't want to create a merge request.
params[:create_merge_request].present? &&
- (different_project? || @start_branch != @branch_name)
+ (different_project? || @start_branch != @branch_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
index 9d4f97aa443..b10147835f3 100644
--- a/app/controllers/concerns/group_tree.rb
+++ b/app/controllers/concerns/group_tree.rb
@@ -1,4 +1,5 @@
module GroupTree
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def render_group_tree(groups)
@groups = if params[:filter].present?
Gitlab::GroupHierarchy.new(groups.search(params[:filter]))
@@ -20,5 +21,6 @@ module GroupTree
render json: serializer.represent(@groups)
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 281756af57a..74a4f437dc8 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -17,7 +17,7 @@ module IssuableActions
end
def update
- @issuable = update_service.execute(issuable)
+ @issuable = update_service.execute(issuable) # rubocop:disable Gitlab/ModuleWithInstanceVariables
respond_to do |format|
format.html do
@@ -55,7 +55,6 @@ module IssuableActions
def destroy
Issuable::DestroyService.new(issuable.project, current_user).execute(issuable)
- TodoService.new.destroy_issuable(issuable, current_user)
name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted."
@@ -81,7 +80,7 @@ module IssuableActions
private
def recaptcha_check_if_spammable(should_redirect = true, &block)
- return yield unless @issuable.is_a? Spammable
+ return yield unless issuable.is_a? Spammable
recaptcha_check_with_fallback(should_redirect, &block)
end
@@ -89,7 +88,7 @@ module IssuableActions
def render_conflict_response
respond_to do |format|
format.html do
- @conflict = true
+ @conflict = true # rubocop:disable Gitlab/ModuleWithInstanceVariables
render :edit
end
@@ -104,7 +103,7 @@ module IssuableActions
end
def labels
- @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+ @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def authorize_destroy_issuable!
@@ -114,7 +113,7 @@ module IssuableActions
end
def authorize_admin_issuable!
- unless can?(current_user, :"admin_#{resource_name}", @project)
+ unless can?(current_user, :"admin_#{resource_name}", @project) # rubocop:disable Gitlab/ModuleWithInstanceVariables
return access_denied!
end
end
@@ -148,6 +147,7 @@ module IssuableActions
@resource_name ||= controller_name.singularize
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def render_entity_json
if @issuable.valid?
render json: serializer.represent(@issuable)
@@ -155,6 +155,7 @@ module IssuableActions
render json: { errors: @issuable.errors.full_messages }, status: :unprocessable_entity
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def serializer
raise NotImplementedError
@@ -165,6 +166,6 @@ module IssuableActions
end
def parent
- @project || @group
+ @project || @group # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index f3c9251225f..b25e753a5ad 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -2,6 +2,7 @@ module IssuableCollections
extend ActiveSupport::Concern
include SortingHelper
include Gitlab::IssuableMetadata
+ include Gitlab::Utils::StrongMemoize
included do
helper_method :finder
@@ -9,6 +10,7 @@ module IssuableCollections
private
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def set_issuables_index
@issuables = issuables_collection
@issuables = @issuables.page(params[:page])
@@ -33,6 +35,7 @@ module IssuableCollections
@users.push(author) if author
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def issuables_collection
finder.execute.preload(preload_for_collection)
@@ -41,7 +44,7 @@ module IssuableCollections
def redirect_out_of_range(total_pages)
return false if total_pages.zero?
- out_of_range = @issuables.current_page > total_pages
+ out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
if out_of_range
redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
@@ -51,7 +54,7 @@ module IssuableCollections
end
def issuable_page_count
- page_count_for_relation(@issuables, finder.row_count)
+ page_count_for_relation(@issuables, finder.row_count) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def page_count_for_relation(relation, row_count)
@@ -66,6 +69,7 @@ module IssuableCollections
finder_class.new(current_user, filter_params)
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def filter_params
set_sort_order_from_cookie
set_default_state
@@ -90,6 +94,7 @@ module IssuableCollections
@filter_params.permit(IssuableFinder::VALID_PARAMS)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def set_default_state
params[:state] = 'opened' if params[:state].blank?
@@ -129,9 +134,9 @@ module IssuableCollections
end
def finder
- return @finder if defined?(@finder)
-
- @finder = issuable_finder_for(@finder_type)
+ strong_memoize(:finder) do
+ issuable_finder_for(@finder_type) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
end
def collection_type
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index ad594903331..d4cccbe6442 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -2,6 +2,7 @@ module IssuesAction
extend ActiveSupport::Concern
include IssuableCollections
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def issues
@finder_type = IssuesFinder
@label = finder.labels.first
@@ -17,4 +18,5 @@ module IssuesAction
format.atom { render layout: 'xml.atom' }
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index 8b569a01afd..4d44df3bba9 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -2,6 +2,7 @@ module MergeRequestsAction
extend ActiveSupport::Concern
include IssuableCollections
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def merge_requests
@finder_type = MergeRequestsFinder
@label = finder.labels.first
@@ -10,6 +11,7 @@ module MergeRequestsAction
@issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
private
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index 081f3336780..d92cf8b4894 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -6,7 +6,7 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_merge_requests_tab", {
- merge_requests: @milestone.sorted_merge_requests,
+ merge_requests: @milestone.sorted_merge_requests, # rubocop:disable Gitlab/ModuleWithInstanceVariables
show_project_name: true
})
end
@@ -18,7 +18,7 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_participants_tab", {
- users: @milestone.participants
+ users: @milestone.participants # rubocop:disable Gitlab/ModuleWithInstanceVariables
})
end
end
@@ -29,7 +29,7 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_labels_tab", {
- labels: @milestone.labels
+ labels: @milestone.labels # rubocop:disable Gitlab/ModuleWithInstanceVariables
})
end
end
@@ -43,6 +43,7 @@ module MilestoneActions
}
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def milestone_redirect_path
if @project
project_milestone_path(@project, @milestone)
@@ -52,4 +53,5 @@ module MilestoneActions
dashboard_milestone_path(@milestone.safe_title, title: @milestone.title)
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index be2e1b47feb..e82a5650935 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -1,5 +1,6 @@
module NotesActions
include RendersNotes
+ include Gitlab::Utils::StrongMemoize
extend ActiveSupport::Concern
included do
@@ -30,6 +31,7 @@ module NotesActions
render json: notes_json
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def create
create_params = note_params.merge(
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
@@ -47,7 +49,9 @@ module NotesActions
format.html { redirect_back_or_default }
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
@@ -60,6 +64,7 @@ module NotesActions
format.html { redirect_back_or_default }
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def destroy
if note.editable?
@@ -138,7 +143,7 @@ module NotesActions
end
else
template = "discussions/_diff_discussion"
- @fresh_discussion = true
+ @fresh_discussion = true # rubocop:disable Gitlab/ModuleWithInstanceVariables
locals = { discussions: [discussion], on_image: on_image }
end
@@ -191,7 +196,7 @@ module NotesActions
end
def noteable
- @noteable ||= notes_finder.target || @note&.noteable
+ @noteable ||= notes_finder.target || @note&.noteable # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def require_noteable!
@@ -211,20 +216,21 @@ module NotesActions
end
def note_project
- return @note_project if defined?(@note_project)
- return nil unless project
+ strong_memoize(:note_project) do
+ return nil unless project
- note_project_id = params[:note_project_id]
+ note_project_id = params[:note_project_id]
- @note_project =
- if note_project_id.present?
- Project.find(note_project_id)
- else
- project
- end
+ the_project =
+ if note_project_id.present?
+ Project.find(note_project_id)
+ else
+ project
+ end
- return access_denied! unless can?(current_user, :create_note, @note_project)
+ return access_denied! unless can?(current_user, :create_note, the_project)
- @note_project
+ the_project
+ end
end
end
diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb
index 9849aa93fa6..f0a68f23566 100644
--- a/app/controllers/concerns/oauth_applications.rb
+++ b/app/controllers/concerns/oauth_applications.rb
@@ -14,6 +14,6 @@ module OauthApplications
end
def load_scopes
- @scopes = Doorkeeper.configuration.scopes
+ @scopes ||= Doorkeeper.configuration.scopes
end
end
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index e9b9e9b38bc..90bb7a87b45 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -1,6 +1,7 @@
module PreviewMarkdown
extend ActiveSupport::Concern
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def preview_markdown
result = PreviewMarkdownService.new(@project, current_user, params).execute
@@ -20,4 +21,5 @@ module PreviewMarkdown
}
}
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb
index bb2c1dfa00a..fb41dc1e8a8 100644
--- a/app/controllers/concerns/renders_commits.rb
+++ b/app/controllers/concerns/renders_commits.rb
@@ -1,6 +1,6 @@
module RendersCommits
def prepare_commits_for_rendering(commits)
- Banzai::CommitRenderer.render(commits, @project, current_user)
+ Banzai::CommitRenderer.render(commits, @project, current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
commits
end
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
index 824ad06465c..e7ef297879f 100644
--- a/app/controllers/concerns/renders_notes.rb
+++ b/app/controllers/concerns/renders_notes.rb
@@ -1,4 +1,5 @@
module RendersNotes
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def prepare_notes_for_rendering(notes, noteable = nil)
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
@@ -7,6 +8,7 @@ module RendersNotes
notes
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
private
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index be2e6c7f193..3d61458c064 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -66,7 +66,7 @@ module ServiceParams
FILTER_BLANK_PARAMS = [:password].freeze
def service_params
- dynamic_params = @service.event_channel_names + @service.event_names
+ dynamic_params = @service.event_channel_names + @service.event_names # rubocop:disable Gitlab/ModuleWithInstanceVariables
service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + dynamic_params)
if service_params[:service].is_a?(Hash)
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index ffea712a833..9095cc7f783 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -4,6 +4,7 @@ module SnippetsActions
def edit
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def raw
disposition = params[:inline] == 'false' ? 'attachment' : 'inline'
@@ -14,6 +15,7 @@ module SnippetsActions
filename: @snippet.sanitized_file_name
)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
private
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index 03d8e188093..922aa58a00f 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -2,6 +2,7 @@ module SpammableActions
extend ActiveSupport::Concern
include Recaptcha::Verify
+ include Gitlab::Utils::StrongMemoize
included do
before_action :authorize_submit_spammable!, only: :mark_as_spam
@@ -18,9 +19,9 @@ module SpammableActions
private
def ensure_spam_config_loaded!
- return @spam_config_loaded if defined?(@spam_config_loaded)
-
- @spam_config_loaded = Gitlab::Recaptcha.load_configurations!
+ strong_memoize(:spam_config_loaded) do
+ Gitlab::Recaptcha.load_configurations!
+ end
end
def recaptcha_check_with_fallback(should_redirect = true, &fallback)
diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb
index 92cb534343e..776583579e8 100644
--- a/app/controllers/concerns/toggle_subscription_action.rb
+++ b/app/controllers/concerns/toggle_subscription_action.rb
@@ -12,7 +12,7 @@ module ToggleSubscriptionAction
private
def subscribable_project
- @project || raise(NotImplementedError)
+ @project ||= raise(NotImplementedError)
end
def subscribable_resource
diff --git a/app/controllers/concerns/with_performance_bar.rb b/app/controllers/concerns/with_performance_bar.rb
index ed253042701..230bbe4b1aa 100644
--- a/app/controllers/concerns/with_performance_bar.rb
+++ b/app/controllers/concerns/with_performance_bar.rb
@@ -6,6 +6,7 @@ module WithPerformanceBar
end
def peek_enabled?
+ return true if Rails.env.development?
return false unless Gitlab::PerformanceBar.enabled?(current_user)
if RequestStore.active?
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
new file mode 100644
index 00000000000..1ff25a45398
--- /dev/null
+++ b/app/controllers/ide_controller.rb
@@ -0,0 +1,6 @@
+class IdeController < ApplicationController
+ layout 'nav_only'
+
+ def index
+ end
+end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 770381472c5..d838b8dc29e 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -5,9 +5,6 @@ class Projects::BlobController < Projects::ApplicationController
include RendersBlob
include ActionView::Helpers::SanitizeHelper
- # Raised when given an invalid file path
- InvalidPathError = Class.new(StandardError)
-
prepend_before_action :authenticate_user!, only: [:edit]
before_action :require_non_empty_project, except: [:new, :create]
@@ -61,7 +58,6 @@ class Projects::BlobController < Projects::ApplicationController
create_commit(Files::UpdateService, success_path: -> { after_edit_path },
failure_view: :edit,
failure_path: project_blob_path(@project, @id))
-
rescue Files::UpdateService::FileChangedError
@conflict = true
render :edit
@@ -132,7 +128,6 @@ class Projects::BlobController < Projects::ApplicationController
def assign_blob_vars
@id = params[:id]
@ref, @path = extract_ref(@id)
-
rescue InvalidPathError
render_404
end
diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb
index c965a055fdd..66a851c52c7 100644
--- a/app/controllers/projects/clusters/gcp_controller.rb
+++ b/app/controllers/projects/clusters/gcp_controller.rb
@@ -65,6 +65,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
params.require(:cluster).permit(
:enabled,
:name,
+ :environment_scope,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
diff --git a/app/controllers/projects/clusters/user_controller.rb b/app/controllers/projects/clusters/user_controller.rb
index d7678512073..d0db64b2fa9 100644
--- a/app/controllers/projects/clusters/user_controller.rb
+++ b/app/controllers/projects/clusters/user_controller.rb
@@ -26,6 +26,7 @@ class Projects::Clusters::UserController < Projects::ApplicationController
params.require(:cluster).permit(
:enabled,
:name,
+ :environment_scope,
platform_kubernetes_attributes: [
:namespace,
:api_url,
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 4a7879db313..1dc7f1b3a7f 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -87,6 +87,7 @@ class Projects::ClustersController < Projects::ApplicationController
if cluster.managed?
params.require(:cluster).permit(
:enabled,
+ :environment_scope,
platform_kubernetes_attributes: [
:namespace
]
@@ -95,6 +96,7 @@ class Projects::ClustersController < Projects::ApplicationController
params.require(:cluster).permit(
:enabled,
:name,
+ :environment_scope,
platform_kubernetes_attributes: [
:api_url,
:token,
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 1c4c09c772f..4865ec3dfe5 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -110,7 +110,7 @@ class Projects::JobsController < Projects::ApplicationController
def erase
if @build.erase(erased_by: current_user)
redirect_to project_job_path(project, @build),
- notice: "Build has been successfully erased!"
+ notice: "Job has been successfully erased!"
else
respond_422
end
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 1511fc08c89..dc524b790a0 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -9,7 +9,10 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
before_action :build_merge_request, except: [:create]
def new
- define_new_vars
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/40934
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ define_new_vars
+ end
end
def create
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index e7b3b73024b..6b59c8461a3 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -131,7 +131,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
.new(project, current_user, wip_event: 'unwip')
.execute(@merge_request)
- render json: serializer.represent(@merge_request)
+ render json: serialize_widget(@merge_request)
end
def commit_change_content
@@ -147,7 +147,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
.new(@project, current_user)
.cancel(@merge_request)
- render json: serializer.represent(@merge_request)
+ render json: serialize_widget(@merge_request)
end
def merge
@@ -304,6 +304,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
end
+ def serialize_widget(merge_request)
+ serializer.represent(merge_request, serializer: 'widget')
+ end
+
def serializer
MergeRequestSerializer.new(current_user: current_user, project: merge_request.project)
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 627cb2bd93c..5940fae8dd0 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -11,7 +11,7 @@ class Projects::NotesController < Projects::ApplicationController
# Controller actions are returned from AbstractController::Base and methods of parent classes are
# excluded in order to return only specific controller related methods.
# That is ok for the app (no :create method in ancestors)
- # but fails for tests because there is a :create method on FactoryGirl (one of the ancestors)
+ # but fails for tests because there is a :create method on FactoryBot (one of the ancestors)
#
# see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78
#
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index ec7c645df5a..b478e7b5e05 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -1,9 +1,11 @@
class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :schedule, except: [:index, :new, :create]
+ before_action :play_rate_limit, only: [:play]
+ before_action :authorize_play_pipeline_schedule!, only: [:play]
before_action :authorize_read_pipeline_schedule!
before_action :authorize_create_pipeline_schedule!, only: [:new, :create]
- before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create]
+ before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create, :play]
before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
def index
@@ -40,6 +42,18 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
end
end
+ def play
+ job_id = RunPipelineScheduleWorker.perform_async(schedule.id, current_user.id)
+
+ if job_id
+ flash[:notice] = "Successfully scheduled a pipeline to run. Go to the <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details.".html_safe
+ else
+ flash[:alert] = 'Unable to schedule a pipeline to run immediately'
+ end
+
+ redirect_to pipeline_schedules_path(@project)
+ end
+
def take_ownership
if schedule.update(owner: current_user)
redirect_to pipeline_schedules_path(@project)
@@ -60,6 +74,17 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
private
+ def play_rate_limit
+ return unless current_user
+
+ limiter = ::Gitlab::ActionRateLimiter.new(action: :play_pipeline_schedule)
+
+ return unless limiter.throttled?([current_user, schedule], 1)
+
+ flash[:alert] = 'You cannot play this scheduled pipeline at the moment. Please wait a minute.'
+ redirect_to pipeline_schedules_path(@project)
+ end
+
def schedule
@schedule ||= project.pipeline_schedules.find(params[:id])
end
@@ -70,6 +95,10 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
variables_attributes: [:id, :key, :value, :_destroy] )
end
+ def authorize_play_pipeline_schedule!
+ return access_denied! unless can?(current_user, :play_pipeline_schedule, schedule)
+ end
+
def authorize_update_pipeline_schedule!
return access_denied! unless can?(current_user, :update_pipeline_schedule, schedule)
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 7ad7b3003af..e146d0d3cd5 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -29,6 +29,8 @@ class Projects::PipelinesController < Projects::ApplicationController
@pipelines_count = PipelinesFinder
.new(project).execute.count
+ @pipelines.map(&:commit) # List commits for batch loading
+
respond_to do |format|
format.html
format.json do
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index f3719059f88..f752a46f828 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -26,6 +26,7 @@ class Projects::TreeController < Projects::ApplicationController
respond_to do |format|
format.html do
+ lfs_blob_ids
@last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 8e9d6766d80..6f609348402 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -9,6 +9,7 @@ class ProjectsController < Projects::ApplicationController
before_action :repository, except: [:index, :new, :create]
before_action :assign_ref_vars, only: [:show], if: :repo_exists?
before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?]
+ before_action :lfs_blob_ids, only: [:show], if: [:repo_exists?, :project_view_files?]
before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
# Authorize
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 4754a67450f..d13407a06c8 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -306,7 +306,7 @@ module ApplicationHelper
cookies["sidebar_collapsed"] == "true"
end
- def show_new_repo?
+ def show_new_ide?
cookies["new_repo"] == "true" && body_data_page != 'projects:show'
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 556ed233ccf..3c2ee2cb5bc 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -8,7 +8,7 @@ module BlobHelper
%w(credits changelog news copying copyright license authors)
end
- def edit_path(project = @project, ref = @ref, path = @path, options = {})
+ def edit_blob_path(project = @project, ref = @ref, path = @path, options = {})
project_edit_blob_path(project,
tree_join(ref, path),
options[:link_opts])
@@ -26,10 +26,10 @@ module BlobHelper
button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
# This condition applies to anonymous or users who can edit directly
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
- link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
+ link_to 'Edit', edit_blob_path(project, ref, path, options), class: "#{common_classes} btn-sm"
elsif current_user && can?(current_user, :fork_project, project)
continue_params = {
- to: edit_path(project, ref, path, options),
+ to: edit_blob_path(project, ref, path, options),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
@@ -41,6 +41,43 @@ module BlobHelper
end
end
+ def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
+ "#{ide_path}/project#{edit_blob_path(project, ref, path, options)}"
+ end
+
+ def ide_edit_text
+ "#{_('Multi Edit')} <span class='label label-primary'>#{_('Beta')}</span>".html_safe
+ end
+
+ def ide_blob_link(project = @project, ref = @ref, path = @path, options = {})
+ return unless show_new_ide?
+
+ blob = options.delete(:blob)
+ blob ||= project.repository.blob_at(ref, path) rescue nil
+
+ return unless blob && blob.readable_text?
+
+ common_classes = "btn js-edit-ide #{options[:extra_class]}"
+
+ if !on_top_of_branch?(project, ref)
+ button_tag ide_edit_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' }
+ # This condition applies to anonymous or users who can edit directly
+ elsif current_user && can_modify_blob?(blob, project, ref)
+ link_to ide_edit_text, ide_edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
+ elsif current_user && can?(current_user, :fork_project, project)
+ continue_params = {
+ to: ide_edit_path(project, ref, path, options),
+ notice: edit_in_new_fork_notice,
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params)
+
+ button_tag ide_edit_text,
+ class: common_classes,
+ data: { fork_path: fork_path }
+ end
+ end
+
def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
return unless current_user
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
new file mode 100644
index 00000000000..7e4eb06b99d
--- /dev/null
+++ b/app/helpers/clusters_helper.rb
@@ -0,0 +1,5 @@
+module ClustersHelper
+ def has_multiple_clusters?(project)
+ false
+ end
+end
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index b5dece38de1..e26ce6da030 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -35,7 +35,7 @@ module FormHelper
multi_select: true,
'input-meta': 'name',
'always-show-selectbox': true,
- current_user_info: current_user.to_json(only: [:id, :name])
+ current_user_info: UserSerializer.new.represent(current_user)
}
}
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index a77aa0ad2cc..7f3c118c7ab 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -182,6 +182,11 @@ module GitlabRoutingHelper
edit_project_pipeline_schedule_path(project, schedule)
end
+ def play_pipeline_schedule_path(schedule, *args)
+ project = schedule.project
+ play_project_pipeline_schedule_path(project, schedule, *args)
+ end
+
def take_ownership_pipeline_schedule_path(schedule, *args)
project = schedule.project
take_ownership_project_pipeline_schedule_path(project, schedule, *args)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 4c60f4b0cd0..2668cf78afe 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -32,7 +32,7 @@ module IssuablesHelper
end
end
- def serialize_issuable(issuable)
+ def serialize_issuable(issuable, serializer: nil)
serializer_klass = case issuable
when Issue
IssueSerializer
@@ -42,7 +42,7 @@ module IssuablesHelper
serializer_klass
.new(current_user: current_user, project: issuable.project)
- .represent(issuable)
+ .represent(issuable, serializer: serializer)
.to_json
end
@@ -362,7 +362,7 @@ module IssuablesHelper
moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
editable: can_edit_issuable,
- currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url),
+ currentUser: UserSerializer.new.represent(current_user),
rootPath: root_path,
fullPath: @project.full_path
}
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 212cdbb8157..0f110bd25c5 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -74,7 +74,7 @@ module IssuesHelper
elsif item.try(:merged?)
'status-box-merged'
elsif item.closed?
- 'status-box-closed'
+ 'status-box-mr-closed'
elsif item.try(:upcoming?)
'status-box-upcoming'
else
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 2f57660516d..0f9ac958f95 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -139,7 +139,7 @@ module SearchHelper
id: "filtered-search-#{type}",
placeholder: 'Search or filter results...',
data: {
- 'username-params' => @users.to_json(only: [:id, :username])
+ 'username-params' => UserSerializer.new.represent(@users)
},
autocomplete: 'off'
}
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index b05eb93b465..36a311dfa8a 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -43,14 +43,20 @@ module SortingHelper
end
def groups_sort_options_hash
- options = {
+ {
+ sort_value_name => sort_title_name,
+ sort_value_name_desc => sort_title_name_desc,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated
}
+ end
- options
+ def admin_groups_sort_options_hash
+ groups_sort_options_hash.merge(
+ sort_value_largest_group => sort_title_largest_group
+ )
end
def member_sort_options_hash
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 77a82b895ce..50e17fe7717 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -5,7 +5,7 @@ module Emails
@commit = @note.noteable
@target_url = project_commit_url(*note_target_url_options)
- mail_answer_thread(@commit, note_thread_options(recipient_id))
+ mail_answer_note_thread(@commit, @note, note_thread_options(recipient_id))
end
def note_issue_email(recipient_id, note_id)
@@ -13,7 +13,7 @@ module Emails
@issue = @note.noteable
@target_url = project_issue_url(*note_target_url_options)
- mail_answer_thread(@issue, note_thread_options(recipient_id))
+ mail_answer_note_thread(@issue, @note, note_thread_options(recipient_id))
end
def note_merge_request_email(recipient_id, note_id)
@@ -21,7 +21,7 @@ module Emails
@merge_request = @note.noteable
@target_url = project_merge_request_url(*note_target_url_options)
- mail_answer_thread(@merge_request, note_thread_options(recipient_id))
+ mail_answer_note_thread(@merge_request, @note, note_thread_options(recipient_id))
end
def note_snippet_email(recipient_id, note_id)
@@ -29,7 +29,7 @@ module Emails
@snippet = @note.noteable
@target_url = project_snippet_url(*note_target_url_options)
- mail_answer_thread(@snippet, note_thread_options(recipient_id))
+ mail_answer_note_thread(@snippet, @note, note_thread_options(recipient_id))
end
def note_personal_snippet_email(recipient_id, note_id)
@@ -37,7 +37,7 @@ module Emails
@snippet = @note.noteable
@target_url = snippet_url(@note.noteable)
- mail_answer_thread(@snippet, note_thread_options(recipient_id))
+ mail_answer_note_thread(@snippet, @note, note_thread_options(recipient_id))
end
private
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 9efabe3f44e..ec886e993c3 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -119,8 +119,8 @@ class Notify < BaseMailer
headers['Reply-To'] = address
fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
- headers['References'] ||= ''
- headers['References'] << ' ' << fallback_reply_message_id
+ headers['References'] ||= []
+ headers['References'] << fallback_reply_message_id
@reply_by_email = true
end
@@ -156,6 +156,18 @@ class Notify < BaseMailer
mail_thread(model, headers)
end
+ def mail_answer_note_thread(model, note, headers = {})
+ headers['Message-ID'] = message_id(note)
+ headers['In-Reply-To'] = message_id(note.references.last)
+ headers['References'] = note.references.map { |ref| message_id(ref) }
+
+ headers['X-GitLab-Discussion-ID'] = note.discussion.id if note.part_of_discussion?
+
+ headers[:subject]&.prepend('Re: ')
+
+ mail_thread(model, headers)
+ end
+
def reply_key
@reply_key ||= SentNotification.reply_key
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 29e762724e3..19ad110db58 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -77,9 +77,15 @@ class Blob < SimpleDelegator
end
def self.lazy(project, commit_id, path)
- BatchLoader.for(commit_id: commit_id, path: path).batch do |items, loader|
- project.repository.blobs_at(items.map(&:values)).each do |blob|
- loader.call({ commit_id: blob.commit_id, path: blob.path }, blob) if blob
+ BatchLoader.for({ project: project, commit_id: commit_id, path: path }).batch do |items, loader|
+ items_by_project = items.group_by { |i| i[:project] }
+
+ items_by_project.each do |project, items|
+ items = items.map { |i| i.values_at(:commit_id, :path) }
+
+ project.repository.blobs_at(items).each do |blob|
+ loader.call({ project: blob.project, commit_id: blob.commit_id, path: blob.path }, blob) if blob
+ end
end
end
end
diff --git a/app/models/blob_viewer/dependency_manager.rb b/app/models/blob_viewer/dependency_manager.rb
index a8d9be945dc..cc4950240af 100644
--- a/app/models/blob_viewer/dependency_manager.rb
+++ b/app/models/blob_viewer/dependency_manager.rb
@@ -27,10 +27,17 @@ module BlobViewer
private
- def package_name_from_json(key)
- prepare!
+ def json_data
+ @json_data ||= begin
+ prepare!
+ JSON.parse(blob.data)
+ rescue
+ {}
+ end
+ end
- JSON.parse(blob.data)[key] rescue nil
+ def package_name_from_json(key)
+ json_data[key]
end
def package_name_from_method_call(name)
diff --git a/app/models/blob_viewer/package_json.rb b/app/models/blob_viewer/package_json.rb
index 09221efb56c..46cd2f04f4d 100644
--- a/app/models/blob_viewer/package_json.rb
+++ b/app/models/blob_viewer/package_json.rb
@@ -16,7 +16,25 @@ module BlobViewer
@package_name ||= package_name_from_json('name')
end
+ def package_type
+ private? ? 'private package' : super
+ end
+
def package_url
+ private? ? homepage : npm_url
+ end
+
+ private
+
+ def private?
+ !!json_data['private']
+ end
+
+ def homepage
+ json_data['homepage']
+ end
+
+ def npm_url
"https://www.npmjs.com/package/#{package_name}"
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 85960f1b6bb..83fe23606d1 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -491,7 +491,6 @@ module Ci
end
def valid_dependency?
- return false unless complete?
return false if artifacts_expired?
return false if erased?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index eebbf7c4218..d4690da3be6 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -228,6 +228,10 @@ module Ci
statuses.select(:stage).distinct.count
end
+ def total_size
+ statuses.count(:id)
+ end
+
def stages_names
statuses.order(:stage_idx).distinct
.pluck(:stage, :stage_idx).map(&:first)
@@ -283,8 +287,12 @@ module Ci
Ci::Pipeline.truncate_sha(sha)
end
+ # NOTE: This is loaded lazily and will never be nil, even if the commit
+ # cannot be found.
+ #
+ # Use constructs like: `pipeline.commit.present?`
def commit
- @commit ||= project.commit_by(oid: sha)
+ @commit ||= Commit.lazy(project, sha)
end
def branch?
@@ -334,12 +342,9 @@ module Ci
end
def latest?
- return false unless ref
-
- commit = project.commit(ref)
- return false unless commit
+ return false unless ref && commit.present?
- commit.sha == sha
+ project.commit(ref) == commit
end
def retried
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 307e4fcedfe..2be07ca7d3c 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -52,6 +52,20 @@ class Commit
diffs.reduce(0) { |sum, d| sum + Gitlab::Git::Util.count_lines(d.diff) }
end
+ def order_by(collection:, order_by:, sort:)
+ return collection unless %w[email name commits].include?(order_by)
+ return collection unless %w[asc desc].include?(sort)
+
+ collection.sort do |a, b|
+ operands = [a, b].tap { |o| o.reverse! if sort == 'desc' }
+
+ attr1, attr2 = operands.first.public_send(order_by), operands.second.public_send(order_by) # rubocop:disable PublicSend
+
+ # use case insensitive comparison for string values
+ order_by.in?(%w[email name]) ? attr1.casecmp(attr2) : attr1 <=> attr2
+ end
+ end
+
# Truncate sha to 8 characters
def truncate_sha(sha)
sha[0..MIN_SHA_LENGTH]
@@ -72,6 +86,20 @@ class Commit
def valid_hash?(key)
!!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key)
end
+
+ def lazy(project, oid)
+ BatchLoader.for({ project: project, oid: oid }).batch do |items, loader|
+ items_by_project = items.group_by { |i| i[:project] }
+
+ items_by_project.each do |project, commit_ids|
+ oids = commit_ids.map { |i| i[:oid] }
+
+ project.repository.commits_by(oids: oids).each do |commit|
+ loader.call({ project: commit.project, oid: commit.id }, commit) if commit
+ end
+ end
+ end
+ end
end
attr_accessor :raw
@@ -89,7 +117,7 @@ class Commit
end
def ==(other)
- (self.class === other) && (raw == other.raw)
+ other.is_a?(self.class) && raw == other.raw
end
def self.reference_prefix
@@ -210,8 +238,8 @@ class Commit
notes.includes(:author)
end
- def method_missing(m, *args, &block)
- @raw.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @raw.__send__(method, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def respond_to_missing?(method, include_private = false)
diff --git a/app/models/concerns/blocks_json_serialization.rb b/app/models/concerns/blocks_json_serialization.rb
new file mode 100644
index 00000000000..8019e6adc1c
--- /dev/null
+++ b/app/models/concerns/blocks_json_serialization.rb
@@ -0,0 +1,16 @@
+# Overrides `as_json` and `to_json` to raise an exception when called in order
+# to prevent accidentally exposing attributes
+#
+# Not that that would ever happen... but just in case.
+module BlocksJsonSerialization
+ extend ActiveSupport::Concern
+
+ JsonSerializationError = Class.new(StandardError)
+
+ def to_json(*)
+ raise JsonSerializationError,
+ "JSON serialization has been disabled on #{self.class.name}"
+ end
+
+ alias_method :as_json, :to_json
+end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index b43eaeaeea0..c013e5a708f 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -44,13 +44,11 @@ module Mentionable
end
def all_references(current_user = nil, extractor: nil)
- @extractors ||= {}
-
# Use custom extractor if it's passed in the function parameters.
if extractor
- @extractors[current_user] = extractor
+ extractors[current_user] = extractor
else
- extractor = @extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user)
+ extractor = extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user)
extractor.reset_memoized_values
end
@@ -69,6 +67,10 @@ module Mentionable
extractor
end
+ def extractors
+ @extractors ||= {}
+ end
+
def mentioned_users(current_user = nil)
all_references(current_user).users
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 7026f565706..fd6703831e4 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -103,9 +103,11 @@ module Milestoneish
end
def memoize_per_user(user, method_name)
- @memoized ||= {}
- @memoized[method_name] ||= {}
- @memoized[method_name][user&.id] ||= yield
+ memoized_users[method_name][user&.id] ||= yield
+ end
+
+ def memoized_users
+ @memoized_users ||= Hash.new { |h, k| h[k] = {} }
end
# override in a class that includes this module to get a faster query
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 5d75b2aa6a3..86f28f30032 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -46,6 +46,7 @@ module Noteable
notes.inc_relations_for_view.grouped_diff_discussions(*args)
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def resolvable_discussions
@resolvable_discussions ||=
if defined?(@discussions)
@@ -54,6 +55,7 @@ module Noteable
discussion_notes.resolvable.discussions(self)
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def discussions_resolvable?
resolvable_discussions.any?(&:resolvable?)
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index ce69fd34ac5..e48bc0be410 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -56,15 +56,17 @@ module Participable
#
# Returns an Array of User instances.
def participants(current_user = nil)
- @participants ||= Hash.new do |hash, user|
- hash[user] = raw_participants(user)
- end
-
- @participants[current_user]
+ all_participants[current_user]
end
private
+ def all_participants
+ @all_participants ||= Hash.new do |hash, user|
+ hash[user] = raw_participants(user)
+ end
+ end
+
def raw_participants(current_user = nil)
current_user ||= author
ext = Gitlab::ReferenceExtractor.new(project, current_user)
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index e961c97e337..835f26aa57b 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -52,7 +52,7 @@ module RelativePositioning
# to its predecessor. This process will recursively move all the predecessors until we have a place
if (after.relative_position - before.relative_position) < 2
before.move_before
- @positionable_neighbours = [before]
+ @positionable_neighbours = [before] # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
self.relative_position = position_between(before.relative_position, after.relative_position)
@@ -65,7 +65,7 @@ module RelativePositioning
if before.shift_after?
issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_after)
issue_to_move.move_after
- @positionable_neighbours = [issue_to_move]
+ @positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables
pos_after = issue_to_move.relative_position
end
@@ -80,7 +80,7 @@ module RelativePositioning
if after.shift_before?
issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_before)
issue_to_move.move_before
- @positionable_neighbours = [issue_to_move]
+ @positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables
pos_before = issue_to_move.relative_position
end
@@ -132,6 +132,7 @@ module RelativePositioning
end
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def save_positionable_neighbours
return unless @positionable_neighbours
@@ -140,4 +141,5 @@ module RelativePositioning
status
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index f006a271327..b6c7b6735b9 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -31,15 +31,11 @@ module ResolvableDiscussion
end
def resolvable?
- return @resolvable if @resolvable.present?
-
- @resolvable = potentially_resolvable? && notes.any?(&:resolvable?)
+ @resolvable ||= potentially_resolvable? && notes.any?(&:resolvable?)
end
def resolved?
- return @resolved if @resolved.present?
-
- @resolved = resolvable? && notes.none?(&:to_be_resolved?)
+ @resolved ||= resolvable? && notes.none?(&:to_be_resolved?)
end
def first_note
@@ -49,13 +45,13 @@ module ResolvableDiscussion
def first_note_to_resolve
return unless resolvable?
- @first_note_to_resolve ||= notes.find(&:to_be_resolved?)
+ @first_note_to_resolve ||= notes.find(&:to_be_resolved?) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def last_resolved_note
return unless resolved?
- @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last
+ @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def resolved_notes
@@ -95,7 +91,7 @@ module ResolvableDiscussion
yield(notes_relation)
# Set the notes array to the updated notes
- @notes = notes_relation.fresh.to_a
+ @notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
self.class.memoized_values.each do |var|
instance_variable_set(:"@#{var}", nil)
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 22fde2eb134..5c1cce98ad4 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -88,7 +88,7 @@ module Routable
def full_name
if route && route.name.present?
- @full_name ||= route.name
+ @full_name ||= route.name # rubocop:disable Gitlab/ModuleWithInstanceVariables
else
update_route if persisted?
@@ -112,7 +112,7 @@ module Routable
def expires_full_path_cache
RequestStore.delete(full_path_key) if RequestStore.active?
- @full_path = nil
+ @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def build_full_path
@@ -127,7 +127,7 @@ module Routable
def uncached_full_path
if route && route.path.present?
- @full_path ||= route.path
+ @full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables
else
update_route if persisted?
@@ -166,7 +166,7 @@ module Routable
route || build_route(source: self)
route.path = build_full_path
route.name = build_full_name
- @full_path = nil
- @full_name = nil
+ @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @full_name = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 731d9b9a745..5e4274619c4 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -12,6 +12,7 @@ module Spammable
attr_accessor :spam
attr_accessor :spam_log
+ alias_method :spam?, :spam
after_validation :check_for_spam, on: [:create, :update]
@@ -34,10 +35,6 @@ module Spammable
end
end
- def spam?
- @spam
- end
-
def check_for_spam
error_msg = if Gitlab::Recaptcha.enabled?
"Your #{spammable_entity_type} has been recognized as spam. "\
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index 25e2d8ea24e..d07041c2fdf 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -39,7 +39,7 @@ module Taskable
def task_list_items
return [] if description.blank?
- @task_list_items ||= Taskable.get_tasks(description)
+ @task_list_items ||= Taskable.get_tasks(description) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def tasks
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 9f403d96ed5..5911b56c34c 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -21,9 +21,10 @@ module TimeTrackable
has_many :timelogs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def spend_time(options)
@time_spent = options[:duration]
- @time_spent_user = options[:user]
+ @time_spent_user = User.find(options[:user_id])
@spent_at = options[:spent_at]
@original_total_time_spent = nil
@@ -36,6 +37,7 @@ module TimeTrackable
end
end
alias_method :spend_time=, :spend_time
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def total_time_spent
timelogs.sum(:time_spent)
@@ -52,9 +54,10 @@ module TimeTrackable
private
def reset_spent_time
- timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user)
+ timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def add_or_subtract_spent_time
timelogs.new(
time_spent: time_spent,
@@ -62,16 +65,19 @@ module TimeTrackable
spent_at: @spent_at
)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def check_negative_time_spent
return if time_spent.nil? || time_spent == :reset
- # we need to cache the total time spent so multiple calls to #valid?
- # doesn't give a false error
- @original_total_time_spent ||= total_time_spent
-
- if time_spent < 0 && (time_spent.abs > @original_total_time_spent)
+ if time_spent < 0 && (time_spent.abs > original_total_time_spent)
errors.add(:time_spent, 'Time to subtract exceeds the total time spent')
end
end
+
+ # we need to cache the total time spent so multiple calls to #valid?
+ # doesn't give a false error
+ def original_total_time_spent
+ @original_total_time_spent ||= total_time_spent
+ end
end
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index 4a65738214b..d67b16584a4 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -22,12 +22,9 @@ class DiffDiscussion < Discussion
def merge_request_version_params
return unless for_merge_request?
- return {} if active?
- if on_merge_request_commit?
- { commit_id: commit_id }
- else
- noteable.version_params_for(position.diff_refs)
+ version_params.tap do |params|
+ params[:commit_id] = commit_id if on_merge_request_commit?
end
end
@@ -37,4 +34,12 @@ class DiffDiscussion < Discussion
position: position.to_json
)
end
+
+ private
+
+ def version_params
+ return {} if active?
+
+ noteable.version_params_for(position.diff_refs)
+ end
end
diff --git a/app/models/identity.rb b/app/models/identity.rb
index ff811e19f8a..b3fa7d8176a 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -8,20 +8,30 @@ class Identity < ActiveRecord::Base
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false }
validates :user_id, uniqueness: { scope: :provider }
+ before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
+
scope :with_provider, ->(provider) { where(provider: provider) }
scope :with_extern_uid, ->(provider, extern_uid) do
iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider)
end
def ldap?
- provider.starts_with?('ldap')
+ Gitlab::OAuth::Provider.ldap_provider?(provider)
end
def self.normalize_uid(provider, uid)
- if provider.to_s.starts_with?('ldap')
+ if Gitlab::OAuth::Provider.ldap_provider?(provider)
Gitlab::LDAP::Person.normalize_dn(uid)
else
uid.to_s
end
end
+
+ private
+
+ def ensure_normalized_extern_uid
+ return if extern_uid.nil?
+
+ self.extern_uid = Identity.normalize_uid(self.provider, self.extern_uid)
+ end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 26a3388602a..c39789b047d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -53,8 +53,8 @@ class MergeRequest < ActiveRecord::Base
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
after_create :ensure_merge_request_diff, unless: :importing?
- after_update :reload_diff_if_branch_changed
after_update :clear_memoized_shas
+ after_update :reload_diff_if_branch_changed
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -85,6 +85,14 @@ class MergeRequest < ActiveRecord::Base
transition locked: :opened
end
+ before_transition any => :opened do |merge_request|
+ merge_request.merge_jid = nil
+
+ merge_request.run_after_commit do
+ UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
+ end
+ end
+
state :opened
state :closed
state :merged
@@ -879,11 +887,11 @@ class MergeRequest < ActiveRecord::Base
def state_icon_name
if merged?
- "check"
+ "git-merge"
elsif closed?
- "times"
+ "close"
else
- "circle-o"
+ "issue-open-m"
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 02f9fd61e49..184fbd5f5ae 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -360,6 +360,16 @@ class Note < ActiveRecord::Base
end
end
+ def references
+ refs = [noteable]
+
+ if part_of_discussion?
+ refs += discussion.notes.take_while { |n| n.id < id }
+ end
+
+ refs
+ end
+
def expire_etag_cache
return unless noteable&.discussions_rendered_on_frontend?
diff --git a/app/models/project.rb b/app/models/project.rb
index 5183a216c53..3440c01b356 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1148,7 +1148,7 @@ class Project < ActiveRecord::Base
def change_head(branch)
if repository.branch_exists?(branch)
repository.before_change_head
- repository.write_ref('HEAD', "refs/heads/#{branch}", force: true)
+ repository.write_ref('HEAD', "refs/heads/#{branch}")
repository.copy_gitattributes(branch)
repository.after_change_head
reload_default_branch
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 1c065e1ddbd..2be35b6ea9d 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -46,6 +46,8 @@ class JiraService < IssueTrackerService
context_path: url.path,
auth_type: :basic,
read_timeout: 120,
+ use_cookies: true,
+ additional_cookies: ['OBBasicAuth=fromDialog'],
use_ssl: url.scheme == 'https'
}
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index c0e31eca8da..a34f5e5439b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -19,7 +19,6 @@ class Repository
attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
- delegate :write_ref, to: :raw_repository
CreateTreeError = Class.new(StandardError)
@@ -118,6 +117,18 @@ class Repository
@commit_cache[oid] = find_commit(oid)
end
+ def commits_by(oids:)
+ return [] unless oids.present?
+
+ commits = Gitlab::Git::Commit.batch_by_oid(raw_repository, oids)
+
+ if commits.present?
+ Commit.decorate(commits, @project)
+ else
+ []
+ end
+ end
+
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
options = {
repo: raw_repository,
@@ -221,6 +232,12 @@ class Repository
branch_names.include?(branch_name)
end
+ def tag_exists?(tag_name)
+ return false unless raw_repository
+
+ tag_names.include?(tag_name)
+ end
+
def ref_exists?(ref)
!!raw_repository&.ref_exists?(ref)
rescue ArgumentError
@@ -238,10 +255,11 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes)
begin
- write_ref(keep_around_ref_name(sha), sha, force: true)
- rescue Gitlab::Git::Repository::GitError => ex
- # Necessary because https://gitlab.com/gitlab-org/gitlab-ce/issues/20156
- return true if ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
+ write_ref(keep_around_ref_name(sha), sha)
+ rescue Rugged::ReferenceError => ex
+ Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
+ rescue Rugged::OSError => ex
+ raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
end
@@ -251,6 +269,10 @@ class Repository
ref_exists?(keep_around_ref_name(sha))
end
+ def write_ref(ref_path, sha)
+ rugged.references.create(ref_path, sha, force: true)
+ end
+
def diverging_commit_counts(branch)
root_ref_hash = raw_repository.commit(root_ref).id
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
@@ -686,7 +708,9 @@ class Repository
def tags_sorted_by(value)
case value
- when 'name'
+ when 'name_asc'
+ VersionSorter.sort(tags) { |tag| tag.name }
+ when 'name_desc'
VersionSorter.rsort(tags) { |tag| tag.name }
when 'updated_desc'
tags_sorted_by_committed_date.reverse
@@ -697,10 +721,14 @@ class Repository
end
end
- def contributors
+ # Params:
+ #
+ # order_by: name|email|commits
+ # sort: asc|desc default: 'asc'
+ def contributors(order_by: nil, sort: 'asc')
commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true)
- commits.group_by(&:author_email).map do |email, commits|
+ commits = commits.group_by(&:author_email).map do |email, commits|
contributor = Gitlab::Contributor.new
contributor.email = email
@@ -714,6 +742,7 @@ class Repository
contributor
end
+ Commit.order_by(collection: commits, order_by: order_by, sort: sort)
end
def refs_contains_sha(ref_type, sha)
@@ -927,7 +956,7 @@ class Repository
def merge_base(first_commit_id, second_commit_id)
first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
second_commit_id = commit(second_commit_id).try(:id) || second_commit_id
- rugged.merge_base(first_commit_id, second_commit_id)
+ raw_repository.merge_base(first_commit_id, second_commit_id)
rescue Rugged::ReferenceError
nil
end
@@ -990,7 +1019,7 @@ class Repository
end
def create_ref(ref, ref_path)
- write_ref(ref_path, ref)
+ raw_repository.write_ref(ref_path, ref)
end
def ls_files(ref)
diff --git a/app/models/user.rb b/app/models/user.rb
index 92b461ce3ed..b52f17cd6a8 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -18,6 +18,7 @@ class User < ActiveRecord::Base
include CreatedAtFilterable
include IgnorableColumn
include BulkMemberAccessLoad
+ include BlocksJsonSerialization
DEFAULT_NOTIFICATION_LEVEL = :participating
@@ -738,7 +739,7 @@ class User < ActiveRecord::Base
def ldap_user?
if identities.loaded?
- identities.find { |identity| identity.provider.start_with?('ldap') && !identity.extern_uid.nil? }
+ identities.find { |identity| Gitlab::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
else
identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
end
diff --git a/app/models/user_synced_attributes_metadata.rb b/app/models/user_synced_attributes_metadata.rb
index 9f374304164..548b99b69d9 100644
--- a/app/models/user_synced_attributes_metadata.rb
+++ b/app/models/user_synced_attributes_metadata.rb
@@ -6,11 +6,11 @@ class UserSyncedAttributesMetadata < ActiveRecord::Base
SYNCABLE_ATTRIBUTES = %i[name email location].freeze
def read_only?(attribute)
- Gitlab.config.omniauth.sync_profile_from_provider && synced?(attribute)
+ sync_profile_from_provider? && synced?(attribute)
end
def read_only_attributes
- return [] unless Gitlab.config.omniauth.sync_profile_from_provider
+ return [] unless sync_profile_from_provider?
SYNCABLE_ATTRIBUTES.select { |key| synced?(key) }
end
@@ -22,4 +22,10 @@ class UserSyncedAttributesMetadata < ActiveRecord::Base
def set_attribute_synced(attribute, value)
write_attribute("#{attribute}_synced", value)
end
+
+ private
+
+ def sync_profile_from_provider?
+ Gitlab::OAuth::Provider.sync_profile_from_provider?(provider)
+ end
end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index 4e689a9efd5..6363c382ff8 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -2,16 +2,18 @@ module Ci
class PipelinePolicy < BasePolicy
delegate { @subject.project }
- condition(:protected_ref) do
- access = ::Gitlab::UserAccess.new(@user, project: @subject.project)
+ condition(:protected_ref) { ref_protected?(@user, @subject.project, @subject.tag?, @subject.ref) }
- if @subject.tag?
- !access.can_create_tag?(@subject.ref)
+ rule { protected_ref }.prevent :update_pipeline
+
+ def ref_protected?(user, project, tag, ref)
+ access = ::Gitlab::UserAccess.new(user, project: project)
+
+ if tag
+ !access.can_create_tag?(ref)
else
- !access.can_update_branch?(@subject.ref)
+ !access.can_update_branch?(ref)
end
end
-
- rule { protected_ref }.prevent :update_pipeline
end
end
diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb
index 6b7598e1821..abcf536b2f7 100644
--- a/app/policies/ci/pipeline_schedule_policy.rb
+++ b/app/policies/ci/pipeline_schedule_policy.rb
@@ -2,13 +2,23 @@ module Ci
class PipelineSchedulePolicy < PipelinePolicy
alias_method :pipeline_schedule, :subject
+ condition(:protected_ref) do
+ ref_protected?(@user, @subject.project, @subject.project.repository.tag_exists?(@subject.ref), @subject.ref)
+ end
+
condition(:owner_of_schedule) do
can?(:developer_access) && pipeline_schedule.owned_by?(@user)
end
+ rule { can?(:developer_access) }.policy do
+ enable :play_pipeline_schedule
+ end
+
rule { can?(:master_access) | owner_of_schedule }.policy do
enable :update_pipeline_schedule
enable :admin_pipeline_schedule
end
+
+ rule { protected_ref }.prevent :play_pipeline_schedule
end
end
diff --git a/app/serializers/concerns/with_pagination.rb b/app/serializers/concerns/with_pagination.rb
index d29e22d6740..89631b73fcf 100644
--- a/app/serializers/concerns/with_pagination.rb
+++ b/app/serializers/concerns/with_pagination.rb
@@ -14,7 +14,7 @@ module WithPagination
# we shouldn't try to paginate single resources
def represent(resource, opts = {})
if paginated? && resource.respond_to?(:page)
- super(@paginator.paginate(resource), opts)
+ super(paginator.paginate(resource), opts)
else
super(resource, opts)
end
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
index 3b5a4fd4f79..6f31fbd6b7c 100644
--- a/app/serializers/issuable_entity.rb
+++ b/app/serializers/issuable_entity.rb
@@ -3,14 +3,6 @@ class IssuableEntity < Grape::Entity
expose :id
expose :iid
- expose :author_id
expose :description
- expose :lock_version
- expose :milestone_id
expose :title
- expose :updated_by_id
- expose :created_at
- expose :updated_at
- expose :milestone, using: API::Entities::Milestone
- expose :labels, using: LabelEntity
end
diff --git a/app/serializers/issuable_sidebar_entity.rb b/app/serializers/issuable_sidebar_entity.rb
index ff23d8bf0c7..29138c803df 100644
--- a/app/serializers/issuable_sidebar_entity.rb
+++ b/app/serializers/issuable_sidebar_entity.rb
@@ -1,4 +1,5 @@
class IssuableSidebarEntity < Grape::Entity
+ include TimeTrackableEntity
include RequestAwareEntity
expose :participants, using: ::API::Entities::UserBasic do |issuable|
@@ -8,9 +9,4 @@ class IssuableSidebarEntity < Grape::Entity
expose :subscribed do |issuable|
issuable.subscribed?(request.current_user, issuable.project)
end
-
- expose :time_estimate
- expose :total_time_spent
- expose :human_time_estimate
- expose :human_total_time_spent
end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 9d52b8d9752..0bdd4d7a272 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -2,7 +2,15 @@ class IssueEntity < IssuableEntity
include TimeTrackableEntity
expose :state
+ expose :milestone_id
+ expose :updated_by_id
+ expose :created_at
+ expose :updated_at
expose :deleted_at
+ expose :milestone, using: API::Entities::Milestone
+ expose :labels, using: LabelEntity
+ expose :lock_version
+ expose :author_id
expose :confidential
expose :discussion_locked
expose :assignees, using: API::Entities::UserBasic
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index e9d98d8baca..caf193bdae3 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -1,14 +1,14 @@
class MergeRequestSerializer < BaseSerializer
# This overrided method takes care of which entity should be used
- # to serialize the `merge_request` based on `basic` key in `opts` param.
+ # to serialize the `merge_request` based on `serializer` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
def represent(merge_request, opts = {})
entity =
case opts[:serializer]
when 'basic', 'sidebar'
MergeRequestBasicEntity
- else
- MergeRequestEntity
+ else # It's 'widget'
+ MergeRequestWidgetEntity
end
super(merge_request, opts, entity)
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_widget_entity.rb
index eece9445dca..f8e59b2ffd7 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -1,8 +1,5 @@
-class MergeRequestEntity < IssuableEntity
- include TimeTrackableEntity
-
+class MergeRequestWidgetEntity < IssuableEntity
expose :state
- expose :deleted_at
expose :in_progress_merge_commit_sha
expose :merge_commit_sha
expose :merge_error
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 85db2760e23..c8b112132b3 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -81,7 +81,7 @@ module Ci
end
def related_merge_requests
- MergeRequest.where(source_project: pipeline.project, source_branch: pipeline.ref)
+ MergeRequest.opened.where(source_project: pipeline.project, source_branch: pipeline.ref)
end
end
end
diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb
index 7d45b4aa26a..26eb274f4d5 100644
--- a/app/services/concerns/issues/resolve_discussions.rb
+++ b/app/services/concerns/issues/resolve_discussions.rb
@@ -1,24 +1,28 @@
module Issues
module ResolveDiscussions
+ include Gitlab::Utils::StrongMemoize
+
attr_reader :merge_request_to_resolve_discussions_of_iid, :discussion_to_resolve_id
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def filter_resolve_discussion_params
@merge_request_to_resolve_discussions_of_iid ||= params.delete(:merge_request_to_resolve_discussions_of)
@discussion_to_resolve_id ||= params.delete(:discussion_to_resolve)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
def merge_request_to_resolve_discussions_of
- return @merge_request_to_resolve_discussions_of if defined?(@merge_request_to_resolve_discussions_of)
-
- @merge_request_to_resolve_discussions_of = MergeRequestsFinder.new(current_user, project_id: project.id)
- .execute
- .find_by(iid: merge_request_to_resolve_discussions_of_iid)
+ 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
def discussions_to_resolve
return [] unless merge_request_to_resolve_discussions_of
- @discussions_to_resolve ||=
+ @discussions_to_resolve ||= # rubocop:disable Gitlab/ModuleWithInstanceVariables
if discussion_to_resolve_id
discussion_or_nil = merge_request_to_resolve_discussions_of
.find_discussion(discussion_to_resolve_id)
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 38231f66009..8d4b9f14780 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -1,11 +1,14 @@
module Files
class BaseService < Commits::CreateService
+ FileChangedError = Class.new(StandardError)
+
def initialize(*args)
super
@author_email = params[:author_email]
@author_name = params[:author_name]
@commit_message = params[:commit_message]
+ @last_commit_sha = params[:last_commit_sha]
@file_path = params[:file_path]
@previous_path = params[:previous_path]
@@ -13,5 +16,16 @@ module Files
@file_content = params[:file_content]
@file_content = Base64.decode64(@file_content) if params[:file_content_encoding] == 'base64'
end
+
+ def file_has_changed?(path, commit_id)
+ return false unless commit_id
+
+ last_commit = Gitlab::Git::Commit
+ .last_for_path(@start_project.repository, @start_branch, path)
+
+ return false unless last_commit
+
+ last_commit.sha != commit_id
+ end
end
end
diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb
index 7952e5c95d4..32a57484d4e 100644
--- a/app/services/files/delete_service.rb
+++ b/app/services/files/delete_service.rb
@@ -11,5 +11,15 @@ module Files
start_project: @start_project,
start_branch_name: @start_branch)
end
+
+ private
+
+ def validate!
+ super
+
+ if file_has_changed?(@file_path, @last_commit_sha)
+ raise FileChangedError, "You are attempting to delete a file that has been previously updated."
+ end
+ end
end
end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index bfacc462847..98a3e83c130 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -1,5 +1,7 @@
module Files
class MultiService < Files::BaseService
+ UPDATE_FILE_ACTIONS = %w(update move delete).freeze
+
def create_commit!
repository.multi_action(
user: current_user,
@@ -20,6 +22,7 @@ module Files
params[:actions].each do |action|
validate_action!(action)
+ validate_file_status!(action)
end
end
@@ -28,5 +31,15 @@ module Files
raise_error("Unknown action '#{action[:action]}'")
end
end
+
+ def validate_file_status!(action)
+ return unless UPDATE_FILE_ACTIONS.include?(action[:action])
+
+ file_path = action[:previous_path] || action[:file_path]
+
+ if file_has_changed?(file_path, action[:last_commit_id])
+ raise_error("The file has changed since you started editing it: #{file_path}")
+ end
+ end
end
end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index bcca1386bed..1902d1cea72 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -1,13 +1,5 @@
module Files
class UpdateService < Files::BaseService
- FileChangedError = Class.new(StandardError)
-
- def initialize(*args)
- super
-
- @last_commit_sha = params[:last_commit_sha]
- end
-
def create_commit!
repository.update_file(current_user, @file_path, @file_content,
message: @commit_message,
@@ -21,21 +13,10 @@ module Files
private
- def file_has_changed?
- return false unless @last_commit_sha && last_commit
-
- @last_commit_sha != last_commit.sha
- end
-
- def last_commit
- @last_commit ||= Gitlab::Git::Commit
- .last_for_path(@start_project.repository, @start_branch, @file_path)
- end
-
def validate!
super
- if file_has_changed?
+ if file_has_changed?(@file_path, @last_commit_sha)
raise FileChangedError, "You are attempting to update a file that has changed since you started editing it."
end
end
diff --git a/app/services/issuable/destroy_service.rb b/app/services/issuable/destroy_service.rb
index 0610b401213..7197a426a72 100644
--- a/app/services/issuable/destroy_service.rb
+++ b/app/services/issuable/destroy_service.rb
@@ -1,8 +1,10 @@
module Issuable
class DestroyService < IssuableBaseService
def execute(issuable)
- if issuable.destroy
- issuable.update_project_counter_caches
+ TodoService.new.destroy_target(issuable) do |issuable|
+ if issuable.destroy
+ issuable.update_project_counter_caches
+ end
end
end
end
diff --git a/app/services/notes/destroy_service.rb b/app/services/notes/destroy_service.rb
index b819bd17039..fb78420d324 100644
--- a/app/services/notes/destroy_service.rb
+++ b/app/services/notes/destroy_service.rb
@@ -1,7 +1,9 @@
module Notes
class DestroyService < BaseService
def execute(note)
- note.destroy
+ TodoService.new.destroy_target(note) do |note|
+ note.destroy
+ end
end
end
end
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index c499f384426..842fe4e09c4 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -5,7 +5,7 @@ module Projects
if fork_source = @project.fork_source
fork_source.lfs_objects.find_each do |lfs_object|
- lfs_object.projects << @project
+ lfs_object.projects << @project unless lfs_object.projects.include?(@project)
end
refresh_forks_count(fork_source)
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 06ac86cd5a9..669c1ba0a22 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -405,7 +405,7 @@ module QuickActions
if time_spent
@updates[:spend_time] = {
duration: time_spent,
- user: current_user,
+ user_id: current_user.id,
spent_at: time_spent_date
}
end
@@ -428,7 +428,7 @@ module QuickActions
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :remove_time_spent do
- @updates[:spend_time] = { duration: :reset, user: current_user }
+ @updates[:spend_time] = { duration: :reset, user_id: current_user.id }
end
desc "Append the comment with #{SHRUG}"
diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb
index 11030bee8f1..d4ade869777 100644
--- a/app/services/spam_check_service.rb
+++ b/app/services/spam_check_service.rb
@@ -7,16 +7,19 @@
# - params with :request
#
module SpamCheckService
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def filter_spam_check_params
@request = params.delete(:request)
@api = params.delete(:api)
@recaptcha_verified = params.delete(:recaptcha_verified)
@spam_log_id = params.delete(:spam_log_id)
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
# In order to be proceed to the spam check process, @spammable has to be
# a dirty instance, which means it should be already assigned with the new
# attribute values.
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def spam_check(spammable, user)
spam_service = SpamService.new(spammable, @request)
@@ -24,4 +27,5 @@ module SpamCheckService
user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true)
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 575853fd66b..c2ca404b179 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -31,12 +31,20 @@ class TodoService
mark_pending_todos_as_done(issue, current_user)
end
- # When we destroy an issuable we should:
+ # When we destroy a todo target we should:
#
- # * refresh the todos count cache for the current user
+ # * refresh the todos count cache for all users with todos on the target
#
- def destroy_issuable(issuable, user)
- user.update_todos_count_cache
+ # This needs to yield back to the caller to destroy the target, because it
+ # collects the todo users before the todos themselves are deleted, then
+ # updates the todo counts for those users.
+ #
+ def destroy_target(target)
+ todo_users = User.where(id: target.todos.pending.select(:user_id)).to_a
+
+ yield target
+
+ todo_users.each(&:update_todos_count_cache)
end
# When we reassign an issue we should:
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 535251fef5e..25946ba6eaf 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -11,23 +11,7 @@
.search-field-holder
= search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name'
= icon("search", class: "search-icon")
- .dropdown
- - toggle_text = @sort.present? ? sort_options_hash[@sort] : sort_title_recently_created
- = dropdown_toggle(toggle_text, { toggle: 'dropdown' })
- %ul.dropdown-menu.dropdown-menu-align-right
- %li.dropdown-header
- Sort by
- %li
- = link_to admin_groups_path(sort: sort_value_recently_created, name: project_name) do
- = sort_title_recently_created
- = link_to admin_groups_path(sort: sort_value_oldest_created, name: project_name) do
- = sort_title_oldest_created
- = link_to admin_groups_path(sort: sort_value_recently_updated, name: project_name) do
- = sort_title_recently_updated
- = link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do
- = sort_title_oldest_updated
- = link_to admin_groups_path(sort: sort_value_largest_group, name: project_name) do
- = sort_title_largest_group
+ = render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
= link_to new_admin_group_path, class: "btn btn-new" do
New group
%ul.content-list
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
index 6bf979a937e..23f9927cfee 100644
--- a/app/views/admin/system_info/show.html.haml
+++ b/app/views/admin/system_info/show.html.haml
@@ -15,7 +15,7 @@
Unable to collect CPU info
.col-sm-4
.light-well
- %h4 Memory
+ %h4 Memory Usage
.data
- if @memory
%h1 #{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}
@@ -24,7 +24,7 @@
Unable to collect memory info
.col-sm-4
.light-well
- %h4 Disks
+ %h4 Disk Usage
.data
- @disks.each do |disk|
%h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}
@@ -34,4 +34,4 @@
.light-well
%h4 Uptime
.data
- %h1= time_ago_with_tooltip(Rails.application.config.booted_at)
+ %h1= distance_of_time_in_words_to_now(Rails.application.config.booted_at)
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 98ff592eb64..63c5a15de1c 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -157,7 +157,6 @@
%ul
%li User will not be able to login
%li User will not be able to access git repositories
- %li User will be removed from joined projects and groups
%li Personal projects will be left
%li Owned groups will be left
%br
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 2bac69bc536..6e399fc7392 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -10,5 +10,7 @@
%p.settings-message.text-center.append-bottom-0
No variables found, add one with the form above.
- else
- = render "ci/variables/table"
- %button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values
+ .js-secret-variable-table
+ = render "ci/variables/table"
+ %button.btn.btn-info.js-secret-value-reveal-button{ data: { secret_reveal_status: 'false' } }
+ = n_('Reveal value', 'Reveal values', @variables.size)
diff --git a/app/views/ci/variables/_table.html.haml b/app/views/ci/variables/_table.html.haml
index 71a0b56c4f4..2298930d0c7 100644
--- a/app/views/ci/variables/_table.html.haml
+++ b/app/views/ci/variables/_table.html.haml
@@ -15,7 +15,11 @@
- if variable.id?
%tr
%td.variable-key= variable.key
- %td.variable-value{ "data-value" => variable.value }******
+ %td.variable-value
+ %span.js-secret-value-placeholder
+ = '*' * 6
+ %span.hide.js-secret-value
+ = variable.value
%td.variable-protected= Gitlab::Utils.boolean_to_yes_no(variable.protected)
%td.variable-menu
= link_to variable.edit_path, class: "btn btn-transparent btn-variable-edit" do
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 9a763887b30..f85f5c5be88 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -7,7 +7,8 @@
%span.pushed #{event.action_name} #{event.ref_type}
%strong
- commits_link = project_commits_path(project, event.ref_name)
- = link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link, class: 'ref-name'
+ - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name)
+ = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name'
= render "events/event_scope", event: event
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 021de4f0caf..b8692009225 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -1,3 +1,5 @@
+= webpack_bundle_tag 'docs'
+
%div
- if current_application_settings.help_page_text.present?
= markdown_field(current_application_settings, :help_page_text)
@@ -37,8 +39,12 @@
Quick help
%ul.well-list
%li= link_to 'See our website for getting help', support_url
- %li= link_to 'Use the search bar on the top of this page', '#', onclick: 'Shortcuts.focusSearch(event)'
- %li= link_to 'Use shortcuts', '#', onclick: 'Shortcuts.toggleHelp()'
+ %li
+ %button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' }
+ Use the search bar on the top of this page
+ %li
+ %button.btn-blank.btn-link.js-trigger-shortcut{ type: 'button' }
+ Use shortcuts
- unless current_application_settings.help_page_hide_commercial_content?
%li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/'
%li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare'
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
new file mode 100644
index 00000000000..8368e7a4563
--- /dev/null
+++ b/app/views/ide/index.html.haml
@@ -0,0 +1,12 @@
+- page_title 'IDE'
+
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'ide'
+
+.ide-flash-container.flash-container
+
+#ide.ide-loading
+ .text-center
+ = icon('spinner spin 2x')
+ %h2.clgray= _('IDE Loading ...')
diff --git a/app/views/layouts/nav_only.html.haml b/app/views/layouts/nav_only.html.haml
new file mode 100644
index 00000000000..6fa4b39dc10
--- /dev/null
+++ b/app/views/layouts/nav_only.html.haml
@@ -0,0 +1,13 @@
+!!! 5
+%html{ lang: I18n.locale, class: page_class }
+ = render "layouts/head"
+ %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page } }
+ = render 'peek/bar'
+ = render "layouts/header/default"
+ = render 'shared/outdated_browser'
+ .mobile-overlay
+ .alert-wrapper
+ = render "layouts/broadcast"
+ = yield :flash_message
+ = render "layouts/flash"
+ = yield
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 574a8f2fa50..bae37292d62 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -109,7 +109,7 @@
API
%tr
%td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" }
- - job_count = @pipeline.statuses.latest.size
+ - job_count = @pipeline.total_size
- stage_count = @pipeline.stages_count
successfully completed
#{job_count} #{'job'.pluralize(job_count)}
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index ddced2279e1..39622cf7f02 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -22,11 +22,11 @@ Committed by: <%= commit.committer_name %>
<% end -%>
<% end -%>
-<% build_count = @pipeline.statuses.latest.size -%>
+<% job_count = @pipeline.total_size -%>
<% stage_count = @pipeline.stages_count -%>
<% if @pipeline.user -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
-successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
+successfully completed <%= job_count %> <%= 'job'.pluralize(job_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 3a7a99462a6..79530e78154 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -7,7 +7,7 @@
.nav-block
= render 'projects/tree/tree_header', tree: @tree
- - if !show_new_repo? && commit
+ - if commit
= render 'shared/commit_well', commit: commit, ref: ref, project: project
= render 'projects/tree/tree_content', tree: @tree, content_url: content_url
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index c5e3a7945bd..8212ab9a31e 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -17,7 +17,7 @@
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview
- %li.md-header-toolbar
+ %li.md-header-toolbar.active
= markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" })
= markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" })
= markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index 1dd8778f800..f6e5712ce81 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -8,7 +8,7 @@
%br
%span.descr
Pipelines need to be configured to enable this feature.
- = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')
+ = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank'
.checkbox
= form.label :only_allow_merge_if_all_discussions_are_resolved do
= form.check_box :only_allow_merge_if_all_discussions_are_resolved
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 281363d2e01..2a77dedd9a2 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -12,6 +12,7 @@
.btn-group{ role: "group" }<
= edit_blob_link
+ = ide_blob_link
- if current_user
= replace_blob_link
= delete_blob_link
diff --git a/app/views/projects/blob/_header_content.html.haml b/app/views/projects/blob/_header_content.html.haml
index 98bedae650a..5d457a50c49 100644
--- a/app/views/projects/blob/_header_content.html.haml
+++ b/app/views/projects/blob/_header_content.html.haml
@@ -8,3 +8,6 @@
%small
= number_to_human_size(blob.raw_size)
+
+ - if blob.stored_externally? && blob.external_storage == :lfs
+ %span.label.label-lfs.append-right-5 LFS
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index c4712bf3736..4d358052d43 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -6,21 +6,14 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'blob'
- - if show_new_repo?
- = webpack_bundle_tag 'common_vue'
- = webpack_bundle_tag 'repo'
-
= render 'projects/last_push'
%div{ class: container_class }
- - if show_new_repo?
- = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_blob_path(@project, @id)
- - else
- #tree-holder.tree-holder
- = render 'blob', blob: @blob
+ #tree-holder.tree-holder
+ = render 'blob', blob: @blob
- if can_modify_blob?(@blob)
= render 'projects/blob/remove'
- - title = "Replace #{@blob.name}"
- = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put
+ - title = "Replace #{@blob.name}"
+ = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put
diff --git a/app/views/projects/blob/viewers/_dependency_manager.html.haml b/app/views/projects/blob/viewers/_dependency_manager.html.haml
index a0f0215a5ff..87aa7c1dbf8 100644
--- a/app/views/projects/blob/viewers/_dependency_manager.html.haml
+++ b/app/views/projects/blob/viewers/_dependency_manager.html.haml
@@ -6,6 +6,6 @@
- if viewer.package_name
and defines a #{viewer.package_type} named
%strong<
- = link_to viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer'
+ = link_to_if viewer.package_url.present?, viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer'
= link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/projects/clusters/_cluster.html.haml
index 18ca01d2d49..ad696daa259 100644
--- a/app/views/projects/clusters/_cluster.html.haml
+++ b/app/views/projects/clusters/_cluster.html.haml
@@ -16,7 +16,8 @@
class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !cluster.can_toggle_cluster?,
- data: { "enabled-text": s_("ClusterIntegration|Active"),
- "disabled-text": s_("ClusterIntegration|Inactive"),
- endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
+ data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
= icon("spinner spin", class: "loading-icon")
+ %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')
diff --git a/app/views/projects/clusters/_enabled.html.haml b/app/views/projects/clusters/_enabled.html.haml
index 70c677f7856..547b3c8446f 100644
--- a/app/views/projects/clusters/_enabled.html.haml
+++ b/app/views/projects/clusters/_enabled.html.haml
@@ -7,8 +7,10 @@
%button{ type: 'button',
class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
- disabled: !can?(current_user, :update_cluster, @cluster),
- data: { "enabled-text": s_("ClusterIntegration|Active"), "disabled-text": s_("ClusterIntegration|Inactive"), } }
+ disabled: !can?(current_user, :update_cluster, @cluster) }
+ %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')
- if can?(current_user, :update_cluster, @cluster)
.form-group
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index 0f6bae97571..e384b60d8d9 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -7,6 +7,9 @@
.form-group
= field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|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')
= field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
.form-group
diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml
index 3fa9f69708a..bde85aed341 100644
--- a/app/views/projects/clusters/gcp/_show.html.haml
+++ b/app/views/projects/clusters/gcp/_show.html.haml
@@ -8,6 +8,11 @@
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
+
+ .form-group
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope')
+ = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), 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')
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
index 4a9bd5186c6..babfca0c567 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -3,6 +3,9 @@
.form-group
= field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|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')
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml
index 5931e0b7f17..89595bca007 100644
--- a/app/views/projects/clusters/user/_show.html.haml
+++ b/app/views/projects/clusters/user/_show.html.haml
@@ -4,6 +4,10 @@
= field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+ .form-group
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope')
+ = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), 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')
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 618a6355d23..d66066a6d0b 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -38,8 +38,8 @@
.commiter
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
- - commit_timeago = time_ago_with_tooltip(commit.committed_date, placement: 'bottom')
- - commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
+ - commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom')
+ - commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
#{ commit_text.html_safe }
.commit-actions.flex-row.hidden-xs
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index d260aaee2d3..1f28d8acff6 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -14,12 +14,12 @@
.detail-page-header
.detail-page-header-body
- .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) }
- = icon('check', class: "hidden-sm hidden-md hidden-lg")
+ .issuable-status-box.status-box.status-box-issue-closed{ class: issue_button_visibility(@issue, false) }
+ = sprite_icon('mobile-issue-close', size: 16, css_class: 'hidden-sm hidden-md hidden-lg')
%span.hidden-xs
Closed
.issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) }
- = icon('circle-o', class: "hidden-sm hidden-md hidden-lg")
+ = sprite_icon('issue-open-m', size: 16, css_class: 'hidden-sm hidden-md hidden-lg')
%span.hidden-xs Open
.issuable-meta
@@ -39,8 +39,6 @@
= icon('caret-down')
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- - if can_update_issue
- %li= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'js-issuable-edit'
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
@@ -52,9 +50,6 @@
%li.divider
%li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
- - if can_update_issue
- = link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped js-issuable-edit'
-
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
- if can_report_spam
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index bc91758110e..22c8b6b513d 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -7,7 +7,7 @@
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
- = icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg")
+ = sprite_icon(@merge_request.state_icon_name, size: 16, css_class: 'hidden-sm hidden-md hidden-lg')
%span.hidden-xs
= @merge_request.state_human_name
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index abff702fd9d..8740c6895df 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -20,7 +20,7 @@
-# haml-lint:disable InlineJavaScript
:javascript
window.gl = window.gl || {};
- window.gl.mrWidgetData = #{serialize_issuable(@merge_request)}
+ window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget')}
#js-vue-mr-widget.mr-widget
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index bd8c38292d6..f8c4005a9e0 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -26,10 +26,12 @@
= pipeline_schedule.owner&.name
%td
.pull-right.btn-group
+ - if can?(current_user, :play_pipeline_schedule, pipeline_schedule)
+ = link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn' do
+ = icon('play')
- if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
= link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do
= s_('PipelineSchedules|Take ownership')
- - if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
= link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn' do
= icon('pencil')
- if can?(current_user, :admin_pipeline_schedule, pipeline_schedule)
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index f5149306734..85946aec1f2 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,6 +1,6 @@
#js-pipeline-header-vue.pipeline-header-container
-- if @commit
+- if @commit.present?
.commit-box
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line)
@@ -8,28 +8,28 @@
%pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line))
-.info-well
- - if @commit.status
- .well-segment.pipeline-info
- .icon-container
- = icon('clock-o')
- = pluralize @pipeline.statuses.count(:id), "job"
- - if @pipeline.ref
- from
- = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
- - if @pipeline.duration
- in
- = time_interval_in_words(@pipeline.duration)
- - if @pipeline.queued_duration
- = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
+ .info-well
+ - if @commit.status
+ .well-segment.pipeline-info
+ .icon-container
+ = icon('clock-o')
+ = pluralize @pipeline.total_size, "job"
+ - if @pipeline.ref
+ from
+ = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
+ - if @pipeline.duration
+ in
+ = time_interval_in_words(@pipeline.duration)
+ - if @pipeline.queued_duration
+ = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
- .well-segment.branch-info
- .icon-container.commit-icon
- = custom_icon("icon_commit")
- = link_to @commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha js-details-short"
- = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
- %span.text-expander
- \...
- %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")
+ .well-segment.branch-info
+ .icon-container.commit-icon
+ = custom_icon("icon_commit")
+ = link_to @commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha js-details-short"
+ = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
+ %span.text-expander
+ \...
+ %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 ad61f033a1c..398a1c46746 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -8,7 +8,7 @@
%li.js-builds-tab-link
= link_to builds_project_pipeline_path(@project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
Jobs
- %span.badge.js-builds-counter= pipeline.statuses.count
+ %span.badge.js-builds-counter= pipeline.total_size
- if failed_builds.present?
%li.js-failures-tab-link
= link_to failures_project_pipeline_path(@project, @pipeline), data: {target: 'div#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index c63e716180c..c5f9f5aa15b 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -40,10 +40,14 @@
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
%hr
- .form-group.append-bottom-default
+ .form-group.append-bottom-default.js-secret-runner-token
= f.label :runners_token, "Runner token", class: 'label-light'
- = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
+ .form-control.js-secret-value-placeholder
+ = '*' * 20
+ = f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89'
%p.help-block 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
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index c51af901699..8c1c532cb3e 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -1,9 +1,12 @@
+- is_lfs_blob = @lfs_blob_ids.include?(blob_item.id)
%tr{ class: "tree-item #{tree_hex_class(blob_item)}" }
%td.tree-item-file-name
= tree_icon(type, blob_item.mode, blob_item.name)
- file_name = blob_item.name
= link_to project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)), class: 'str-truncated', title: file_name do
%span= file_name
+ - if is_lfs_blob
+ %span.label.label-lfs.prepend-left-5 LFS
%td.hidden-xs.tree-commit
%td.tree-time-ago.cgray.text-right
= render 'projects/tree/spinner'
diff --git a/app/views/projects/tree/_old_tree_content.html.haml b/app/views/projects/tree/_old_tree_content.html.haml
deleted file mode 100644
index 6ea78851b8d..00000000000
--- a/app/views/projects/tree/_old_tree_content.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-.tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
- .table-holder
- %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
- %thead
- %tr
- %th= s_('ProjectFileTree|Name')
- %th.hidden-xs
- .pull-left= _('Last commit')
- %th.text-right= _('Last update')
- - if @path.present?
- %tr.tree-item
- %td.tree-item-file-name
- = link_to "..", project_tree_path(@project, up_dir_path), class: 'prepend-left-10'
- %td
- %td.hidden-xs
-
- = render_tree(tree)
-
- - if tree.readme
- = render "projects/tree/readme", readme: tree.readme
-
-- if can_edit_tree?
- = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
- = render 'projects/blob/new_dir'
diff --git a/app/views/projects/tree/_old_tree_header.html.haml b/app/views/projects/tree/_old_tree_header.html.haml
deleted file mode 100644
index 7f636b7e0e8..00000000000
--- a/app/views/projects/tree/_old_tree_header.html.haml
+++ /dev/null
@@ -1,64 +0,0 @@
-- if on_top_of_branch?
- - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
-- else
- - addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
-
-%ul.breadcrumb.repo-breadcrumb
- %li
- = link_to project_tree_path(@project, @ref) do
- = @project.path
- - path_breadcrumbs do |title, path|
- %li
- = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
-
- - if current_user
- %li
- %a.btn.add-to-tree{ addtotree_toggle_attributes }
- = sprite_icon('plus', size: 16, css_class: 'pull-left')
- = sprite_icon('arrow-down', size: 16, css_class: 'pull-left')
- - if on_top_of_branch?
- .add-to-tree-dropdown
- %ul.dropdown-menu
- - if can_edit_tree?
- %li
- = link_to project_new_blob_path(@project, @id) do
- #{ _('New file') }
- %li
- = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
- #{ _('Upload file') }
- %li
- = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
- #{ _('New directory') }
- - elsif can?(current_user, :fork_project, @project)
- %li
- - continue_params = { to: project_new_blob_path(@project, @id),
- notice: edit_in_new_fork_notice,
- notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
- continue: continue_params)
- = link_to fork_path, method: :post do
- #{ _('New file') }
- %li
- - continue_params = { to: request.fullpath,
- notice: edit_in_new_fork_notice + " Try to upload a file again.",
- notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
- continue: continue_params)
- = link_to fork_path, method: :post do
- #{ _('Upload file') }
- %li
- - continue_params = { to: request.fullpath,
- notice: edit_in_new_fork_notice + " Try to create a new directory again.",
- notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
- continue: continue_params)
- = link_to fork_path, method: :post do
- #{ _('New directory') }
-
- %li.divider
- %li
- = link_to new_project_branch_path(@project) do
- #{ _('New branch') }
- %li
- = link_to new_project_tag_path(@project) do
- #{ _('New tag') }
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index a4bdd67209d..6ea78851b8d 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,5 +1,24 @@
-- content_url = local_assigns.fetch(:content_url, nil)
-- if show_new_repo?
- = render 'shared/repo/repo', project: @project, content_url: content_url
-- else
- = render 'projects/tree/old_tree_content', tree: tree
+.tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
+ .table-holder
+ %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
+ %thead
+ %tr
+ %th= s_('ProjectFileTree|Name')
+ %th.hidden-xs
+ .pull-left= _('Last commit')
+ %th.text-right= _('Last update')
+ - if @path.present?
+ %tr.tree-item
+ %td.tree-item-file-name
+ = link_to "..", project_tree_path(@project, up_dir_path), class: 'prepend-left-10'
+ %td
+ %td.hidden-xs
+
+ = render_tree(tree)
+
+ - if tree.readme
+ = render "projects/tree/readme", readme: tree.readme
+
+- if can_edit_tree?
+ = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
+ = render 'projects/blob/new_dir'
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index c02f7ee37ed..d1ecef39475 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -2,16 +2,78 @@
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- - if show_new_repo? && can_push_branch?(@project, @ref)
- .js-new-dropdown
- - else
- = render 'projects/tree/old_tree_header'
+ - if on_top_of_branch?
+ - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
+ - else
+ - addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
+
+ %ul.breadcrumb.repo-breadcrumb
+ %li
+ = link_to project_tree_path(@project, @ref) do
+ = @project.path
+ - path_breadcrumbs do |title, path|
+ %li
+ = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
+
+ - if current_user
+ %li
+ %a.btn.add-to-tree{ addtotree_toggle_attributes }
+ = sprite_icon('plus', size: 16, css_class: 'pull-left')
+ = sprite_icon('arrow-down', size: 16, css_class: 'pull-left')
+ - if on_top_of_branch?
+ .add-to-tree-dropdown
+ %ul.dropdown-menu
+ - if can_edit_tree?
+ %li
+ = link_to project_new_blob_path(@project, @id) do
+ #{ _('New file') }
+ %li
+ = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
+ #{ _('Upload file') }
+ %li
+ = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
+ #{ _('New directory') }
+ - elsif can?(current_user, :fork_project, @project)
+ %li
+ - continue_params = { to: project_new_blob_path(@project, @id),
+ notice: edit_in_new_fork_notice,
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ #{ _('New file') }
+ %li
+ - continue_params = { to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to upload a file again.",
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ #{ _('Upload file') }
+ %li
+ - continue_params = { to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to create a new directory again.",
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ #{ _('New directory') }
+
+ %li.divider
+ %li
+ = link_to new_project_branch_path(@project) do
+ #{ _('New branch') }
+ %li
+ = link_to new_project_tag_path(@project) do
+ #{ _('New tag') }
.tree-controls
- - if show_new_repo?
- .editable-mode
- - else
- = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
+ - if show_new_ide?
+ = succeed " " do
+ = link_to ide_edit_path(@project, @id), class: 'btn btn-default' do
+ = ide_edit_text
+
+ = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
= render 'projects/find_file_link'
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 64cc70053ef..3b4057e56d0 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -6,11 +6,6 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
-- if show_new_repo?
- - content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
- = webpack_bundle_tag 'repo'
-
-%div{ class: [(container_class unless show_new_repo?), ("limit-container-width" unless fluid_layout)] }
+%div{ class: [(container_class), ("limit-container-width" unless fluid_layout)] }
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index f4a4bfaec54..479bd2cdb38 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,6 +1,6 @@
- show_create = local_assigns.fetch(:show_create, false)
-- show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project)
+- show_new_branch_form = show_new_ide? && show_create && can?(current_user, :push_code, @project)
- dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= hidden_field_tag :destination, destination
diff --git a/app/views/shared/_show_aside.html.haml b/app/views/shared/_show_aside.html.haml
deleted file mode 100644
index 3ac9b11b4fa..00000000000
--- a/app/views/shared/_show_aside.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-= link_to '#aside', class: 'show-aside' do
- %i.fa.fa-angle-left
diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml
index b3f73e96b81..8e5e32e9f16 100644
--- a/app/views/shared/boards/components/_sidebar.html.haml
+++ b/app/views/shared/boards/components/_sidebar.html.haml
@@ -1,5 +1,4 @@
-%board-sidebar{ "inline-template" => true,
- ":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" }
+%board-sidebar{ "inline-template" => true, ":current-user" => (UserSerializer.new.represent(current_user) || {}).to_json }
%transition{ name: "boards-sidebar-slide" }
%aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
.issuable-sidebar
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index e039a73cd3b..62437f5fc9d 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -8,16 +8,17 @@
= image_tag 'illustrations/issues.svg'
.col-xs-12
.text-content
- - if has_button && current_user
+ - if current_user
%h4
= _("The Issue Tracker is the place to add things that need to be improved or solved in a project")
%p
= _("Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.")
- .text-center
- - if project_select_button
- = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues
- - else
- = link_to 'New issue', button_path, class: 'btn btn-success', title: 'New issue', id: 'new_issue_link'
+ - if has_button
+ .text-center
+ - if project_select_button
+ = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues
+ - else
+ = link_to 'New issue', button_path, class: 'btn btn-success', title: 'New issue', id: 'new_issue_link'
- else
%h4.text-center= _("There are no issues to show")
%p
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
index 8e6747ca740..1a259b679c7 100644
--- a/app/views/shared/groups/_dropdown.html.haml
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -1,3 +1,4 @@
+- options_hash = local_assigns.fetch(:options_hash, groups_sort_options_hash)
- show_archive_options = local_assigns.fetch(:show_archive_options, false)
- if @sort.present?
- default_sort_by = @sort
@@ -10,12 +11,12 @@
.dropdown.inline.js-group-filter-dropdown-wrap.append-right-10
%button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.dropdown-label
- = sort_options_hash[default_sort_by]
+ = options_hash[default_sort_by]
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li.dropdown-header
= _("Sort by")
- - groups_sort_options_hash.each do |value, title|
+ - options_hash.each do |value, title|
%li.js-filter-sort-order
= link_to filter_groups_path(sort: value), class: ("is-active" if default_sort_by == value) do
= title
diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml
index 217af7c9fac..fc86f855865 100644
--- a/app/views/shared/issuable/_assignees.html.haml
+++ b/app/views/shared/issuable/_assignees.html.haml
@@ -1,14 +1,10 @@
-- max_render = 3
-- max = [max_render, issue.assignees.length].min
+- max_render = 4
+- assignees_rendering_overflow = issue.assignees.size > max_render
+- render_count = assignees_rendering_overflow ? max_render - 1 : max_render
+- more_assignees_count = issue.assignees.size - render_count
-- issue.assignees.take(max).each do |assignee|
+- issue.assignees.take(render_count).each do |assignee|
= link_to_member(@project, assignee, name: false, title: "Assigned to :name")
-- if issue.assignees.length > max_render
- - counter = issue.assignees.length - max_render
-
- %span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', 'original-title' => "+#{counter} more assignees" } }
- - if counter < 99
- = "+#{counter}"
- - else
- 99+
+- if more_assignees_count.positive?
+ %span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', 'original-title' => "+#{more_assignees_count} more assignees" } } +#{more_assignees_count}
diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml
deleted file mode 100644
index 87e8c416194..00000000000
--- a/app/views/shared/repo/_repo.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- @no_container = true;
-#repo{ data: { root: @path.empty?.to_s,
- root_url: project_tree_path(project),
- url: content_url,
- current_branch: @ref,
- ref: @commit.id,
- project_name: project.name,
- project_url: project_path(project),
- project_id: project.id,
- new_merge_request_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '' }),
- can_commit: (!!can_push_branch?(project, @ref)).to_s,
- on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s,
- current_path: @path } }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
new file mode 100644
index 00000000000..268b7028fd9
--- /dev/null
+++ b/app/workers/all_queues.yml
@@ -0,0 +1,99 @@
+---
+- cronjob:admin_email
+- cronjob:expire_build_artifacts
+- cronjob:gitlab_usage_ping
+- cronjob:import_export_project_cleanup
+- cronjob:pipeline_schedule
+- cronjob:prune_old_events
+- cronjob:remove_expired_group_links
+- cronjob:remove_expired_members
+- cronjob:remove_old_web_hook_logs
+- cronjob:remove_unreferenced_lfs_objects
+- cronjob:repository_archive_cache
+- cronjob:repository_check_batch
+- cronjob:requests_profiles
+- cronjob:schedule_update_user_activity
+- cronjob:stuck_ci_jobs
+- cronjob:stuck_import_jobs
+- cronjob:stuck_merge_jobs
+- cronjob:trending_projects
+
+- gcp_cluster:cluster_install_app
+- gcp_cluster:cluster_provision
+- gcp_cluster:cluster_wait_for_app_installation
+- gcp_cluster:wait_for_cluster_creation
+
+- github_import_advance_stage
+- github_importer:github_import_import_diff_note
+- github_importer:github_import_import_issue
+- github_importer:github_import_import_note
+- 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_pull_requests
+- github_importer:github_import_stage_import_repository
+
+- pipeline_cache:expire_job_cache
+- pipeline_cache:expire_pipeline_cache
+- pipeline_creation:create_pipeline
+- pipeline_creation:run_pipeline_schedule
+- pipeline_default:build_coverage
+- pipeline_default:build_trace_sections
+- pipeline_default:pipeline_metrics
+- pipeline_default:pipeline_notification
+- pipeline_default:update_head_pipeline_for_merge_request
+- pipeline_hooks:build_hooks
+- pipeline_hooks:pipeline_hooks
+- pipeline_processing:build_finished
+- pipeline_processing:build_queue
+- pipeline_processing:build_success
+- pipeline_processing:pipeline_process
+- pipeline_processing:pipeline_success
+- pipeline_processing:pipeline_update
+- pipeline_processing:stage_update
+
+- repository_check:repository_check_clear
+- repository_check:repository_check_single_repository
+
+- default
+- mailers # ActionMailer::DeliveryJob.queue_name
+
+- authorized_projects
+- background_migration
+- create_gpg_signature
+- delete_merged_branches
+- delete_user
+- email_receiver
+- emails_on_push
+- expire_build_instance_artifacts
+- git_garbage_collect
+- gitlab_shell
+- group_destroy
+- invalid_gpg_signature_update
+- irker
+- merge
+- namespaceless_project_destroy
+- new_issue
+- new_merge_request
+- new_note
+- pages
+- post_receive
+- process_commit
+- project_cache
+- project_destroy
+- project_export
+- project_migrate_hashed_storage
+- project_service
+- propagate_service_template
+- reactive_caching
+- repository_fork
+- repository_import
+- storage_migrator
+- system_hook_push
+- update_merge_requests
+- update_user_activity
+- upload_checksum
+- web_hook
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index 5efa9180f5e..97d80305bec 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -2,7 +2,7 @@ class BuildFinishedWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
index 6705a1c2709..cbfca8c342c 100644
--- a/app/workers/build_hooks_worker.rb
+++ b/app/workers/build_hooks_worker.rb
@@ -2,7 +2,7 @@ class BuildHooksWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :hooks
+ queue_namespace :pipeline_hooks
def perform(build_id)
Ci::Build.find_by(id: build_id)
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
index fc775a84dc0..e4f4e6c1d9e 100644
--- a/app/workers/build_queue_worker.rb
+++ b/app/workers/build_queue_worker.rb
@@ -2,7 +2,7 @@ class BuildQueueWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index ec049821ad7..4b9097bc5e4 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -2,7 +2,7 @@ class BuildSuccessWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb
index 9c3bdabc49e..37586e161c9 100644
--- a/app/workers/concerns/application_worker.rb
+++ b/app/workers/concerns/application_worker.rb
@@ -3,13 +3,23 @@ Sidekiq::Worker.extend ActiveSupport::Concern
module ApplicationWorker
extend ActiveSupport::Concern
- include Sidekiq::Worker
+ include Sidekiq::Worker # rubocop:disable Cop/IncludeSidekiqWorker
included do
- sidekiq_options queue: base_queue_name
+ set_queue
end
module ClassMethods
+ def inherited(subclass)
+ subclass.set_queue
+ end
+
+ def set_queue
+ queue_name = [queue_namespace, base_queue_name].compact.join(':')
+
+ sidekiq_options queue: queue_name # rubocop:disable Cop/SidekiqOptionsQueue
+ end
+
def base_queue_name
name
.sub(/\AGitlab::/, '')
@@ -18,6 +28,16 @@ module ApplicationWorker
.tr('/', '_')
end
+ def queue_namespace(new_namespace = nil)
+ if new_namespace
+ sidekiq_options queue_namespace: new_namespace
+
+ set_queue
+ else
+ get_sidekiq_options['queue_namespace']&.to_s
+ end
+ end
+
def queue
get_sidekiq_options['queue'].to_s
end
diff --git a/app/workers/concerns/cluster_queue.rb b/app/workers/concerns/cluster_queue.rb
index a5074d13220..24b9f145220 100644
--- a/app/workers/concerns/cluster_queue.rb
+++ b/app/workers/concerns/cluster_queue.rb
@@ -5,6 +5,6 @@ module ClusterQueue
extend ActiveSupport::Concern
included do
- sidekiq_options queue: :gcp_cluster
+ queue_namespace :gcp_cluster
end
end
diff --git a/app/workers/concerns/cronjob_queue.rb b/app/workers/concerns/cronjob_queue.rb
index e918bb011e0..b6581779f6a 100644
--- a/app/workers/concerns/cronjob_queue.rb
+++ b/app/workers/concerns/cronjob_queue.rb
@@ -4,6 +4,7 @@ module CronjobQueue
extend ActiveSupport::Concern
included do
- sidekiq_options queue: :cronjob, retry: false
+ queue_namespace :cronjob
+ sidekiq_options retry: false
end
end
diff --git a/app/workers/concerns/gitlab/github_import/queue.rb b/app/workers/concerns/gitlab/github_import/queue.rb
index a2bee361b86..22c2ce458e8 100644
--- a/app/workers/concerns/gitlab/github_import/queue.rb
+++ b/app/workers/concerns/gitlab/github_import/queue.rb
@@ -4,12 +4,14 @@ module Gitlab
extend ActiveSupport::Concern
included do
+ queue_namespace :github_importer
+
# If a job produces an error it may block a stage from advancing
# forever. To prevent this from happening we prevent jobs from going to
# the dead queue. This does mean some resources may not be imported, but
# this is better than a project being stuck in the "import" state
# forever.
- sidekiq_options queue: 'github_importer', dead: false, retry: 5
+ sidekiq_options dead: false, retry: 5
end
end
end
diff --git a/app/workers/concerns/new_issuable.rb b/app/workers/concerns/new_issuable.rb
index eb0d6c9c36c..526ed0bad07 100644
--- a/app/workers/concerns/new_issuable.rb
+++ b/app/workers/concerns/new_issuable.rb
@@ -9,15 +9,15 @@ module NewIssuable
end
def set_user(user_id)
- @user = User.find_by(id: user_id)
+ @user = User.find_by(id: user_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- log_error(User, user_id) unless @user
+ log_error(User, user_id) unless @user # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def set_issuable(issuable_id)
- @issuable = issuable_class.find_by(id: issuable_id)
+ @issuable = issuable_class.find_by(id: issuable_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- log_error(issuable_class, issuable_id) unless @issuable
+ log_error(issuable_class, issuable_id) unless @issuable # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def log_error(record_class, record_id)
diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb
index ddf45b91345..e77093a6902 100644
--- a/app/workers/concerns/pipeline_queue.rb
+++ b/app/workers/concerns/pipeline_queue.rb
@@ -5,14 +5,6 @@ module PipelineQueue
extend ActiveSupport::Concern
included do
- sidekiq_options queue: 'pipeline_default'
- end
-
- class_methods do
- def enqueue_in(group:)
- raise ArgumentError, 'Unspecified queue group!' if group.empty?
-
- sidekiq_options queue: "pipeline_#{group}"
- end
+ queue_namespace :pipeline_default
end
end
diff --git a/app/workers/concerns/project_import_options.rb b/app/workers/concerns/project_import_options.rb
new file mode 100644
index 00000000000..10b971344f7
--- /dev/null
+++ b/app/workers/concerns/project_import_options.rb
@@ -0,0 +1,23 @@
+module ProjectImportOptions
+ extend ActiveSupport::Concern
+
+ included do
+ IMPORT_RETRY_COUNT = 5
+
+ sidekiq_options retry: IMPORT_RETRY_COUNT, status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
+
+ # We only want to mark the project as failed once we exhausted all retries
+ sidekiq_retries_exhausted do |job|
+ project = Project.find(job['args'].first)
+
+ action = if project.forked?
+ "fork"
+ else
+ "import"
+ end
+
+ project.mark_import_as_failed("Every #{action} attempt has failed: #{job['error_message']}. Please try again.")
+ Sidekiq.logger.warn "Failed #{job['class']} with #{job['args']}: #{job['error_message']}"
+ end
+ end
+end
diff --git a/app/workers/concerns/project_start_import.rb b/app/workers/concerns/project_start_import.rb
index 0704ebbb0fd..4e55a1ee3d6 100644
--- a/app/workers/concerns/project_start_import.rb
+++ b/app/workers/concerns/project_start_import.rb
@@ -1,3 +1,4 @@
+# Used in EE by mirroring
module ProjectStartImport
def start(project)
if project.import_started? && project.import_jid == self.jid
diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb
index a597321ccf4..43fb66c31b0 100644
--- a/app/workers/concerns/repository_check_queue.rb
+++ b/app/workers/concerns/repository_check_queue.rb
@@ -3,6 +3,8 @@ module RepositoryCheckQueue
extend ActiveSupport::Concern
included do
- sidekiq_options queue: :repository_check, retry: false
+ queue_namespace :repository_check
+
+ sidekiq_options retry: false
end
end
diff --git a/app/workers/create_pipeline_worker.rb b/app/workers/create_pipeline_worker.rb
index 00cd7b85b9f..c3ac35e54f5 100644
--- a/app/workers/create_pipeline_worker.rb
+++ b/app/workers/create_pipeline_worker.rb
@@ -2,7 +2,7 @@ class CreatePipelineWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :creation
+ queue_namespace :pipeline_creation
def perform(project_id, user_id, ref, source, params = {})
project = Project.find(project_id)
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
index a591e2da519..7217364a9f2 100644
--- a/app/workers/expire_job_cache_worker.rb
+++ b/app/workers/expire_job_cache_worker.rb
@@ -2,7 +2,7 @@ class ExpireJobCacheWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :cache
+ queue_namespace :pipeline_cache
def perform(job_id)
job = CommitStatus.joins(:pipeline, :project).find_by(id: job_id)
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
index a3ac32b437d..db73d37868a 100644
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -2,7 +2,7 @@ class ExpirePipelineCacheWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :cache
+ queue_namespace :pipeline_cache
def perform(pipeline_id)
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
@@ -13,7 +13,7 @@ class ExpirePipelineCacheWorker
store.touch(project_pipelines_path(project))
store.touch(project_pipeline_path(project, pipeline))
- store.touch(commit_pipelines_path(project, pipeline.commit)) if pipeline.commit
+ store.touch(commit_pipelines_path(project, pipeline.commit)) unless pipeline.commit.nil?
store.touch(new_merge_request_pipelines_path(project))
each_pipelines_merge_request_path(project, pipeline) do |path|
store.touch(path)
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index 400396d5755..f7f498af840 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -9,7 +9,7 @@ module Gitlab
class AdvanceStageWorker
include ApplicationWorker
- sidekiq_options queue: 'github_importer_advance_stage', dead: false
+ sidekiq_options dead: false
INTERVAL = 30.seconds.to_i
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index 62f733c02fc..3ec81d040b4 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -1,7 +1,7 @@
class PagesWorker
include ApplicationWorker
- sidekiq_options queue: :pages, retry: false
+ sidekiq_options retry: false
def perform(action, *arg)
send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index 661c29efe88..c94918ff4ee 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -2,7 +2,7 @@ class PipelineHooksWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :hooks
+ queue_namespace :pipeline_hooks
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index 07dbf6a971e..24424b3f472 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -2,7 +2,7 @@ class PipelineProcessWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index 68c40a259e1..2ab0739a17f 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -2,7 +2,7 @@ class PipelineSuccessWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
index 24a8a9fbed5..fc9da2d45b1 100644
--- a/app/workers/pipeline_update_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -2,7 +2,7 @@ class PipelineUpdateWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index a07ef1705a1..d1c57b82681 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -1,11 +1,8 @@
class RepositoryForkWorker
- ForkError = Class.new(StandardError)
-
include ApplicationWorker
include Gitlab::ShellAdapter
include ProjectStartImport
-
- sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
+ include ProjectImportOptions
def perform(project_id, forked_from_repository_storage_path, source_disk_path)
project = Project.find(project_id)
@@ -18,20 +15,12 @@ class RepositoryForkWorker
result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_disk_path,
project.repository_storage_path, project.disk_path)
- raise ForkError, "Unable to fork project #{project_id} for repository #{source_disk_path} -> #{project.disk_path}" unless result
+ raise "Unable to fork project #{project_id} for repository #{source_disk_path} -> #{project.disk_path}" unless result
project.repository.after_import
- raise ForkError, "Project #{project_id} had an invalid repository after fork" unless project.valid_repo?
+ raise "Project #{project_id} had an invalid repository after fork" unless project.valid_repo?
project.import_finish
- rescue ForkError => ex
- fail_fork(project, ex.message)
- raise
- rescue => ex
- return unless project
-
- fail_fork(project, ex.message)
- raise ForkError, "#{ex.class} #{ex.message}"
end
private
@@ -42,9 +31,4 @@ class RepositoryForkWorker
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.")
false
end
-
- def fail_fork(project, message)
- Rails.logger.error(message)
- project.mark_import_as_failed(message)
- end
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 55715c83cb1..31e2798c36b 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -1,11 +1,8 @@
class RepositoryImportWorker
- ImportError = Class.new(StandardError)
-
include ApplicationWorker
include ExceptionBacktrace
include ProjectStartImport
-
- sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
+ include ProjectImportOptions
def perform(project_id)
project = Project.find(project_id)
@@ -23,17 +20,9 @@ class RepositoryImportWorker
# to those importers to mark the import process as complete.
return if service.async?
- raise ImportError, result[:message] if result[:status] == :error
+ raise result[:message] if result[:status] == :error
project.after_import
- rescue ImportError => ex
- fail_import(project, ex.message)
- raise
- rescue => ex
- return unless project
-
- fail_import(project, ex.message)
- raise ImportError, "#{ex.class} #{ex.message}"
end
private
@@ -44,8 +33,4 @@ class RepositoryImportWorker
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false
end
-
- def fail_import(project, message)
- project.mark_import_as_failed(message)
- end
end
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
new file mode 100644
index 00000000000..8f5138fc873
--- /dev/null
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -0,0 +1,22 @@
+class RunPipelineScheduleWorker
+ include ApplicationWorker
+ include PipelineQueue
+
+ queue_namespace :pipeline_creation
+
+ def perform(schedule_id, user_id)
+ schedule = Ci::PipelineSchedule.find_by(id: schedule_id)
+ user = User.find_by(id: user_id)
+
+ return unless schedule && user
+
+ run_pipeline_schedule(schedule, user)
+ end
+
+ def run_pipeline_schedule(schedule, user)
+ Ci::CreatePipelineService.new(schedule.project,
+ user,
+ ref: schedule.ref)
+ .execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
+ end
+end
diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb
index 69f2318d83b..e4b683fca33 100644
--- a/app/workers/stage_update_worker.rb
+++ b/app/workers/stage_update_worker.rb
@@ -2,7 +2,7 @@ class StageUpdateWorker
include ApplicationWorker
include PipelineQueue
- enqueue_in group: :processing
+ queue_namespace :pipeline_processing
def perform(stage_id)
Ci::Stage.find_by(id: stage_id).try do |stage|
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index 36d2a2e6466..16394293c79 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -23,7 +23,12 @@ class StuckMergeJobsWorker
merge_requests = MergeRequest.where(id: completed_ids)
merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged)
- merge_requests.where(merge_commit_sha: nil).update_all(state: :opened, merge_jid: nil)
+
+ merge_requests_to_reopen = merge_requests.where(merge_commit_sha: nil)
+
+ # Do not reopen merge requests using direct queries.
+ # We rely on state machine callbacks to update head_pipeline_id
+ merge_requests_to_reopen.each(&:unlock_mr)
Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}")
end
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 0a2e9b63578..f09d89aa170 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -1,15 +1,25 @@
class UpdateHeadPipelineForMergeRequestWorker
include ApplicationWorker
-
- sidekiq_options queue: 'pipeline_default'
+ include PipelineQueue
def perform(merge_request_id)
merge_request = MergeRequest.find(merge_request_id)
pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last
return unless pipeline && pipeline.latest?
- raise ArgumentError, 'merge request sha does not equal pipeline sha' if merge_request.diff_head_sha != pipeline.sha
+
+ if merge_request.diff_head_sha != pipeline.sha
+ log_error_message_for(merge_request)
+
+ return
+ end
merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end
+
+ def log_error_message_for(merge_request)
+ Rails.logger.error(
+ "Outdated head pipeline for active merge request: id=#{merge_request.id}, source_branch=#{merge_request.source_branch}, diff_head_sha=#{merge_request.diff_head_sha}"
+ )
+ end
end