summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorLuke "Jared" Bennett <lbennett@gitlab.com>2017-04-24 11:45:05 +0100
committerLuke "Jared" Bennett <lbennett@gitlab.com>2017-04-24 11:45:05 +0100
commit7dabca1b969a76a0c9a6e336b49937d84e002637 (patch)
treede2f24bd1c269f088f9b6d53c3e55efa62e0cc21 /app
parenta695b855e078b1900d0ded11ae3fab1e547e442f (diff)
parentc768026474b9dff9f6f988372e4eefb85b1d8be9 (diff)
downloadgitlab-ce-7dabca1b969a76a0c9a6e336b49937d84e002637.tar.gz
Merge branch 'master' into droplab-templating-xss-fix
Diffstat (limited to 'app')
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_canceled.ico (renamed from app/assets/images/ci_favicons/icon_status_canceled.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_created.ico (renamed from app/assets/images/ci_favicons/icon_status_created.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_failed.ico (renamed from app/assets/images/ci_favicons/icon_status_failed.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_manual.ico (renamed from app/assets/images/ci_favicons/icon_status_manual.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_not_found.ico (renamed from app/assets/images/ci_favicons/icon_status_not_found.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_pending.ico (renamed from app/assets/images/ci_favicons/icon_status_pending.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_running.ico (renamed from app/assets/images/ci_favicons/icon_status_running.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_skipped.ico (renamed from app/assets/images/ci_favicons/icon_status_skipped.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_success.ico (renamed from app/assets/images/ci_favicons/icon_status_success.ico)bin5430 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/favicon_status_warning.ico (renamed from app/assets/images/ci_favicons/icon_status_warning.ico)bin5430 -> 5430 bytes
-rw-r--r--app/assets/javascripts/awards_handler.js3
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js1
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js2
-rw-r--r--app/assets/javascripts/blob/blob_fork_suggestion.js58
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js3
-rw-r--r--app/assets/javascripts/boards/components/board_list.js9
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js16
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js3
-rw-r--r--app/assets/javascripts/boards/models/list.js11
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js3
-rw-r--r--app/assets/javascripts/build.js24
-rw-r--r--app/assets/javascripts/ci_status_icons.js34
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js33
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js100
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js260
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js310
-rw-r--r--app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js42
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js202
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js36
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js96
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js50
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js110
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js90
-rw-r--r--app/assets/javascripts/dispatcher.js25
-rw-r--r--app/assets/javascripts/droplab/constants.js2
-rw-r--r--app/assets/javascripts/droplab/drop_down.js3
-rw-r--r--app/assets/javascripts/dropzone_input.js14
-rw-r--r--app/assets/javascripts/due_date_select.js11
-rw-r--r--app/assets/javascripts/environments/components/environment.js2
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js97
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue103
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js30
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue33
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue (renamed from app/assets/javascripts/environments/components/environment_item.js)250
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.js31
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue33
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue (renamed from app/assets/javascripts/environments/components/environment_rollback.js)39
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue (renamed from app/assets/javascripts/environments/components/environment_stop.js)35
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue (renamed from app/assets/javascripts/environments/components/environment_terminal_button.js)22
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js97
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue117
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js122
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js76
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js106
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js298
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js194
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js298
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js778
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js168
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js100
-rw-r--r--app/assets/javascripts/groups_select.js6
-rw-r--r--app/assets/javascripts/issuable_form.js3
-rw-r--r--app/assets/javascripts/issue_show/issue_title.vue23
-rw-r--r--app/assets/javascripts/labels_select.js8
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js15
-rw-r--r--app/assets/javascripts/lib/utils/constants.js2
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js12
-rw-r--r--app/assets/javascripts/main.js9
-rw-r--r--app/assets/javascripts/member_expiration_date.js3
-rw-r--r--app/assets/javascripts/merge_request_widget.js8
-rw-r--r--app/assets/javascripts/milestone_select.js3
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js2
-rw-r--r--app/assets/javascripts/notes.js22
-rw-r--r--app/assets/javascripts/pipelines/components/async_button.vue (renamed from app/assets/javascripts/vue_pipelines_index/components/async_button.vue)14
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue (renamed from app/assets/javascripts/vue_pipelines_index/components/empty_state.vue)2
-rw-r--r--app/assets/javascripts/pipelines/components/error_state.vue (renamed from app/assets/javascripts/vue_pipelines_index/components/error_state.vue)0
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.js (renamed from app/assets/javascripts/vue_pipelines_index/components/nav_controls.js)0
-rw-r--r--app/assets/javascripts/pipelines/components/navigation_tabs.js (renamed from app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js)0
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.js (renamed from app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js)0
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.js (renamed from app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js)3
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.js (renamed from app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js)0
-rw-r--r--app/assets/javascripts/pipelines/components/stage.js (renamed from app/assets/javascripts/vue_pipelines_index/components/stage.js)28
-rw-r--r--app/assets/javascripts/pipelines/components/status.js (renamed from app/assets/javascripts/vue_pipelines_index/components/status.js)0
-rw-r--r--app/assets/javascripts/pipelines/components/time_ago.js (renamed from app/assets/javascripts/vue_pipelines_index/components/time_ago.js)0
-rw-r--r--app/assets/javascripts/pipelines/event_hub.js (renamed from app/assets/javascripts/vue_pipelines_index/event_hub.js)0
-rw-r--r--app/assets/javascripts/pipelines/index.js (renamed from app/assets/javascripts/vue_pipelines_index/index.js)0
-rw-r--r--app/assets/javascripts/pipelines/pipelines.js (renamed from app/assets/javascripts/vue_pipelines_index/pipelines.js)0
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js (renamed from app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js)0
-rw-r--r--app/assets/javascripts/pipelines/stores/pipelines_store.js (renamed from app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js)0
-rw-r--r--app/assets/javascripts/shortcuts.js7
-rw-r--r--app/assets/javascripts/shortcuts_wiki.js16
-rw-r--r--app/assets/javascripts/usage_ping.js15
-rw-r--r--app/assets/javascripts/user_tabs.js22
-rw-r--r--app/assets/javascripts/users_select.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js14
-rw-r--r--app/assets/stylesheets/framework/animations.scss14
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss75
-rw-r--r--app/assets/stylesheets/framework/header.scss13
-rw-r--r--app/assets/stylesheets/framework/nav.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss29
-rw-r--r--app/assets/stylesheets/framework/variables.scss11
-rw-r--r--app/assets/stylesheets/pages/builds.scss13
-rw-r--r--app/assets/stylesheets/pages/commits.scss7
-rw-r--r--app/assets/stylesheets/pages/diff.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/notes.scss6
-rw-r--r--app/assets/stylesheets/pages/profile.scss59
-rw-r--r--app/assets/stylesheets/pages/projects.scss32
-rw-r--r--app/assets/stylesheets/pages/search.scss15
-rw-r--r--app/controllers/admin/application_settings_controller.rb13
-rw-r--r--app/controllers/admin/cohorts_controller.rb11
-rw-r--r--app/controllers/admin/groups_controller.rb8
-rw-r--r--app/controllers/admin/spam_logs_controller.rb2
-rw-r--r--app/controllers/concerns/creates_commit.rb62
-rw-r--r--app/controllers/concerns/membership_actions.rb42
-rw-r--r--app/controllers/groups/group_members_controller.rb24
-rw-r--r--app/controllers/projects/application_controller.rb5
-rw-r--r--app/controllers/projects/blob_controller.rb18
-rw-r--r--app/controllers/projects/commit_controller.rb10
-rw-r--r--app/controllers/projects/git_http_controller.rb6
-rw-r--r--app/controllers/projects/project_members_controller.rb24
-rw-r--r--app/controllers/projects/tree_controller.rb6
-rw-r--r--app/controllers/sessions_controller.rb5
-rw-r--r--app/helpers/blob_helper.rb40
-rw-r--r--app/helpers/issuables_helper.rb11
-rw-r--r--app/helpers/projects_helper.rb23
-rw-r--r--app/helpers/snippets_helper.rb2
-rw-r--r--app/helpers/sorting_helper.rb8
-rw-r--r--app/helpers/submodule_helper.rb12
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/models/abuse_report.rb2
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/concerns/cache_markdown_field.rb126
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/identity.rb2
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/member.rb28
-rw-r--r--app/models/members/group_member.rb12
-rw-r--r--app/models/members/project_member.rb4
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_services/chat_notification_service.rb2
-rw-r--r--app/models/project_team.rb4
-rw-r--r--app/models/repository.rb10
-rw-r--r--app/models/spam_log.rb4
-rw-r--r--app/models/user.rb15
-rw-r--r--app/serializers/cohort_activity_month_entity.rb11
-rw-r--r--app/serializers/cohort_entity.rb17
-rw-r--r--app/serializers/cohorts_entity.rb4
-rw-r--r--app/serializers/cohorts_serializer.rb3
-rw-r--r--app/serializers/status_entity.rb6
-rw-r--r--app/services/cohorts_service.rb100
-rw-r--r--app/services/commits/change_service.rb52
-rw-r--r--app/services/commits/cherry_pick_service.rb2
-rw-r--r--app/services/commits/create_service.rb74
-rw-r--r--app/services/commits/revert_service.rb2
-rw-r--r--app/services/delete_merged_branches_service.rb11
-rw-r--r--app/services/event_create_service.rb2
-rw-r--r--app/services/files/base_service.rb80
-rw-r--r--app/services/files/create_dir_service.rb15
-rw-r--r--app/services/files/create_service.rb36
-rw-r--r--app/services/files/delete_service.rb (renamed from app/services/files/destroy_service.rb)6
-rw-r--r--app/services/files/multi_service.rb125
-rw-r--r--app/services/files/update_service.rb30
-rw-r--r--app/services/members/authorized_destroy_service.rb24
-rw-r--r--app/services/members/create_service.rb8
-rw-r--r--app/services/projects/import_service.rb1
-rw-r--r--app/services/search/global_service.rb11
-rw-r--r--app/services/search/group_service.rb18
-rw-r--r--app/services/search_service.rb2
-rw-r--r--app/services/users/activity_service.rb22
-rw-r--r--app/services/users/destroy_service.rb2
-rw-r--r--app/services/users/migrate_to_ghost_user_service.rb34
-rw-r--r--app/services/validate_new_branch_service.rb5
-rw-r--r--app/views/admin/application_settings/_form.html.haml15
-rw-r--r--app/views/admin/cohorts/_cohorts_table.html.haml28
-rw-r--r--app/views/admin/cohorts/_usage_ping.html.haml10
-rw-r--r--app/views/admin/cohorts/index.html.haml16
-rw-r--r--app/views/admin/dashboard/_head.html.haml4
-rw-r--r--app/views/help/_shortcuts.html.haml8
-rw-r--r--app/views/layouts/header/_default.html.haml9
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml4
-rw-r--r--app/views/layouts/nav/_project.html.haml4
-rw-r--r--app/views/projects/blob/_header.html.haml11
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/show.html.haml2
-rw-r--r--app/views/projects/branches/index.html.haml16
-rw-r--r--app/views/projects/builds/show.html.haml10
-rw-r--r--app/views/projects/diffs/_content.html.haml2
-rw-r--r--app/views/projects/environments/metrics.html.haml3
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_pipelines.html.haml3
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_error.html.haml6
-rw-r--r--app/views/projects/milestones/edit.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml5
-rw-r--r--app/views/projects/notes/_comment_button.html.haml2
-rw-r--r--app/views/projects/pipelines/index.html.haml2
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/projects/snippets/edit.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/triggers/_form.html.haml2
-rw-r--r--app/views/projects/variables/_table.html.haml2
-rw-r--r--app/views/projects/wikis/_main_links.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml5
-rw-r--r--app/views/search/results/_merge_request.html.haml9
-rw-r--r--app/views/shared/_branch_switcher.html.haml6
-rw-r--r--app/views/shared/_new_commit_form.html.haml6
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml3
-rw-r--r--app/views/shared/_user_callout.html.haml17
-rw-r--r--app/views/shared/groups/_group.html.haml3
-rw-r--r--app/views/shared/icons/_icon_arrow_circle_o_right.svg2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml15
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml4
-rw-r--r--app/views/shared/projects/_project.html.haml9
-rw-r--r--app/views/snippets/edit.html.haml2
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/views/users/show.html.haml10
-rw-r--r--app/workers/clear_database_cache_worker.rb24
-rw-r--r--app/workers/gitlab_usage_ping_worker.rb31
-rw-r--r--app/workers/schedule_update_user_activity_worker.rb10
-rw-r--r--app/workers/system_hook_worker.rb2
-rw-r--r--app/workers/update_user_activity_worker.rb26
229 files changed, 3715 insertions, 3024 deletions
diff --git a/app/assets/images/ci_favicons/icon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico
index 5a19458f2a2..5a19458f2a2 100755
--- a/app/assets/images/ci_favicons/icon_status_canceled.ico
+++ b/app/assets/images/ci_favicons/favicon_status_canceled.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico
index 4dca9640cb3..4dca9640cb3 100755
--- a/app/assets/images/ci_favicons/icon_status_created.ico
+++ b/app/assets/images/ci_favicons/favicon_status_created.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico
index c961ff9a69b..c961ff9a69b 100755
--- a/app/assets/images/ci_favicons/icon_status_failed.ico
+++ b/app/assets/images/ci_favicons/favicon_status_failed.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico
index 5fbbc99ea7c..5fbbc99ea7c 100755
--- a/app/assets/images/ci_favicons/icon_status_manual.ico
+++ b/app/assets/images/ci_favicons/favicon_status_manual.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico
index 21afa9c72e6..21afa9c72e6 100755
--- a/app/assets/images/ci_favicons/icon_status_not_found.ico
+++ b/app/assets/images/ci_favicons/favicon_status_not_found.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico
index 8be32dab85a..8be32dab85a 100755
--- a/app/assets/images/ci_favicons/icon_status_pending.ico
+++ b/app/assets/images/ci_favicons/favicon_status_pending.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico
index f328ff1a5ed..f328ff1a5ed 100755
--- a/app/assets/images/ci_favicons/icon_status_running.ico
+++ b/app/assets/images/ci_favicons/favicon_status_running.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico
index b4394e1b4af..b4394e1b4af 100755
--- a/app/assets/images/ci_favicons/icon_status_skipped.ico
+++ b/app/assets/images/ci_favicons/favicon_status_skipped.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico
index 4f436c95242..4f436c95242 100755
--- a/app/assets/images/ci_favicons/icon_status_success.ico
+++ b/app/assets/images/ci_favicons/favicon_status_success.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico
index 805cc20cdec..805cc20cdec 100755
--- a/app/assets/images/ci_favicons/icon_status_warning.ico
+++ b/app/assets/images/ci_favicons/favicon_status_warning.ico
Binary files differ
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index f93208944a1..adb45b0606d 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -239,6 +239,9 @@ AwardsHandler
if (menu) {
menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
}
+ }).catch((err) => {
+ emojiContentElement.insertAdjacentHTML('beforeend', '<p>We encountered an error while adding the remaining categories</p>');
+ throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
});
};
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 4c9ad128e6c..77e92ff8caf 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -22,6 +22,7 @@ $(() => {
}
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
+ e.target.classList.toggle('open');
toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase();
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index c9fe23aec75..4568b86f298 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -35,7 +35,7 @@ export default class BlobFileDropzone {
this.removeFile(file);
});
this.on('sending', function (file, xhr, formData) {
- formData.append('target_branch', form.find('input[name="target_branch"]').val());
+ formData.append('branch_name', form.find('input[name="branch_name"]').val());
formData.append('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').val());
});
diff --git a/app/assets/javascripts/blob/blob_fork_suggestion.js b/app/assets/javascripts/blob/blob_fork_suggestion.js
index aa9a4e1c99a..3baf81905fe 100644
--- a/app/assets/javascripts/blob/blob_fork_suggestion.js
+++ b/app/assets/javascripts/blob/blob_fork_suggestion.js
@@ -1,15 +1,63 @@
-function BlobForkSuggestion(openButton, cancelButton, suggestionSection) {
- if (openButton) {
- openButton.addEventListener('click', () => {
+const defaults = {
+ // Buttons that will show the `suggestionSections`
+ // has `data-fork-path`, and `data-action`
+ openButtons: [],
+ // Update the href(from `openButton` -> `data-fork-path`)
+ // whenever a `openButton` is clicked
+ forkButtons: [],
+ // Buttons to hide the `suggestionSections`
+ cancelButtons: [],
+ // Section to show/hide
+ suggestionSections: [],
+ // Pieces of text that need updating depending on the action, `edit`, `replace`, `delete`
+ actionTextPieces: [],
+};
+
+class BlobForkSuggestion {
+ constructor(options) {
+ this.elementMap = Object.assign({}, defaults, options);
+ this.onClickWrapper = this.onClick.bind(this);
+
+ document.addEventListener('click', this.onClickWrapper);
+ }
+
+ showSuggestionSection(forkPath, action = 'edit') {
+ [].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => {
suggestionSection.classList.remove('hidden');
});
+
+ [].forEach.call(this.elementMap.forkButtons, (forkButton) => {
+ forkButton.setAttribute('href', forkPath);
+ });
+
+ [].forEach.call(this.elementMap.actionTextPieces, (actionTextPiece) => {
+ // eslint-disable-next-line no-param-reassign
+ actionTextPiece.textContent = action;
+ });
}
- if (cancelButton) {
- cancelButton.addEventListener('click', () => {
+ hideSuggestionSection() {
+ [].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => {
suggestionSection.classList.add('hidden');
});
}
+
+ onClick(e) {
+ const el = e.target;
+
+ if ([].includes.call(this.elementMap.openButtons, el)) {
+ const { forkPath, action } = el.dataset;
+ this.showSuggestionSection(forkPath, action);
+ }
+
+ if ([].includes.call(this.elementMap.cancelButtons, el)) {
+ this.hideSuggestionSection();
+ }
+ }
+
+ destroy() {
+ document.removeEventListener('click', this.onClickWrapper);
+ }
}
export default BlobForkSuggestion;
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index b749ef43cd3..b6dee8177d2 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -1,5 +1,6 @@
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global BoardService */
+/* global Flash */
import Vue from 'vue';
import VueResource from 'vue-resource';
@@ -93,7 +94,7 @@ $(() => {
Store.addBlankState();
this.loading = false;
- });
+ }).catch(() => new Flash('An error occurred. Please try again.'));
},
methods: {
updateTokens() {
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index adbd82cb687..b13386536bf 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -57,12 +57,15 @@ export default {
},
loadNextPage() {
const getIssues = this.list.nextPage();
+ const loadingDone = () => {
+ this.list.loadingMore = false;
+ };
if (getIssues) {
this.list.loadingMore = true;
- getIssues.then(() => {
- this.list.loadingMore = false;
- });
+ getIssues
+ .then(loadingDone)
+ .catch(loadingDone);
}
},
toggleForm() {
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index fb0aac3c0e4..fdab317dc23 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -51,11 +51,13 @@ gl.issueBoards.IssuesModal = Vue.extend({
showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true;
+ const loadingDone = () => {
+ this.loading = false;
+ };
this.loadIssues()
- .then(() => {
- this.loading = false;
- });
+ .then(loadingDone)
+ .catch(loadingDone);
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
@@ -67,11 +69,13 @@ gl.issueBoards.IssuesModal = Vue.extend({
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
+ const loadingDone = () => {
+ this.filterLoading = false;
+ };
this.loadIssues(true)
- .then(() => {
- this.filterLoading = false;
- });
+ .then(loadingDone)
+ .catch(loadingDone);
}
},
deep: true,
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 22f20305624..7e3bb79af1d 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -1,4 +1,5 @@
-/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */
+/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var,
+ promise/catch-or-return */
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 91e5fb2a666..f2b79a88a4a 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -3,6 +3,8 @@
/* global ListLabel */
import queryData from '../utils/query_data';
+const PER_PAGE = 20;
+
class List {
constructor (obj) {
this.id = obj.id;
@@ -58,7 +60,9 @@ class List {
nextPage () {
if (this.issuesSize > this.issues.length) {
- this.page += 1;
+ if (this.issues.length / PER_PAGE >= 1) {
+ this.page += 1;
+ }
return this.getIssues(false);
}
@@ -145,10 +149,7 @@ class List {
}
updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
- gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid)
- .then(() => {
- listFrom.getIssues(false);
- });
+ gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid);
}
findIssue (id) {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 66384d9c038..ccb00099215 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -36,6 +36,9 @@ gl.issueBoards.BoardsStore = {
.save()
.then(() => {
this.state.lists = _.sortBy(this.state.lists, 'position');
+ })
+ .catch(() => {
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
});
this.removeBlankState();
},
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 0aad95c2fe3..97f279e4be4 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -2,6 +2,8 @@
consistent-return, prefer-rest-params */
/* global Breakpoints */
+import { bytesToKiB } from './lib/utils/number_utils';
+
const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; };
const AUTO_SCROLL_OFFSET = 75;
const DOWN_BUILD_TRACE = '#down-build-trace';
@@ -20,6 +22,7 @@ window.Build = (function () {
this.state = this.options.logState;
this.buildStage = this.options.buildStage;
this.$document = $(document);
+ this.logBytes = 0;
this.updateDropdown = bind(this.updateDropdown, this);
@@ -98,15 +101,22 @@ window.Build = (function () {
if (log.append) {
$buildContainer.append(log.html);
+ this.logBytes += log.size;
} else {
$buildContainer.html(log.html);
- if (log.truncated) {
- $('.js-truncated-info-size').html(` ${log.size} `);
- this.$truncatedInfo.removeClass('hidden');
- this.initAffixTruncatedInfo();
- } else {
- this.$truncatedInfo.addClass('hidden');
- }
+ this.logBytes = log.size;
+ }
+
+ // if the incremental sum of logBytes we received is less than the total
+ // we need to show a message warning the user about that.
+ if (this.logBytes < log.total) {
+ // size is in bytes, we need to calculate KiB
+ const size = bytesToKiB(this.logBytes);
+ $('.js-truncated-info-size').html(`${size}`);
+ this.$truncatedInfo.removeClass('hidden');
+ this.initAffixTruncatedInfo();
+ } else {
+ this.$truncatedInfo.addClass('hidden');
}
this.checkAutoscroll();
diff --git a/app/assets/javascripts/ci_status_icons.js b/app/assets/javascripts/ci_status_icons.js
new file mode 100644
index 00000000000..f16616873b2
--- /dev/null
+++ b/app/assets/javascripts/ci_status_icons.js
@@ -0,0 +1,34 @@
+import CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
+import CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
+import FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
+import MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
+import PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
+import RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
+import SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
+import SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
+import WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
+
+const StatusIconEntityMap = {
+ icon_status_canceled: CANCELED_SVG,
+ icon_status_created: CREATED_SVG,
+ icon_status_failed: FAILED_SVG,
+ icon_status_manual: MANUAL_SVG,
+ icon_status_pending: PENDING_SVG,
+ icon_status_running: RUNNING_SVG,
+ icon_status_skipped: SKIPPED_SVG,
+ icon_status_success: SUCCESS_SVG,
+ icon_status_warning: WARNING_SVG,
+};
+
+export {
+ CANCELED_SVG,
+ CREATED_SVG,
+ FAILED_SVG,
+ MANUAL_SVG,
+ PENDING_SVG,
+ RUNNING_SVG,
+ SKIPPED_SVG,
+ SUCCESS_SVG,
+ WARNING_SVG,
+ StatusIconEntityMap as default,
+};
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index 1d16c64e07e..68a1c1de1df 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import Visibility from 'visibilityjs';
import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
-import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
-import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
-import eventHub from '../../vue_pipelines_index/event_hub';
-import EmptyState from '../../vue_pipelines_index/components/empty_state.vue';
-import ErrorState from '../../vue_pipelines_index/components/error_state.vue';
+import PipelinesService from '../../pipelines/services/pipelines_service';
+import PipelineStore from '../../pipelines/stores/pipelines_store';
+import eventHub from '../../pipelines/event_hub';
+import EmptyState from '../../pipelines/components/empty_state.vue';
+import ErrorState from '../../pipelines/components/error_state.vue';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
import Poll from '../../lib/utils/poll';
@@ -55,7 +55,15 @@ export default Vue.component('pipelines-table', {
},
shouldRenderEmptyState() {
- return !this.state.pipelines.length && !this.isLoading;
+ return !this.state.pipelines.length &&
+ !this.isLoading &&
+ !this.hasError;
+ },
+
+ shouldRenderTable() {
+ return !this.isLoading &&
+ this.state.pipelines.length > 0 &&
+ !this.hasError;
},
},
@@ -145,8 +153,12 @@ export default Vue.component('pipelines-table', {
template: `
<div class="content-list pipelines">
- <div class="realtime-loading" v-if="isLoading">
- <i class="fa fa-spinner fa-spin"></i>
+ <div
+ class="realtime-loading"
+ v-if="isLoading">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
</div>
<empty-state
@@ -155,8 +167,9 @@ export default Vue.component('pipelines-table', {
<error-state v-if="shouldRenderErrorState" />
- <div class="table-holder"
- v-if="!isLoading && state.pipelines.length > 0">
+ <div
+ class="table-holder"
+ v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service" />
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 3253eebd9b5..cb054a2a197 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -1,6 +1,7 @@
// ECMAScript polyfills
import 'core-js/fn/array/find';
import 'core-js/fn/array/from';
+import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
index eb76b7d15fd..aed7cac4e62 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
@@ -3,65 +3,63 @@
import Vue from 'vue';
-(() => {
- const CommentAndResolveBtn = Vue.extend({
- props: {
- discussionId: String,
+const CommentAndResolveBtn = Vue.extend({
+ props: {
+ discussionId: String,
+ },
+ data() {
+ return {
+ textareaIsEmpty: true,
+ discussion: {},
+ };
+ },
+ computed: {
+ showButton: function () {
+ if (this.discussion) {
+ return this.discussion.isResolvable();
+ } else {
+ return false;
+ }
},
- data() {
- return {
- textareaIsEmpty: true,
- discussion: {},
- };
+ isDiscussionResolved: function () {
+ return this.discussion.isResolved();
},
- computed: {
- showButton: function () {
- if (this.discussion) {
- return this.discussion.isResolvable();
+ buttonText: function () {
+ if (this.isDiscussionResolved) {
+ if (this.textareaIsEmpty) {
+ return "Unresolve discussion";
} else {
- return false;
+ return "Comment & unresolve discussion";
}
- },
- isDiscussionResolved: function () {
- return this.discussion.isResolved();
- },
- buttonText: function () {
- if (this.isDiscussionResolved) {
- if (this.textareaIsEmpty) {
- return "Unresolve discussion";
- } else {
- return "Comment & unresolve discussion";
- }
+ } else {
+ if (this.textareaIsEmpty) {
+ return "Resolve discussion";
} else {
- if (this.textareaIsEmpty) {
- return "Resolve discussion";
- } else {
- return "Comment & resolve discussion";
- }
+ return "Comment & resolve discussion";
}
}
- },
- created() {
- if (this.discussionId) {
- this.discussion = CommentsStore.state[this.discussionId];
- }
- },
- mounted: function () {
- if (!this.discussionId) return;
+ }
+ },
+ created() {
+ if (this.discussionId) {
+ this.discussion = CommentsStore.state[this.discussionId];
+ }
+ },
+ mounted: function () {
+ if (!this.discussionId) return;
- const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`);
- this.textareaIsEmpty = $textarea.val() === '';
+ const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`);
+ this.textareaIsEmpty = $textarea.val() === '';
- $textarea.on('input.comment-and-resolve-btn', () => {
- this.textareaIsEmpty = $textarea.val() === '';
- });
- },
- destroyed: function () {
- if (!this.discussionId) return;
+ $textarea.on('input.comment-and-resolve-btn', () => {
+ this.textareaIsEmpty = $textarea.val() === '';
+ });
+ },
+ destroyed: function () {
+ if (!this.discussionId) return;
- $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn');
- }
- });
+ $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn');
+ }
+});
- Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
-})(window);
+Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
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 0297add94d5..f3a688fbf2f 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -4,155 +4,153 @@
import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg';
-(() => {
- const DiffNoteAvatars = Vue.extend({
- props: ['discussionId'],
- data() {
- return {
- isVisible: false,
- lineType: '',
- storeState: CommentsStore.state,
- shownAvatars: 3,
- collapseIcon,
- };
- },
- template: `
- <div class="diff-comment-avatar-holders"
- v-show="notesCount !== 0">
- <div v-if="!isVisible">
- <img v-for="note in notesSubset"
- class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar"
- width="19"
- height="19"
- role="button"
- data-container="body"
- data-placement="top"
- data-html="true"
- :data-line-type="lineType"
- :title="note.authorName + ': ' + note.noteTruncated"
- :src="note.authorAvatar"
- @click="clickedAvatar($event)" />
- <span v-if="notesCount > shownAvatars"
- class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
- data-container="body"
- data-placement="top"
- ref="extraComments"
- role="button"
- :data-line-type="lineType"
- :title="extraNotesTitle"
- @click="clickedAvatar($event)">{{ moreText }}</span>
- </div>
- <button class="diff-notes-collapse js-diff-comment-avatar"
- type="button"
- aria-label="Show comments"
+const DiffNoteAvatars = Vue.extend({
+ props: ['discussionId'],
+ data() {
+ return {
+ isVisible: false,
+ lineType: '',
+ storeState: CommentsStore.state,
+ shownAvatars: 3,
+ collapseIcon,
+ };
+ },
+ template: `
+ <div class="diff-comment-avatar-holders"
+ v-show="notesCount !== 0">
+ <div v-if="!isVisible">
+ <img v-for="note in notesSubset"
+ class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar"
+ width="19"
+ height="19"
+ role="button"
+ data-container="body"
+ data-placement="top"
+ data-html="true"
+ :data-line-type="lineType"
+ :title="note.authorName + ': ' + note.noteTruncated"
+ :src="note.authorAvatar"
+ @click="clickedAvatar($event)" />
+ <span v-if="notesCount > shownAvatars"
+ class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
+ data-container="body"
+ data-placement="top"
+ ref="extraComments"
+ role="button"
:data-line-type="lineType"
- @click="clickedAvatar($event)"
- v-if="isVisible"
- v-html="collapseIcon">
- </button>
+ :title="extraNotesTitle"
+ @click="clickedAvatar($event)">{{ moreText }}</span>
</div>
- `,
- mounted() {
+ <button class="diff-notes-collapse js-diff-comment-avatar"
+ type="button"
+ aria-label="Show comments"
+ :data-line-type="lineType"
+ @click="clickedAvatar($event)"
+ v-if="isVisible"
+ v-html="collapseIcon">
+ </button>
+ </div>
+ `,
+ mounted() {
+ this.$nextTick(() => {
+ this.addNoCommentClass();
+ this.setDiscussionVisible();
+
+ this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
+ });
+
+ $(document).on('toggle.comments', () => {
this.$nextTick(() => {
- this.addNoCommentClass();
this.setDiscussionVisible();
-
- this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
});
-
- $(document).on('toggle.comments', () => {
+ });
+ },
+ destroyed() {
+ $(document).off('toggle.comments');
+ },
+ watch: {
+ storeState: {
+ handler() {
this.$nextTick(() => {
- this.setDiscussionVisible();
+ $('.has-tooltip', this.$el).tooltip('fixTitle');
+
+ // We need to add/remove a class to an element that is outside the Vue instance
+ this.addNoCommentClass();
});
- });
- },
- destroyed() {
- $(document).off('toggle.comments');
- },
- watch: {
- storeState: {
- handler() {
- this.$nextTick(() => {
- $('.has-tooltip', this.$el).tooltip('fixTitle');
-
- // We need to add/remove a class to an element that is outside the Vue instance
- this.addNoCommentClass();
- });
- },
- deep: true,
},
+ deep: true,
},
- computed: {
- notesSubset() {
- let notes = [];
-
- if (this.discussion) {
- notes = Object.keys(this.discussion.notes)
- .slice(0, this.shownAvatars)
- .map(noteId => this.discussion.notes[noteId]);
- }
-
- return notes;
- },
- extraNotesTitle() {
- if (this.discussion) {
- const extra = this.discussion.notesCount() - this.shownAvatars;
+ },
+ computed: {
+ notesSubset() {
+ let notes = [];
+
+ if (this.discussion) {
+ notes = Object.keys(this.discussion.notes)
+ .slice(0, this.shownAvatars)
+ .map(noteId => this.discussion.notes[noteId]);
+ }
+
+ return notes;
+ },
+ extraNotesTitle() {
+ if (this.discussion) {
+ const extra = this.discussion.notesCount() - this.shownAvatars;
- return `${extra} more comment${extra > 1 ? 's' : ''}`;
- }
+ return `${extra} more comment${extra > 1 ? 's' : ''}`;
+ }
- return '';
- },
- discussion() {
- return this.storeState[this.discussionId];
- },
- notesCount() {
- if (this.discussion) {
- return this.discussion.notesCount();
- }
+ return '';
+ },
+ discussion() {
+ return this.storeState[this.discussionId];
+ },
+ notesCount() {
+ if (this.discussion) {
+ return this.discussion.notesCount();
+ }
- return 0;
- },
- moreText() {
- const plusSign = this.notesCount < 100 ? '+' : '';
+ return 0;
+ },
+ moreText() {
+ const plusSign = this.notesCount < 100 ? '+' : '';
- return `${plusSign}${this.notesCount - this.shownAvatars}`;
- },
+ return `${plusSign}${this.notesCount - this.shownAvatars}`;
},
- methods: {
- clickedAvatar(e) {
- notes.addDiffNote(e);
+ },
+ methods: {
+ clickedAvatar(e) {
+ notes.addDiffNote(e);
- // Toggle the active state of the toggle all button
- this.toggleDiscussionsToggleState();
+ // Toggle the active state of the toggle all button
+ this.toggleDiscussionsToggleState();
- this.$nextTick(() => {
- this.setDiscussionVisible();
+ this.$nextTick(() => {
+ this.setDiscussionVisible();
- $('.has-tooltip', this.$el).tooltip('fixTitle');
- $('.has-tooltip', this.$el).tooltip('hide');
- });
- },
- addNoCommentClass() {
- const notesCount = this.notesCount;
+ $('.has-tooltip', this.$el).tooltip('fixTitle');
+ $('.has-tooltip', this.$el).tooltip('hide');
+ });
+ },
+ addNoCommentClass() {
+ const notesCount = this.notesCount;
- $(this.$el).closest('.js-avatar-container')
- .toggleClass('js-no-comment-btn', notesCount > 0)
- .nextUntil('.js-avatar-container')
- .toggleClass('js-no-comment-btn', notesCount > 0);
- },
- toggleDiscussionsToggleState() {
- const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
- const $visibleNotesHolders = $notesHolders.filter(':visible');
- const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments');
+ $(this.$el).closest('.js-avatar-container')
+ .toggleClass('js-no-comment-btn', notesCount > 0)
+ .nextUntil('.js-avatar-container')
+ .toggleClass('js-no-comment-btn', notesCount > 0);
+ },
+ toggleDiscussionsToggleState() {
+ const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
+ const $visibleNotesHolders = $notesHolders.filter(':visible');
+ const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments');
- $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length);
- },
- setDiscussionVisible() {
- this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
- },
+ $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length);
+ },
+ setDiscussionVisible() {
+ this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
},
- });
+ },
+});
- Vue.component('diff-note-avatars', DiffNoteAvatars);
-})();
+Vue.component('diff-note-avatars', DiffNoteAvatars);
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index 8edc45130fc..8a0fd3bb4a7 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -4,192 +4,190 @@
import Vue from 'vue';
-(() => {
- const JumpToDiscussion = Vue.extend({
- mixins: [DiscussionMixins],
- props: {
- discussionId: String
+const JumpToDiscussion = Vue.extend({
+ mixins: [DiscussionMixins],
+ props: {
+ discussionId: String
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state,
+ discussion: {},
+ };
+ },
+ computed: {
+ allResolved: function () {
+ return this.unresolvedDiscussionCount === 0;
},
- data: function () {
- return {
- discussions: CommentsStore.state,
- discussion: {},
- };
- },
- computed: {
- allResolved: function () {
- return this.unresolvedDiscussionCount === 0;
- },
- showButton: function () {
- if (this.discussionId) {
- if (this.unresolvedDiscussionCount > 1) {
- return true;
- } else {
- return this.discussionId !== this.lastResolvedId;
- }
+ showButton: function () {
+ if (this.discussionId) {
+ if (this.unresolvedDiscussionCount > 1) {
+ return true;
} else {
- return this.unresolvedDiscussionCount >= 1;
+ return this.discussionId !== this.lastResolvedId;
}
- },
- lastResolvedId: function () {
- let lastId;
- for (const discussionId in this.discussions) {
- const discussion = this.discussions[discussionId];
-
- if (!discussion.isResolved()) {
- lastId = discussion.id;
- }
- }
- return lastId;
+ } else {
+ return this.unresolvedDiscussionCount >= 1;
}
},
- methods: {
- jumpToNextUnresolvedDiscussion: function () {
- let discussionsSelector;
- let discussionIdsInScope;
- let firstUnresolvedDiscussionId;
- let nextUnresolvedDiscussionId;
- let activeTab = window.mrTabs.currentAction;
- let hasDiscussionsToJumpTo = true;
- let jumpToFirstDiscussion = !this.discussionId;
-
- const discussionIdsForElements = function(elements) {
- return elements.map(function() {
- return $(this).attr('data-discussion-id');
- }).toArray();
- };
-
- const discussions = this.discussions;
-
- if (activeTab === 'diffs') {
- discussionsSelector = '.diffs .notes[data-discussion-id]';
- discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
-
- let unresolvedDiscussionCount = 0;
-
- for (let i = 0; i < discussionIdsInScope.length; i += 1) {
- const discussionId = discussionIdsInScope[i];
- const discussion = discussions[discussionId];
- if (discussion && !discussion.isResolved()) {
- unresolvedDiscussionCount += 1;
- }
- }
+ lastResolvedId: function () {
+ let lastId;
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
- if (this.discussionId && !this.discussion.isResolved()) {
- // If this is the last unresolved discussion on the diffs tab,
- // there are no discussions to jump to.
- if (unresolvedDiscussionCount === 1) {
- hasDiscussionsToJumpTo = false;
- }
- } else {
- // If there are no unresolved discussions on the diffs tab at all,
- // there are no discussions to jump to.
- if (unresolvedDiscussionCount === 0) {
- hasDiscussionsToJumpTo = false;
- }
- }
- } else if (activeTab !== 'notes') {
- // If we are on the commits or builds tabs,
- // there are no discussions to jump to.
- hasDiscussionsToJumpTo = false;
+ if (!discussion.isResolved()) {
+ lastId = discussion.id;
}
+ }
+ return lastId;
+ }
+ },
+ methods: {
+ jumpToNextUnresolvedDiscussion: function () {
+ let discussionsSelector;
+ let discussionIdsInScope;
+ let firstUnresolvedDiscussionId;
+ let nextUnresolvedDiscussionId;
+ let activeTab = window.mrTabs.currentAction;
+ let hasDiscussionsToJumpTo = true;
+ let jumpToFirstDiscussion = !this.discussionId;
+
+ const discussionIdsForElements = function(elements) {
+ return elements.map(function() {
+ return $(this).attr('data-discussion-id');
+ }).toArray();
+ };
- if (!hasDiscussionsToJumpTo) {
- // If there are no discussions to jump to on the current page,
- // switch to the notes tab and jump to the first disucssion there.
- window.mrTabs.activateTab('notes');
- activeTab = 'notes';
- jumpToFirstDiscussion = true;
- }
+ const discussions = this.discussions;
- if (activeTab === 'notes') {
- discussionsSelector = '.discussion[data-discussion-id]';
- discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
- }
+ if (activeTab === 'diffs') {
+ discussionsSelector = '.diffs .notes[data-discussion-id]';
+ discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+
+ let unresolvedDiscussionCount = 0;
- let currentDiscussionFound = false;
for (let i = 0; i < discussionIdsInScope.length; i += 1) {
const discussionId = discussionIdsInScope[i];
const discussion = discussions[discussionId];
+ if (discussion && !discussion.isResolved()) {
+ unresolvedDiscussionCount += 1;
+ }
+ }
- if (!discussion) {
- // Discussions for comments on commits in this MR don't have a resolved status.
- continue;
+ if (this.discussionId && !this.discussion.isResolved()) {
+ // If this is the last unresolved discussion on the diffs tab,
+ // there are no discussions to jump to.
+ if (unresolvedDiscussionCount === 1) {
+ hasDiscussionsToJumpTo = false;
+ }
+ } else {
+ // If there are no unresolved discussions on the diffs tab at all,
+ // there are no discussions to jump to.
+ if (unresolvedDiscussionCount === 0) {
+ hasDiscussionsToJumpTo = false;
}
+ }
+ } else if (activeTab !== 'notes') {
+ // If we are on the commits or builds tabs,
+ // there are no discussions to jump to.
+ hasDiscussionsToJumpTo = false;
+ }
- if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
- firstUnresolvedDiscussionId = discussionId;
+ if (!hasDiscussionsToJumpTo) {
+ // If there are no discussions to jump to on the current page,
+ // switch to the notes tab and jump to the first disucssion there.
+ window.mrTabs.activateTab('notes');
+ activeTab = 'notes';
+ jumpToFirstDiscussion = true;
+ }
- if (jumpToFirstDiscussion) {
- break;
- }
+ if (activeTab === 'notes') {
+ discussionsSelector = '.discussion[data-discussion-id]';
+ discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+ }
+
+ let currentDiscussionFound = false;
+ for (let i = 0; i < discussionIdsInScope.length; i += 1) {
+ const discussionId = discussionIdsInScope[i];
+ const discussion = discussions[discussionId];
+
+ if (!discussion) {
+ // Discussions for comments on commits in this MR don't have a resolved status.
+ continue;
+ }
+
+ if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
+ firstUnresolvedDiscussionId = discussionId;
+
+ if (jumpToFirstDiscussion) {
+ break;
}
+ }
- if (!jumpToFirstDiscussion) {
- if (currentDiscussionFound) {
- if (!discussion.isResolved()) {
- nextUnresolvedDiscussionId = discussionId;
- break;
- }
- else {
- continue;
- }
+ if (!jumpToFirstDiscussion) {
+ if (currentDiscussionFound) {
+ if (!discussion.isResolved()) {
+ nextUnresolvedDiscussionId = discussionId;
+ break;
}
-
- if (discussionId === this.discussionId) {
- currentDiscussionFound = true;
+ else {
+ continue;
}
}
+
+ if (discussionId === this.discussionId) {
+ currentDiscussionFound = true;
+ }
}
+ }
- nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
+ nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
- if (!nextUnresolvedDiscussionId) {
- return;
- }
+ if (!nextUnresolvedDiscussionId) {
+ return;
+ }
- let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
+ let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
- if (activeTab === 'notes') {
- $target = $target.closest('.note-discussion');
+ if (activeTab === 'notes') {
+ $target = $target.closest('.note-discussion');
- // If the next discussion is closed, toggle it open.
- if ($target.find('.js-toggle-content').is(':hidden')) {
- $target.find('.js-toggle-button i').trigger('click');
+ // If the next discussion is closed, toggle it open.
+ if ($target.find('.js-toggle-content').is(':hidden')) {
+ $target.find('.js-toggle-button i').trigger('click');
+ }
+ } else if (activeTab === 'diffs') {
+ // Resolved discussions are hidden in the diffs tab by default.
+ // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
+ // When jumping between unresolved discussions on the diffs tab, we show them.
+ $target.closest(".content").show();
+
+ $target = $target.closest("tr.notes_holder");
+ $target.show();
+
+ // If we are on the diffs tab, we don't scroll to the discussion itself, but to
+ // 4 diff lines above it: the line the discussion was in response to + 3 context
+ let prevEl;
+ for (let i = 0; i < 4; i += 1) {
+ prevEl = $target.prev();
+
+ // If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
+ if (!prevEl.hasClass("line_holder")) {
+ break;
}
- } else if (activeTab === 'diffs') {
- // Resolved discussions are hidden in the diffs tab by default.
- // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
- // When jumping between unresolved discussions on the diffs tab, we show them.
- $target.closest(".content").show();
-
- $target = $target.closest("tr.notes_holder");
- $target.show();
-
- // If we are on the diffs tab, we don't scroll to the discussion itself, but to
- // 4 diff lines above it: the line the discussion was in response to + 3 context
- let prevEl;
- for (let i = 0; i < 4; i += 1) {
- prevEl = $target.prev();
-
- // If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
- if (!prevEl.hasClass("line_holder")) {
- break;
- }
- $target = prevEl;
- }
+ $target = prevEl;
}
-
- $.scrollTo($target, {
- offset: 0
- });
}
- },
- created() {
- this.discussion = this.discussions[this.discussionId];
- },
- });
- Vue.component('jump-to-discussion', JumpToDiscussion);
-})();
+ $.scrollTo($target, {
+ offset: 0
+ });
+ }
+ },
+ created() {
+ this.discussion = this.discussions[this.discussionId];
+ },
+});
+
+Vue.component('jump-to-discussion', JumpToDiscussion);
diff --git a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
index 8eb0e10b832..e0c09aa0eee 100644
--- a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
@@ -2,29 +2,27 @@
import Vue from 'vue';
-(() => {
- const NewIssueForDiscussion = Vue.extend({
- props: {
- discussionId: {
- type: String,
- required: true,
- },
+const NewIssueForDiscussion = Vue.extend({
+ props: {
+ discussionId: {
+ type: String,
+ required: true,
},
- data() {
- return {
- discussions: CommentsStore.state,
- };
+ },
+ data() {
+ return {
+ discussions: CommentsStore.state,
+ };
+ },
+ computed: {
+ discussion() {
+ return this.discussions[this.discussionId];
},
- computed: {
- discussion() {
- return this.discussions[this.discussionId];
- },
- showButton() {
- if (this.discussion) return !this.discussion.isResolved();
- return false;
- },
+ showButton() {
+ if (this.discussion) return !this.discussion.isResolved();
+ return false;
},
- });
+ },
+});
- Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
-})();
+Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index dcfc40c1013..92f6fd654b3 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -5,117 +5,119 @@
import Vue from 'vue';
-(() => {
- const ResolveBtn = Vue.extend({
- props: {
- noteId: Number,
- discussionId: String,
- resolved: Boolean,
- canResolve: Boolean,
- resolvedBy: String,
- authorName: String,
- authorAvatar: String,
- noteTruncated: String,
+const ResolveBtn = Vue.extend({
+ props: {
+ noteId: Number,
+ discussionId: String,
+ resolved: Boolean,
+ canResolve: Boolean,
+ resolvedBy: String,
+ authorName: String,
+ authorAvatar: String,
+ noteTruncated: String,
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state,
+ loading: false
+ };
+ },
+ watch: {
+ 'discussions': {
+ handler: 'updateTooltip',
+ deep: true
+ }
+ },
+ computed: {
+ discussion: function () {
+ return this.discussions[this.discussionId];
},
- data: function () {
- return {
- discussions: CommentsStore.state,
- loading: false
- };
+ note: function () {
+ return this.discussion ? this.discussion.getNote(this.noteId) : {};
},
- watch: {
- 'discussions': {
- handler: 'updateTooltip',
- deep: true
+ buttonText: function () {
+ if (this.isResolved) {
+ return `Resolved by ${this.resolvedByName}`;
+ } else if (this.canResolve) {
+ return 'Mark as resolved';
+ } else {
+ return 'Unable to resolve';
}
},
- computed: {
- discussion: function () {
- return this.discussions[this.discussionId];
- },
- note: function () {
- return this.discussion ? this.discussion.getNote(this.noteId) : {};
- },
- buttonText: function () {
- if (this.isResolved) {
- return `Resolved by ${this.resolvedByName}`;
- } else if (this.canResolve) {
- return 'Mark as resolved';
- } else {
- return 'Unable to resolve';
- }
- },
- isResolved: function () {
- if (this.note) {
- return this.note.resolved;
- } else {
- return false;
- }
- },
- resolvedByName: function () {
- return this.note.resolved_by;
- },
+ isResolved: function () {
+ if (this.note) {
+ return this.note.resolved;
+ } else {
+ return false;
+ }
+ },
+ resolvedByName: function () {
+ return this.note.resolved_by;
},
- methods: {
- updateTooltip: function () {
- this.$nextTick(() => {
- $(this.$refs.button)
- .tooltip('hide')
- .tooltip('fixTitle');
- });
- },
- resolve: function () {
- if (!this.canResolve) return;
+ },
+ methods: {
+ updateTooltip: function () {
+ this.$nextTick(() => {
+ $(this.$refs.button)
+ .tooltip('hide')
+ .tooltip('fixTitle');
+ });
+ },
+ resolve: function () {
+ const errorFlashMsg = 'An error occurred when trying to resolve a comment. Please try again.';
- let promise;
- this.loading = true;
+ if (!this.canResolve) return;
- if (this.isResolved) {
- promise = ResolveService
- .unresolve(this.noteId);
- } else {
- promise = ResolveService
- .resolve(this.noteId);
- }
+ let promise;
+ this.loading = true;
- promise.then((response) => {
- this.loading = false;
+ if (this.isResolved) {
+ promise = ResolveService
+ .unresolve(this.noteId);
+ } else {
+ promise = ResolveService
+ .resolve(this.noteId);
+ }
- if (response.status === 200) {
- const data = response.json();
- const resolved_by = data ? data.resolved_by : null;
+ promise.then((response) => {
+ this.loading = false;
- CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
- this.discussion.updateHeadline(data);
- } else {
- new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
- }
+ if (response.status === 200) {
+ const data = response.json();
+ const resolved_by = data ? data.resolved_by : null;
- this.updateTooltip();
- });
- }
- },
- mounted: function () {
- $(this.$refs.button).tooltip({
- container: 'body'
- });
- },
- beforeDestroy: function () {
- CommentsStore.delete(this.discussionId, this.noteId);
- },
- created: function () {
- CommentsStore.create({
- discussionId: this.discussionId,
- noteId: this.noteId,
- canResolve: this.canResolve,
- resolved: this.resolved,
- resolvedBy: this.resolvedBy,
- authorName: this.authorName,
- authorAvatar: this.authorAvatar,
- noteTruncated: this.noteTruncated,
+ CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
+ this.discussion.updateHeadline(data);
+ } else {
+ new Flash(errorFlashMsg);
+ }
+
+ this.updateTooltip();
+ }).catch(() => {
+ new Flash(errorFlashMsg);
});
}
- });
+ },
+ mounted: function () {
+ $(this.$refs.button).tooltip({
+ container: 'body'
+ });
+ },
+ beforeDestroy: function () {
+ CommentsStore.delete(this.discussionId, this.noteId);
+ },
+ created: function () {
+ CommentsStore.create({
+ discussionId: this.discussionId,
+ noteId: this.noteId,
+ canResolve: this.canResolve,
+ resolved: this.resolved,
+ resolvedBy: this.resolvedBy,
+ authorName: this.authorName,
+ authorAvatar: this.authorAvatar,
+ noteTruncated: this.noteTruncated,
+ });
+ }
+});
- Vue.component('resolve-btn', ResolveBtn);
-})();
+Vue.component('resolve-btn', ResolveBtn);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js
index 27147ac6b5c..96e5a440357 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js
@@ -4,24 +4,22 @@
import Vue from 'vue';
-((w) => {
- w.ResolveCount = Vue.extend({
- mixins: [DiscussionMixins],
- props: {
- loggedOut: Boolean
+window.ResolveCount = Vue.extend({
+ mixins: [DiscussionMixins],
+ props: {
+ loggedOut: Boolean
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state
+ };
+ },
+ computed: {
+ allResolved: function () {
+ return this.resolvedDiscussionCount === this.discussionCount;
},
- data: function () {
- return {
- discussions: CommentsStore.state
- };
- },
- computed: {
- allResolved: function () {
- return this.resolvedDiscussionCount === this.discussionCount;
- },
- resolvedCountText() {
- return this.discussionCount === 1 ? 'discussion' : 'discussions';
- }
+ resolvedCountText() {
+ return this.discussionCount === 1 ? 'discussion' : 'discussions';
}
- });
-})(window);
+ }
+});
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
index a964b7d0c6b..6a036e96171 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
@@ -4,59 +4,57 @@
import Vue from 'vue';
-(() => {
- const ResolveDiscussionBtn = Vue.extend({
- props: {
- discussionId: String,
- mergeRequestId: Number,
- canResolve: Boolean,
- },
- data: function() {
- return {
- discussion: {},
- };
+const ResolveDiscussionBtn = Vue.extend({
+ props: {
+ discussionId: String,
+ mergeRequestId: Number,
+ canResolve: Boolean,
+ },
+ data: function() {
+ return {
+ discussion: {},
+ };
+ },
+ computed: {
+ showButton: function () {
+ if (this.discussion) {
+ return this.discussion.isResolvable();
+ } else {
+ return false;
+ }
},
- computed: {
- showButton: function () {
- if (this.discussion) {
- return this.discussion.isResolvable();
- } else {
- return false;
- }
- },
- isDiscussionResolved: function () {
- if (this.discussion) {
- return this.discussion.isResolved();
- } else {
- return false;
- }
- },
- buttonText: function () {
- if (this.isDiscussionResolved) {
- return "Unresolve discussion";
- } else {
- return "Resolve discussion";
- }
- },
- loading: function () {
- if (this.discussion) {
- return this.discussion.loading;
- } else {
- return false;
- }
+ isDiscussionResolved: function () {
+ if (this.discussion) {
+ return this.discussion.isResolved();
+ } else {
+ return false;
}
},
- methods: {
- resolve: function () {
- ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
+ buttonText: function () {
+ if (this.isDiscussionResolved) {
+ return "Unresolve discussion";
+ } else {
+ return "Resolve discussion";
}
},
- created: function () {
- CommentsStore.createDiscussion(this.discussionId, this.canResolve);
-
- this.discussion = CommentsStore.state[this.discussionId];
+ loading: function () {
+ if (this.discussion) {
+ return this.discussion.loading;
+ } else {
+ return false;
+ }
+ }
+ },
+ methods: {
+ resolve: function () {
+ ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
}
- });
+ },
+ created: function () {
+ CommentsStore.createDiscussion(this.discussionId, this.canResolve);
+
+ this.discussion = CommentsStore.state[this.discussionId];
+ }
+});
- Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
-})();
+Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js b/app/assets/javascripts/diff_notes/mixins/discussion.js
index 3c08c222f46..36c4abf02cf 100644
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js
@@ -1,37 +1,35 @@
/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */
-((w) => {
- w.DiscussionMixins = {
- computed: {
- discussionCount: function () {
- return Object.keys(this.discussions).length;
- },
- resolvedDiscussionCount: function () {
- let resolvedCount = 0;
+window.DiscussionMixins = {
+ computed: {
+ discussionCount: function () {
+ return Object.keys(this.discussions).length;
+ },
+ resolvedDiscussionCount: function () {
+ let resolvedCount = 0;
- for (const discussionId in this.discussions) {
- const discussion = this.discussions[discussionId];
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
- if (discussion.isResolved()) {
- resolvedCount += 1;
- }
+ if (discussion.isResolved()) {
+ resolvedCount += 1;
}
+ }
- return resolvedCount;
- },
- unresolvedDiscussionCount: function () {
- let unresolvedCount = 0;
+ return resolvedCount;
+ },
+ unresolvedDiscussionCount: function () {
+ let unresolvedCount = 0;
- for (const discussionId in this.discussions) {
- const discussion = this.discussions[discussionId];
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
- if (!discussion.isResolved()) {
- unresolvedCount += 1;
- }
+ if (!discussion.isResolved()) {
+ unresolvedCount += 1;
}
-
- return unresolvedCount;
}
+
+ return unresolvedCount;
}
- };
-})(window);
+ }
+};
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index bfa4fc9037a..4ea6ba8a73d 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -9,76 +9,76 @@ require('../../vue_shared/vue_resource_interceptor');
Vue.use(VueResource);
-(() => {
- window.gl = window.gl || {};
+window.gl = window.gl || {};
- class ResolveServiceClass {
- constructor(root) {
- this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`);
- this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`);
- }
-
- resolve(noteId) {
- return this.noteResource.save({ noteId }, {});
- }
+class ResolveServiceClass {
+ constructor(root) {
+ this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`);
+ this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`);
+ }
- unresolve(noteId) {
- return this.noteResource.delete({ noteId }, {});
- }
+ resolve(noteId) {
+ return this.noteResource.save({ noteId }, {});
+ }
- toggleResolveForDiscussion(mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId];
- const isResolved = discussion.isResolved();
- let promise;
+ unresolve(noteId) {
+ return this.noteResource.delete({ noteId }, {});
+ }
- if (isResolved) {
- promise = this.unResolveAll(mergeRequestId, discussionId);
- } else {
- promise = this.resolveAll(mergeRequestId, discussionId);
- }
+ toggleResolveForDiscussion(mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
+ const isResolved = discussion.isResolved();
+ let promise;
- promise.then((response) => {
- discussion.loading = false;
+ if (isResolved) {
+ promise = this.unResolveAll(mergeRequestId, discussionId);
+ } else {
+ promise = this.resolveAll(mergeRequestId, discussionId);
+ }
- if (response.status === 200) {
- const data = response.json();
- const resolved_by = data ? data.resolved_by : null;
+ promise.then((response) => {
+ discussion.loading = false;
- if (isResolved) {
- discussion.unResolveAllNotes();
- } else {
- discussion.resolveAllNotes(resolved_by);
- }
+ if (response.status === 200) {
+ const data = response.json();
+ const resolved_by = data ? data.resolved_by : null;
- discussion.updateHeadline(data);
+ if (isResolved) {
+ discussion.unResolveAllNotes();
} else {
- new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
+ discussion.resolveAllNotes(resolved_by);
}
- });
- }
- resolveAll(mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId];
+ discussion.updateHeadline(data);
+ } else {
+ throw new Error('An error occurred when trying to resolve discussion.');
+ }
+ }).catch(() => {
+ new Flash('An error occurred when trying to resolve a discussion. Please try again.');
+ });
+ }
- discussion.loading = true;
+ resolveAll(mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
- return this.discussionResource.save({
- mergeRequestId,
- discussionId
- }, {});
- }
+ discussion.loading = true;
+
+ return this.discussionResource.save({
+ mergeRequestId,
+ discussionId
+ }, {});
+ }
- unResolveAll(mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId];
+ unResolveAll(mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
- discussion.loading = true;
+ discussion.loading = true;
- return this.discussionResource.delete({
- mergeRequestId,
- discussionId
- }, {});
- }
+ return this.discussionResource.delete({
+ mergeRequestId,
+ discussionId
+ }, {});
}
+}
- gl.DiffNotesResolveServiceClass = ResolveServiceClass;
-})();
+gl.DiffNotesResolveServiceClass = ResolveServiceClass;
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js
index e6cbda56c91..d802db7d3af 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js
+++ b/app/assets/javascripts/diff_notes/stores/comments.js
@@ -3,56 +3,54 @@
import Vue from 'vue';
-((w) => {
- w.CommentsStore = {
- state: {},
- get: function (discussionId, noteId) {
- return this.state[discussionId].getNote(noteId);
- },
- createDiscussion: function (discussionId, canResolve) {
- let discussion = this.state[discussionId];
- if (!this.state[discussionId]) {
- discussion = new DiscussionModel(discussionId);
- Vue.set(this.state, discussionId, discussion);
- }
+window.CommentsStore = {
+ state: {},
+ get: function (discussionId, noteId) {
+ return this.state[discussionId].getNote(noteId);
+ },
+ createDiscussion: function (discussionId, canResolve) {
+ let discussion = this.state[discussionId];
+ if (!this.state[discussionId]) {
+ discussion = new DiscussionModel(discussionId);
+ Vue.set(this.state, discussionId, discussion);
+ }
- if (canResolve !== undefined) {
- discussion.canResolve = canResolve;
- }
+ if (canResolve !== undefined) {
+ discussion.canResolve = canResolve;
+ }
- return discussion;
- },
- create: function (noteObj) {
- const discussion = this.createDiscussion(noteObj.discussionId);
+ return discussion;
+ },
+ create: function (noteObj) {
+ const discussion = this.createDiscussion(noteObj.discussionId);
+
+ discussion.createNote(noteObj);
+ },
+ update: function (discussionId, noteId, resolved, resolved_by) {
+ const discussion = this.state[discussionId];
+ const note = discussion.getNote(noteId);
+ note.resolved = resolved;
+ note.resolved_by = resolved_by;
+ },
+ delete: function (discussionId, noteId) {
+ const discussion = this.state[discussionId];
+ discussion.deleteNote(noteId);
+
+ if (discussion.notesCount() === 0) {
+ Vue.delete(this.state, discussionId);
+ }
+ },
+ unresolvedDiscussionIds: function () {
+ const ids = [];
- discussion.createNote(noteObj);
- },
- update: function (discussionId, noteId, resolved, resolved_by) {
- const discussion = this.state[discussionId];
- const note = discussion.getNote(noteId);
- note.resolved = resolved;
- note.resolved_by = resolved_by;
- },
- delete: function (discussionId, noteId) {
+ for (const discussionId in this.state) {
const discussion = this.state[discussionId];
- discussion.deleteNote(noteId);
- if (discussion.notesCount() === 0) {
- Vue.delete(this.state, discussionId);
+ if (!discussion.isResolved()) {
+ ids.push(discussion.id);
}
- },
- unresolvedDiscussionIds: function () {
- const ids = [];
-
- for (const discussionId in this.state) {
- const discussion = this.state[discussionId];
-
- if (!discussion.isResolved()) {
- ids.push(discussion.id);
- }
- }
-
- return ids;
}
- };
-})(window);
+
+ return ids;
+ }
+};
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index f277e1dddc7..20db2698ba8 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -33,6 +33,7 @@
/* global Labels */
/* global Shortcuts */
/* global Sidebar */
+/* global ShortcutsWiki */
import Issue from './issue';
@@ -46,6 +47,7 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
+import ShortcutsWiki from './shortcuts_wiki';
const ShortcutsBlob = require('./shortcuts_blob');
@@ -89,11 +91,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
fileBlobPermalinkUrl,
});
- new BlobForkSuggestion(
- document.querySelector('.js-edit-blob-link-fork-toggler'),
- document.querySelector('.js-cancel-fork-suggestion'),
- document.querySelector('.js-file-fork-suggestion-section'),
- );
+ new BlobForkSuggestion({
+ openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'),
+ forkButtons: document.querySelectorAll('.js-fork-suggestion-button'),
+ cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
+ suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'),
+ actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'),
+ });
}
switch (page) {
@@ -148,13 +152,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:milestones:new':
case 'projects:milestones:edit':
case 'projects:milestones:update':
+ case 'groups:milestones:new':
+ case 'groups:milestones:edit':
+ case 'groups:milestones:update':
new ZenMode();
new gl.DueDateSelectors();
new gl.GLForm($('.milestone-form'));
break;
- case 'groups:milestones:new':
- new ZenMode();
- break;
case 'projects:compare:show':
new gl.Diff();
break;
@@ -365,6 +369,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'admin':
new Admin();
switch (path[1]) {
+ case 'cohorts':
+ new gl.UsagePing();
+ break;
case 'groups':
new UsersSelect();
break;
@@ -416,7 +423,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
break;
case 'wikis':
new gl.Wikis();
- shortcut_handler = new ShortcutsNavigation();
+ shortcut_handler = new ShortcutsWiki();
new ZenMode();
new gl.GLForm($('.wiki-form'));
break;
diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js
index 34b1aee73c7..0579b5de459 100644
--- a/app/assets/javascripts/droplab/constants.js
+++ b/app/assets/javascripts/droplab/constants.js
@@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active';
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
+const IGNORE_CLASS = 'droplab-item-ignore';
export {
DATA_TRIGGER,
@@ -10,4 +11,5 @@ export {
SELECTED_CLASS,
ACTIVE_CLASS,
TEMPLATE_REGEX,
+ IGNORE_CLASS,
};
diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js
index 084d57e2e1f..de3927d683c 100644
--- a/app/assets/javascripts/droplab/drop_down.js
+++ b/app/assets/javascripts/droplab/drop_down.js
@@ -1,7 +1,7 @@
/* eslint-disable */
import utils from './utils';
-import { SELECTED_CLASS } from './constants';
+import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
var DropDown = function(list) {
this.currentIndex = 0;
@@ -36,6 +36,7 @@ Object.assign(DropDown.prototype, {
clickEvent: function(e) {
if (e.target.tagName === 'UL') return;
+ if (e.target.classList.contains(IGNORE_CLASS)) return;
var selected = utils.closest(e.target, 'LI');
if (!selected) return;
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index df0e3f46827..b70d242269d 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -38,6 +38,9 @@ window.DropzoneInput = (function() {
"opacity": 0,
"display": "none"
});
+
+ if (!project_uploads_path) return;
+
dropzone = form_dropzone.dropzone({
url: project_uploads_path,
dictDefaultMessage: "",
@@ -130,13 +133,15 @@ window.DropzoneInput = (function() {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
var formattedText = text;
if (shouldPad) formattedText += "\n\n";
- caretStart = $(child)[0].selectionStart;
- caretEnd = $(child)[0].selectionEnd;
+ const textarea = child.get(0);
+ caretStart = textarea.selectionStart;
+ caretEnd = textarea.selectionEnd;
textEnd = $(child).val().length;
beforeSelection = $(child).val().substring(0, caretStart);
afterSelection = $(child).val().substring(caretEnd, textEnd);
$(child).val(beforeSelection + formattedText + afterSelection);
- child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
+ textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
+ textarea.style.height = `${textarea.scrollHeight}px`;
return form_textarea.trigger("input");
};
getFilename = function(e) {
@@ -180,7 +185,7 @@ window.DropzoneInput = (function() {
};
insertToTextArea = function(filename, url) {
return $(child).val(function(index, val) {
- return val.replace("{{" + filename + "}}", url + "\n");
+ return val.replace("{{" + filename + "}}", url);
});
};
appendToTextArea = function(url) {
@@ -215,6 +220,7 @@ window.DropzoneInput = (function() {
form.find(".markdown-selector").click(function(e) {
e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click();
+ form_textarea.focus();
});
}
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index db10b383913..a8fc5b41fb4 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -115,11 +115,13 @@ class DueDateSelect {
this.$dropdown.trigger('loading.gl.dropdown');
this.$selectbox.hide();
this.$value.css('display', '');
+ const fadeOutLoader = () => {
+ this.$loading.fadeOut();
+ };
gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
- .then(() => {
- this.$loading.fadeOut();
- });
+ .then(fadeOutLoader)
+ .catch(fadeOutLoader);
}
submitSelectedDate(isDropdown) {
@@ -168,8 +170,9 @@ class DueDateSelectors {
const $datePicker = $(this);
const calendar = new Pikaday({
field: $datePicker.get(0),
- theme: 'gitlab-theme',
+ theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
+ container: $datePicker.parent().get(0),
onSelect(dateText) {
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js
index 0518422e475..f7175e412da 100644
--- a/app/assets/javascripts/environments/components/environment.js
+++ b/app/assets/javascripts/environments/components/environment.js
@@ -2,7 +2,7 @@
/* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
-import EnvironmentTable from './environments_table';
+import EnvironmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import TablePaginationComponent from '../../vue_shared/components/table_pagination';
import '../../lib/utils/common_utils';
diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js
deleted file mode 100644
index 1418e8d86ee..00000000000
--- a/app/assets/javascripts/environments/components/environment_actions.js
+++ /dev/null
@@ -1,97 +0,0 @@
-/* global Flash */
-/* eslint-disable no-new */
-
-import playIconSvg from 'icons/_icon_play.svg';
-import eventHub from '../event_hub';
-
-export default {
- props: {
- actions: {
- type: Array,
- required: false,
- default: () => [],
- },
-
- service: {
- type: Object,
- required: true,
- },
- },
-
- data() {
- return {
- playIconSvg,
- isLoading: false,
- };
- },
-
- computed: {
- title() {
- return 'Deploy to...';
- },
- },
-
- methods: {
- onClickAction(endpoint) {
- this.isLoading = true;
-
- this.service.postAction(endpoint)
- .then(() => {
- this.isLoading = false;
- eventHub.$emit('refreshEnvironments');
- })
- .catch(() => {
- this.isLoading = false;
- new Flash('An error occured while making the request.');
- });
- },
-
- isActionDisabled(action) {
- if (action.playable === undefined) {
- return false;
- }
-
- return !action.playable;
- },
- },
-
- template: `
- <div class="btn-group" role="group">
- <button
- type="button"
- class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
- data-container="body"
- data-toggle="dropdown"
- :title="title"
- :aria-label="title"
- :disabled="isLoading">
- <span>
- <span v-html="playIconSvg"></span>
- <i
- class="fa fa-caret-down"
- aria-hidden="true"/>
- <i
- v-if="isLoading"
- class="fa fa-spinner fa-spin"
- aria-hidden="true"/>
- </span>
- </button>
-
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for="action in actions">
- <button
- type="button"
- class="js-manual-action-link no-btn btn"
- @click="onClickAction(action.play_path)"
- :class="{ 'disabled': isActionDisabled(action) }"
- :disabled="isActionDisabled(action)">
- ${playIconSvg}
- <span>
- {{action.name}}
- </span>
- </button>
- </li>
- </ul>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
new file mode 100644
index 00000000000..e81c97260d7
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -0,0 +1,103 @@
+<script>
+/* global Flash */
+/* eslint-disable no-new */
+
+import playIconSvg from 'icons/_icon_play.svg';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ playIconSvg,
+ isLoading: false,
+ };
+ },
+
+ computed: {
+ title() {
+ return 'Deploy to...';
+ },
+ },
+
+ methods: {
+ onClickAction(endpoint) {
+ this.isLoading = true;
+
+ $(this.$refs.tooltip).tooltip('destroy');
+
+ this.service.postAction(endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshEnvironments');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+
+ isActionDisabled(action) {
+ if (action.playable === undefined) {
+ return false;
+ }
+
+ return !action.playable;
+ },
+ },
+};
+</script>
+<template>
+ <div
+ class="btn-group"
+ role="group">
+ <button
+ type="button"
+ class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
+ data-container="body"
+ data-toggle="dropdown"
+ ref="tooltip"
+ :title="title"
+ :aria-label="title"
+ :disabled="isLoading">
+ <span>
+ <span v-html="playIconSvg"></span>
+ <i
+ class="fa fa-caret-down"
+ aria-hidden="true"/>
+ <i
+ v-if="isLoading"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"/>
+ </span>
+ </button>
+
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="action in actions">
+ <button
+ type="button"
+ class="js-manual-action-link no-btn btn"
+ @click="onClickAction(action.play_path)"
+ :class="{ disabled: isActionDisabled(action) }"
+ :disabled="isActionDisabled(action)">
+ <span v-html="playIconSvg"></span>
+ <span>
+ {{action.name}}
+ </span>
+ </button>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js
deleted file mode 100644
index d79b916c360..00000000000
--- a/app/assets/javascripts/environments/components/environment_external_url.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Renders the external url link in environments table.
- */
-export default {
- props: {
- externalUrl: {
- type: String,
- default: '',
- },
- },
-
- computed: {
- title() {
- return 'Open';
- },
- },
-
- template: `
- <a
- class="btn external-url has-tooltip"
- data-container="body"
- :href="externalUrl"
- target="_blank"
- rel="noopener noreferrer nofollow"
- :title="title"
- :aria-label="title">
- <i class="fa fa-external-link" aria-hidden="true"></i>
- </a>
- `,
-};
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
new file mode 100644
index 00000000000..eaeec2bc53c
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -0,0 +1,33 @@
+<script>
+/**
+ * Renders the external url link in environments table.
+ */
+export default {
+ props: {
+ externalUrl: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ title() {
+ return 'Open';
+ },
+ },
+};
+</script>
+<template>
+ <a
+ class="btn external-url has-tooltip"
+ data-container="body"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ :title="title"
+ :aria-label="title"
+ :href="externalUrl">
+ <i
+ class="fa fa-external-link"
+ aria-hidden="true" />
+ </a>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.vue
index d9b49287dec..73679de6039 100644
--- a/app/assets/javascripts/environments/components/environment_item.js
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,11 +1,12 @@
+<script>
import Timeago from 'timeago.js';
import '../../lib/utils/text_utility';
-import ActionsComponent from './environment_actions';
-import ExternalUrlComponent from './environment_external_url';
-import StopComponent from './environment_stop';
-import RollbackComponent from './environment_rollback';
-import TerminalButtonComponent from './environment_terminal_button';
-import MonitoringButtonComponent from './environment_monitoring';
+import ActionsComponent from './environment_actions.vue';
+import ExternalUrlComponent from './environment_external_url.vue';
+import StopComponent from './environment_stop.vue';
+import RollbackComponent from './environment_rollback.vue';
+import TerminalButtonComponent from './environment_terminal_button.vue';
+import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit';
import eventHub from '../event_hub';
@@ -434,117 +435,140 @@ export default {
eventHub.$emit('toggleFolder', this.model, this.folderUrl);
},
},
-
- template: `
- <tr :class="{ 'js-child-row': model.isChildren }">
- <td>
- <a v-if="!model.isFolder"
- class="environment-name"
- :class="{ 'prepend-left-default': model.isChildren }"
- :href="environmentPath">
- {{model.name}}
- </a>
- <span v-else
- class="folder-name"
- @click="onClickFolder"
- role="button">
-
- <span class="folder-icon">
- <i
- v-show="model.isOpen"
- class="fa fa-caret-down"
- aria-hidden="true" />
- <i
- v-show="!model.isOpen"
- class="fa fa-caret-right"
- aria-hidden="true"/>
- </span>
-
- <span class="folder-icon">
- <i class="fa fa-folder" aria-hidden="true"></i>
- </span>
-
- <span>
- {{model.folderName}}
- </span>
-
- <span class="badge">
- {{model.size}}
- </span>
+};
+</script>
+<template>
+ <tr :class="{ 'js-child-row': model.isChildren }">
+ <td>
+ <a
+ v-if="!model.isFolder"
+ class="environment-name"
+ :class="{ 'prepend-left-default': model.isChildren }"
+ :href="environmentPath">
+ {{model.name}}
+ </a>
+ <span
+ v-else
+ class="folder-name"
+ @click="onClickFolder"
+ role="button">
+
+ <span class="folder-icon">
+ <i
+ v-show="model.isOpen"
+ class="fa fa-caret-down"
+ aria-hidden="true" />
+ <i
+ v-show="!model.isOpen"
+ class="fa fa-caret-right"
+ aria-hidden="true"/>
</span>
- </td>
- <td class="deployment-column">
- <span v-if="shouldRenderDeploymentID">
- {{deploymentInternalId}}
+ <span class="folder-icon">
+ <i
+ class="fa fa-folder"
+ aria-hidden="true" />
</span>
- <span v-if="!model.isFolder && deploymentHasUser">
- by
- <a :href="deploymentUser.web_url" class="js-deploy-user-container">
- <img class="avatar has-tooltip s20"
- :src="deploymentUser.avatar_url"
- :alt="userImageAltDescription"
- :title="deploymentUser.username" />
- </a>
+ <span>
+ {{model.folderName}}
</span>
- </td>
- <td class="environments-build-cell">
- <a v-if="shouldRenderBuildName"
- class="build-link"
- :href="buildPath">
- {{buildName}}
- </a>
- </td>
-
- <td>
- <div v-if="!model.isFolder && hasLastDeploymentKey" class="js-commit-component">
- <commit-component
- :tag="commitTag"
- :commit-ref="commitRef"
- :commit-url="commitUrl"
- :short-sha="commitShortSha"
- :title="commitTitle"
- :author="commitAuthor"/>
- </div>
- <p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title">
- No deployments yet
- </p>
- </td>
-
- <td>
- <span v-if="!model.isFolder && canShowDate"
- class="environment-created-date-timeago">
- {{createdDate}}
+ <span class="badge">
+ {{model.size}}
</span>
- </td>
-
- <td class="environments-actions">
- <div v-if="!model.isFolder" class="btn-group pull-right" role="group">
- <actions-component v-if="hasManualActions && canCreateDeployment"
- :service="service"
- :actions="manualActions"/>
-
- <external-url-component v-if="externalURL && canReadEnvironment"
- :external-url="externalURL"/>
-
- <monitoring-button-component v-if="monitoringUrl && canReadEnvironment"
- :monitoring-url="monitoringUrl"/>
-
- <terminal-button-component v-if="model && model.terminal_path"
- :terminal-path="model.terminal_path"/>
-
- <stop-component v-if="hasStopAction && canCreateDeployment"
- :stop-url="model.stop_path"
- :service="service"/>
-
- <rollback-component v-if="canRetry && canCreateDeployment"
- :is-last-deployment="isLastDeployment"
- :retry-url="retryUrl"
- :service="service"/>
- </div>
- </td>
- </tr>
- `,
-};
+ </span>
+ </td>
+
+ <td class="deployment-column">
+ <span v-if="shouldRenderDeploymentID">
+ {{deploymentInternalId}}
+ </span>
+
+ <span v-if="!model.isFolder && deploymentHasUser">
+ by
+ <a
+ :href="deploymentUser.web_url"
+ class="js-deploy-user-container">
+ <img
+ class="avatar has-tooltip s20"
+ :src="deploymentUser.avatar_url"
+ :alt="userImageAltDescription"
+ :title="deploymentUser.username" />
+ </a>
+ </span>
+ </td>
+
+ <td class="environments-build-cell">
+ <a
+ v-if="shouldRenderBuildName"
+ class="build-link"
+ :href="buildPath">
+ {{buildName}}
+ </a>
+ </td>
+
+ <td>
+ <div
+ v-if="!model.isFolder && hasLastDeploymentKey"
+ class="js-commit-component">
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"/>
+ </div>
+ <p
+ v-if="!model.isFolder && !hasLastDeploymentKey"
+ class="commit-title">
+ No deployments yet
+ </p>
+ </td>
+
+ <td>
+ <span
+ v-if="!model.isFolder && canShowDate"
+ class="environment-created-date-timeago">
+ {{createdDate}}
+ </span>
+ </td>
+
+ <td class="environments-actions">
+ <div
+ v-if="!model.isFolder"
+ class="btn-group pull-right"
+ role="group">
+
+ <actions-component
+ v-if="hasManualActions && canCreateDeployment"
+ :service="service"
+ :actions="manualActions"/>
+
+ <external-url-component
+ v-if="externalURL && canReadEnvironment"
+ :external-url="externalURL"/>
+
+ <monitoring-button-component
+ v-if="monitoringUrl && canReadEnvironment"
+ :monitoring-url="monitoringUrl"/>
+
+ <terminal-button-component
+ v-if="model && model.terminal_path"
+ :terminal-path="model.terminal_path"/>
+
+ <stop-component
+ v-if="hasStopAction && canCreateDeployment"
+ :stop-url="model.stop_path"
+ :service="service"/>
+
+ <rollback-component
+ v-if="canRetry && canCreateDeployment"
+ :is-last-deployment="isLastDeployment"
+ :retry-url="retryUrl"
+ :service="service"/>
+ </div>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.js b/app/assets/javascripts/environments/components/environment_monitoring.js
deleted file mode 100644
index 064e2fc7434..00000000000
--- a/app/assets/javascripts/environments/components/environment_monitoring.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Renders the Monitoring (Metrics) link in environments table.
- */
-export default {
- props: {
- monitoringUrl: {
- type: String,
- default: '',
- required: true,
- },
- },
-
- computed: {
- title() {
- return 'Monitoring';
- },
- },
-
- template: `
- <a
- class="btn monitoring-url has-tooltip"
- data-container="body"
- :href="monitoringUrl"
- target="_blank"
- rel="noopener noreferrer nofollow"
- :title="title"
- :aria-label="title">
- <i class="fa fa-area-chart" aria-hidden="true"></i>
- </a>
- `,
-};
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
new file mode 100644
index 00000000000..4b030a27900
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -0,0 +1,33 @@
+<script>
+/**
+ * Renders the Monitoring (Metrics) link in environments table.
+ */
+export default {
+ props: {
+ monitoringUrl: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ title() {
+ return 'Monitoring';
+ },
+ },
+};
+</script>
+<template>
+ <a
+ class="btn monitoring-url has-tooltip"
+ data-container="body"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ :href="monitoringUrl"
+ :title="title"
+ :aria-label="title">
+ <i
+ class="fa fa-area-chart"
+ aria-hidden="true" />
+ </a>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js b/app/assets/javascripts/environments/components/environment_rollback.vue
index baa15d9e5b5..f139f24036f 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.js
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -1,3 +1,4 @@
+<script>
/* global Flash */
/* eslint-disable no-new */
/**
@@ -36,6 +37,8 @@ export default {
onClick() {
this.isLoading = true;
+ $(this.$el).tooltip('destroy');
+
this.service.postAction(this.retryUrl)
.then(() => {
this.isLoading = false;
@@ -47,21 +50,25 @@ export default {
});
},
},
+};
+</script>
+<template>
+ <button
+ type="button"
+ class="btn"
+ @click="onClick"
+ :disabled="isLoading">
- template: `
- <button type="button"
- class="btn"
- @click="onClick"
- :disabled="isLoading">
-
- <span v-if="isLastDeployment">
- Re-deploy
- </span>
- <span v-else>
- Rollback
- </span>
+ <span v-if="isLastDeployment">
+ Re-deploy
+ </span>
+ <span v-else>
+ Rollback
+ </span>
- <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
- </button>
- `,
-};
+ <i
+ v-if="isLoading"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ </button>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.vue
index 47102692024..11e9aff7b92 100644
--- a/app/assets/javascripts/environments/components/environment_stop.js
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -1,3 +1,4 @@
+<script>
/* global Flash */
/* eslint-disable no-new, no-alert */
/**
@@ -36,6 +37,8 @@ export default {
if (confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
+ $(this.$el).tooltip('destroy');
+
this.service.postAction(this.retryUrl)
.then(() => {
this.isLoading = false;
@@ -48,17 +51,23 @@ export default {
}
},
},
-
- template: `
- <button type="button"
- class="btn stop-env-link has-tooltip"
- data-container="body"
- @click="onClick"
- :disabled="isLoading"
- :title="title"
- :aria-label="title">
- <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
- <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
- </button>
- `,
};
+</script>
+<template>
+ <button
+ type="button"
+ class="btn stop-env-link has-tooltip"
+ data-container="body"
+ @click="onClick"
+ :disabled="isLoading"
+ :title="title"
+ :aria-label="title">
+ <i
+ class="fa fa-stop stop-env-icon"
+ aria-hidden="true" />
+ <i
+ v-if="isLoading"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ </button>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 092a50a0d6f..c8c1f17d4d8 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.js
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -1,3 +1,4 @@
+<script>
/**
* Renders a terminal button to open a web terminal.
* Used in environments table.
@@ -24,14 +25,15 @@ export default {
return 'Terminal';
},
},
-
- template: `
- <a class="btn terminal-button has-tooltip"
- data-container="body"
- :title="title"
- :aria-label="title"
- :href="terminalPath">
- ${terminalIconSvg}
- </a>
- `,
};
+</script>
+<template>
+ <a
+ class="btn terminal-button has-tooltip"
+ data-container="body"
+ :title="title"
+ :aria-label="title"
+ :href="terminalPath"
+ v-html="terminalIconSvg">
+ </a>
+</template>
diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js
deleted file mode 100644
index 5e6af3a1d45..00000000000
--- a/app/assets/javascripts/environments/components/environments_table.js
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * Render environments table.
- */
-import EnvironmentTableRowComponent from './environment_item';
-
-export default {
- components: {
- 'environment-item': EnvironmentTableRowComponent,
- },
-
- props: {
- environments: {
- type: Array,
- required: true,
- default: () => ([]),
- },
-
- canReadEnvironment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- canCreateDeployment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- service: {
- type: Object,
- required: true,
- },
-
- isLoadingFolderContent: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-
- methods: {
- folderUrl(model) {
- return `${window.location.pathname}/folders/${model.folderName}`;
- },
- },
-
- template: `
- <table class="table ci-table">
- <thead>
- <tr>
- <th class="environments-name">Environment</th>
- <th class="environments-deploy">Last deployment</th>
- <th class="environments-build">Job</th>
- <th class="environments-commit">Commit</th>
- <th class="environments-date">Updated</th>
- <th class="environments-actions"></th>
- </tr>
- </thead>
- <tbody>
- <template v-for="model in environments"
- v-bind:model="model">
- <tr is="environment-item"
- :model="model"
- :can-create-deployment="canCreateDeployment"
- :can-read-environment="canReadEnvironment"
- :service="service"></tr>
-
- <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
- <tr v-if="isLoadingFolderContent">
- <td colspan="6" class="text-center">
- <i class="fa fa-spin fa-spinner fa-2x" aria-hidden="true"/>
- </td>
- </tr>
-
- <template v-else>
- <tr is="environment-item"
- v-for="children in model.children"
- :model="children"
- :can-create-deployment="canCreateDeployment"
- :can-read-environment="canReadEnvironment"
- :service="service"></tr>
-
- <tr>
- <td colspan="6" class="text-center">
- <a :href="folderUrl(model)" class="btn btn-default">
- Show all
- </a>
- </td>
- </tr>
- </template>
- </template>
- </template>
- </tbody>
- </table>
- `,
-};
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
new file mode 100644
index 00000000000..87f7cb4a536
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -0,0 +1,117 @@
+<script>
+/**
+ * Render environments table.
+ */
+import EnvironmentTableRowComponent from './environment_item.vue';
+
+export default {
+ components: {
+ 'environment-item': EnvironmentTableRowComponent,
+ },
+
+ props: {
+ environments: {
+ type: Array,
+ required: true,
+ default: () => ([]),
+ },
+
+ canReadEnvironment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ canCreateDeployment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+
+ isLoadingFolderContent: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ methods: {
+ folderUrl(model) {
+ return `${window.location.pathname}/folders/${model.folderName}`;
+ },
+ },
+};
+</script>
+<template>
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th class="environments-name">
+ Environment
+ </th>
+ <th class="environments-deploy">
+ Last deployment
+ </th>
+ <th class="environments-build">
+ Job
+ </th>
+ <th class="environments-commit">
+ Commit
+ </th>
+ <th class="environments-date">
+ Updated
+ </th>
+ <th class="environments-actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template
+ v-for="model in environments"
+ v-bind:model="model">
+ <tr
+ is="environment-item"
+ :model="model"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ :service="service" />
+
+ <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
+ <tr v-if="isLoadingFolderContent">
+ <td colspan="6" class="text-center">
+ <i
+ class="fa fa-spin fa-spinner fa-2x"
+ aria-hidden="true" />
+ </td>
+ </tr>
+
+ <template v-else>
+ <tr
+ is="environment-item"
+ v-for="children in model.children"
+ :model="children"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ :service="service" />
+
+ <tr>
+ <td
+ colspan="6"
+ class="text-center">
+ <a
+ :href="folderUrl(model)"
+ class="btn btn-default">
+ Show all
+ </a>
+ </td>
+ </tr>
+ </template>
+ </template>
+ </template>
+ </tbody>
+ </table>
+</template>
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js
index d2514593e3a..05d44f77d1d 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js
@@ -2,7 +2,7 @@
/* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
-import EnvironmentTable from '../components/environments_table';
+import EnvironmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import TablePaginationComponent from '../../vue_shared/components/table_pagination';
import '../../lib/utils/common_utils';
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 5b7b059666a..5e9434fd48f 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -2,82 +2,80 @@ import Filter from '~/droplab/plugins/filter';
require('./filtered_search_dropdown');
-(() => {
- class DropdownHint extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter) {
- super(droplab, dropdown, input, filter);
- this.config = {
- Filter: {
- template: 'hint',
- filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
- },
- };
- }
-
- itemClicked(e) {
- const { selected } = e.detail;
+class DropdownHint extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ super(droplab, dropdown, input, filter);
+ this.config = {
+ Filter: {
+ template: 'hint',
+ filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
+ },
+ };
+ }
- if (selected.tagName === 'LI') {
- if (selected.hasAttribute('data-value')) {
- this.dismissDropdown();
- } else if (selected.getAttribute('data-action') === 'submit') {
- this.dismissDropdown();
- this.dispatchFormSubmitEvent();
- } else {
- const token = selected.querySelector('.js-filter-hint').innerText.trim();
- const tag = selected.querySelector('.js-filter-tag').innerText.trim();
+ itemClicked(e) {
+ const { selected } = e.detail;
- if (tag.length) {
- // Get previous input values in the input field and convert them into visual tokens
- const previousInputValues = this.input.value.split(' ');
- const searchTerms = [];
+ if (selected.tagName === 'LI') {
+ if (selected.hasAttribute('data-value')) {
+ this.dismissDropdown();
+ } else if (selected.getAttribute('data-action') === 'submit') {
+ this.dismissDropdown();
+ this.dispatchFormSubmitEvent();
+ } else {
+ const token = selected.querySelector('.js-filter-hint').innerText.trim();
+ const tag = selected.querySelector('.js-filter-tag').innerText.trim();
- previousInputValues.forEach((value, index) => {
- searchTerms.push(value);
+ if (tag.length) {
+ // Get previous input values in the input field and convert them into visual tokens
+ const previousInputValues = this.input.value.split(' ');
+ const searchTerms = [];
- if (index === previousInputValues.length - 1
- && token.indexOf(value.toLowerCase()) !== -1) {
- searchTerms.pop();
- }
- });
+ previousInputValues.forEach((value, index) => {
+ searchTerms.push(value);
- if (searchTerms.length > 0) {
- gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
+ if (index === previousInputValues.length - 1
+ && token.indexOf(value.toLowerCase()) !== -1) {
+ searchTerms.pop();
}
+ });
- gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
+ if (searchTerms.length > 0) {
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
}
- this.dismissDropdown();
- this.dispatchInputEvent();
+
+ gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
}
+ this.dismissDropdown();
+ this.dispatchInputEvent();
}
}
+ }
- renderContent() {
- const dropdownData = [];
+ renderContent() {
+ const dropdownData = [];
- [].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
- const { icon, hint, tag, type } = dropdownMenu.dataset;
- if (icon && hint && tag) {
- dropdownData.push(
- Object.assign({
- icon: `fa-${icon}`,
- hint,
- tag: `<${tag}>`,
- }, type && { type }),
- );
- }
- });
+ [].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
+ const { icon, hint, tag, type } = dropdownMenu.dataset;
+ if (icon && hint && tag) {
+ dropdownData.push(
+ Object.assign({
+ icon: `fa-${icon}`,
+ hint,
+ tag: `<${tag}>`,
+ }, type && { type }),
+ );
+ }
+ });
- this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
- this.droplab.setData(this.hookId, dropdownData);
- }
+ this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
+ this.droplab.setData(this.hookId, dropdownData);
+ }
- init() {
- this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
- }
+ init() {
+ this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
}
+}
- window.gl = window.gl || {};
- gl.DropdownHint = DropdownHint;
-})();
+window.gl = window.gl || {};
+gl.DropdownHint = DropdownHint;
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index 6296965b911..982dc4b61be 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -5,48 +5,46 @@ import Filter from '~/droplab/plugins/filter';
require('./filtered_search_dropdown');
-(() => {
- class DropdownNonUser extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter, endpoint, symbol) {
- super(droplab, dropdown, input, filter);
- this.symbol = symbol;
- this.config = {
- Ajax: {
- endpoint,
- method: 'setData',
- loadingTemplate: this.loadingTemplate,
- onError() {
- /* eslint-disable no-new */
- new Flash('An error occured fetching the dropdown data.');
- /* eslint-enable no-new */
- },
+class DropdownNonUser extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter, endpoint, symbol) {
+ super(droplab, dropdown, input, filter);
+ this.symbol = symbol;
+ this.config = {
+ Ajax: {
+ endpoint,
+ method: 'setData',
+ loadingTemplate: this.loadingTemplate,
+ onError() {
+ /* eslint-disable no-new */
+ new Flash('An error occured fetching the dropdown data.');
+ /* eslint-enable no-new */
},
- Filter: {
- filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
- template: 'title',
- },
- };
- }
+ },
+ Filter: {
+ filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
+ template: 'title',
+ },
+ };
+ }
- itemClicked(e) {
- super.itemClicked(e, (selected) => {
- const title = selected.querySelector('.js-data-value').innerText.trim();
- return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
- });
- }
+ itemClicked(e) {
+ super.itemClicked(e, (selected) => {
+ const title = selected.querySelector('.js-data-value').innerText.trim();
+ return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
+ });
+ }
- renderContent(forceShowList = false) {
- this.droplab
- .changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
- super.renderContent(forceShowList);
- }
+ renderContent(forceShowList = false) {
+ this.droplab
+ .changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
+ super.renderContent(forceShowList);
+ }
- init() {
- this.droplab
- .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
- }
+ init() {
+ this.droplab
+ .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
}
+}
- window.gl = window.gl || {};
- gl.DropdownNonUser = DropdownNonUser;
-})();
+window.gl = window.gl || {};
+gl.DropdownNonUser = DropdownNonUser;
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 38b5d315bcf..74cec3d75fe 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -4,69 +4,67 @@ import AjaxFilter from '~/droplab/plugins/ajax_filter';
require('./filtered_search_dropdown');
-(() => {
- class DropdownUser extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter) {
- super(droplab, dropdown, input, filter);
- this.config = {
- AjaxFilter: {
- endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
- searchKey: 'search',
- params: {
- per_page: 20,
- active: true,
- project_id: this.getProjectId(),
- current_user: true,
- },
- searchValueFunction: this.getSearchInput.bind(this),
- loadingTemplate: this.loadingTemplate,
- onError() {
- /* eslint-disable no-new */
- new Flash('An error occured fetching the dropdown data.');
- /* eslint-enable no-new */
- },
+class DropdownUser extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ super(droplab, dropdown, input, filter);
+ this.config = {
+ AjaxFilter: {
+ endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
+ searchKey: 'search',
+ params: {
+ per_page: 20,
+ active: true,
+ project_id: this.getProjectId(),
+ current_user: true,
},
- };
- }
-
- itemClicked(e) {
- super.itemClicked(e,
- selected => selected.querySelector('.dropdown-light-content').innerText.trim());
- }
-
- renderContent(forceShowList = false) {
- this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
- super.renderContent(forceShowList);
- }
+ searchValueFunction: this.getSearchInput.bind(this),
+ loadingTemplate: this.loadingTemplate,
+ onError() {
+ /* eslint-disable no-new */
+ new Flash('An error occured fetching the dropdown data.');
+ /* eslint-enable no-new */
+ },
+ },
+ };
+ }
- getProjectId() {
- return this.input.getAttribute('data-project-id');
- }
+ itemClicked(e) {
+ super.itemClicked(e,
+ selected => selected.querySelector('.dropdown-light-content').innerText.trim());
+ }
- getSearchInput() {
- const query = gl.DropdownUtils.getSearchInput(this.input);
- const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
+ renderContent(forceShowList = false) {
+ this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
+ super.renderContent(forceShowList);
+ }
- let value = lastToken || '';
+ getProjectId() {
+ return this.input.getAttribute('data-project-id');
+ }
- if (value[0] === '@') {
- value = value.slice(1);
- }
+ getSearchInput() {
+ const query = gl.DropdownUtils.getSearchInput(this.input);
+ const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
- // Removes the first character if it is a quotation so that we can search
- // with multiple words
- if (value[0] === '"' || value[0] === '\'') {
- value = value.slice(1);
- }
+ let value = lastToken || '';
- return value;
+ if (value[0] === '@') {
+ value = value.slice(1);
}
- init() {
- this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init();
+ // Removes the first character if it is a quotation so that we can search
+ // with multiple words
+ if (value[0] === '"' || value[0] === '\'') {
+ value = value.slice(1);
}
+
+ return value;
+ }
+
+ init() {
+ this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init();
}
+}
- window.gl = window.gl || {};
- gl.DropdownUser = DropdownUser;
-})();
+window.gl = window.gl || {};
+gl.DropdownUser = DropdownUser;
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 6c5c20447f7..bc7c1dffece 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -1,183 +1,181 @@
import FilteredSearchContainer from './container';
-(() => {
- class DropdownUtils {
- static getEscapedText(text) {
- let escapedText = text;
- const hasSpace = text.indexOf(' ') !== -1;
- const hasDoubleQuote = text.indexOf('"') !== -1;
-
- // Encapsulate value with quotes if it has spaces
- // Known side effect: values's with both single and double quotes
- // won't escape properly
- if (hasSpace) {
- if (hasDoubleQuote) {
- escapedText = `'${text}'`;
- } else {
- // Encapsulate singleQuotes or if it hasSpace
- escapedText = `"${text}"`;
- }
+class DropdownUtils {
+ static getEscapedText(text) {
+ let escapedText = text;
+ const hasSpace = text.indexOf(' ') !== -1;
+ const hasDoubleQuote = text.indexOf('"') !== -1;
+
+ // Encapsulate value with quotes if it has spaces
+ // Known side effect: values's with both single and double quotes
+ // won't escape properly
+ if (hasSpace) {
+ if (hasDoubleQuote) {
+ escapedText = `'${text}'`;
+ } else {
+ // Encapsulate singleQuotes or if it hasSpace
+ escapedText = `"${text}"`;
}
-
- return escapedText;
}
- static filterWithSymbol(filterSymbol, input, item) {
- const updatedItem = item;
- const searchInput = gl.DropdownUtils.getSearchInput(input);
+ return escapedText;
+ }
+
+ static filterWithSymbol(filterSymbol, input, item) {
+ const updatedItem = item;
+ const searchInput = gl.DropdownUtils.getSearchInput(input);
- const title = updatedItem.title.toLowerCase();
- let value = searchInput.toLowerCase();
- let symbol = '';
+ const title = updatedItem.title.toLowerCase();
+ let value = searchInput.toLowerCase();
+ let symbol = '';
- // Remove the symbol for filter
- if (value[0] === filterSymbol) {
- symbol = value[0];
- value = value.slice(1);
- }
+ // Remove the symbol for filter
+ if (value[0] === filterSymbol) {
+ symbol = value[0];
+ value = value.slice(1);
+ }
- // Removes the first character if it is a quotation so that we can search
- // with multiple words
- if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
- value = value.slice(1);
- }
+ // Removes the first character if it is a quotation so that we can search
+ // with multiple words
+ if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
+ value = value.slice(1);
+ }
+
+ // Eg. filterSymbol = ~ for labels
+ const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
+ const match = title.indexOf(`${symbol}${value}`) !== -1;
- // Eg. filterSymbol = ~ for labels
- const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
- const match = title.indexOf(`${symbol}${value}`) !== -1;
+ updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
- updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
+ return updatedItem;
+ }
- return updatedItem;
+ static filterHint(input, item) {
+ const updatedItem = item;
+ const searchInput = gl.DropdownUtils.getSearchQuery(input);
+ const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput);
+ const lastKey = lastToken.key || lastToken || '';
+ const allowMultiple = item.type === 'array';
+ const itemInExistingTokens = tokens.some(t => t.key === item.hint);
+
+ if (!allowMultiple && itemInExistingTokens) {
+ updatedItem.droplab_hidden = true;
+ } else if (!lastKey || searchInput.split('').last() === ' ') {
+ updatedItem.droplab_hidden = false;
+ } else if (lastKey) {
+ const split = lastKey.split(':');
+ const tokenName = split[0].split(' ').last();
+
+ const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
+ updatedItem.droplab_hidden = tokenName ? match : false;
}
- static filterHint(input, item) {
- const updatedItem = item;
- const searchInput = gl.DropdownUtils.getSearchQuery(input);
- const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput);
- const lastKey = lastToken.key || lastToken || '';
- const allowMultiple = item.type === 'array';
- const itemInExistingTokens = tokens.some(t => t.key === item.hint);
-
- if (!allowMultiple && itemInExistingTokens) {
- updatedItem.droplab_hidden = true;
- } else if (!lastKey || searchInput.split('').last() === ' ') {
- updatedItem.droplab_hidden = false;
- } else if (lastKey) {
- const split = lastKey.split(':');
- const tokenName = split[0].split(' ').last();
-
- const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
- updatedItem.droplab_hidden = tokenName ? match : false;
- }
+ return updatedItem;
+ }
+
+ static setDataValueIfSelected(filter, selected) {
+ const dataValue = selected.getAttribute('data-value');
- return updatedItem;
+ if (dataValue) {
+ gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
}
- static setDataValueIfSelected(filter, selected) {
- const dataValue = selected.getAttribute('data-value');
+ // Return boolean based on whether it was set
+ return dataValue !== null;
+ }
- if (dataValue) {
- gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
- }
+ // Determines the full search query (visual tokens + input)
+ static getSearchQuery(untilInput = false) {
+ const container = FilteredSearchContainer.container;
+ const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
+ const values = [];
- // Return boolean based on whether it was set
- return dataValue !== null;
+ if (untilInput) {
+ const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
+ // Add one to include input-token to the tokens array
+ tokens.splice(inputIndex + 1);
}
- // Determines the full search query (visual tokens + input)
- static getSearchQuery(untilInput = false) {
- const container = FilteredSearchContainer.container;
- const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
- const values = [];
+ tokens.forEach((token) => {
+ if (token.classList.contains('js-visual-token')) {
+ const name = token.querySelector('.name');
+ const value = token.querySelector('.value');
+ const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
+ let valueText = '';
- if (untilInput) {
- const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
- // Add one to include input-token to the tokens array
- tokens.splice(inputIndex + 1);
- }
+ if (value && value.innerText) {
+ valueText = value.innerText;
+ }
- tokens.forEach((token) => {
- if (token.classList.contains('js-visual-token')) {
- const name = token.querySelector('.name');
- const value = token.querySelector('.value');
- const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
- let valueText = '';
-
- if (value && value.innerText) {
- valueText = value.innerText;
- }
-
- if (token.className.indexOf('filtered-search-token') !== -1) {
- values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
- } else {
- values.push(name.innerText);
- }
- } else if (token.classList.contains('input-token')) {
- const { isLastVisualTokenValid } =
- gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
-
- const input = FilteredSearchContainer.container.querySelector('.filtered-search');
- const inputValue = input && input.value;
-
- if (isLastVisualTokenValid) {
- values.push(inputValue);
- } else {
- const previous = values.pop();
- values.push(`${previous}${inputValue}`);
- }
+ if (token.className.indexOf('filtered-search-token') !== -1) {
+ values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
+ } else {
+ values.push(name.innerText);
}
- });
+ } else if (token.classList.contains('input-token')) {
+ const { isLastVisualTokenValid } =
+ gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- return values
- .map(value => value.trim())
- .join(' ');
- }
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+ const inputValue = input && input.value;
- static getSearchInput(filteredSearchInput) {
- const inputValue = filteredSearchInput.value;
- const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
+ if (isLastVisualTokenValid) {
+ values.push(inputValue);
+ } else {
+ const previous = values.pop();
+ values.push(`${previous}${inputValue}`);
+ }
+ }
+ });
- return inputValue.slice(0, right);
- }
+ return values
+ .map(value => value.trim())
+ .join(' ');
+ }
- static getInputSelectionPosition(input) {
- const selectionStart = input.selectionStart;
- let inputValue = input.value;
- // Replace all spaces inside quote marks with underscores
- // (will continue to match entire string until an end quote is found if any)
- // This helps with matching the beginning & end of a token:key
- inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
-
- // Get the right position for the word selected
- // Regex matches first space
- let right = inputValue.slice(selectionStart).search(/\s/);
-
- if (right >= 0) {
- right += selectionStart;
- } else if (right < 0) {
- right = inputValue.length;
- }
+ static getSearchInput(filteredSearchInput) {
+ const inputValue = filteredSearchInput.value;
+ const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
- // Get the left position for the word selected
- // Regex matches last non-whitespace character
- let left = inputValue.slice(0, right).search(/\S+$/);
+ return inputValue.slice(0, right);
+ }
- if (selectionStart === 0) {
- left = 0;
- } else if (selectionStart === inputValue.length && left < 0) {
- left = inputValue.length;
- } else if (left < 0) {
- left = selectionStart;
- }
+ static getInputSelectionPosition(input) {
+ const selectionStart = input.selectionStart;
+ let inputValue = input.value;
+ // Replace all spaces inside quote marks with underscores
+ // (will continue to match entire string until an end quote is found if any)
+ // This helps with matching the beginning & end of a token:key
+ inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
+
+ // Get the right position for the word selected
+ // Regex matches first space
+ let right = inputValue.slice(selectionStart).search(/\s/);
+
+ if (right >= 0) {
+ right += selectionStart;
+ } else if (right < 0) {
+ right = inputValue.length;
+ }
+
+ // Get the left position for the word selected
+ // Regex matches last non-whitespace character
+ let left = inputValue.slice(0, right).search(/\S+$/);
- return {
- left,
- right,
- };
+ if (selectionStart === 0) {
+ left = 0;
+ } else if (selectionStart === inputValue.length && left < 0) {
+ left = inputValue.length;
+ } else if (left < 0) {
+ left = selectionStart;
}
+
+ return {
+ left,
+ right,
+ };
}
+}
- window.gl = window.gl || {};
- gl.DropdownUtils = DropdownUtils;
-})();
+window.gl = window.gl || {};
+gl.DropdownUtils = DropdownUtils;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index d58eeeebf81..4209ca0d6e2 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -1,124 +1,122 @@
-(() => {
- const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
-
- class FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter) {
- this.droplab = droplab;
- this.hookId = input && input.id;
- this.input = input;
- this.filter = filter;
- this.dropdown = dropdown;
- this.loadingTemplate = `<div class="filter-dropdown-loading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>`;
- this.bindEvents();
- }
-
- bindEvents() {
- this.itemClickedWrapper = this.itemClicked.bind(this);
- this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
- }
+const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
+
+class FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ this.droplab = droplab;
+ this.hookId = input && input.id;
+ this.input = input;
+ this.filter = filter;
+ this.dropdown = dropdown;
+ this.loadingTemplate = `<div class="filter-dropdown-loading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>`;
+ this.bindEvents();
+ }
- unbindEvents() {
- this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
- }
+ bindEvents() {
+ this.itemClickedWrapper = this.itemClicked.bind(this);
+ this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
+ }
- getCurrentHook() {
- return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
- }
+ unbindEvents() {
+ this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
+ }
- itemClicked(e, getValueFunction) {
- const { selected } = e.detail;
+ getCurrentHook() {
+ return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
+ }
- if (selected.tagName === 'LI' && selected.innerHTML) {
- const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
+ itemClicked(e, getValueFunction) {
+ const { selected } = e.detail;
- if (!dataValueSet) {
- const value = getValueFunction(selected);
- gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
- }
+ if (selected.tagName === 'LI' && selected.innerHTML) {
+ const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
- this.resetFilters();
- this.dismissDropdown();
- this.dispatchInputEvent();
+ if (!dataValueSet) {
+ const value = getValueFunction(selected);
+ gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
- }
- setAsDropdown() {
- this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
+ this.resetFilters();
+ this.dismissDropdown();
+ this.dispatchInputEvent();
}
+ }
- setOffset(offset = 0) {
- if (window.innerWidth > 480) {
- this.dropdown.style.left = `${offset}px`;
- } else {
- this.dropdown.style.left = '0px';
- }
+ setAsDropdown() {
+ this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
+ }
+
+ setOffset(offset = 0) {
+ if (window.innerWidth > 480) {
+ this.dropdown.style.left = `${offset}px`;
+ } else {
+ this.dropdown.style.left = '0px';
}
+ }
- renderContent(forceShowList = false) {
- const currentHook = this.getCurrentHook();
- if (forceShowList && currentHook && currentHook.list.hidden) {
- currentHook.list.show();
- }
+ renderContent(forceShowList = false) {
+ const currentHook = this.getCurrentHook();
+ if (forceShowList && currentHook && currentHook.list.hidden) {
+ currentHook.list.show();
}
+ }
- render(forceRenderContent = false, forceShowList = false) {
- this.setAsDropdown();
+ render(forceRenderContent = false, forceShowList = false) {
+ this.setAsDropdown();
- const currentHook = this.getCurrentHook();
- const firstTimeInitialized = currentHook === null;
+ const currentHook = this.getCurrentHook();
+ const firstTimeInitialized = currentHook === null;
- if (firstTimeInitialized || forceRenderContent) {
- this.renderContent(forceShowList);
- } else if (currentHook.list.list.id !== this.dropdown.id) {
- this.renderContent(forceShowList);
- }
+ if (firstTimeInitialized || forceRenderContent) {
+ this.renderContent(forceShowList);
+ } else if (currentHook.list.list.id !== this.dropdown.id) {
+ this.renderContent(forceShowList);
}
+ }
- dismissDropdown() {
- // Focusing on the input will dismiss dropdown
- // (default droplab functionality)
- this.input.focus();
- }
+ dismissDropdown() {
+ // Focusing on the input will dismiss dropdown
+ // (default droplab functionality)
+ this.input.focus();
+ }
- dispatchInputEvent() {
- // Propogate input change to FilteredSearchDropdownManager
- // so that it can determine which dropdowns to open
- this.input.dispatchEvent(new CustomEvent('input', {
- bubbles: true,
- cancelable: true,
- }));
- }
+ dispatchInputEvent() {
+ // Propogate input change to FilteredSearchDropdownManager
+ // so that it can determine which dropdowns to open
+ this.input.dispatchEvent(new CustomEvent('input', {
+ bubbles: true,
+ cancelable: true,
+ }));
+ }
- dispatchFormSubmitEvent() {
- // dispatchEvent() is necessary as form.submit() does not
- // trigger event handlers
- this.input.form.dispatchEvent(new Event('submit'));
- }
+ dispatchFormSubmitEvent() {
+ // dispatchEvent() is necessary as form.submit() does not
+ // trigger event handlers
+ this.input.form.dispatchEvent(new Event('submit'));
+ }
- hideDropdown() {
- const currentHook = this.getCurrentHook();
- if (currentHook) {
- currentHook.list.hide();
- }
+ hideDropdown() {
+ const currentHook = this.getCurrentHook();
+ if (currentHook) {
+ currentHook.list.hide();
}
+ }
- resetFilters() {
- const hook = this.getCurrentHook();
-
- if (hook) {
- const data = hook.list.data || [];
- const results = data.map((o) => {
- const updated = o;
- updated.droplab_hidden = false;
- return updated;
- });
- hook.list.render(results);
- }
+ resetFilters() {
+ const hook = this.getCurrentHook();
+
+ if (hook) {
+ const data = hook.list.data || [];
+ const results = data.map((o) => {
+ const updated = o;
+ updated.droplab_hidden = false;
+ return updated;
+ });
+ hook.list.render(results);
}
}
+}
- window.gl = window.gl || {};
- gl.FilteredSearchDropdown = FilteredSearchDropdown;
-})();
+window.gl = window.gl || {};
+gl.FilteredSearchDropdown = FilteredSearchDropdown;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index ec481b9ef97..49a6cd1ac77 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -1,191 +1,189 @@
import DropLab from '~/droplab/drop_lab';
import FilteredSearchContainer from './container';
-(() => {
- class FilteredSearchDropdownManager {
- constructor(baseEndpoint = '', page) {
- this.container = FilteredSearchContainer.container;
- this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
- this.tokenizer = gl.FilteredSearchTokenizer;
- this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
- this.filteredSearchInput = this.container.querySelector('.filtered-search');
- this.page = page;
-
- this.setupMapping();
-
- this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('beforeunload', this.cleanupWrapper);
+class FilteredSearchDropdownManager {
+ constructor(baseEndpoint = '', page) {
+ this.container = FilteredSearchContainer.container;
+ this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
+ this.tokenizer = gl.FilteredSearchTokenizer;
+ this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
+ this.filteredSearchInput = this.container.querySelector('.filtered-search');
+ this.page = page;
+
+ this.setupMapping();
+
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ cleanup() {
+ if (this.droplab) {
+ this.droplab.destroy();
+ this.droplab = null;
}
- cleanup() {
- if (this.droplab) {
- this.droplab.destroy();
- this.droplab = null;
- }
+ this.setupMapping();
- this.setupMapping();
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
+ }
- document.removeEventListener('beforeunload', this.cleanupWrapper);
- }
+ setupMapping() {
+ this.mapping = {
+ author: {
+ reference: null,
+ gl: 'DropdownUser',
+ element: this.container.querySelector('#js-dropdown-author'),
+ },
+ assignee: {
+ reference: null,
+ gl: 'DropdownUser',
+ element: this.container.querySelector('#js-dropdown-assignee'),
+ },
+ milestone: {
+ reference: null,
+ gl: 'DropdownNonUser',
+ extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
+ element: this.container.querySelector('#js-dropdown-milestone'),
+ },
+ label: {
+ reference: null,
+ gl: 'DropdownNonUser',
+ extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
+ element: this.container.querySelector('#js-dropdown-label'),
+ },
+ hint: {
+ reference: null,
+ gl: 'DropdownHint',
+ element: this.container.querySelector('#js-dropdown-hint'),
+ },
+ };
+ }
- setupMapping() {
- this.mapping = {
- author: {
- reference: null,
- gl: 'DropdownUser',
- element: this.container.querySelector('#js-dropdown-author'),
- },
- assignee: {
- reference: null,
- gl: 'DropdownUser',
- element: this.container.querySelector('#js-dropdown-assignee'),
- },
- milestone: {
- reference: null,
- gl: 'DropdownNonUser',
- extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
- element: this.container.querySelector('#js-dropdown-milestone'),
- },
- label: {
- reference: null,
- gl: 'DropdownNonUser',
- extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
- element: this.container.querySelector('#js-dropdown-label'),
- },
- hint: {
- reference: null,
- gl: 'DropdownHint',
- element: this.container.querySelector('#js-dropdown-hint'),
- },
- };
+ static addWordToInput(tokenName, tokenValue = '', clicked = false) {
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
+ input.value = '';
+
+ if (clicked) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
}
+ }
- static addWordToInput(tokenName, tokenValue = '', clicked = false) {
- const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+ updateCurrentDropdownOffset() {
+ this.updateDropdownOffset(this.currentDropdown);
+ }
- gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
- input.value = '';
+ updateDropdownOffset(key) {
+ // Always align dropdown with the input field
+ let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
- if (clicked) {
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
- }
- }
+ const maxInputWidth = 240;
+ const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
- updateCurrentDropdownOffset() {
- this.updateDropdownOffset(this.currentDropdown);
+ // Make sure offset never exceeds the input container
+ const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
+ if (offsetMaxWidth < offset) {
+ offset = offsetMaxWidth;
}
- updateDropdownOffset(key) {
- // Always align dropdown with the input field
- let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
+ this.mapping[key].reference.setOffset(offset);
+ }
- const maxInputWidth = 240;
- const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
+ load(key, firstLoad = false) {
+ const mappingKey = this.mapping[key];
+ const glClass = mappingKey.gl;
+ const element = mappingKey.element;
+ let forceShowList = false;
- // Make sure offset never exceeds the input container
- const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
- if (offsetMaxWidth < offset) {
- offset = offsetMaxWidth;
- }
+ if (!mappingKey.reference) {
+ const dl = this.droplab;
+ const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
+ const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
- this.mapping[key].reference.setOffset(offset);
+ // Passing glArguments to `new gl[glClass](<arguments>)`
+ mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
}
- load(key, firstLoad = false) {
- const mappingKey = this.mapping[key];
- const glClass = mappingKey.gl;
- const element = mappingKey.element;
- let forceShowList = false;
-
- if (!mappingKey.reference) {
- const dl = this.droplab;
- const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
- const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
+ if (firstLoad) {
+ mappingKey.reference.init();
+ }
- // Passing glArguments to `new gl[glClass](<arguments>)`
- mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
- }
+ if (this.currentDropdown === 'hint') {
+ // Force the dropdown to show if it was clicked from the hint dropdown
+ forceShowList = true;
+ }
- if (firstLoad) {
- mappingKey.reference.init();
- }
+ this.updateDropdownOffset(key);
+ mappingKey.reference.render(firstLoad, forceShowList);
- if (this.currentDropdown === 'hint') {
- // Force the dropdown to show if it was clicked from the hint dropdown
- forceShowList = true;
- }
+ this.currentDropdown = key;
+ }
- this.updateDropdownOffset(key);
- mappingKey.reference.render(firstLoad, forceShowList);
+ loadDropdown(dropdownName = '') {
+ let firstLoad = false;
- this.currentDropdown = key;
+ if (!this.droplab) {
+ firstLoad = true;
+ this.droplab = new DropLab();
}
- loadDropdown(dropdownName = '') {
- let firstLoad = false;
+ const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
+ const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
+ && this.mapping[match.key];
+ const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
- if (!this.droplab) {
- firstLoad = true;
- this.droplab = new DropLab();
- }
+ if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
+ const key = match && match.key ? match.key : 'hint';
+ this.load(key, firstLoad);
+ }
+ }
- const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
- const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
- && this.mapping[match.key];
- const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
+ setDropdown() {
+ const query = gl.DropdownUtils.getSearchQuery(true);
+ const { lastToken, searchToken } = this.tokenizer.processTokens(query);
- if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
- const key = match && match.key ? match.key : 'hint';
- this.load(key, firstLoad);
- }
+ if (this.currentDropdown) {
+ this.updateCurrentDropdownOffset();
}
- setDropdown() {
- const query = gl.DropdownUtils.getSearchQuery(true);
- const { lastToken, searchToken } = this.tokenizer.processTokens(query);
-
- if (this.currentDropdown) {
- this.updateCurrentDropdownOffset();
- }
-
- if (lastToken === searchToken && lastToken !== null) {
- // Token is not fully initialized yet because it has no value
- // Eg. token = 'label:'
-
- const split = lastToken.split(':');
- const dropdownName = split[0].split(' ').last();
- this.loadDropdown(split.length > 1 ? dropdownName : '');
- } else if (lastToken) {
- // Token has been initialized into an object because it has a value
- this.loadDropdown(lastToken.key);
- } else {
- this.loadDropdown('hint');
- }
+ if (lastToken === searchToken && lastToken !== null) {
+ // Token is not fully initialized yet because it has no value
+ // Eg. token = 'label:'
+
+ const split = lastToken.split(':');
+ const dropdownName = split[0].split(' ').last();
+ this.loadDropdown(split.length > 1 ? dropdownName : '');
+ } else if (lastToken) {
+ // Token has been initialized into an object because it has a value
+ this.loadDropdown(lastToken.key);
+ } else {
+ this.loadDropdown('hint');
}
+ }
- resetDropdowns() {
- if (!this.currentDropdown) {
- return;
- }
+ resetDropdowns() {
+ if (!this.currentDropdown) {
+ return;
+ }
- // Force current dropdown to hide
- this.mapping[this.currentDropdown].reference.hideDropdown();
+ // Force current dropdown to hide
+ this.mapping[this.currentDropdown].reference.hideDropdown();
- // Re-Load dropdown
- this.setDropdown();
+ // Re-Load dropdown
+ this.setDropdown();
- // Reset filters for current dropdown
- this.mapping[this.currentDropdown].reference.resetFilters();
+ // Reset filters for current dropdown
+ this.mapping[this.currentDropdown].reference.resetFilters();
- // Reposition dropdown so that it is aligned with cursor
- this.updateDropdownOffset(this.currentDropdown);
- }
+ // Reposition dropdown so that it is aligned with cursor
+ this.updateDropdownOffset(this.currentDropdown);
+ }
- destroyDroplab() {
- this.droplab.destroy();
- }
+ destroyDroplab() {
+ this.droplab.destroy();
}
+}
- window.gl = window.gl || {};
- gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
-})();
+window.gl = window.gl || {};
+gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index b93a8f1d322..68a832102a0 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -6,489 +6,489 @@ import RecentSearchesStore from './stores/recent_searches_store';
import RecentSearchesService from './services/recent_searches_service';
import eventHub from './event_hub';
-(() => {
- class FilteredSearchManager {
- constructor(page) {
- this.container = FilteredSearchContainer.container;
- this.filteredSearchInput = this.container.querySelector('.filtered-search');
- this.filteredSearchInputForm = this.filteredSearchInput.form;
- this.clearSearchButton = this.container.querySelector('.clear-search');
- this.tokensContainer = this.container.querySelector('.tokens-container');
- this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
-
- this.recentSearchesStore = new RecentSearchesStore();
- let recentSearchesKey = 'issue-recent-searches';
- if (page === 'merge_requests') {
- recentSearchesKey = 'merge-request-recent-searches';
- }
- this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
-
- // Fetch recent searches from localStorage
- this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
- .catch(() => {
- // eslint-disable-next-line no-new
- new Flash('An error occured while parsing recent searches');
- // Gracefully fail to empty array
- return [];
- })
- .then((searches) => {
- // Put any searches that may have come in before
- // we fetched the saved searches ahead of the already saved ones
- const resultantSearches = this.recentSearchesStore.setRecentSearches(
- this.recentSearchesStore.state.recentSearches.concat(searches),
- );
- this.recentSearchesService.save(resultantSearches);
- });
-
- if (this.filteredSearchInput) {
- this.tokenizer = gl.FilteredSearchTokenizer;
- this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
-
- this.recentSearchesRoot = new RecentSearchesRoot(
- this.recentSearchesStore,
- this.recentSearchesService,
- document.querySelector('.js-filtered-search-history-dropdown'),
+class FilteredSearchManager {
+ constructor(page) {
+ this.container = FilteredSearchContainer.container;
+ this.filteredSearchInput = this.container.querySelector('.filtered-search');
+ this.filteredSearchInputForm = this.filteredSearchInput.form;
+ this.clearSearchButton = this.container.querySelector('.clear-search');
+ this.tokensContainer = this.container.querySelector('.tokens-container');
+ this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
+
+ this.recentSearchesStore = new RecentSearchesStore();
+ let recentSearchesKey = 'issue-recent-searches';
+ if (page === 'merge_requests') {
+ recentSearchesKey = 'merge-request-recent-searches';
+ }
+ this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
+
+ // Fetch recent searches from localStorage
+ this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
+ .catch(() => {
+ // eslint-disable-next-line no-new
+ new Flash('An error occured while parsing recent searches');
+ // Gracefully fail to empty array
+ return [];
+ })
+ .then((searches) => {
+ // Put any searches that may have come in before
+ // we fetched the saved searches ahead of the already saved ones
+ const resultantSearches = this.recentSearchesStore.setRecentSearches(
+ this.recentSearchesStore.state.recentSearches.concat(searches),
);
- this.recentSearchesRoot.init();
+ this.recentSearchesService.save(resultantSearches);
+ });
- this.bindEvents();
- this.loadSearchParamsFromURL();
- this.dropdownManager.setDropdown();
+ if (this.filteredSearchInput) {
+ this.tokenizer = gl.FilteredSearchTokenizer;
+ this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
- this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('beforeunload', this.cleanupWrapper);
- }
- }
+ this.recentSearchesRoot = new RecentSearchesRoot(
+ this.recentSearchesStore,
+ this.recentSearchesService,
+ document.querySelector('.js-filtered-search-history-dropdown'),
+ );
+ this.recentSearchesRoot.init();
- cleanup() {
- this.unbindEvents();
- document.removeEventListener('beforeunload', this.cleanupWrapper);
+ this.bindEvents();
+ this.loadSearchParamsFromURL();
+ this.dropdownManager.setDropdown();
- if (this.recentSearchesRoot) {
- this.recentSearchesRoot.destroy();
- }
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
}
+ }
- bindEvents() {
- this.handleFormSubmit = this.handleFormSubmit.bind(this);
- this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
- this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
- this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
- this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
- this.checkForEnterWrapper = this.checkForEnter.bind(this);
- this.onClearSearchWrapper = this.onClearSearch.bind(this);
- this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
- this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
- this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
- this.editTokenWrapper = this.editToken.bind(this);
- this.tokenChange = this.tokenChange.bind(this);
- this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
- this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
- this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
-
- this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
- this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
- this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
- this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
- this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
- this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
- this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
- this.filteredSearchInput.addEventListener('click', this.tokenChange);
- this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
- this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
- this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
- this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
- this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
- document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
- document.addEventListener('click', this.unselectEditTokensWrapper);
- document.addEventListener('click', this.removeInputContainerFocusWrapper);
- document.addEventListener('keydown', this.removeSelectedTokenWrapper);
- eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
- }
+ cleanup() {
+ this.unbindEvents();
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
- unbindEvents() {
- this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
- this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
- this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
- this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
- this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
- this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
- this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
- this.filteredSearchInput.removeEventListener('click', this.tokenChange);
- this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
- this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
- this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
- this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
- this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
- document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
- document.removeEventListener('click', this.unselectEditTokensWrapper);
- document.removeEventListener('click', this.removeInputContainerFocusWrapper);
- document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
- eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
+ if (this.recentSearchesRoot) {
+ this.recentSearchesRoot.destroy();
}
+ }
- checkForBackspace(e) {
- // 8 = Backspace Key
- // 46 = Delete Key
- if (e.keyCode === 8 || e.keyCode === 46) {
- const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ bindEvents() {
+ this.handleFormSubmit = this.handleFormSubmit.bind(this);
+ this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
+ this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
+ this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
+ this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
+ this.checkForEnterWrapper = this.checkForEnter.bind(this);
+ this.onClearSearchWrapper = this.onClearSearch.bind(this);
+ this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
+ this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
+ this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
+ this.editTokenWrapper = this.editToken.bind(this);
+ this.tokenChange = this.tokenChange.bind(this);
+ this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
+ this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
+ this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
+
+ this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
+ this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
+ this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
+ this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
+ this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
+ this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
+ this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
+ this.filteredSearchInput.addEventListener('click', this.tokenChange);
+ this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
+ this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
+ this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
+ this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
+ this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
+ document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+ document.addEventListener('click', this.unselectEditTokensWrapper);
+ document.addEventListener('click', this.removeInputContainerFocusWrapper);
+ document.addEventListener('keydown', this.removeSelectedTokenWrapper);
+ eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
+ }
- if (this.filteredSearchInput.value === '' && lastVisualToken) {
- this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
- gl.FilteredSearchVisualTokens.removeLastTokenPartial();
- }
+ unbindEvents() {
+ this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
+ this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
+ this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
+ this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
+ this.filteredSearchInput.removeEventListener('click', this.tokenChange);
+ this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
+ this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
+ this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
+ this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
+ this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
+ document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+ document.removeEventListener('click', this.unselectEditTokensWrapper);
+ document.removeEventListener('click', this.removeInputContainerFocusWrapper);
+ document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
+ eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
+ }
+
+ checkForBackspace(e) {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- // Reposition dropdown so that it is aligned with cursor
- this.dropdownManager.updateCurrentDropdownOffset();
+ if (this.filteredSearchInput.value === '' && lastVisualToken) {
+ this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
}
- }
- checkForEnter(e) {
- if (e.keyCode === 38 || e.keyCode === 40) {
- const selectionStart = this.filteredSearchInput.selectionStart;
+ // Reposition dropdown so that it is aligned with cursor
+ this.dropdownManager.updateCurrentDropdownOffset();
+ }
+ }
- e.preventDefault();
- this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
- }
+ checkForEnter(e) {
+ if (e.keyCode === 38 || e.keyCode === 40) {
+ const selectionStart = this.filteredSearchInput.selectionStart;
- if (e.keyCode === 13) {
- const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
- const dropdownEl = dropdown.element;
- const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
+ e.preventDefault();
+ this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
+ }
- e.preventDefault();
+ if (e.keyCode === 13) {
+ const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+ const dropdownEl = dropdown.element;
+ const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
- if (!activeElements.length) {
- if (this.isHandledAsync) {
- e.stopImmediatePropagation();
+ e.preventDefault();
- this.filteredSearchInput.blur();
- this.dropdownManager.resetDropdowns();
- } else {
- // Prevent droplab from opening dropdown
- this.dropdownManager.destroyDroplab();
- }
+ if (!activeElements.length) {
+ if (this.isHandledAsync) {
+ e.stopImmediatePropagation();
- this.search();
+ this.filteredSearchInput.blur();
+ this.dropdownManager.resetDropdowns();
+ } else {
+ // Prevent droplab from opening dropdown
+ this.dropdownManager.destroyDroplab();
}
+
+ this.search();
}
}
+ }
- addInputContainerFocus() {
- const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
+ addInputContainerFocus() {
+ const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
- if (inputContainer) {
- inputContainer.classList.add('focus');
- }
+ if (inputContainer) {
+ inputContainer.classList.add('focus');
}
+ }
- removeInputContainerFocus(e) {
- const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
- const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
- const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
- const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
+ removeInputContainerFocus(e) {
+ const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
+ const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+ const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+ const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
- if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
- !isElementInStaticFilterDropdown && inputContainer) {
- inputContainer.classList.remove('focus');
- }
+ if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
+ !isElementInStaticFilterDropdown && inputContainer) {
+ inputContainer.classList.remove('focus');
}
+ }
- static selectToken(e) {
- const button = e.target.closest('.selectable');
+ static selectToken(e) {
+ const button = e.target.closest('.selectable');
- if (button) {
- e.preventDefault();
- e.stopPropagation();
- gl.FilteredSearchVisualTokens.selectToken(button);
- }
+ if (button) {
+ e.preventDefault();
+ e.stopPropagation();
+ gl.FilteredSearchVisualTokens.selectToken(button);
}
+ }
- unselectEditTokens(e) {
- const inputContainer = this.container.querySelector('.filtered-search-box');
- const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
- const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
- const isElementTokensContainer = e.target.classList.contains('tokens-container');
+ unselectEditTokens(e) {
+ const inputContainer = this.container.querySelector('.filtered-search-box');
+ const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+ const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+ const isElementTokensContainer = e.target.classList.contains('tokens-container');
- if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
- this.dropdownManager.resetDropdowns();
- }
+ if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ this.dropdownManager.resetDropdowns();
}
+ }
- editToken(e) {
- const token = e.target.closest('.js-visual-token');
+ editToken(e) {
+ const token = e.target.closest('.js-visual-token');
- if (token) {
- gl.FilteredSearchVisualTokens.editToken(token);
- this.tokenChange();
- }
+ if (token) {
+ gl.FilteredSearchVisualTokens.editToken(token);
+ this.tokenChange();
}
+ }
- toggleClearSearchButton() {
- const query = gl.DropdownUtils.getSearchQuery();
- const hidden = 'hidden';
- const hasHidden = this.clearSearchButton.classList.contains(hidden);
+ toggleClearSearchButton() {
+ const query = gl.DropdownUtils.getSearchQuery();
+ const hidden = 'hidden';
+ const hasHidden = this.clearSearchButton.classList.contains(hidden);
- if (query.length === 0 && !hasHidden) {
- this.clearSearchButton.classList.add(hidden);
- } else if (query.length && hasHidden) {
- this.clearSearchButton.classList.remove(hidden);
- }
+ if (query.length === 0 && !hasHidden) {
+ this.clearSearchButton.classList.add(hidden);
+ } else if (query.length && hasHidden) {
+ this.clearSearchButton.classList.remove(hidden);
}
+ }
- handleInputPlaceholder() {
- const query = gl.DropdownUtils.getSearchQuery();
- const placeholder = 'Search or filter results...';
- const currentPlaceholder = this.filteredSearchInput.placeholder;
+ handleInputPlaceholder() {
+ const query = gl.DropdownUtils.getSearchQuery();
+ const placeholder = 'Search or filter results...';
+ const currentPlaceholder = this.filteredSearchInput.placeholder;
- if (query.length === 0 && currentPlaceholder !== placeholder) {
- this.filteredSearchInput.placeholder = placeholder;
- } else if (query.length > 0 && currentPlaceholder !== '') {
- this.filteredSearchInput.placeholder = '';
- }
+ if (query.length === 0 && currentPlaceholder !== placeholder) {
+ this.filteredSearchInput.placeholder = placeholder;
+ } else if (query.length > 0 && currentPlaceholder !== '') {
+ this.filteredSearchInput.placeholder = '';
}
+ }
- removeSelectedToken(e) {
- // 8 = Backspace Key
- // 46 = Delete Key
- if (e.keyCode === 8 || e.keyCode === 46) {
- gl.FilteredSearchVisualTokens.removeSelectedToken();
- this.handleInputPlaceholder();
- this.toggleClearSearchButton();
- }
+ removeSelectedToken(e) {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ gl.FilteredSearchVisualTokens.removeSelectedToken();
+ this.handleInputPlaceholder();
+ this.toggleClearSearchButton();
}
+ }
- onClearSearch(e) {
- e.preventDefault();
- this.clearSearch();
- }
+ onClearSearch(e) {
+ e.preventDefault();
+ this.clearSearch();
+ }
- clearSearch() {
- this.filteredSearchInput.value = '';
+ clearSearch() {
+ this.filteredSearchInput.value = '';
- const removeElements = [];
+ const removeElements = [];
- [].forEach.call(this.tokensContainer.children, (t) => {
- if (t.classList.contains('js-visual-token')) {
- removeElements.push(t);
- }
- });
+ [].forEach.call(this.tokensContainer.children, (t) => {
+ if (t.classList.contains('js-visual-token')) {
+ removeElements.push(t);
+ }
+ });
- removeElements.forEach((el) => {
- el.parentElement.removeChild(el);
- });
+ removeElements.forEach((el) => {
+ el.parentElement.removeChild(el);
+ });
- this.clearSearchButton.classList.add('hidden');
- this.handleInputPlaceholder();
+ this.clearSearchButton.classList.add('hidden');
+ this.handleInputPlaceholder();
- this.dropdownManager.resetDropdowns();
+ this.dropdownManager.resetDropdowns();
- if (this.isHandledAsync) {
- this.search();
- }
+ if (this.isHandledAsync) {
+ this.search();
}
+ }
- handleInputVisualToken() {
- const input = this.filteredSearchInput;
- const { tokens, searchToken }
- = gl.FilteredSearchTokenizer.processTokens(input.value);
- const { isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
-
- if (isLastVisualTokenValid) {
- tokens.forEach((t) => {
- input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
- gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
- });
-
- const fragments = searchToken.split(':');
- if (fragments.length > 1) {
- const inputValues = fragments[0].split(' ');
- const tokenKey = inputValues.last();
-
- if (inputValues.length > 1) {
- inputValues.pop();
- const searchTerms = inputValues.join(' ');
-
- input.value = input.value.replace(searchTerms, '');
- gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
- }
+ handleInputVisualToken() {
+ const input = this.filteredSearchInput;
+ const { tokens, searchToken }
+ = gl.FilteredSearchTokenizer.processTokens(input.value);
+ const { isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (isLastVisualTokenValid) {
+ tokens.forEach((t) => {
+ input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
+ });
- gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
- input.value = input.value.replace(`${tokenKey}:`, '');
- }
- } else {
- // Keep listening to token until we determine that the user is done typing the token value
- const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
+ const fragments = searchToken.split(':');
+ if (fragments.length > 1) {
+ const inputValues = fragments[0].split(' ');
+ const tokenKey = inputValues.last();
- if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
- gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
+ if (inputValues.length > 1) {
+ inputValues.pop();
+ const searchTerms = inputValues.join(' ');
- // Trim the last space as seen in the if statement above
- input.value = input.value.replace(searchToken, '').trim();
+ input.value = input.value.replace(searchTerms, '');
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
}
+
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
+ input.value = input.value.replace(`${tokenKey}:`, '');
}
- }
+ } else {
+ // Keep listening to token until we determine that the user is done typing the token value
+ const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
- handleFormSubmit(e) {
- e.preventDefault();
- this.search();
- }
+ if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
- saveCurrentSearchQuery() {
- // Don't save before we have fetched the already saved searches
- this.fetchingRecentSearchesPromise.then(() => {
- const searchQuery = gl.DropdownUtils.getSearchQuery();
- if (searchQuery.length > 0) {
- const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
- this.recentSearchesService.save(resultantSearches);
- }
- });
+ // Trim the last space as seen in the if statement above
+ input.value = input.value.replace(searchToken, '').trim();
+ }
}
+ }
- loadSearchParamsFromURL() {
- const params = gl.utils.getUrlParamsArray();
- const usernameParams = this.getUsernameParams();
- let hasFilteredSearch = false;
+ handleFormSubmit(e) {
+ e.preventDefault();
+ this.search();
+ }
- params.forEach((p) => {
- const split = p.split('=');
- const keyParam = decodeURIComponent(split[0]);
- const value = split[1];
+ saveCurrentSearchQuery() {
+ // Don't save before we have fetched the already saved searches
+ this.fetchingRecentSearchesPromise.then(() => {
+ const searchQuery = gl.DropdownUtils.getSearchQuery();
+ if (searchQuery.length > 0) {
+ const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
+ this.recentSearchesService.save(resultantSearches);
+ }
+ }).catch(() => {
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
+ });
+ }
- // Check if it matches edge conditions listed in this.filteredSearchTokenKeys
- const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
+ loadSearchParamsFromURL() {
+ const params = gl.utils.getUrlParamsArray();
+ const usernameParams = this.getUsernameParams();
+ let hasFilteredSearch = false;
- if (condition) {
- hasFilteredSearch = true;
- gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
- } else {
- // Sanitize value since URL converts spaces into +
- // Replace before decode so that we know what was originally + versus the encoded +
- const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
- const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
-
- if (match) {
- const indexOf = keyParam.indexOf('_');
- const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
- const symbol = match.symbol;
- let quotationsToUse = '';
-
- if (sanitizedValue.indexOf(' ') !== -1) {
- // Prefer ", but use ' if required
- quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
- }
+ params.forEach((p) => {
+ const split = p.split('=');
+ const keyParam = decodeURIComponent(split[0]);
+ const value = split[1];
+
+ // Check if it matches edge conditions listed in this.filteredSearchTokenKeys
+ const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
+ if (condition) {
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
+ } else {
+ // Sanitize value since URL converts spaces into +
+ // Replace before decode so that we know what was originally + versus the encoded +
+ const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
+ const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
+
+ if (match) {
+ const indexOf = keyParam.indexOf('_');
+ const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
+ const symbol = match.symbol;
+ let quotationsToUse = '';
+
+ if (sanitizedValue.indexOf(' ') !== -1) {
+ // Prefer ", but use ' if required
+ quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
+ }
+
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
+ } else if (!match && keyParam === 'assignee_id') {
+ const id = parseInt(value, 10);
+ if (usernameParams[id]) {
hasFilteredSearch = true;
- gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
- } else if (!match && keyParam === 'assignee_id') {
- const id = parseInt(value, 10);
- if (usernameParams[id]) {
- hasFilteredSearch = true;
- gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
- }
- } else if (!match && keyParam === 'author_id') {
- const id = parseInt(value, 10);
- if (usernameParams[id]) {
- hasFilteredSearch = true;
- gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
- }
- } else if (!match && keyParam === 'search') {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
+ }
+ } else if (!match && keyParam === 'author_id') {
+ const id = parseInt(value, 10);
+ if (usernameParams[id]) {
hasFilteredSearch = true;
- this.filteredSearchInput.value = sanitizedValue;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
}
+ } else if (!match && keyParam === 'search') {
+ hasFilteredSearch = true;
+ this.filteredSearchInput.value = sanitizedValue;
}
- });
+ }
+ });
- this.saveCurrentSearchQuery();
+ this.saveCurrentSearchQuery();
- if (hasFilteredSearch) {
- this.clearSearchButton.classList.remove('hidden');
- this.handleInputPlaceholder();
- }
+ if (hasFilteredSearch) {
+ this.clearSearchButton.classList.remove('hidden');
+ this.handleInputPlaceholder();
}
+ }
- search() {
- const paths = [];
- const searchQuery = gl.DropdownUtils.getSearchQuery();
-
- this.saveCurrentSearchQuery();
+ search() {
+ const paths = [];
+ const searchQuery = gl.DropdownUtils.getSearchQuery();
- const { tokens, searchToken }
- = this.tokenizer.processTokens(searchQuery);
- const currentState = gl.utils.getParameterByName('state') || 'opened';
- paths.push(`state=${currentState}`);
+ this.saveCurrentSearchQuery();
- tokens.forEach((token) => {
- const condition = this.filteredSearchTokenKeys
- .searchByConditionKeyValue(token.key, token.value.toLowerCase());
- const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
- const keyParam = param ? `${token.key}_${param}` : token.key;
- let tokenPath = '';
+ const { tokens, searchToken }
+ = this.tokenizer.processTokens(searchQuery);
+ const currentState = gl.utils.getParameterByName('state') || 'opened';
+ paths.push(`state=${currentState}`);
- if (condition) {
- tokenPath = condition.url;
- } else {
- let tokenValue = token.value;
+ tokens.forEach((token) => {
+ const condition = this.filteredSearchTokenKeys
+ .searchByConditionKeyValue(token.key, token.value.toLowerCase());
+ const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
+ const keyParam = param ? `${token.key}_${param}` : token.key;
+ let tokenPath = '';
- if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
- (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
- tokenValue = tokenValue.slice(1, tokenValue.length - 1);
- }
+ if (condition) {
+ tokenPath = condition.url;
+ } else {
+ let tokenValue = token.value;
- tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
+ if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
+ (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
+ tokenValue = tokenValue.slice(1, tokenValue.length - 1);
}
- paths.push(tokenPath);
- });
-
- if (searchToken) {
- const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
- paths.push(`search=${sanitized}`);
+ tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
}
- const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
+ paths.push(tokenPath);
+ });
- if (this.updateObject) {
- this.updateObject(parameterizedUrl);
- } else {
- gl.utils.visitUrl(parameterizedUrl);
- }
+ if (searchToken) {
+ const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
+ paths.push(`search=${sanitized}`);
}
- getUsernameParams() {
- const usernamesById = {};
- try {
- const attribute = this.filteredSearchInput.getAttribute('data-username-params');
- JSON.parse(attribute).forEach((user) => {
- usernamesById[user.id] = user.username;
- });
- } catch (e) {
- // do nothing
- }
- return usernamesById;
+ const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
+
+ if (this.updateObject) {
+ this.updateObject(parameterizedUrl);
+ } else {
+ gl.utils.visitUrl(parameterizedUrl);
}
+ }
- tokenChange() {
- const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+ getUsernameParams() {
+ const usernamesById = {};
+ try {
+ const attribute = this.filteredSearchInput.getAttribute('data-username-params');
+ JSON.parse(attribute).forEach((user) => {
+ usernamesById[user.id] = user.username;
+ });
+ } catch (e) {
+ // do nothing
+ }
+ return usernamesById;
+ }
- if (dropdown) {
- const currentDropdownRef = dropdown.reference;
+ tokenChange() {
+ const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
- this.setDropdownWrapper();
- currentDropdownRef.dispatchInputEvent();
- }
- }
+ if (dropdown) {
+ const currentDropdownRef = dropdown.reference;
- onrecentSearchesItemSelected(text) {
- this.clearSearch();
- this.filteredSearchInput.value = text;
- this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
- this.search();
+ this.setDropdownWrapper();
+ currentDropdownRef.dispatchInputEvent();
}
}
- window.gl = window.gl || {};
- gl.FilteredSearchManager = FilteredSearchManager;
-})();
+ onrecentSearchesItemSelected(text) {
+ this.clearSearch();
+ this.filteredSearchInput.value = text;
+ this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
+ this.search();
+ }
+}
+
+window.gl = window.gl || {};
+gl.FilteredSearchManager = FilteredSearchManager;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index 6d5df86f2a5..1abad9d1b73 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -1,100 +1,98 @@
-(() => {
- const tokenKeys = [{
- key: 'author',
- type: 'string',
- param: 'username',
- symbol: '@',
- }, {
- key: 'assignee',
- type: 'string',
- param: 'username',
- symbol: '@',
- }, {
- key: 'milestone',
- type: 'string',
- param: 'title',
- symbol: '%',
- }, {
- key: 'label',
- type: 'array',
- param: 'name[]',
- symbol: '~',
- }];
+const tokenKeys = [{
+ key: 'author',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+}, {
+ key: 'assignee',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+}, {
+ key: 'milestone',
+ type: 'string',
+ param: 'title',
+ symbol: '%',
+}, {
+ key: 'label',
+ type: 'array',
+ param: 'name[]',
+ symbol: '~',
+}];
- const alternativeTokenKeys = [{
- key: 'label',
- type: 'string',
- param: 'name',
- symbol: '~',
- }];
+const alternativeTokenKeys = [{
+ key: 'label',
+ type: 'string',
+ param: 'name',
+ symbol: '~',
+}];
- const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
+const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
- const conditions = [{
- url: 'assignee_id=0',
- tokenKey: 'assignee',
- value: 'none',
- }, {
- url: 'milestone_title=No+Milestone',
- tokenKey: 'milestone',
- value: 'none',
- }, {
- url: 'milestone_title=%23upcoming',
- tokenKey: 'milestone',
- value: 'upcoming',
- }, {
- url: 'milestone_title=%23started',
- tokenKey: 'milestone',
- value: 'started',
- }, {
- url: 'label_name[]=No+Label',
- tokenKey: 'label',
- value: 'none',
- }];
+const conditions = [{
+ url: 'assignee_id=0',
+ tokenKey: 'assignee',
+ value: 'none',
+}, {
+ url: 'milestone_title=No+Milestone',
+ tokenKey: 'milestone',
+ value: 'none',
+}, {
+ url: 'milestone_title=%23upcoming',
+ tokenKey: 'milestone',
+ value: 'upcoming',
+}, {
+ url: 'milestone_title=%23started',
+ tokenKey: 'milestone',
+ value: 'started',
+}, {
+ url: 'label_name[]=No+Label',
+ tokenKey: 'label',
+ value: 'none',
+}];
- class FilteredSearchTokenKeys {
- static get() {
- return tokenKeys;
- }
+class FilteredSearchTokenKeys {
+ static get() {
+ return tokenKeys;
+ }
- static getAlternatives() {
- return alternativeTokenKeys;
- }
+ static getAlternatives() {
+ return alternativeTokenKeys;
+ }
- static getConditions() {
- return conditions;
- }
+ static getConditions() {
+ return conditions;
+ }
- static searchByKey(key) {
- return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
- }
+ static searchByKey(key) {
+ return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
+ }
- static searchBySymbol(symbol) {
- return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
- }
+ static searchBySymbol(symbol) {
+ return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
+ }
- static searchByKeyParam(keyParam) {
- return tokenKeysWithAlternative.find((tokenKey) => {
- let tokenKeyParam = tokenKey.key;
+ static searchByKeyParam(keyParam) {
+ return tokenKeysWithAlternative.find((tokenKey) => {
+ let tokenKeyParam = tokenKey.key;
- if (tokenKey.param) {
- tokenKeyParam += `_${tokenKey.param}`;
- }
+ if (tokenKey.param) {
+ tokenKeyParam += `_${tokenKey.param}`;
+ }
- return keyParam === tokenKeyParam;
- }) || null;
- }
+ return keyParam === tokenKeyParam;
+ }) || null;
+ }
- static searchByConditionUrl(url) {
- return conditions.find(condition => condition.url === url) || null;
- }
+ static searchByConditionUrl(url) {
+ return conditions.find(condition => condition.url === url) || null;
+ }
- static searchByConditionKeyValue(key, value) {
- return conditions
- .find(condition => condition.tokenKey === key && condition.value === value) || null;
- }
+ static searchByConditionKeyValue(key, value) {
+ return conditions
+ .find(condition => condition.tokenKey === key && condition.value === value) || null;
}
+}
- window.gl = window.gl || {};
- gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
-})();
+window.gl = window.gl || {};
+gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
index a2729dc0e95..2808e4b238a 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
@@ -1,58 +1,56 @@
require('./filtered_search_token_keys');
-(() => {
- class FilteredSearchTokenizer {
- static processTokens(input) {
- const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key);
- // Regex extracts `(token):(symbol)(value)`
- // Values that start with a double quote must end in a double quote (same for single)
- const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
- const tokens = [];
- const tokenIndexes = []; // stores key+value for simple search
- let lastToken = null;
- const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
- let tokenValue = v1 || v2 || v3;
- let tokenSymbol = symbol;
- let tokenIndex = '';
-
- if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
- tokenSymbol = tokenValue;
- tokenValue = '';
- }
-
- tokenIndex = `${key}:${tokenValue}`;
-
- // Prevent adding duplicates
- if (tokenIndexes.indexOf(tokenIndex) === -1) {
- tokenIndexes.push(tokenIndex);
-
- tokens.push({
- key,
- value: tokenValue || '',
- symbol: tokenSymbol || '',
- });
- }
-
- return '';
- }).replace(/\s{2,}/g, ' ').trim() || '';
-
- if (tokens.length > 0) {
- const last = tokens[tokens.length - 1];
- const lastString = `${last.key}:${last.symbol}${last.value}`;
- lastToken = input.lastIndexOf(lastString) ===
- input.length - lastString.length ? last : searchToken;
- } else {
- lastToken = searchToken;
+class FilteredSearchTokenizer {
+ static processTokens(input) {
+ const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key);
+ // Regex extracts `(token):(symbol)(value)`
+ // Values that start with a double quote must end in a double quote (same for single)
+ const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
+ const tokens = [];
+ const tokenIndexes = []; // stores key+value for simple search
+ let lastToken = null;
+ const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
+ let tokenValue = v1 || v2 || v3;
+ let tokenSymbol = symbol;
+ let tokenIndex = '';
+
+ if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
+ tokenSymbol = tokenValue;
+ tokenValue = '';
}
- return {
- tokens,
- lastToken,
- searchToken,
- };
+ tokenIndex = `${key}:${tokenValue}`;
+
+ // Prevent adding duplicates
+ if (tokenIndexes.indexOf(tokenIndex) === -1) {
+ tokenIndexes.push(tokenIndex);
+
+ tokens.push({
+ key,
+ value: tokenValue || '',
+ symbol: tokenSymbol || '',
+ });
+ }
+
+ return '';
+ }).replace(/\s{2,}/g, ' ').trim() || '';
+
+ if (tokens.length > 0) {
+ const last = tokens[tokens.length - 1];
+ const lastString = `${last.key}:${last.symbol}${last.value}`;
+ lastToken = input.lastIndexOf(lastString) ===
+ input.length - lastString.length ? last : searchToken;
+ } else {
+ lastToken = searchToken;
}
+
+ return {
+ tokens,
+ lastToken,
+ searchToken,
+ };
}
+}
- window.gl = window.gl || {};
- gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
-})();
+window.gl = window.gl || {};
+gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 10363c16bae..acfa4bd4c6b 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var,
+ camelcase, one-var-declaration-per-line, quotes, object-shorthand,
+ prefer-arrow-callback, comma-dangle, consistent-return, yoda,
+ prefer-rest-params, prefer-spread, no-unused-vars, prefer-template,
+ promise/catch-or-return */
/* global Api */
var slice = [].slice;
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index de184ab2675..687c2bb6110 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -39,8 +39,9 @@
if ($issuableDueDate.length) {
calendar = new Pikaday({
field: $issuableDueDate.get(0),
- theme: 'gitlab-theme',
+ theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
+ container: $issuableDueDate.parent().get(0),
onSelect: function(dateText) {
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
diff --git a/app/assets/javascripts/issue_show/issue_title.vue b/app/assets/javascripts/issue_show/issue_title.vue
index ba54178a310..00b0e56030a 100644
--- a/app/assets/javascripts/issue_show/issue_title.vue
+++ b/app/assets/javascripts/issue_show/issue_title.vue
@@ -34,17 +34,6 @@ export default {
};
},
methods: {
- fetch() {
- this.poll.makeRequest();
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- this.poll.restart();
- } else {
- this.poll.stop();
- }
- });
- },
renderResponse(res) {
const body = JSON.parse(res.body);
this.triggerAnimation(body);
@@ -71,7 +60,17 @@ export default {
},
},
created() {
- this.fetch();
+ if (!Visibility.hidden()) {
+ this.poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
},
};
</script>
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 443fb3e0ca9..9a60f5464df 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -332,6 +332,9 @@
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e, isMarking) {
var isIssueIndex, isMRIndex, page, boardsModel;
+ var fadeOutLoader = () => {
+ $loading.fadeOut();
+ };
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
@@ -396,9 +399,8 @@
$loading.fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
- .then(function () {
- $loading.fadeOut();
- });
+ .then(fadeOutLoader)
+ .catch(fadeOutLoader);
}
else {
if ($dropdown.hasClass('js-multiselect')) {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 33c900e264b..8058672eaa9 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -169,7 +169,10 @@
w.gl.utils.getSelectedFragment = () => {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
- const documentFragment = selection.getRangeAt(0).cloneContents();
+ const documentFragment = document.createDocumentFragment();
+ for (let i = 0; i < selection.rangeCount; i += 1) {
+ documentFragment.appendChild(selection.getRangeAt(i).cloneContents());
+ }
if (documentFragment.textContent.length === 0) return null;
return documentFragment;
@@ -368,9 +371,9 @@
});
};
- w.gl.utils.setFavicon = (iconName) => {
- if (faviconEl && iconName) {
- faviconEl.setAttribute('href', `/assets/${iconName}.ico`);
+ w.gl.utils.setFavicon = (faviconPath) => {
+ if (faviconEl && faviconPath) {
+ faviconEl.setAttribute('href', faviconPath);
}
};
@@ -385,8 +388,8 @@
url: pageUrl,
dataType: 'json',
success: function(data) {
- if (data && data.icon) {
- gl.utils.setFavicon(`ci_favicons/${data.icon}`);
+ if (data && data.favicon) {
+ gl.utils.setFavicon(data.favicon);
} else {
gl.utils.resetFavicon();
}
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
new file mode 100644
index 00000000000..1e96c7ab5cd
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -0,0 +1,2 @@
+/* eslint-disable import/prefer-default-export */
+export const BYTES_IN_KIB = 1024;
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index e2bf69ee52e..f1b07408671 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -1,4 +1,4 @@
-/* eslint-disable import/prefer-default-export */
+import { BYTES_IN_KIB } from './constants';
/**
* Function that allows a number with an X amount of decimals
@@ -32,3 +32,13 @@ export function formatRelevantDigits(number) {
}
return formattedNumber;
}
+
+/**
+ * Utility function that calculates KiB of the given bytes.
+ *
+ * @param {Number} number bytes
+ * @return {Number} KiB
+ */
+export function bytesToKiB(number) {
+ return number / BYTES_IN_KIB;
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index c50ec24c818..be3c2c9fbb1 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -165,6 +165,7 @@ import './syntax_highlight';
import './task_list';
import './todos';
import './tree';
+import './usage_ping';
import './user';
import './user_tabs';
import './username_validator';
@@ -210,6 +211,14 @@ $(function () {
}
});
+ if (bootstrapBreakpoint === 'xs') {
+ const $rightSidebar = $('aside.right-sidebar, .page-with-sidebar');
+
+ $rightSidebar
+ .removeClass('right-sidebar-expanded')
+ .addClass('right-sidebar-collapsed');
+ }
+
// prevent default action for disabled buttons
$('.btn').click(function(e) {
if ($(this).hasClass('disabled')) {
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index 129d2dc5f0a..e034729bd39 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -18,9 +18,10 @@
const calendar = new Pikaday({
field: $input.get(0),
- theme: 'gitlab-theme',
+ theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
minDate: new Date(),
+ container: $input.parent().get(0),
onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index b0254b17dd2..42ecf0d6cb2 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -157,7 +157,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
$('.ci-widget-fetching').show();
return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) {
- var message, status, title;
+ var message, status, title, callback;
_this.status = data.status;
_this.hasCi = data.has_ci;
_this.updateMergeButton(_this.status, _this.hasCi);
@@ -179,6 +179,12 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.opts.ci_sha = data.sha;
_this.updateCommitUrls(data.sha);
}
+ if (data.status === "success" || data.status === "failed") {
+ callback = function() {
+ return _this.getMergeStatus();
+ };
+ return setTimeout(callback, 2000);
+ }
if (showNotification && data.status) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 773fe3233a7..bebd0aa357e 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -164,6 +164,9 @@
.then(function () {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
+ })
+ .catch(() => {
+ $loading.fadeOut();
});
} else {
selected = $selectbox.find('input[type="hidden"]').val();
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index d82a4eb9642..aff507abb91 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -71,6 +71,8 @@ class PrometheusGraph {
this.transformData(metricsResponse);
this.createGraph();
}
+ }).catch(() => {
+ new Flash('An error occurred when trying to load metrics. Please try again.');
});
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 15f7a813626..974fb0d83da 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -308,8 +308,10 @@ require('./task_list');
if (this.isNewNote(note)) {
this.note_ids.push(note.id);
- $notesList = $('ul.main-notes-list');
- $notesList.append(note.html).syntaxHighlight();
+
+ $notesList = window.$('ul.main-notes-list');
+ Notes.animateAppendNote(note.html, $notesList);
+
// Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
this.collapseLongCommitList();
@@ -348,7 +350,7 @@ require('./task_list');
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
- discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
+ discussionContainer = window.$(`.notes[data-discussion-id="${note.discussion_id}"]`);
if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes');
}
@@ -370,14 +372,13 @@ require('./task_list');
row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
}
}
-
// Init discussion on 'Discussion' page if it is merge request page
- if ($('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) {
- $('ul.main-notes-list').append($(note.discussion_html).renderGFM());
+ if (window.$('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) {
+ Notes.animateAppendNote(note.discussion_html, window.$('ul.main-notes-list'));
}
} else {
// append new note to all matching discussions
- discussionContainer.append($(note.html).renderGFM());
+ Notes.animateAppendNote(note.html, discussionContainer);
}
if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_resolvable) {
@@ -1063,6 +1064,13 @@ require('./task_list');
return $form;
};
+ Notes.animateAppendNote = function(noteHTML, $notesList) {
+ const $note = window.$(noteHTML);
+
+ $note.addClass('fade-in').renderGFM();
+ $notesList.append($note);
+ };
+
return Notes;
})();
}).call(window);
diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue
index 11da6e908b7..d1c60b570de 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/async_button.vue
+++ b/app/assets/javascripts/pipelines/components/async_button.vue
@@ -65,6 +65,8 @@ export default {
makeRequest() {
this.isLoading = true;
+ $(this.$el).tooltip('destroy');
+
this.service.postAction(this.endpoint)
.then(() => {
this.isLoading = false;
@@ -88,9 +90,13 @@ export default {
:aria-label="title"
data-container="body"
data-placement="top"
- :disabled="isLoading"
- >
- <i :class="iconClass" aria-hidden="true"></i>
- <i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading"></i>
+ :disabled="isLoading">
+ <i
+ :class="iconClass"
+ aria-hidden="true" />
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"
+ v-if="isLoading" />
</button>
</template>
diff --git a/app/assets/javascripts/vue_pipelines_index/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index ba158bc4a1e..3db64339a62 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -13,7 +13,7 @@ export default {
</script>
<template>
- <div class="row empty-state">
+ <div class="row empty-state js-empty-state">
<div class="col-xs-12">
<div class="svg-content" v-html="pipelinesEmptyStateSVG" />
</div>
diff --git a/app/assets/javascripts/vue_pipelines_index/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue
index 90cee68163e..90cee68163e 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/error_state.vue
+++ b/app/assets/javascripts/pipelines/components/error_state.vue
diff --git a/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js b/app/assets/javascripts/pipelines/components/nav_controls.js
index 6aa10531034..6aa10531034 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js
+++ b/app/assets/javascripts/pipelines/components/nav_controls.js
diff --git a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js b/app/assets/javascripts/pipelines/components/navigation_tabs.js
index 1626ae17a30..1626ae17a30 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
+++ b/app/assets/javascripts/pipelines/components/navigation_tabs.js
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js b/app/assets/javascripts/pipelines/components/pipeline_url.js
index 4e183d5c8ec..4e183d5c8ec 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.js
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/pipelines/components/pipelines_actions.js
index 12d80768646..ffda18d2e0f 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.js
@@ -28,6 +28,8 @@ export default {
onClickAction(endpoint) {
this.isLoading = true;
+ $(this.$refs.tooltip).tooltip('destroy');
+
this.service.postAction(endpoint)
.then(() => {
this.isLoading = false;
@@ -57,6 +59,7 @@ export default {
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
+ ref="tooltip"
:disabled="isLoading">
${playIconSvg}
<i
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/pipelines/components/pipelines_artifacts.js
index f18e2dfadaf..f18e2dfadaf 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.js
diff --git a/app/assets/javascripts/vue_pipelines_index/components/stage.js b/app/assets/javascripts/pipelines/components/stage.js
index a2c29002707..b8cc3630611 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/stage.js
+++ b/app/assets/javascripts/pipelines/components/stage.js
@@ -1,32 +1,11 @@
/* global Flash */
-import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
-import createdSvg from 'icons/_icon_status_created_borderless.svg';
-import failedSvg from 'icons/_icon_status_failed_borderless.svg';
-import manualSvg from 'icons/_icon_status_manual_borderless.svg';
-import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
-import runningSvg from 'icons/_icon_status_running_borderless.svg';
-import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
-import successSvg from 'icons/_icon_status_success_borderless.svg';
-import warningSvg from 'icons/_icon_status_warning_borderless.svg';
+import StatusIconEntityMap from '../../ci_status_icons';
export default {
data() {
- const svgsDictionary = {
- icon_status_canceled: canceledSvg,
- icon_status_created: createdSvg,
- icon_status_failed: failedSvg,
- icon_status_manual: manualSvg,
- icon_status_pending: pendingSvg,
- icon_status_running: runningSvg,
- icon_status_skipped: skippedSvg,
- icon_status_success: successSvg,
- icon_status_warning: warningSvg,
- };
-
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
- svg: svgsDictionary[this.stage.status.icon],
};
},
@@ -89,6 +68,9 @@ export default {
triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
},
+ svgHTML() {
+ return StatusIconEntityMap[this.stage.status.icon];
+ },
},
template: `
<div>
@@ -100,7 +82,7 @@ export default {
data-toggle="dropdown"
type="button"
:aria-label="stage.title">
- <span v-html="svg" aria-hidden="true"></span>
+ <span v-html="svgHTML" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
diff --git a/app/assets/javascripts/vue_pipelines_index/components/status.js b/app/assets/javascripts/pipelines/components/status.js
index 21a281af438..21a281af438 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/status.js
+++ b/app/assets/javascripts/pipelines/components/status.js
diff --git a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js b/app/assets/javascripts/pipelines/components/time_ago.js
index 498d0715f54..498d0715f54 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
+++ b/app/assets/javascripts/pipelines/components/time_ago.js
diff --git a/app/assets/javascripts/vue_pipelines_index/event_hub.js b/app/assets/javascripts/pipelines/event_hub.js
index 0948c2e5352..0948c2e5352 100644
--- a/app/assets/javascripts/vue_pipelines_index/event_hub.js
+++ b/app/assets/javascripts/pipelines/event_hub.js
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/pipelines/index.js
index 48f9181a8d9..48f9181a8d9 100644
--- a/app/assets/javascripts/vue_pipelines_index/index.js
+++ b/app/assets/javascripts/pipelines/index.js
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js
index 6eea4812f33..6eea4812f33 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js
+++ b/app/assets/javascripts/pipelines/pipelines.js
diff --git a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 255cd513490..255cd513490 100644
--- a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
diff --git a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js
index 377ec8ba2cc..377ec8ba2cc 100644
--- a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
+++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 5b6bb2bf3f5..85659d7fa39 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -57,8 +57,11 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode
- if ($(e.target).hasClass('js-note-text')) {
- $('.js-md-preview-button').focus();
+ const $target = $(e.target);
+ const $form = $target.closest('form');
+
+ if ($target.hasClass('js-note-text')) {
+ $('.js-md-preview-button', $form).focus();
}
return $(document).triggerHandler('markdown-preview:toggle', [e]);
};
diff --git a/app/assets/javascripts/shortcuts_wiki.js b/app/assets/javascripts/shortcuts_wiki.js
new file mode 100644
index 00000000000..8a075062a48
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_wiki.js
@@ -0,0 +1,16 @@
+/* eslint-disable class-methods-use-this */
+/* global Mousetrap */
+/* global ShortcutsNavigation */
+
+import findAndFollowLink from './shortcuts_dashboard_navigation';
+
+export default class ShortcutsWiki extends ShortcutsNavigation {
+ constructor() {
+ super();
+ Mousetrap.bind('e', this.editWiki);
+ }
+
+ editWiki() {
+ findAndFollowLink('.js-wiki-edit');
+ }
+}
diff --git a/app/assets/javascripts/usage_ping.js b/app/assets/javascripts/usage_ping.js
new file mode 100644
index 00000000000..fd3af7d7ab6
--- /dev/null
+++ b/app/assets/javascripts/usage_ping.js
@@ -0,0 +1,15 @@
+function UsagePing() {
+ const usageDataUrl = $('.usage-data').data('endpoint');
+
+ $.ajax({
+ type: 'GET',
+ url: usageDataUrl,
+ dataType: 'html',
+ success(html) {
+ $('.usage-data').html(html);
+ },
+ });
+}
+
+window.gl = window.gl || {};
+window.gl.UsagePing = UsagePing;
diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js
index 5db0d936ad8..ce7eb76dc71 100644
--- a/app/assets/javascripts/user_tabs.js
+++ b/app/assets/javascripts/user_tabs.js
@@ -94,15 +94,17 @@ content on the Users#show page.
e.preventDefault();
$('.tab-pane.active').empty();
- this.loadTab($(e.target).attr('href'), this.getCurrentAction());
+ const endpoint = $(e.target).attr('href');
+ this.loadTab(this.getCurrentAction(), endpoint);
}
tabShown(event) {
const $target = $(event.target);
const action = $target.data('action');
const source = $target.attr('href');
- this.setTab(source, action);
- return this.setCurrentAction(source, action);
+ const endpoint = $target.data('endpoint');
+ this.setTab(action, endpoint);
+ return this.setCurrentAction(source);
}
activateTab(action) {
@@ -110,27 +112,27 @@ content on the Users#show page.
.tab('show');
}
- setTab(source, action) {
+ setTab(action, endpoint) {
if (this.loaded[action]) {
return;
}
if (action === 'activity') {
- this.loadActivities(source);
+ this.loadActivities();
}
const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
if (loadableActions.indexOf(action) > -1) {
- return this.loadTab(source, action);
+ return this.loadTab(action, endpoint);
}
}
- loadTab(source, action) {
+ loadTab(action, endpoint) {
return $.ajax({
beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
- url: source,
+ url: endpoint,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
@@ -140,7 +142,7 @@ content on the Users#show page.
});
}
- loadActivities(source) {
+ loadActivities() {
if (this.loaded['activity']) {
return;
}
@@ -155,7 +157,7 @@ content on the Users#show page.
.toggle(status);
}
- setCurrentAction(source, action) {
+ setCurrentAction(source) {
let new_state = source;
new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 3325a7d429c..30902767705 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -56,6 +56,9 @@
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
$loading.fadeOut();
+ })
+ .catch(function () {
+ $loading.fadeOut();
});
};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
index 8ebe12cb1c5..62b7131de51 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -1,12 +1,12 @@
/* eslint-disable no-param-reassign */
-import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button.vue';
-import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
-import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
-import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
-import PipelinesStageComponent from '../../vue_pipelines_index/components/stage';
-import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url';
-import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago';
+import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
+import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
+import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
+import PipelinesStatusComponent from '../../pipelines/components/status';
+import PipelinesStageComponent from '../../pipelines/components/stage';
+import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
+import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
import CommitComponent from './commit';
/**
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 90935b9616b..7c50b80fd2b 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -145,3 +145,17 @@ a {
.dropdown-menu-nav a {
transition: none;
}
+
+@keyframes fadeIn {
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+}
+
+.fade-in {
+ animation: fadeIn $fade-in-duration 1;
+}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 52425262925..f3e2a5db0a6 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -230,7 +230,6 @@
float: right;
margin-top: 8px;
padding-bottom: 8px;
- border-bottom: 1px solid $border-color;
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 2c33b235980..0fd7203e72b 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -40,6 +40,10 @@
line-height: 24px;
}
+.bold {
+ font-weight: 600;
+}
+
.tab-content {
overflow: visible;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 7767826b033..30d785464ac 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -14,14 +14,32 @@
}
}
+@mixin set-visible {
+ transform: translateY(0);
+ visibility: visible;
+ opacity: 1;
+ transition-duration: 100ms, 150ms, 25ms;
+ transition-delay: 35ms, 50ms, 25ms;
+}
+
+@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;
+}
+
.open {
.dropdown-menu,
.dropdown-menu-nav {
display: block;
+ @include set-visible;
@media (max-width: $screen-xs-max) {
width: 100%;
- min-width: 240px;
}
}
@@ -161,8 +179,9 @@
.dropdown-menu,
.dropdown-menu-nav {
- display: none;
+ display: block;
position: absolute;
+ width: 100%;
top: 100%;
left: 0;
z-index: 9;
@@ -176,6 +195,12 @@
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
+ overflow: hidden;
+ @include set-invisible;
+
+ @media (max-width: $screen-sm-min) {
+ width: 100%;
+ }
&.is-loading {
.dropdown-content {
@@ -252,6 +277,23 @@
}
}
+.filtered-search-box-input-container .dropdown-menu,
+.filtered-search-box-input-container .dropdown-menu-nav,
+.comment-type-dropdown .dropdown-menu {
+ display: none;
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
+
+.filtered-search-box-input-container {
+ .dropdown-menu,
+ .dropdown-menu-nav {
+ max-width: 280px;
+ width: auto;
+ }
+}
+
.dropdown-menu-drop-up {
top: auto;
bottom: 100%;
@@ -326,6 +368,10 @@
.dropdown-select {
width: $dropdown-width;
+
+ @media (max-width: $screen-sm-min) {
+ width: 100%;
+ }
}
.dropdown-menu-align-right {
@@ -564,3 +610,28 @@
color: $gl-text-color-secondary;
}
}
+
+.droplab-item-ignore {
+ pointer-events: none;
+}
+
+.pika-single.animate-picker.is-bound,
+.pika-single.animate-picker.is-bound.is-hidden {
+ /*
+ * Having `!important` is not recommended but
+ * since `pikaday` sets positioning inline
+ * there's no way it can be gracefully overridden
+ * using config options.
+ */
+ position: absolute !important;
+ display: block;
+}
+
+.pika-single.animate-picker.is-bound {
+ @include set-visible;
+}
+
+.pika-single.animate-picker.is-bound.is-hidden {
+ @include set-invisible;
+ overflow: hidden;
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index abb092623c0..6d9218310eb 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -155,7 +155,7 @@ header {
.header-logo {
display: inline-block;
- margin: 0 7px 0 2px;
+ margin: 0 12px 0 2px;
position: relative;
top: 10px;
transition-duration: .3s;
@@ -186,7 +186,7 @@ header {
display: flex;
align-items: flex-start;
flex: 1 1 auto;
- padding-top: (($header-height - 19) / 2);
+ padding-top: 14px;
overflow: hidden;
}
@@ -329,8 +329,17 @@ header {
.header-user {
.dropdown-menu-nav {
+ width: auto;
min-width: 140px;
margin-top: -5px;
+
+ .current-user {
+ padding: 5px 18px;
+
+ .user-name {
+ display: block;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index e6d808717f3..b6cf5101d60 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -110,7 +110,7 @@
.top-area {
@include clearfix;
- border-bottom: 1px solid $white-normal;
+ border-bottom: 1px solid $border-color;
.nav-text {
padding-top: 16px;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 664539e93e1..1839cadcc10 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -338,3 +338,32 @@ h4 {
.idiff.addition {
background: $line-added-dark;
}
+
+
+/**
+ * form text input i.e. search bar, comments, forms, etc.
+ */
+input,
+textarea {
+ &::-webkit-input-placeholder {
+ color: $placeholder-text-color;
+ }
+
+ // support firefox 19+ vendor prefix
+ &::-moz-placeholder {
+ color: $placeholder-text-color;
+ opacity: 1; // FF defaults to 0.54
+ }
+
+ // scss-lint:disable PseudoElement
+ // support Edge vendor prefix
+ &::-ms-input-placeholder {
+ color: $placeholder-text-color;
+ }
+
+ // scss-lint:disable PseudoElement
+ // support IE vendor prefix
+ &:-ms-input-placeholder {
+ color: $placeholder-text-color;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 20ef9a774e4..49741c963df 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -111,6 +111,7 @@ $gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
$gl-header-color: #4c4e54;
$gl-header-nav-hover-color: #434343;
+$placeholder-text-color: rgba(0, 0, 0, .42);
/*
* Lists
@@ -458,6 +459,11 @@ $label-remove-border: rgba(0, 0, 0, .1);
$label-border-radius: 100px;
/*
+* Animation
+*/
+$fade-in-duration: 200ms;
+
+/*
* Lint
*/
$lint-incorrect-color: $red-500;
@@ -556,3 +562,8 @@ $filter-name-text-color: rgba(0, 0, 0, 0.55);
$filter-value-text-color: rgba(0, 0, 0, 0.85);
$filter-name-selected-color: #ebebeb;
$filter-value-selected-color: #d7d7d7;
+
+/*
+Animation Functions
+*/
+$dropdown-animation-timing: cubic-bezier(0.23, 1, 0.32, 1);
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 144adbcdaef..411f1c4442b 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -61,8 +61,9 @@
.truncated-info {
text-align: center;
border-bottom: 1px solid;
- background-color: $black-transparent;
+ background-color: $black;
height: 45px;
+ padding: 15px;
&.affix {
top: 0;
@@ -87,6 +88,16 @@
right: 5px;
left: 5px;
}
+
+ .truncated-info-size {
+ margin: 0 5px;
+ }
+
+ .raw-link {
+ color: inherit;
+ margin-left: 5px;
+ text-decoration: underline;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 0dad91ba128..9e3142c8aa3 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -135,7 +135,7 @@
.text-expander {
display: inline-block;
- background: $gray-light;
+ background: $white-light;
color: $gl-text-color-secondary;
padding: 0 5px;
cursor: pointer;
@@ -146,6 +146,11 @@
line-height: $gl-font-size;
outline: none;
+ &.open {
+ background: $gray-light;
+ box-shadow: inset 0 0 2px rgba($black, 0.2);
+ }
+
&:hover {
background-color: darken($gray-light, 10%);
text-decoration: none;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 1aa1079903c..1b4694377b3 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -106,6 +106,10 @@
span {
white-space: pre-wrap;
}
+
+ .line {
+ word-wrap: break-word;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 0bca3e93e4c..8d3d1a72b9b 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -210,10 +210,6 @@
}
}
- .bold {
- font-weight: 600;
- }
-
.light {
font-weight: normal;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 220b6d6f141..2ea2ff8362b 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -123,6 +123,9 @@ ul.notes {
}
.note-emoji-button {
+ position: relative;
+ line-height: 1;
+
.fa-spinner {
display: none;
}
@@ -443,7 +446,8 @@ ul.notes {
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
- margin-left: -20px;
+ top: 0;
+ left: 0;
opacity: 0;
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 8c6dd392865..fe084eb9397 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -289,8 +289,12 @@ table.u2f-registrations {
margin: 0 auto;
.bordered-box {
- border: 1px solid $border-color;
+ border: 1px solid $blue-300;
border-radius: $border-radius-default;
+ background-color: $blue-25;
+ position: relative;
+ display: flex;
+ justify-content: center;
}
.landing {
@@ -298,28 +302,59 @@ table.u2f-registrations {
margin-bottom: $gl-padding;
.close {
- margin-right: 20px;
- }
+ position: absolute;
+ right: 20px;
+ opacity: 1;
+
+ .dismiss-icon {
+ float: right;
+ cursor: pointer;
+ color: $blue-300;
+ }
- .dismiss-icon {
- float: right;
- cursor: pointer;
- color: $cycle-analytics-dismiss-icon-color;
+ &:hover {
+ background-color: transparent;
+ border: 0;
+
+ .dismiss-icon {
+ color: $blue-400;
+ }
+ }
}
.svg-container {
- text-align: center;
+ margin-right: 30px;
+ display: inline-block;
svg {
- width: 136px;
- height: 136px;
+ height: 110px;
+ vertical-align: top;
}
}
+
+ .user-callout-copy {
+ display: inline-block;
+ vertical-align: top;
+ }
}
@media(max-width: $screen-xs-max) {
- .inner-content {
- padding-left: 30px;
+ text-align: center;
+
+ .bordered-box {
+ display: block;
+ }
+
+ .landing {
+ .svg-container,
+ .user-callout-copy {
+ margin: 0;
+ display: block;
+
+ svg {
+ height: 75px;
+ }
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 717ebb44a23..28a8f9cb335 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -596,6 +596,10 @@ pre.light-well {
.avatar-container {
align-self: flex-start;
+
+ > a {
+ width: 100%;
+ }
}
.project-details {
@@ -929,27 +933,23 @@ pre.light-well {
}
.variable-key {
- width: 300px;
- max-width: 300px;
+ max-width: 120px;
overflow: hidden;
word-wrap: break-word;
-
- // override bootstrap
- white-space: normal!important;
-
- @media (max-width: $screen-sm-max) {
- width: 150px;
- max-width: 150px;
- }
+ white-space: nowrap;
+ text-overflow: ellipsis;
}
.variable-value {
- @media(max-width: $screen-xs-max) {
- width: 150px;
- max-width: 150px;
- overflow: hidden;
- word-wrap: break-word;
- }
+ max-width: 150px;
+ overflow: hidden;
+ word-wrap: break-word;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .variable-menu {
+ text-align: right;
}
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 543d2ece3df..b9818ffcf42 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -124,7 +124,13 @@ input[type="checkbox"]:hover {
// Custom dropdown positioning
.dropdown-menu {
- top: 37px;
+ 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;
padding: 0;
@@ -156,6 +162,13 @@ input[type="checkbox"]:hover {
color: $layout-link-gray;
}
}
+
+ .dropdown-menu {
+ transition-duration: 100ms, 75ms;
+ transition-delay: 75ms, 100ms;
+ transform: translateY(13px);
+ opacity: 1;
+ }
}
&.has-value {
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 515d8e1523b..643993d035e 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -17,6 +17,18 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
end
+ def usage_data
+ respond_to do |format|
+ format.html do
+ usage_data = Gitlab::UsageData.data
+ usage_data_json = params[:pretty] ? JSON.pretty_generate(usage_data) : usage_data.to_json
+
+ render html: Gitlab::Highlight.highlight('payload.json', usage_data_json)
+ end
+ format.json { render json: Gitlab::UsageData.to_json }
+ end
+ end
+
def reset_runners_token
@application_setting.reset_runners_registration_token!
flash[:notice] = 'New runners registration token has been generated!'
@@ -135,6 +147,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:version_check_enabled,
:terminal_max_session_time,
:polling_interval_multiplier,
+ :usage_ping_enabled,
disabled_oauth_sign_in_sources: [],
import_sources: [],
diff --git a/app/controllers/admin/cohorts_controller.rb b/app/controllers/admin/cohorts_controller.rb
new file mode 100644
index 00000000000..9b77c554908
--- /dev/null
+++ b/app/controllers/admin/cohorts_controller.rb
@@ -0,0 +1,11 @@
+class Admin::CohortsController < Admin::ApplicationController
+ def index
+ if current_application_settings.usage_ping_enabled
+ cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
+ CohortsService.new.execute
+ end
+
+ @cohorts = CohortsSerializer.new.represent(cohorts_results)
+ end
+ end
+end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index f28bbdeff5a..fc8d4d02ddf 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -43,9 +43,13 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
- @group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user)
+ status = Members::CreateService.new(@group, current_user, params).execute
- redirect_to [:admin, @group], notice: 'Users were successfully added.'
+ if status
+ redirect_to [:admin, @group], notice: 'Users were successfully added.'
+ else
+ redirect_to [:admin, @group], alert: 'No users specified.'
+ end
end
def destroy
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index 2abfa22712d..1d66955bb71 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -7,7 +7,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
spam_log = SpamLog.find(params[:id])
if params[:remove_user]
- spam_log.remove_user
+ spam_log.remove_user(deleted_by: current_user)
redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed."
else
spam_log.destroy
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 9ac8197e45a..183eb00ef67 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -1,17 +1,29 @@
module CreatesCommit
extend ActiveSupport::Concern
+ def set_start_branch_to_branch_name
+ branch_exists = @repository.find_branch(@branch_name)
+ @start_branch = @branch_name if branch_exists
+ end
+
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
- set_commit_variables
+ if can?(current_user, :push_code, @project)
+ @project_to_commit_into = @project
+ @branch_name ||= @ref
+ else
+ @project_to_commit_into = current_user.fork_of(@project)
+ @branch_name ||= @project_to_commit_into.repository.next_branch('patch')
+ end
+
+ @start_branch ||= @ref || @branch_name
commit_params = @commit_params.merge(
- start_project: @mr_target_project,
- start_branch: @mr_target_branch,
- target_branch: @mr_source_branch
+ start_project: @project,
+ start_branch: @start_branch,
+ branch_name: @branch_name
)
- result = service.new(
- @mr_source_project, current_user, commit_params).execute
+ result = service.new(@project_to_commit_into, current_user, commit_params).execute
if result[:status] == :success
update_flash_notice(success_notice)
@@ -72,30 +84,30 @@ module CreatesCommit
def new_merge_request_path
new_namespace_project_merge_request_path(
- @mr_source_project.namespace,
- @mr_source_project,
+ @project_to_commit_into.namespace,
+ @project_to_commit_into,
merge_request: {
- source_project_id: @mr_source_project.id,
- target_project_id: @mr_target_project.id,
- source_branch: @mr_source_branch,
- target_branch: @mr_target_branch
+ source_project_id: @project_to_commit_into.id,
+ target_project_id: @project.id,
+ source_branch: @branch_name,
+ target_branch: @start_branch
}
)
end
def existing_merge_request_path
- namespace_project_merge_request_path(@mr_target_project.namespace, @mr_target_project, @merge_request)
+ namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
end
def merge_request_exists?
return @merge_request if defined?(@merge_request)
- @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened.
- find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch, source_project_id: @mr_source_project)
+ @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)
end
def different_project?
- @mr_source_project != @mr_target_project
+ @project_to_commit_into != @project
end
def create_merge_request?
@@ -103,22 +115,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? || @mr_target_branch != @mr_source_branch)
- end
-
- def set_commit_variables
- if can?(current_user, :push_code, @project)
- @mr_source_project = @project
- @target_branch ||= @ref
- else
- @mr_source_project = current_user.fork_of(@project)
- @target_branch ||= @mr_source_project.repository.next_branch('patch')
- end
-
- # Merge request to this project
- @mr_target_project = @project
- @mr_target_branch ||= @ref || @target_branch
-
- @mr_source_branch = @target_branch
+ (different_project? || @start_branch != @branch_name)
end
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index c13333641d3..b1bacc8ffe5 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -1,6 +1,32 @@
module MembershipActions
extend ActiveSupport::Concern
+ def create
+ status = Members::CreateService.new(membershipable, current_user, params).execute
+
+ redirect_url = members_page_url
+
+ if status
+ redirect_to redirect_url, notice: 'Users were successfully added.'
+ else
+ redirect_to redirect_url, alert: 'No users specified.'
+ end
+ end
+
+ def destroy
+ Members::DestroyService.new(membershipable, current_user, params).
+ execute(:all)
+
+ respond_to do |format|
+ format.html do
+ message = "User was successfully removed from #{source_type}."
+ redirect_to members_page_url, notice: message
+ end
+
+ format.js { head :ok }
+ end
+ end
+
def request_access
membershipable.request_access(current_user)
@@ -11,20 +37,20 @@ module MembershipActions
def approve_access_request
Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
- redirect_to polymorphic_url([membershipable, :members])
+ redirect_to members_page_url
end
def leave
member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
execute(:all)
- source_type = membershipable.class.to_s.humanize(capitalize: false)
notice =
if member.request?
"Your access request to the #{source_type} has been withdrawn."
else
"You left the \"#{membershipable.human_name}\" #{source_type}."
end
+
redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice
@@ -35,4 +61,16 @@ module MembershipActions
def membershipable
raise NotImplementedError
end
+
+ def members_page_url
+ if membershipable.is_a?(Project)
+ project_settings_members_path(membershipable)
+ else
+ polymorphic_url([membershipable, :members])
+ end
+ end
+
+ def source_type
+ @source_type ||= membershipable.class.to_s.humanize(capitalize: false)
+ end
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 00c50f9d0ad..8fc234a62b1 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -21,21 +21,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member = @group.group_members.new
end
- def create
- if params[:user_ids].blank?
- return redirect_to(group_group_members_path(@group), alert: 'No users specified.')
- end
-
- @group.add_users(
- params[:user_ids].split(','),
- params[:access_level],
- current_user: current_user,
- expires_at: params[:expires_at]
- )
-
- redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
- end
-
def update
@group_member = @group.group_members.find(params[:id])
@@ -44,15 +29,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member.update_attributes(member_params)
end
- def destroy
- Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
-
- respond_to do |format|
- format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
- format.js { head :ok }
- end
- end
-
def resend_invite
redirect_path = group_group_members_path(@group)
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index f1a93ccb3ad..e2f81b09adc 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -89,9 +89,4 @@ class Projects::ApplicationController < ApplicationController
def builds_enabled
return render_404 unless @project.feature_available?(:builds, current_user)
end
-
- def update_ref
- branch_exists = @repository.find_branch(@target_branch)
- @ref = @target_branch if branch_exists
- end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 73706bf8dae..9fce1db6742 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -25,10 +25,10 @@ class Projects::BlobController < Projects::ApplicationController
end
def create
- update_ref
+ set_start_branch_to_branch_name
create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
- success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) },
+ success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @file_path)) },
failure_view: :new,
failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
end
@@ -69,10 +69,10 @@ class Projects::BlobController < Projects::ApplicationController
end
def destroy
- create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.",
- success_path: -> { namespace_project_tree_path(@project.namespace, @project, @target_branch) },
- failure_view: :show,
- failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
+ create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.",
+ success_path: -> { namespace_project_tree_path(@project.namespace, @project, @branch_name) },
+ failure_view: :show,
+ failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
end
def diff
@@ -127,16 +127,16 @@ class Projects::BlobController < Projects::ApplicationController
def after_edit_path
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid])
- if from_merge_request && @target_branch == @ref
+ if from_merge_request && @branch_name == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"##{hexdigest(@path)}"
else
- namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
+ namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @path))
end
end
def editor_variables
- @target_branch = params[:target_branch]
+ @branch_name = params[:branch_name]
@file_path =
if action_name.to_s == 'create'
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index d25bbddd1bb..2b5f0383ac1 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -56,9 +56,7 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @start_branch.blank?
- @target_branch = create_new_branch? ? @commit.revert_branch_name : @start_branch
-
- @mr_target_branch = @start_branch
+ @branch_name = create_new_branch? ? @commit.revert_branch_name : @start_branch
create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
success_path: -> { successful_change_path }, failure_path: failed_change_path)
@@ -69,9 +67,7 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @start_branch.blank?
- @target_branch = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch
-
- @mr_target_branch = @start_branch
+ @branch_name = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch
create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.",
success_path: -> { successful_change_path }, failure_path: failed_change_path)
@@ -84,7 +80,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def successful_change_path
- referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch)
+ referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @branch_name)
end
def failed_change_path
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 37f6f637ff0..10adddb4636 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -5,6 +5,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs
if upload_pack? && upload_pack_allowed?
+ log_user_activity
+
render_ok
elsif receive_pack? && receive_pack_allowed?
render_ok
@@ -106,4 +108,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
def access_klass
@access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
end
+
+ def log_user_activity
+ Users::ActivityService.new(user, 'pull').execute
+ end
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 6e158e685e9..d2d26738582 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -10,18 +10,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort)
end
- def create
- status = Members::CreateService.new(@project, current_user, params).execute
-
- redirect_url = namespace_project_settings_members_path(@project.namespace, @project)
-
- if status
- redirect_to redirect_url, notice: 'Users were successfully added.'
- else
- redirect_to redirect_url, alert: 'No users or groups specified.'
- end
- end
-
def update
@project_member = @project.project_members.find(params[:id])
@@ -30,18 +18,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_member.update_attributes(member_params)
end
- def destroy
- Members::DestroyService.new(@project, current_user, params).
- execute(:all)
-
- respond_to do |format|
- format.html do
- redirect_to namespace_project_settings_members_path(@project.namespace, @project)
- end
- format.js { head :ok }
- end
- end
-
def resend_invite
redirect_path = namespace_project_settings_members_path(@project.namespace, @project)
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 637b61504d8..5e2182c883e 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -34,16 +34,16 @@ class Projects::TreeController < Projects::ApplicationController
def create_dir
return render_404 unless @commit_params.values.all?
- update_ref
+ set_start_branch_to_branch_name
create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
- success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)),
+ success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@branch_name, @dir_name)),
failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
end
private
def assign_dir_vars
- @target_branch = params[:target_branch]
+ @branch_name = params[:branch_name]
@dir_name = File.join(@path, params[:dir_name])
@commit_params = {
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index d3091a4f8e9..8c6ba4915cd 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -35,6 +35,7 @@ class SessionsController < Devise::SessionsController
# hide the signed-in notification
flash[:notice] = nil
log_audit_event(current_user, with: authentication_method)
+ log_user_activity(current_user)
end
end
@@ -123,6 +124,10 @@ class SessionsController < Devise::SessionsController
for_authentication.security_event
end
+ def log_user_activity(user)
+ Users::ActivityService.new(user, 'login').execute
+ end
+
def load_recaptcha
Gitlab::Recaptcha.load_configurations!
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 6c3f3a61e0a..3736e1ffcbb 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -14,15 +14,6 @@ module BlobHelper
options[:link_opts])
end
- def fork_path(project = @project, ref = @ref, path = @path, options = {})
- continue_params = {
- to: edit_path,
- notice: edit_in_new_fork_notice,
- notice_now: edit_in_new_fork_notice_now
- }
- namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
- end
-
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil
@@ -34,10 +25,19 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
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_edit_blob?(blob, project, ref))
+ 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"
elsif current_user && can?(current_user, :fork_project, project)
- button_tag 'Edit', class: "#{common_classes} js-edit-blob-link-fork-toggler"
+ continue_params = {
+ to: edit_path,
+ notice: edit_in_new_fork_notice,
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
+
+ button_tag 'Edit',
+ class: "#{common_classes} js-edit-blob-link-fork-toggler",
+ data: { action: 'edit', fork_path: fork_path }
end
end
@@ -48,21 +48,25 @@ module BlobHelper
return unless blob
+ common_classes = "btn btn-#{btn_class}"
+
if !on_top_of_branch?(project, ref)
- button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
+ button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.lfs_pointer?
- button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
- elsif can_edit_blob?(blob, project, ref)
- button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
+ button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
+ elsif can_modify_blob?(blob, project, ref)
+ button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
continue_params = {
- to: request.fullpath,
+ to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to #{action} this file again.",
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
- link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post
+ button_tag label,
+ class: "#{common_classes} js-edit-blob-link-fork-toggler",
+ data: { action: action, fork_path: fork_path }
end
end
@@ -90,7 +94,7 @@ module BlobHelper
)
end
- def can_edit_blob?(blob, project = @project, ref = @ref)
+ def can_modify_blob?(blob, project = @project, ref = @ref)
!blob.lfs_pointer? && can_edit_tree?(project, ref)
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index ec57fec4f99..0b13dbf5f8d 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -165,11 +165,8 @@ module IssuablesHelper
html.html_safe
end
- def cached_assigned_issuables_count(assignee, issuable_type, state)
- cache_key = hexdigest(['assigned_issuables_count', assignee.id, issuable_type, state].join('-'))
- Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
- assigned_issuables_count(assignee, issuable_type, state)
- end
+ def assigned_issuables_count(issuable_type)
+ current_user.public_send("assigned_open_#{issuable_type}_count")
end
def issuable_filter_params
@@ -192,10 +189,6 @@ module IssuablesHelper
private
- def assigned_issuables_count(assignee, issuable_type, state)
- assignee.public_send("assigned_#{issuable_type}").public_send(state).count
- end
-
def sidebar_gutter_collapsed?
cookies[:collapsed_gutter] == 'true'
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 6b9e4267281..5f97e6114ea 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -24,7 +24,7 @@ module ProjectsHelper
return "(deleted)" unless author
- author_html = ""
+ author_html = ""
# Build avatar image tag
author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar]
@@ -45,7 +45,7 @@ module ProjectsHelper
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else
title = opts[:title].sub(":name", sanitize(author.name))
- link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe
+ link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' }).html_safe
end
end
@@ -272,14 +272,14 @@ module ProjectsHelper
end
end
- def add_special_file_path(project, file_name:, commit_message: nil, target_branch: nil, context: nil)
+ def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil)
namespace_project_new_blob_path(
project.namespace,
project,
project.default_branch || 'master',
file_name: file_name,
commit_message: commit_message || "Add #{file_name.downcase}",
- target_branch: target_branch,
+ branch_name: branch_name,
context: context
)
end
@@ -430,13 +430,22 @@ module ProjectsHelper
end
def visibility_select_options(project, selected_level)
- levels_options_array = Gitlab::VisibilityLevel.values.map do |level|
- [
+ level_options = Gitlab::VisibilityLevel.values.each_with_object([]) do |level, level_options|
+ next if restricted_levels.include?(level)
+
+ level_options << [
visibility_level_label(level),
{ data: { description: visibility_level_description(level, project) } },
level
]
end
- options_for_select(levels_options_array, selected_level)
+
+ options_for_select(level_options, selected_level)
+ end
+
+ def restricted_levels
+ return [] if current_user.admin?
+
+ current_application_settings.restricted_visibility_levels || []
end
end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 8c02b4061ca..979264c9421 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -42,7 +42,7 @@ module SnippetsHelper
0,
lined_content.size,
surrounding_lines
- ) if line.include?(query)
+ ) if line.downcase.include?(query.downcase)
end
used_lines.uniq.sort
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 3a5d1b97c36..2fda98cae90 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -62,6 +62,14 @@ module SortingHelper
}
end
+ def branches_sort_options_hash
+ {
+ sort_value_name => sort_title_name,
+ sort_value_recently_updated => sort_title_recently_updated,
+ sort_value_oldest_updated => sort_title_oldest_updated
+ }
+ end
+
def sort_title_priority
'Priority'
end
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index fb95f2b565e..a762b320d56 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -5,7 +5,7 @@ module SubmoduleHelper
def submodule_links(submodule_item, ref = nil, repository = @repository)
url = repository.submodule_url_for(ref, submodule_item.path)
- return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/
+ return url, nil unless url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace = $1
project = $2
@@ -37,14 +37,16 @@ module SubmoduleHelper
end
def self_url?(url, namespace, project)
- return true if url == [Gitlab.config.gitlab.url, '/', namespace, '/',
- project, '.git'].join('')
- url == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
+ url_no_dotgit = url.chomp('.git')
+ return true if url_no_dotgit == [Gitlab.config.gitlab.url, '/', namespace, '/',
+ project].join('')
+ url_with_dotgit = url_no_dotgit + '.git'
+ url_with_dotgit == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
end
def relative_self_url?(url)
# (./)?(../repo.git) || (./)?(../../project/repo.git) )
- url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/
+ url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*(\.git)?\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*(\.git)?\z/
end
def standard_links(host, namespace, project, commit)
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 4a76c679bad..f1dab60524e 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -35,7 +35,7 @@ module TreeHelper
end
def on_top_of_branch?(project = @project, ref = @ref)
- project.repository.branch_names.include?(ref)
+ project.repository.branch_exists?(ref)
end
def can_edit_tree?(project = nil, ref = nil)
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 2340453831e..0d7c2d20029 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -16,7 +16,7 @@ class AbuseReport < ActiveRecord::Base
def remove_user(deleted_by:)
user.block
- DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
+ DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true)
end
def notify
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 2961e16f5e0..dd1a6922968 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -238,7 +238,8 @@ class ApplicationSetting < ActiveRecord::Base
terminal_max_session_time: 0,
two_factor_grace_period: 48,
user_default_external: false,
- polling_interval_multiplier: 1
+ polling_interval_multiplier: 1,
+ usage_ping_enabled: true
}
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 8ea95beed79..2eedc143968 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -8,6 +8,14 @@
#
# Corresponding foo_html, bar_html and baz_html fields should exist.
module CacheMarkdownField
+ extend ActiveSupport::Concern
+
+ # Increment this number every time the renderer changes its output
+ CACHE_VERSION = 1
+
+ # changes to these attributes cause the cache to be invalidates
+ INVALIDATED_BY = %w[author project].freeze
+
# Knows about the relationship between markdown and html field names, and
# stores the rendering contexts for the latter
class FieldData
@@ -30,60 +38,71 @@ module CacheMarkdownField
end
end
- # Dynamic registries don't really work in Rails as it's not guaranteed that
- # every class will be loaded, so hardcode the list.
- CACHING_CLASSES = %w[
- AbuseReport
- Appearance
- ApplicationSetting
- BroadcastMessage
- Issue
- Label
- MergeRequest
- Milestone
- Namespace
- Note
- Project
- Release
- Snippet
- ].freeze
-
- def self.caching_classes
- CACHING_CLASSES.map(&:constantize)
- end
-
def skip_project_check?
false
end
- extend ActiveSupport::Concern
+ # Returns the default Banzai render context for the cached markdown field.
+ def banzai_render_context(field)
+ raise ArgumentError.new("Unknown field: #{field.inspect}") unless
+ cached_markdown_fields.markdown_fields.include?(field)
- included do
- cattr_reader :cached_markdown_fields do
- FieldData.new
- end
+ # Always include a project key, or Banzai complains
+ project = self.project if self.respond_to?(:project)
+ context = cached_markdown_fields[field].merge(project: project)
- # Returns the default Banzai render context for the cached markdown field.
- def banzai_render_context(field)
- raise ArgumentError.new("Unknown field: #{field.inspect}") unless
- cached_markdown_fields.markdown_fields.include?(field)
+ # Banzai is less strict about authors, so don't always have an author key
+ context[:author] = self.author if self.respond_to?(:author)
- # Always include a project key, or Banzai complains
- project = self.project if self.respond_to?(:project)
- context = cached_markdown_fields[field].merge(project: project)
+ context
+ end
- # Banzai is less strict about authors, so don't always have an author key
- context[:author] = self.author if self.respond_to?(:author)
+ # Update every column in a row if any one is invalidated, as we only store
+ # one version per row
+ def refresh_markdown_cache!(do_update: false)
+ options = { skip_project_check: skip_project_check? }
- context
- end
+ updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
+ [
+ cached_markdown_fields.html_field(markdown_field),
+ Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
+ ]
+ end.to_h
+ updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
- # Allow callers to look up the cache field name, rather than hardcoding it
- def markdown_cache_field_for(field)
- raise ArgumentError.new("Unknown field: #{field}") unless
- cached_markdown_fields.markdown_fields.include?(field)
+ updates.each {|html_field, data| write_attribute(html_field, data) }
- cached_markdown_fields.html_field(field)
+ update_columns(updates) if persisted? && do_update
+ end
+
+ def cached_html_up_to_date?(markdown_field)
+ html_field = cached_markdown_fields.html_field(markdown_field)
+
+ markdown_changed = attribute_changed?(markdown_field) || false
+ html_changed = attribute_changed?(html_field) || false
+
+ CacheMarkdownField::CACHE_VERSION == cached_markdown_version &&
+ (html_changed || markdown_changed == html_changed)
+ end
+
+ def invalidated_markdown_cache?
+ cached_markdown_fields.html_fields.any? {|html_field| attribute_invalidated?(html_field) }
+ end
+
+ def attribute_invalidated?(attr)
+ __send__("#{attr}_invalidated?")
+ end
+
+ def cached_html_for(markdown_field)
+ raise ArgumentError.new("Unknown field: #{field}") unless
+ cached_markdown_fields.markdown_fields.include?(markdown_field)
+
+ __send__(cached_markdown_fields.html_field(markdown_field))
+ end
+
+ included do
+ cattr_reader :cached_markdown_fields do
+ FieldData.new
end
# Always exclude _html fields from attributes (including serialization).
@@ -92,12 +111,16 @@ module CacheMarkdownField
def attributes
attrs = attributes_before_markdown_cache
+ attrs.delete('cached_markdown_version')
+
cached_markdown_fields.html_fields.each do |field|
attrs.delete(field)
end
attrs
end
+
+ before_save :refresh_markdown_cache!, if: :invalidated_markdown_cache?
end
class_methods do
@@ -107,31 +130,18 @@ module CacheMarkdownField
# a corresponding _html field. Any custom rendering options may be provided
# as a context.
def cache_markdown_field(markdown_field, context = {})
- raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless
- CacheMarkdownField::CACHING_CLASSES.include?(self.to_s)
-
cached_markdown_fields[markdown_field] = context
html_field = cached_markdown_fields.html_field(markdown_field)
- cache_method = "#{markdown_field}_cache_refresh".to_sym
invalidation_method = "#{html_field}_invalidated?".to_sym
- define_method(cache_method) do
- options = { skip_project_check: skip_project_check? }
- html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
- __send__("#{html_field}=", html)
- true
- end
-
# The HTML becomes invalid if any dependent fields change. For now, assume
# author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do
changed_fields = changed_attributes.keys
- invalidations = changed_fields & [markdown_field.to_s, "author", "project"]
- !invalidations.empty?
+ invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
+ !invalidations.empty? || !cached_html_up_to_date?(markdown_field)
end
-
- before_save cache_method, if: invalidation_method
end
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 3d2258d5e3e..26dbf4d9570 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -23,7 +23,7 @@ module Issuable
included do
cache_markdown_field :title, pipeline: :single_line
- cache_markdown_field :description
+ cache_markdown_field :description, issuable_state_filter_enabled: true
belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User"
diff --git a/app/models/group.rb b/app/models/group.rb
index 106084175ff..cbc10b00cf5 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -125,7 +125,7 @@ class Group < Namespace
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
- GroupMember.add_users_to_group(
+ GroupMember.add_users(
self,
users,
access_level,
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 3bacc450e6e..920a25932b4 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -7,6 +7,8 @@ class Identity < ActiveRecord::Base
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
validates :user_id, uniqueness: { scope: :provider }
+ scope :with_extern_uid, ->(provider, extern_uid) { where(extern_uid: extern_uid, provider: provider) }
+
def ldap?
provider.starts_with?('ldap')
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d39ae3a6c92..305fc01f041 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -199,7 +199,7 @@ class Issue < ActiveRecord::Base
# Returns `true` if the current issue can be viewed by either a logged in User
# or an anonymous user.
def visible_to_user?(user = nil)
- return false unless project.feature_available?(:issues, user)
+ return false unless project && project.feature_available?(:issues, user)
user ? readable_by?(user) : publicly_visible?
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 0545bd4eedf..97fba501759 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -151,6 +151,22 @@ class Member < ActiveRecord::Base
member
end
+ def add_users(source, users, access_level, current_user: nil, expires_at: nil)
+ return [] unless users.present?
+
+ self.transaction do
+ users.map do |user|
+ add_user(
+ source,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
+ end
+ end
+ end
+
def access_levels
Gitlab::Access.sym_options
end
@@ -173,18 +189,6 @@ class Member < ActiveRecord::Base
# There is no current user for bulk actions, in which case anything is allowed
!current_user || current_user.can?(:"update_#{member.type.underscore}", member)
end
-
- def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil)
- users.each do |user|
- add_user(
- source,
- user,
- access_level,
- current_user: current_user,
- expires_at: expires_at
- )
- end
- end
end
def real_source_type
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 483425cd30f..28e10bc6172 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -21,18 +21,6 @@ class GroupMember < Member
Gitlab::Access.sym_options_with_owner
end
- def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil)
- self.transaction do
- add_users_to_source(
- group,
- users,
- access_level,
- current_user: current_user,
- expires_at: expires_at
- )
- end
- end
-
def group
source
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 912820b51ac..b3a91feb091 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -16,7 +16,7 @@ class ProjectMember < Member
before_destroy :delete_member_todos
class << self
- # Add users to project teams with passed access option
+ # Add users to projects with passed access option
#
# access can be an integer representing a access code
# or symbol like :master representing role
@@ -39,7 +39,7 @@ class ProjectMember < Member
project_ids.each do |project_id|
project = Project.find(project_id)
- add_users_to_source(
+ add_users(
project,
users,
access_level,
diff --git a/app/models/note.rb b/app/models/note.rb
index 630d0adbece..e720bfba030 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -16,7 +16,7 @@ class Note < ActiveRecord::Base
ignore_column :original_discussion_id
- cache_markdown_field :note, pipeline: :note
+ cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
# Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer.
diff --git a/app/models/project.rb b/app/models/project.rb
index a160efba912..73593f04283 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -181,7 +181,7 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true
- delegate :add_user, to: :team
+ delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
delegate :empty_repo?, to: :repository
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index fa782c6fbb7..f2dfb87dbda 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -22,7 +22,7 @@ class ChatNotificationService < Service
end
def can_test?
- valid?
+ super && valid?
end
def self.supported_events
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 6d6644053f8..543b9b293e0 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -50,8 +50,8 @@ class ProjectTeam
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
- ProjectMember.add_users_to_projects(
- [project.id],
+ ProjectMember.add_users(
+ project,
users,
access_level,
current_user: current_user,
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 2b11ed6128e..7bb874d7744 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -19,7 +19,7 @@ class Repository
#
# For example, for entry `:readme` there's a method called `readme` which
# stores its data in the `readme` cache key.
- CACHED_METHODS = %i(size commit_count readme version contribution_guide
+ CACHED_METHODS = %i(size commit_count readme contribution_guide
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? empty? root_ref).freeze
@@ -32,7 +32,6 @@ class Repository
changelog: :changelog,
license: %i(license_blob license_key),
contributing: :contribution_guide,
- version: :version,
gitignore: :gitignore,
koding: :koding_yml,
gitlab_ci: :gitlab_ci_yml,
@@ -109,7 +108,7 @@ class Repository
offset: offset,
after: after,
before: before,
- follow: path.present?,
+ follow: Array(path).length == 1,
skip_merges: skip_merges
}
@@ -530,11 +529,6 @@ class Repository
end
cache_method :readme
- def version
- file_on_head(:version)
- end
- cache_method :version
-
def contribution_guide
file_on_head(:contributing)
end
diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb
index 3b8b9833565..dd21ee15c6c 100644
--- a/app/models/spam_log.rb
+++ b/app/models/spam_log.rb
@@ -3,9 +3,9 @@ class SpamLog < ActiveRecord::Base
validates :user, presence: true
- def remove_user
+ def remove_user(deleted_by:)
user.block
- user.destroy
+ DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true)
end
def text
diff --git a/app/models/user.rb b/app/models/user.rb
index 457ba05fb04..774d4caa806 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -99,9 +99,6 @@ class User < ActiveRecord::Base
has_many :award_emoji, dependent: :destroy
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id
- has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
- has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
-
# Issues that a user owns are expected to be moved to the "ghost" user before
# the user is destroyed. If the user owns any issues during deletion, this
# should be treated as an exceptional condition.
@@ -197,7 +194,7 @@ class User < ActiveRecord::Base
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
- scope :active, -> { with_state(:active) }
+ scope :active, -> { with_state(:active).non_internal }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
@@ -891,20 +888,20 @@ class User < ActiveRecord::Base
@global_notification_setting
end
- def assigned_open_merge_request_count(force: false)
- Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do
- assigned_merge_requests.opened.count
+ def assigned_open_merge_requests_count(force: false)
+ Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do
+ MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
def assigned_open_issues_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
- assigned_issues.opened.count
+ IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
def update_cache_counts
- assigned_open_merge_request_count(force: true)
+ assigned_open_merge_requests_count(force: true)
assigned_open_issues_count(force: true)
end
diff --git a/app/serializers/cohort_activity_month_entity.rb b/app/serializers/cohort_activity_month_entity.rb
new file mode 100644
index 00000000000..e6788a8b596
--- /dev/null
+++ b/app/serializers/cohort_activity_month_entity.rb
@@ -0,0 +1,11 @@
+class CohortActivityMonthEntity < Grape::Entity
+ include ActionView::Helpers::NumberHelper
+
+ expose :total do |cohort_activity_month|
+ number_with_delimiter(cohort_activity_month[:total])
+ end
+
+ expose :percentage do |cohort_activity_month|
+ number_to_percentage(cohort_activity_month[:percentage], precision: 0)
+ end
+end
diff --git a/app/serializers/cohort_entity.rb b/app/serializers/cohort_entity.rb
new file mode 100644
index 00000000000..7cdba5b0484
--- /dev/null
+++ b/app/serializers/cohort_entity.rb
@@ -0,0 +1,17 @@
+class CohortEntity < Grape::Entity
+ include ActionView::Helpers::NumberHelper
+
+ expose :registration_month do |cohort|
+ cohort[:registration_month].strftime('%b %Y')
+ end
+
+ expose :total do |cohort|
+ number_with_delimiter(cohort[:total])
+ end
+
+ expose :inactive do |cohort|
+ number_with_delimiter(cohort[:inactive])
+ end
+
+ expose :activity_months, using: CohortActivityMonthEntity
+end
diff --git a/app/serializers/cohorts_entity.rb b/app/serializers/cohorts_entity.rb
new file mode 100644
index 00000000000..98f5995ba6f
--- /dev/null
+++ b/app/serializers/cohorts_entity.rb
@@ -0,0 +1,4 @@
+class CohortsEntity < Grape::Entity
+ expose :months_included
+ expose :cohorts, using: CohortEntity
+end
diff --git a/app/serializers/cohorts_serializer.rb b/app/serializers/cohorts_serializer.rb
new file mode 100644
index 00000000000..fe9367b13d8
--- /dev/null
+++ b/app/serializers/cohorts_serializer.rb
@@ -0,0 +1,3 @@
+class CohortsSerializer < AnalyticsGenericSerializer
+ entity CohortsEntity
+end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index dfd9d1584a1..944472f3e51 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -1,8 +1,12 @@
class StatusEntity < Grape::Entity
include RequestAwareEntity
- expose :icon, :favicon, :text, :label, :group
+ expose :icon, :text, :label, :group
expose :has_details?, as: :has_details
expose :details_path
+
+ expose :favicon do |status|
+ ActionController::Base.helpers.image_path(File.join('ci_favicons', "#{status.favicon}.ico"))
+ end
end
diff --git a/app/services/cohorts_service.rb b/app/services/cohorts_service.rb
new file mode 100644
index 00000000000..6781533af28
--- /dev/null
+++ b/app/services/cohorts_service.rb
@@ -0,0 +1,100 @@
+class CohortsService
+ MONTHS_INCLUDED = 12
+
+ def execute
+ {
+ months_included: MONTHS_INCLUDED,
+ cohorts: cohorts
+ }
+ end
+
+ # Get an array of hashes that looks like:
+ #
+ # [
+ # {
+ # registration_month: Date.new(2017, 3),
+ # activity_months: [3, 2, 1],
+ # total: 3
+ # inactive: 0
+ # },
+ # etc.
+ #
+ # The `months` array is always from oldest to newest, so it's always
+ # non-strictly decreasing from left to right.
+ def cohorts
+ months = Array.new(MONTHS_INCLUDED) { |i| i.months.ago.beginning_of_month.to_date }
+
+ Array.new(MONTHS_INCLUDED) do
+ registration_month = months.last
+ activity_months = running_totals(months, registration_month)
+
+ # Even if no users registered in this month, we always want to have a
+ # value to fill in the table.
+ inactive = counts_by_month[[registration_month, nil]].to_i
+
+ months.pop
+
+ {
+ registration_month: registration_month,
+ activity_months: activity_months,
+ total: activity_months.first[:total],
+ inactive: inactive
+ }
+ end
+ end
+
+ private
+
+ # Calculate a running sum of active users, so users active in later months
+ # count as active in this month, too. Start with the most recent month first,
+ # for calculating the running totals, and then reverse for displaying in the
+ # table.
+ #
+ # Each month has a total, and a percentage of the overall total, as keys.
+ def running_totals(all_months, registration_month)
+ month_totals =
+ all_months
+ .map { |activity_month| counts_by_month[[registration_month, activity_month]] }
+ .reduce([]) { |result, total| result << result.last.to_i + total.to_i }
+ .reverse
+
+ overall_total = month_totals.first
+
+ month_totals.map do |total|
+ { total: total, percentage: total.zero? ? 0 : 100 * total / overall_total }
+ end
+ end
+
+ # Get a hash that looks like:
+ #
+ # {
+ # [created_at_month, last_activity_on_month] => count,
+ # [created_at_month, last_activity_on_month_2] => count_2,
+ # # etc.
+ # }
+ #
+ # created_at_month can never be nil, but last_activity_on_month can (when a
+ # user has never logged in, just been created). This covers the last
+ # MONTHS_INCLUDED months.
+ def counts_by_month
+ @counts_by_month ||=
+ begin
+ created_at_month = column_to_date('created_at')
+ last_activity_on_month = column_to_date('last_activity_on')
+
+ User
+ .where('created_at > ?', MONTHS_INCLUDED.months.ago.end_of_month)
+ .group(created_at_month, last_activity_on_month)
+ .reorder("#{created_at_month} ASC", "#{last_activity_on_month} ASC")
+ .count
+ end
+ end
+
+ def column_to_date(column)
+ if Gitlab::Database.postgresql?
+ "CAST(DATE_TRUNC('month', #{column}) AS date)"
+ else
+ "STR_TO_DATE(DATE_FORMAT(#{column}, '%Y-%m-01'), '%Y-%m-%d')"
+ end
+ end
+end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 1297a792259..a48d6a976f0 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -1,69 +1,27 @@
module Commits
- class ChangeService < ::BaseService
- ValidationError = Class.new(StandardError)
- ChangeError = Class.new(StandardError)
+ class ChangeService < Commits::CreateService
+ def initialize(*args)
+ super
- def execute
- @start_project = params[:start_project] || @project
- @start_branch = params[:start_branch]
- @target_branch = params[:target_branch]
@commit = params[:commit]
-
- check_push_permissions
-
- commit
- rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
- ValidationError, ChangeError => ex
- error(ex.message)
end
private
- def commit
- raise NotImplementedError
- end
-
def commit_change(action)
raise NotImplementedError unless repository.respond_to?(action)
- validate_target_branch if different_branch?
-
repository.public_send(
action,
current_user,
@commit,
- @target_branch,
+ @branch_name,
start_project: @start_project,
start_branch_name: @start_branch)
-
- success
rescue Repository::CreateTreeError
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
- A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content."
+ This #{@commit.change_type_title(current_user)} may already have been #{action.to_s.dasherize}ed, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg
end
-
- def check_push_permissions
- allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
-
- unless allowed
- raise ValidationError.new('You are not allowed to push into this branch')
- end
-
- true
- end
-
- def validate_target_branch
- result = ValidateNewBranchService.new(@project, current_user)
- .execute(@target_branch)
-
- if result[:status] == :error
- raise ChangeError, "There was an error creating the source branch: #{result[:message]}"
- end
- end
-
- def different_branch?
- @start_branch != @target_branch || @start_project != @project
- end
end
end
diff --git a/app/services/commits/cherry_pick_service.rb b/app/services/commits/cherry_pick_service.rb
index 605cca36f9c..320e229560d 100644
--- a/app/services/commits/cherry_pick_service.rb
+++ b/app/services/commits/cherry_pick_service.rb
@@ -1,6 +1,6 @@
module Commits
class CherryPickService < ChangeService
- def commit
+ def create_commit!
commit_change(:cherry_pick)
end
end
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
new file mode 100644
index 00000000000..c58f04a252b
--- /dev/null
+++ b/app/services/commits/create_service.rb
@@ -0,0 +1,74 @@
+module Commits
+ class CreateService < ::BaseService
+ ValidationError = Class.new(StandardError)
+ ChangeError = Class.new(StandardError)
+
+ def initialize(*args)
+ super
+
+ @start_project = params[:start_project] || @project
+ @start_branch = params[:start_branch]
+ @branch_name = params[:branch_name]
+ end
+
+ def execute
+ validate!
+
+ new_commit = create_commit!
+
+ success(result: new_commit)
+ rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Repository::CommitError, GitHooksService::PreReceiveError => ex
+ error(ex.message)
+ end
+
+ private
+
+ def create_commit!
+ raise NotImplementedError
+ end
+
+ def raise_error(message)
+ raise ValidationError, message
+ end
+
+ def different_branch?
+ @start_branch != @branch_name || @start_project != @project
+ end
+
+ def validate!
+ validate_permissions!
+ validate_on_branch!
+ validate_branch_existance!
+
+ validate_new_branch_name! if different_branch?
+ end
+
+ def validate_permissions!
+ allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@branch_name)
+
+ unless allowed
+ raise_error("You are not allowed to push into this branch")
+ end
+ end
+
+ def validate_on_branch!
+ if !@start_project.empty_repo? && !@start_project.repository.branch_exists?(@start_branch)
+ raise_error('You can only create or edit files when you are on a branch')
+ end
+ end
+
+ def validate_branch_existance!
+ if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name)
+ raise_error("A branch called '#{@branch_name}' already exists. Switch to that branch in order to make changes")
+ end
+ end
+
+ def validate_new_branch_name!
+ result = ValidateNewBranchService.new(project, current_user).execute(@branch_name)
+
+ if result[:status] == :error
+ raise_error("Something went wrong when we tried to create '#{@branch_name}' for you: #{result[:message]}")
+ end
+ end
+ end
+end
diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb
index addd55cb32f..dc27399e047 100644
--- a/app/services/commits/revert_service.rb
+++ b/app/services/commits/revert_service.rb
@@ -1,6 +1,6 @@
module Commits
class RevertService < ChangeService
- def commit
+ def create_commit!
commit_change(:revert)
end
end
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
index 1b5623baebe..3b611588466 100644
--- a/app/services/delete_merged_branches_service.rb
+++ b/app/services/delete_merged_branches_service.rb
@@ -8,9 +8,20 @@ class DeleteMergedBranchesService < BaseService
branches = project.repository.branch_names
branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
+ # Prevent deletion of branches relevant to open merge requests
+ branches -= merge_request_branch_names
branches.each do |branch|
DeleteBranchService.new(project, current_user).execute(branch)
end
end
+
+ private
+
+ def merge_request_branch_names
+ # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY
+ source_names = project.origin_merge_requests.opened.reorder(nil).uniq.pluck(:source_branch)
+ target_names = project.merge_requests.opened.reorder(nil).uniq.pluck(:target_branch)
+ (source_names + target_names).uniq
+ end
end
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index e24cc66e0fe..0f3a485a3fd 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -72,6 +72,8 @@ class EventCreateService
def push(project, current_user, push_data)
create_event(project, current_user, Event::PUSHED, data: push_data)
+
+ Users::ActivityService.new(current_user, 'push').execute
end
private
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index c8a60422bf4..38231f66009 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -1,79 +1,17 @@
module Files
- class BaseService < ::BaseService
- ValidationError = Class.new(StandardError)
-
- def execute
- @start_project = params[:start_project] || @project
- @start_branch = params[:start_branch]
- @target_branch = params[:target_branch]
+ class BaseService < Commits::CreateService
+ def initialize(*args)
+ super
+ @author_email = params[:author_email]
+ @author_name = params[:author_name]
@commit_message = params[:commit_message]
- @file_path = params[:file_path]
- @previous_path = params[:previous_path]
- @file_content = if params[:file_content_encoding] == 'base64'
- Base64.decode64(params[:file_content])
- else
- params[:file_content]
- end
- @last_commit_sha = params[:last_commit_sha]
- @author_email = params[:author_email]
- @author_name = params[:author_name]
-
- # Validate parameters
- validate
-
- # Create new branch if it different from start_branch
- validate_target_branch if different_branch?
-
- result = commit
- if result
- success(result: result)
- else
- error('Something went wrong. Your changes were not committed')
- end
- rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError => ex
- error(ex.message)
- end
-
- private
-
- def different_branch?
- @start_branch != @target_branch || @start_project != @project
- end
-
- def file_has_changed?
- return false unless @last_commit_sha && last_commit
-
- @last_commit_sha != last_commit.sha
- end
-
- def raise_error(message)
- raise ValidationError.new(message)
- end
-
- def validate
- allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
-
- unless allowed
- raise_error("You are not allowed to push into this branch")
- end
-
- if !@start_project.empty_repo? && !@start_project.repository.branch_exists?(@start_branch)
- raise ValidationError, 'You can only create or edit files when you are on a branch'
- end
-
- if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name)
- raise ValidationError, "A branch called #{@branch_name} already exists. Switch to that branch in order to make changes"
- end
- end
- def validate_target_branch
- result = ValidateNewBranchService.new(project, current_user).
- execute(@target_branch)
+ @file_path = params[:file_path]
+ @previous_path = params[:previous_path]
- if result[:status] == :error
- raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}")
- end
+ @file_content = params[:file_content]
+ @file_content = Base64.decode64(@file_content) if params[:file_content_encoding] == 'base64'
end
end
end
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index 083ffdc634c..8ecac6115bd 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -1,26 +1,15 @@
module Files
class CreateDirService < Files::BaseService
- def commit
+ def create_commit!
repository.create_dir(
current_user,
@file_path,
message: @commit_message,
- branch_name: @target_branch,
+ branch_name: @branch_name,
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
start_branch_name: @start_branch)
end
-
- def validate
- super
-
- unless @file_path =~ Gitlab::Regex.file_path_regex
- raise_error(
- 'Your changes could not be committed, because the file path ' +
- Gitlab::Regex.file_path_regex_message
- )
- end
- end
end
end
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 65b5537fb68..00a8dcf0934 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -1,48 +1,16 @@
module Files
class CreateService < Files::BaseService
- def commit
+ def create_commit!
repository.create_file(
current_user,
@file_path,
@file_content,
message: @commit_message,
- branch_name: @target_branch,
+ branch_name: @branch_name,
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
start_branch_name: @start_branch)
end
-
- def validate
- super
-
- if @file_content.nil?
- raise_error("You must provide content.")
- end
-
- if @file_path =~ Gitlab::Regex.directory_traversal_regex
- raise_error(
- 'Your changes could not be committed, because the file name ' +
- Gitlab::Regex.directory_traversal_regex_message
- )
- end
-
- unless @file_path =~ Gitlab::Regex.file_path_regex
- raise_error(
- 'Your changes could not be committed, because the file name ' +
- Gitlab::Regex.file_path_regex_message
- )
- end
-
- unless project.empty_repo?
- @file_path.slice!(0) if @file_path.start_with?('/')
-
- blob = repository.blob_at_branch(@start_branch, @file_path)
-
- if blob
- raise_error('Your changes could not be committed because a file with the same name already exists')
- end
- end
- end
end
end
diff --git a/app/services/files/destroy_service.rb b/app/services/files/delete_service.rb
index e294659bc98..7952e5c95d4 100644
--- a/app/services/files/destroy_service.rb
+++ b/app/services/files/delete_service.rb
@@ -1,11 +1,11 @@
module Files
- class DestroyService < Files::BaseService
- def commit
+ class DeleteService < Files::BaseService
+ def create_commit!
repository.delete_file(
current_user,
@file_path,
message: @commit_message,
- branch_name: @target_branch,
+ branch_name: @branch_name,
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index 700f9f4f6f0..bfacc462847 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -1,14 +1,10 @@
module Files
class MultiService < Files::BaseService
- FileChangedError = Class.new(StandardError)
-
- ACTIONS = %w[create update delete move].freeze
-
- def commit
+ def create_commit!
repository.multi_action(
user: current_user,
message: @commit_message,
- branch_name: @target_branch,
+ branch_name: @branch_name,
actions: params[:actions],
author_email: @author_email,
author_name: @author_name,
@@ -19,122 +15,17 @@ module Files
private
- def validate
+ def validate!
super
- params[:actions].each_with_index do |action, index|
- if ACTIONS.include?(action[:action].to_s)
- action[:action] = action[:action].to_sym
- else
- raise_error("Unknown action type `#{action[:action]}`.")
- end
-
- unless action[:file_path].present?
- raise_error("You must specify a file_path.")
- end
-
- action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
- action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
-
- regex_check(action[:file_path])
- regex_check(action[:previous_path]) if action[:previous_path]
-
- if project.empty_repo? && action[:action] != :create
- raise_error("No files to #{action[:action]}.")
- end
-
- validate_file_exists(action)
-
- case action[:action]
- when :create
- validate_create(action)
- when :update
- validate_update(action)
- when :delete
- validate_delete(action)
- when :move
- validate_move(action, index)
- end
- end
- end
-
- def validate_file_exists(action)
- return if action[:action] == :create
-
- file_path = action[:file_path]
- file_path = action[:previous_path] if action[:action] == :move
-
- blob = repository.blob_at_branch(params[:branch], file_path)
-
- unless blob
- raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.")
+ params[:actions].each do |action|
+ validate_action!(action)
end
end
- def last_commit
- Gitlab::Git::Commit.last_for_path(repository, @start_branch, @file_path)
- end
-
- def regex_check(file)
- if file =~ Gitlab::Regex.directory_traversal_regex
- raise_error(
- 'Your changes could not be committed, because the file name, `' +
- file +
- '` ' +
- Gitlab::Regex.directory_traversal_regex_message
- )
- end
-
- unless file =~ Gitlab::Regex.file_path_regex
- raise_error(
- 'Your changes could not be committed, because the file name, `' +
- file +
- '` ' +
- Gitlab::Regex.file_path_regex_message
- )
- end
- end
-
- def validate_create(action)
- return if project.empty_repo?
-
- if repository.blob_at_branch(params[:branch], action[:file_path])
- raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
- end
-
- if action[:content].nil?
- raise_error("You must provide content.")
- end
- end
-
- def validate_update(action)
- if action[:content].nil?
- raise_error("You must provide content.")
- end
-
- if file_has_changed?
- raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
- end
- end
-
- def validate_delete(action)
- end
-
- def validate_move(action, index)
- if action[:previous_path].nil?
- raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.")
- end
-
- blob = repository.blob_at_branch(params[:branch], action[:file_path])
-
- if blob
- raise_error("Move destination `#{action[:file_path]}` already exists.")
- end
-
- if action[:content].nil?
- blob = repository.blob_at_branch(params[:branch], action[:previous_path])
- blob.load_all_data!(repository) if blob.truncated?
- params[:actions][index][:content] = blob.data
+ def validate_action!(action)
+ unless Gitlab::Git::Index::ACTIONS.include?(action[:action].to_s)
+ raise_error("Unknown action '#{action[:action]}'")
end
end
end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index fbbab97632e..f23a9f6d57c 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -2,10 +2,16 @@ module Files
class UpdateService < Files::BaseService
FileChangedError = Class.new(StandardError)
- def commit
+ 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,
- branch_name: @target_branch,
+ branch_name: @branch_name,
previous_path: @previous_path,
author_email: @author_email,
author_name: @author_name,
@@ -15,21 +21,23 @@ module Files
private
- def validate
- super
-
- if @file_content.nil?
- raise_error("You must provide content.")
- end
+ def file_has_changed?
+ return false unless @last_commit_sha && last_commit
- if file_has_changed?
- raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.")
- end
+ @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?
+ raise FileChangedError, "You are attempting to update a file that has changed since you started editing it."
+ end
+ end
end
end
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
index b7a244c2029..1711be7211c 100644
--- a/app/services/members/authorized_destroy_service.rb
+++ b/app/services/members/authorized_destroy_service.rb
@@ -9,7 +9,11 @@ module Members
def execute
return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
- member.destroy
+ Member.transaction do
+ unassign_issues_and_merge_requests(member)
+
+ member.destroy
+ end
if member.request? && member.user != user
notification_service.decline_access_request(member)
@@ -17,5 +21,23 @@ module Members
member
end
+
+ private
+
+ def unassign_issues_and_merge_requests(member)
+ if member.is_a?(GroupMember)
+ IssuesFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
+ execute.
+ update_all(assignee_id: nil)
+ MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
+ execute.
+ update_all(assignee_id: nil)
+ else
+ project = member.source
+ project.issues.opened.assigned_to(member.user).update_all(assignee_id: nil)
+ project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
+ member.user.update_cache_counts
+ end
+ end
end
end
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index e4b24ccef92..3a58f6c065d 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -1,9 +1,15 @@
module Members
class CreateService < BaseService
+ def initialize(source, current_user, params = {})
+ @source = source
+ @current_user = current_user
+ @params = params
+ end
+
def execute
return false if params[:user_ids].blank?
- project.team.add_users(
+ @source.add_users(
params[:user_ids].split(','),
params[:access_level],
expires_at: params[:expires_at],
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 4c72d5e117d..eea17e24903 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -59,7 +59,6 @@ module Projects
project.repository.add_remote(project.import_type, project.import_url)
project.repository.set_remote_as_mirror(project.import_type)
project.repository.fetch_remote(project.import_type, forced: true)
- project.repository.remove_remote(project.import_type)
end
def import_data
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index 8409b592b72..ff188102b62 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -7,16 +7,13 @@ module Search
end
def execute
- group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
- projects = ProjectsFinder.new(current_user: current_user).execute
-
- if group
- projects = projects.inside_path(group.full_path)
- end
-
Gitlab::SearchResults.new(current_user, projects, params[:search])
end
+ def projects
+ @projects ||= ProjectsFinder.new(current_user: current_user).execute
+ end
+
def scope
@scope ||= begin
allowed_scopes = %w[issues merge_requests milestones]
diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb
new file mode 100644
index 00000000000..29478e3251f
--- /dev/null
+++ b/app/services/search/group_service.rb
@@ -0,0 +1,18 @@
+module Search
+ class GroupService < Search::GlobalService
+ attr_accessor :group
+
+ def initialize(user, group, params)
+ super(user, params)
+
+ @group = group
+ end
+
+ def projects
+ return Project.none unless group
+ return @projects if defined? @projects
+
+ @projects = super.inside_path(group.full_path)
+ end
+ end
+end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 8d46a8dab3e..22736c71725 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -54,6 +54,8 @@ class SearchService
Search::ProjectService.new(project, current_user, params)
elsif show_snippets?
Search::SnippetService.new(current_user, params)
+ elsif group
+ Search::GroupService.new(current_user, group, params)
else
Search::GlobalService.new(current_user, params)
end
diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb
new file mode 100644
index 00000000000..facf21a7f5c
--- /dev/null
+++ b/app/services/users/activity_service.rb
@@ -0,0 +1,22 @@
+module Users
+ class ActivityService
+ def initialize(author, activity)
+ @author = author.respond_to?(:user) ? author.user : author
+ @activity = activity
+ end
+
+ def execute
+ return unless @author && @author.is_a?(User)
+
+ record_activity
+ end
+
+ private
+
+ def record_activity
+ Gitlab::UserActivities.record(@author.id)
+
+ Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}")
+ end
+ end
+end
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index ba58b174cc0..9eb6a600f6b 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -26,7 +26,7 @@ module Users
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end
- MigrateToGhostUserService.new(user).execute
+ MigrateToGhostUserService.new(user).execute unless options[:hard_delete]
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
namespace = user.namespace
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
index 1e1ed1791ec..4628c4c6f6e 100644
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ b/app/services/users/migrate_to_ghost_user_service.rb
@@ -15,27 +15,39 @@ module Users
end
def execute
- # Block the user before moving records to prevent a data race.
- # For example, if the user creates an issue after `migrate_issues`
- # runs and before the user is destroyed, the destroy will fail with
- # an exception.
- user.block
+ transition = user.block_transition
user.transaction do
+ # Block the user before moving records to prevent a data race.
+ # For example, if the user creates an issue after `migrate_issues`
+ # runs and before the user is destroyed, the destroy will fail with
+ # an exception.
+ user.block
+
+ # Reverse the user block if record migration fails
+ if !migrate_records && transition
+ transition.rollback
+ user.save!
+ end
+ end
+
+ user.reload
+ end
+
+ private
+
+ def migrate_records
+ user.transaction(requires_new: true) do
@ghost_user = User.ghost
migrate_issues
migrate_merge_requests
migrate_notes
migrate_abuse_reports
- migrate_award_emoji
+ migrate_award_emojis
end
-
- user.reload
end
- private
-
def migrate_issues
user.issues.update_all(author_id: ghost_user.id)
end
@@ -52,7 +64,7 @@ module Users
user.reported_abuse_reports.update_all(reporter_id: ghost_user.id)
end
- def migrate_award_emoji
+ def migrate_award_emojis
user.award_emoji.update_all(user_id: ghost_user.id)
end
end
diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb
index 2f61be184ce..d232e85cd33 100644
--- a/app/services/validate_new_branch_service.rb
+++ b/app/services/validate_new_branch_service.rb
@@ -8,10 +8,7 @@ class ValidateNewBranchService < BaseService
return error('Branch name is invalid')
end
- repository = project.repository
- existing_branch = repository.find_branch(branch_name)
-
- if existing_branch
+ if project.repository.branch_exists?(branch_name)
return error('Branch already exists')
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index f4ba44096d3..0dc1103eece 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -477,7 +477,7 @@
diagrams in Asciidoc documents using an external PlantUML service.
%fieldset
- %legend Usage statistics
+ %legend#usage-statistics Usage statistics
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
@@ -486,6 +486,19 @@
Version check enabled
.help-block
Let GitLab inform you when an update is available.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :usage_ping_enabled do
+ = f.check_box :usage_ping_enabled
+ Usage ping enabled
+ = link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-data")
+ .help-block
+ Every week GitLab will report license usage back to GitLab, Inc.
+ Disable this option if you do not want this to occur. To see the
+ JSON payload that will be sent, visit the
+ = succeed '.' do
+ = link_to "Cohorts page", admin_cohorts_path(anchor: 'usage-ping')
%fieldset
%legend Email
diff --git a/app/views/admin/cohorts/_cohorts_table.html.haml b/app/views/admin/cohorts/_cohorts_table.html.haml
new file mode 100644
index 00000000000..701a4e62b39
--- /dev/null
+++ b/app/views/admin/cohorts/_cohorts_table.html.haml
@@ -0,0 +1,28 @@
+.bs-callout.clearfix
+ %p
+ User cohorts are shown for the last #{@cohorts[:months_included]}
+ months. Only users with activity are counted in the cohort total; inactive
+ users are counted separately.
+ = link_to icon('question-circle'), help_page_path('user/admin_area/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank'
+
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %th Registration month
+ %th Inactive users
+ %th Cohort total
+ - @cohorts[:months_included].times do |i|
+ %th Month #{i}
+ %tbody
+ - @cohorts[:cohorts].each do |cohort|
+ %tr
+ %td= cohort[:registration_month]
+ %td= cohort[:inactive]
+ %td= cohort[:total]
+ - cohort[:activity_months].each do |activity_month|
+ %td
+ - next if cohort[:total] == '0'
+ = activity_month[:percentage]
+ %br
+ = activity_month[:total]
diff --git a/app/views/admin/cohorts/_usage_ping.html.haml b/app/views/admin/cohorts/_usage_ping.html.haml
new file mode 100644
index 00000000000..73aa95d84f1
--- /dev/null
+++ b/app/views/admin/cohorts/_usage_ping.html.haml
@@ -0,0 +1,10 @@
+%h2#usage-ping Usage ping
+
+.bs-callout.clearfix
+ %p
+ User cohorts are shown because the usage ping is enabled. The data sent with
+ this is shown below. To disable this, visit
+ = succeed '.' do
+ = link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
+
+%pre.usage-data.js-syntax-highlight.code.highlight{ data: { endpoint: usage_data_admin_application_settings_path(format: :html, pretty: true) } }
diff --git a/app/views/admin/cohorts/index.html.haml b/app/views/admin/cohorts/index.html.haml
new file mode 100644
index 00000000000..46fe12a5a99
--- /dev/null
+++ b/app/views/admin/cohorts/index.html.haml
@@ -0,0 +1,16 @@
+- @no_container = true
+= render "admin/dashboard/head"
+
+%div{ class: container_class }
+ - if @cohorts
+ = render 'cohorts_table'
+ = render 'usage_ping'
+ - else
+ .bs-callout.bs-callout-warning.clearfix
+ %p
+ User cohorts are only shown when the
+ = link_to 'usage ping', help_page_path('user/admin_area/usage_statistics'), target: '_blank'
+ is enabled. To enable it and see user cohorts,
+ visit
+ = succeed '.' do
+ = link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index 7893c1dee97..163bd5662b0 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -27,3 +27,7 @@
= link_to admin_runners_path, title: 'Runners' do
%span
Runners
+ = nav_link path: 'cohorts#index' do
+ = link_to admin_cohorts_path, title: 'Cohorts' do
+ %span
+ Cohorts
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 700c5e61a14..ea8bbe92d86 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -318,3 +318,11 @@
%td.shortcut
.key l
%td Change Label
+ %tbody.hidden-shortcut.wiki{ style: 'display:none' }
+ %tr
+ %th
+ %th Wiki pages
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit wiki page
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index a9893dea68f..659d548df18 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -47,13 +47,13 @@
%li
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('hashtag fw')
- - issues_count = cached_assigned_issuables_count(current_user, :issues, :opened)
+ - issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
%li
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold')
- - merge_requests_count = cached_assigned_issuables_count(current_user, :merge_requests, :opened)
+ - merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
%li
@@ -67,6 +67,11 @@
= icon('caret-down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
+ %li.current-user
+ .user-name.bold
+ = current_user.name
+ @#{current_user.username}
+ %li.divider
%li
= link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 444ecc414c0..ac222ad8c82 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -44,7 +44,7 @@
I
%span
Issues
- .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
+ .badge= number_with_delimiter(assigned_issuables_count(:issues))
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
.shortcut-mappings
@@ -53,7 +53,7 @@
M
%span
Merge Requests
- .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
+ .badge= number_with_delimiter(assigned_issuables_count(:merge_requests))
= nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
.shortcut-mappings
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index e34cddeb3e2..37429c7cfc0 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -11,13 +11,13 @@
Project
- if project_nav_tab? :files
- = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases graphs network)) do
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do
= link_to project_files_path(@project), title: 'Repository', class: 'shortcuts-tree' do
%span
Repository
- if project_nav_tab? :container_registry
- = nav_link(controller: %w(container_registry)) do
+ = nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
%span
Registry
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 7a4a293548c..d46e4534497 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -38,10 +38,15 @@
- if current_user
= replace_blob_link
= delete_blob_link
+
- if current_user
.js-file-fork-suggestion-section.file-fork-suggestion.hidden
%span.file-fork-suggestion-note
- You don't have permission to edit this file. Try forking this project to edit the file.
- = link_to 'Fork', fork_path, method: :post, class: 'btn btn-grouped btn-inverted btn-new'
- %button.js-cancel-fork-suggestion.btn.btn-grouped{ type: 'button' }
+ You're not allowed to
+ %span.js-file-fork-suggestion-section-action
+ edit
+ files in this project directly. Please fork this project,
+ make your changes there, and submit a merge request.
+ = link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button btn btn-grouped btn-inverted btn-new'
+ %button.js-cancel-fork-suggestion-button.btn.btn-grouped{ type: 'button' }
Cancel
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 4b26f944733..4af62461151 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -9,7 +9,7 @@
- if @conflict
.alert.alert-danger
Someone edited the file the same time you did. Please check out
- = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer'
+ = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@branch_name, @file_path)), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs.
.editor-title-row
%h3.page-title.blob-edit-page-title
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index b6738c3380f..b9b3f3ec7a3 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -8,7 +8,7 @@
#tree-holder.tree-holder
= render 'blob', blob: @blob
- - if can_edit_blob?(@blob)
+ - if can_modify_blob?(@blob)
= render 'projects/blob/remove'
- title = "Replace #{@blob.name}"
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index bd1f2d96f56..91b86280e4c 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -15,16 +15,14 @@
.dropdown.inline>
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light
- = projects_sort_options_hash[@sort]
+ = branches_sort_options_hash[@sort]
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- %li
- = link_to filter_branches_path(sort: sort_value_name) do
- = sort_title_name
- = link_to filter_branches_path(sort: sort_value_recently_updated) do
- = sort_title_recently_updated
- = link_to filter_branches_path(sort: sort_value_oldest_updated) do
- = sort_title_oldest_updated
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ Sort by
+ - branches_sort_options_hash.each do |value, title|
+ %li
+ = link_to title, filter_branches_path(sort: value), class: ("is-active" if @sort == value)
- if can? current_user, :push_code, @project
= link_to namespace_project_merged_branches_path(@project.namespace, @project), class: 'btn btn-inverted btn-remove has-tooltip', title: "Delete all branches that are merged into '#{@project.repository.root_ref}'", method: :delete, data: { confirm: "Deleting the merged branches cannot be undone. Are you sure?", container: 'body' } do
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 0faad57a312..7cb2ec83cc7 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -71,11 +71,11 @@
= custom_icon('scroll_down_hover_active')
#up-build-trace
%pre.build-trace#build-trace
- .js-truncated-info.truncated-info.hidden
- %span<
- Showing last
- %span.js-truncated-info-size><
- KiB of log
+ .js-truncated-info.truncated-info.hidden<
+ Showing last
+ %span.js-truncated-info-size.truncated-info-size><
+ KiB of log -
+ %a.js-raw-link.raw-link{ :href => raw_namespace_project_build_path(@project.namespace, @project, @build) }>< Complete Raw
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index 5c38b5ad9c0..438a98c3e95 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -4,7 +4,7 @@
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
- elsif blob.only_display_raw?
- .nothing-here-block This file is too large to display.
+ .nothing-here-block The file could not be displayed because it is too large.
- elsif blob_text_viewable?(blob)
- if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index 2e54af698aa..766f119116f 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -13,9 +13,6 @@
Environment:
= link_to @environment.name, environment_path(@environment)
- .col-sm-6
- .nav-controls
- = render 'projects/deployments/actions', deployment: @environment.last_deployment
.prometheus-state
.js-getting-started.hidden
.row
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 03069804c86..da79ca2ee75 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -46,7 +46,7 @@
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
- = render 'projects/merge_requests/show/pipelines', endpoint: url_for(params.merge(format: :json))
+ = render 'projects/merge_requests/show/pipelines', endpoint: url_for(params.merge(format: :json)), disable_initialization: true
.mr-loading-status
= spinner
diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml
index de4aa255bbd..2f1dbe87619 100644
--- a/app/views/projects/merge_requests/show/_pipelines.html.haml
+++ b/app/views/projects/merge_requests/show/_pipelines.html.haml
@@ -1,3 +1,4 @@
- endpoint_path = local_assigns[:endpoint] || pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :json)
+- disable_initialization = local_assigns.fetch(:disable_initialization, false)
-= render 'projects/commit/pipelines_list', endpoint: endpoint_path
+= render 'projects/commit/pipelines_list', endpoint: endpoint_path, disable_initialization: disable_initialization
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index bc426f1dc0c..0872a1a0503 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -19,6 +19,8 @@
= render 'projects/merge_requests/widget/open/conflicts'
- elsif @merge_request.work_in_progress?
= render 'projects/merge_requests/widget/open/wip'
+ - elsif @merge_request.merge_when_pipeline_succeeds? && @merge_request.merge_error.present?
+ = render 'projects/merge_requests/widget/open/error'
- elsif @merge_request.merge_when_pipeline_succeeds?
= render 'projects/merge_requests/widget/open/merge_when_pipeline_succeeds'
- elsif !@merge_request.can_be_merged_by?(current_user)
diff --git a/app/views/projects/merge_requests/widget/open/_error.html.haml b/app/views/projects/merge_requests/widget/open/_error.html.haml
new file mode 100644
index 00000000000..bbdc053609f
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/open/_error.html.haml
@@ -0,0 +1,6 @@
+%h4
+ = icon('exclamation-triangle')
+ This merge request failed to be merged automatically
+
+%p
+ = @merge_request.merge_error
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index e57a76dbfd2..1e66c6079e3 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -5,7 +5,7 @@
%div{ class: container_class }
%h3.page-title
- Edit Milestone #{@milestone.to_reference}
+ Edit Milestone
%hr
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index a173117984d..e8c9d7f8429 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -17,7 +17,7 @@
.header-text-content
%span.identifier
%strong
- Milestone #{@milestone.to_reference}
+ Milestone
- if @milestone.due_date || @milestone.start_date
= milestone_date_range(@milestone)
.milestone-buttons
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 0c7b53e5a9a..9e292729425 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -145,7 +145,8 @@
}
});
+ $('#project_import_url').disable();
$('.import_git').click(function( event ) {
- $projectImportUrl = $('#project_import_url')
- $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'))
+ $projectImportUrl = $('#project_import_url');
+ $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
});
diff --git a/app/views/projects/notes/_comment_button.html.haml b/app/views/projects/notes/_comment_button.html.haml
index 6bb55f04b6e..29cf5825292 100644
--- a/app/views/projects/notes/_comment_button.html.haml
+++ b/app/views/projects/notes/_comment_button.html.haml
@@ -16,7 +16,7 @@
%p
Add a general comment to this #{noteable_name}.
- %li.divider
+ %li.divider.droplab-item-ignore
%li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => 'Start discussion', 'close-text' => "Start discussion & close #{noteable_name}", 'reopen-text' => "Start discussion & reopen #{noteable_name}" } }
%a{ href: '#' }
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 3d73284699f..38237d2d97d 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -17,4 +17,4 @@
"ci-lint-path" => ci_lint_path } }
= page_specific_javascript_bundle_tag('common_vue')
-= page_specific_javascript_bundle_tag('vue_pipelines')
+= page_specific_javascript_bundle_tag('pipelines')
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index fd7bd21677c..d6c4195e2d0 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -70,7 +70,7 @@
= link_to 'Set up Koding', add_koding_stack_path(@project)
- if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
%li.missing
- = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do
+ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do
Set up auto deploy
- if @repository.commit
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index fb39028529d..24b92094b7d 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -1,4 +1,4 @@
-- page_title "Edit", @snippet.title, "Snippets"
+- page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
%h3.page-title
Edit Snippet
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index e35385f4cab..7c6be003d4c 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,4 +1,4 @@
-- page_title @snippet.title, "Snippets"
+- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
= render 'shared/snippets/header'
diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml
index 8582bcbb8cc..70d654fa9a0 100644
--- a/app/views/projects/triggers/_form.html.haml
+++ b/app/views/projects/triggers/_form.html.haml
@@ -19,7 +19,7 @@
%strong Schedule trigger (experimental)
.help-block
If checked, this trigger will be executed periodically according to cron and timezone.
- = link_to icon('question-circle'), help_page_path('ci/triggers', anchor: 'schedule')
+ = link_to icon('question-circle'), help_page_path('ci/triggers/README', anchor: 'using-scheduled-triggers')
.form-group
= schedule_fields.label :cron, "Cron", class: "label-light"
= schedule_fields.text_field :cron, class: "form-control", title: 'Cron specification is required.', placeholder: "0 1 * * *"
diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml
index c7cebf45160..0ce597dcf21 100644
--- a/app/views/projects/variables/_table.html.haml
+++ b/app/views/projects/variables/_table.html.haml
@@ -14,7 +14,7 @@
%tr
%td.variable-key= variable.key
%td.variable-value{ "data-value" => variable.value }******
- %td
+ %td.variable-menu
= link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do
%span.sr-only
Update
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 86178257af8..6a578dbf640 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -5,5 +5,5 @@
= link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
Page history
- if can?(current_user, :create_wiki, @project) && @page.latest?
- = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do
+ = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn js-wiki-edit" do
Edit
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index e010f21de5a..fc4385865a4 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -3,6 +3,8 @@
= confidential_icon(issue)
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title
+ - if issue.closed?
+ %span.label.label-danger.prepend-left-5 Closed
.pull-right ##{issue.iid}
- if issue.description.present?
.description.term
@@ -10,6 +12,3 @@
= search_md_sanitize(issue, :description)
%span.light
#{issue.project.name_with_namespace}
- - if issue.closed?
- .pull-right
- %span.label.label-danger Closed
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 2e6adf3027c..9b583285d02 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -2,6 +2,10 @@
%h4
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
%span.term.str-truncated= merge_request.title
+ - if merge_request.merged?
+ %span.label.label-primary.prepend-left-5 Merged
+ - elsif merge_request.closed?
+ %span.label.label-danger.prepend-left-5 Closed
.pull-right= merge_request.to_reference
- if merge_request.description.present?
.description.term
@@ -9,8 +13,3 @@
= search_md_sanitize(merge_request, :description)
%span.light
#{merge_request.project.name_with_namespace}
- .pull-right
- - if merge_request.merged?
- %span.label.label-primary Merged
- - elsif merge_request.closed?
- %span.label.label-danger Closed
diff --git a/app/views/shared/_branch_switcher.html.haml b/app/views/shared/_branch_switcher.html.haml
index 7799aff6b5b..69e3f3042a9 100644
--- a/app/views/shared/_branch_switcher.html.haml
+++ b/app/views/shared/_branch_switcher.html.haml
@@ -1,8 +1,8 @@
-- dropdown_toggle_text = @target_branch || tree_edit_branch
-= hidden_field_tag 'target_branch', dropdown_toggle_text
+- dropdown_toggle_text = @branch_name || tree_edit_branch
+= hidden_field_tag 'branch_name', dropdown_toggle_text
.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'target_branch', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' }
+ = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'branch_name', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' }
.dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging.dropdown-menu-branches
= render partial: 'shared/projects/blob/branch_page_default'
= render partial: 'shared/projects/blob/branch_page_create'
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
index 3ac5e15d1c4..0b37fe3013b 100644
--- a/app/views/shared/_new_commit_form.html.haml
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -1,11 +1,11 @@
= render 'shared/commit_message_container', placeholder: placeholder
- if @project.empty_repo?
- = hidden_field_tag 'target_branch', @ref
+ = hidden_field_tag 'branch_name', @ref
- else
- if can?(current_user, :push_code, @project)
.form-group.branch
- = label_tag 'target_branch', 'Target branch', class: 'control-label'
+ = label_tag 'branch_name', 'Target branch', class: 'control-label'
.col-sm-10
= render 'shared/branch_switcher'
@@ -16,7 +16,7 @@
= check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
Start a <strong>new merge request</strong> with these changes
- else
- = hidden_field_tag 'target_branch', @target_branch || tree_edit_branch
+ = hidden_field_tag 'branch_name', @branch_name || tree_edit_branch
= hidden_field_tag 'create_merge_request', 1
= hidden_field_tag 'original_branch', @ref, class: 'js-original-branch'
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index e8062848fc3..b20055a564e 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -30,9 +30,10 @@
new Pikaday({
field: $dateField.get(0),
- theme: 'gitlab-theme',
+ theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
minDate: new Date(),
+ container: $dateField.parent().get(0),
onSelect: function(dateText) {
$dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
diff --git a/app/views/shared/_user_callout.html.haml b/app/views/shared/_user_callout.html.haml
index 8f1293adcb1..8308baa7829 100644
--- a/app/views/shared/_user_callout.html.haml
+++ b/app/views/shared/_user_callout.html.haml
@@ -3,12 +3,11 @@
%button.btn.btn-default.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss customize experience box' }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
- .row
- .col-sm-3.col-xs-12.svg-container
- = custom_icon('icon_customization')
- .col-sm-8.col-xs-12.inner-content
- %h4
- Customize your experience
- %p
- Change syntax themes, default project pages, and more in preferences.
- = link_to 'Check it out', profile_preferences_path, class: 'btn btn-default js-close-callout'
+ .svg-container
+ = custom_icon('icon_customization')
+ .user-callout-copy
+ %h4
+ Customize your experience
+ %p
+ Change syntax themes, default project pages, and more in preferences.
+ = link_to 'Check it out', profile_preferences_path, class: 'btn btn-primary js-close-callout'
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 09f946f1d88..b361ec86ced 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -27,7 +27,8 @@
= visibility_level_icon(group.visibility_level, fw: false)
.avatar-container.s40
- = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ = link_to group do
+ = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group_name, group, class: 'group-name'
diff --git a/app/views/shared/icons/_icon_arrow_circle_o_right.svg b/app/views/shared/icons/_icon_arrow_circle_o_right.svg
index db28b5e2d7a..5e45c6c15ce 100644
--- a/app/views/shared/icons/_icon_arrow_circle_o_right.svg
+++ b/app/views/shared/icons/_icon_arrow_circle_o_right.svg
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1280 896q0 14-9 23l-320 320q-9 9-23 9-13 0-22.5-9.5t-9.5-22.5v-192h-352q-13 0-22.5-9.5t-9.5-22.5v-192q0-13 9.5-22.5t22.5-9.5h352v-192q0-14 9-23t23-9q12 0 24 10l319 319q9 9 9 23zm160 0q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><g fill-rule="evenodd"><path fill-rule="nonzero" d="m0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7m1 0c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6"/><path d="m7 6h-2.702c-.154 0-.298.132-.298.295v1.41c0 .164.133.295.298.295h2.702v1.694c0 .18.095.209.213.09l2.539-2.568c.115-.116.118-.312 0-.432l-2.539-2.568c-.115-.116-.213-.079-.213.09v1.694"/></g></svg>
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index b447996a8ab..f1350169bbe 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -12,13 +12,14 @@
class: "check_all_issues left"
.issues-other-filters.filtered-search-wrapper
.filtered-search-box
- = dropdown_tag(content_tag(:i, '', class: 'fa fa-history'),
- options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
- toggle_class: "filtered-search-history-dropdown-toggle-button",
- dropdown_class: "filtered-search-history-dropdown",
- content_class: "filtered-search-history-dropdown-content",
- title: "Recent searches" }) do
- .js-filtered-search-history-dropdown
+ - if type != :boards_modal && type != :boards
+ = dropdown_tag(content_tag(:i, '', class: 'fa fa-history'),
+ options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
+ toggle_class: "filtered-search-history-dropdown-toggle-button",
+ dropdown_class: "filtered-search-history-dropdown",
+ content_class: "filtered-search-history-dropdown-content",
+ title: "Recent searches" }) do
+ .js-filtered-search-history-dropdown
.filtered-search-box-input-container
.scroll-container
%ul.tokens-container.list-unstyled
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index ed94773ef89..a74cdbe274b 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -3,10 +3,10 @@
= f.label :start_date, "Start Date", class: "control-label"
.col-sm-10
= f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date"
- %a.inline.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
+ %a.inline.pull-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
.col-md-6
.form-group
= f.label :due_date, "Due Date", class: "control-label"
.col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
- %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
+ %a.inline.pull-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 761f0b606b5..c3b40433c9a 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -12,10 +12,11 @@
= cache(cache_key) do
- if avatar
.avatar-container.s40
- - if use_creator_avatar
- = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- - else
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ = link_to project_path(project), class: dom_class(project) do
+ - if use_creator_avatar
+ = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
+ - else
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
.project-details
%h3.prepend-top-0.append-bottom-0
= link_to project_path(project), class: dom_class(project) do
diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml
index 915bf98eb3e..18ebeb78f87 100644
--- a/app/views/snippets/edit.html.haml
+++ b/app/views/snippets/edit.html.haml
@@ -1,4 +1,4 @@
-- page_title "Edit", @snippet.title, "Snippets"
+- page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
%h3.page-title
Edit Snippet
%hr
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index da9fb755a36..e5711ca79c7 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,4 +1,4 @@
-- page_title @snippet.title, "Snippets"
+- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
= render 'shared/snippets/header'
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 969ea7ab9e6..03e5dd97405 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -84,19 +84,19 @@
.fade-right= icon('angle-right')
%ul.nav-links.center.user-profile-nav.scrolling-tabs
%li.js-activity-tab
- = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
+ = link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
Activity
%li.js-groups-tab
- = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
+ = link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
Groups
%li.js-contributed-tab
- = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
+ = link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
Contributed projects
%li.js-projects-tab
- = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
+ = link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
Personal projects
%li.js-snippets-tab
- = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do
+ = link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
Snippets
%div{ class: container_class }
diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb
deleted file mode 100644
index c4cb4733482..00000000000
--- a/app/workers/clear_database_cache_worker.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# This worker clears all cache fields in the database, working in batches.
-class ClearDatabaseCacheWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
-
- BATCH_SIZE = 1000
-
- def perform
- CacheMarkdownField.caching_classes.each do |kls|
- fields = kls.cached_markdown_fields.html_fields
- clear_cache_fields = fields.each_with_object({}) do |field, memo|
- memo[field] = nil
- end
-
- Rails.logger.debug("Clearing Markdown cache for #{kls}: #{fields.inspect}")
-
- kls.unscoped.in_batches(of: BATCH_SIZE) do |relation|
- relation.update_all(clear_cache_fields)
- end
- end
-
- nil
- end
-end
diff --git a/app/workers/gitlab_usage_ping_worker.rb b/app/workers/gitlab_usage_ping_worker.rb
new file mode 100644
index 00000000000..2f02235b0ac
--- /dev/null
+++ b/app/workers/gitlab_usage_ping_worker.rb
@@ -0,0 +1,31 @@
+class GitlabUsagePingWorker
+ LEASE_TIMEOUT = 86400
+
+ include Sidekiq::Worker
+ include CronjobQueue
+ include HTTParty
+
+ def perform
+ return unless current_application_settings.usage_ping_enabled
+
+ # Multiple Sidekiq workers could run this. We should only do this at most once a day.
+ return unless try_obtain_lease
+
+ begin
+ HTTParty.post(url,
+ body: Gitlab::UsageData.to_json(force_refresh: true),
+ headers: { 'Content-type' => 'application/json' }
+ )
+ rescue HTTParty::Error => e
+ Rails.logger.info "Unable to contact GitLab, Inc.: #{e}"
+ end
+ end
+
+ def try_obtain_lease
+ Gitlab::ExclusiveLease.new('gitlab_usage_ping_worker:ping', timeout: LEASE_TIMEOUT).try_obtain
+ end
+
+ def url
+ 'https://version.gitlab.com/usage_data'
+ end
+end
diff --git a/app/workers/schedule_update_user_activity_worker.rb b/app/workers/schedule_update_user_activity_worker.rb
new file mode 100644
index 00000000000..6c2c3e437f3
--- /dev/null
+++ b/app/workers/schedule_update_user_activity_worker.rb
@@ -0,0 +1,10 @@
+class ScheduleUpdateUserActivityWorker
+ include Sidekiq::Worker
+ include CronjobQueue
+
+ def perform(batch_size = 500)
+ Gitlab::UserActivities.new.each_slice(batch_size) do |batch|
+ UpdateUserActivityWorker.perform_async(Hash[batch])
+ end
+ end
+end
diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb
index baf2f12eeac..55d4e7d6dab 100644
--- a/app/workers/system_hook_worker.rb
+++ b/app/workers/system_hook_worker.rb
@@ -2,6 +2,8 @@ class SystemHookWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ sidekiq_options retry: 4
+
def perform(hook_id, data, hook_name)
SystemHook.find(hook_id).execute(data, hook_name)
end
diff --git a/app/workers/update_user_activity_worker.rb b/app/workers/update_user_activity_worker.rb
new file mode 100644
index 00000000000..b3c2f13aa33
--- /dev/null
+++ b/app/workers/update_user_activity_worker.rb
@@ -0,0 +1,26 @@
+class UpdateUserActivityWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(pairs)
+ pairs = cast_data(pairs)
+ ids = pairs.keys
+ conditions = 'WHEN id = ? THEN ? ' * ids.length
+
+ User.where(id: ids).
+ update_all([
+ "last_activity_on = CASE #{conditions} ELSE last_activity_on END",
+ *pairs.to_a.flatten
+ ])
+
+ Gitlab::UserActivities.new.delete(*ids)
+ end
+
+ private
+
+ def cast_data(pairs)
+ pairs.each_with_object({}) do |(key, value), new_pairs|
+ new_pairs[key.to_i] = Time.at(value.to_i).to_s(:db)
+ end
+ end
+end