summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--app/assets/javascripts/abuse_reports.js63
-rw-r--r--app/assets/javascripts/activities.js51
-rw-r--r--app/assets/javascripts/admin.js120
-rw-r--r--app/assets/javascripts/api.js290
-rw-r--r--app/assets/javascripts/aside.js45
-rw-r--r--app/assets/javascripts/autosave.js109
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js2
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js2
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js2
-rw-r--r--app/assets/javascripts/blob/blob_line_permalink_updater.js35
-rw-r--r--app/assets/javascripts/blob/create_branch_dropdown.js88
-rw-r--r--app/assets/javascripts/blob/target_branch_dropdown.js152
-rw-r--r--app/assets/javascripts/breakpoints.js108
-rw-r--r--app/assets/javascripts/broadcast_message.js61
-rw-r--r--app/assets/javascripts/build.js530
-rw-r--r--app/assets/javascripts/build_artifacts.js43
-rw-r--r--app/assets/javascripts/ci_lint_editor.js27
-rw-r--r--app/assets/javascripts/commit.js18
-rw-r--r--app/assets/javascripts/commits.js118
-rw-r--r--app/assets/javascripts/commons/bootstrap.js8
-rw-r--r--app/assets/javascripts/commons/index.js1
-rw-r--r--app/assets/javascripts/commons/polyfills.js10
-rw-r--r--app/assets/javascripts/commons/polyfills/custom_event.js9
-rw-r--r--app/assets/javascripts/commons/polyfills/element.js20
-rw-r--r--app/assets/javascripts/compare.js165
-rw-r--r--app/assets/javascripts/compare_autocomplete.js120
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js57
-rw-r--r--app/assets/javascripts/copy_as_gfm.js637
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js93
-rw-r--r--app/assets/javascripts/create_label.js207
-rw-r--r--app/assets/javascripts/diff.js206
-rw-r--r--app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js29
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js3
-rw-r--r--app/assets/javascripts/dispatcher.js44
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js3
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax_filter.js3
-rw-r--r--app/assets/javascripts/dropzone_input.js406
-rw-r--r--app/assets/javascripts/due_date_select.js337
-rw-r--r--app/assets/javascripts/environments/components/environment.js76
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js56
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js14
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js34
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.js49
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js50
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js9
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js16
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js2
-rw-r--r--app/assets/javascripts/environments/event_hub.js3
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.js19
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js15
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js7
-rw-r--r--app/assets/javascripts/extensions/array.js28
-rw-r--r--app/assets/javascripts/extensions/custom_event.js12
-rw-r--r--app/assets/javascripts/extensions/element.js20
-rw-r--r--app/assets/javascripts/extensions/jquery.js16
-rw-r--r--app/assets/javascripts/extensions/object.js26
-rw-r--r--app/assets/javascripts/extensions/string.js2
-rw-r--r--app/assets/javascripts/files_comment_button.js220
-rw-r--r--app/assets/javascripts/filterable_list.js1
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js5
-rw-r--r--app/assets/javascripts/flash.js73
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js718
-rw-r--r--app/assets/javascripts/gl_dropdown.js1488
-rw-r--r--app/assets/javascripts/gl_field_error.js288
-rw-r--r--app/assets/javascripts/gl_field_errors.js69
-rw-r--r--app/assets/javascripts/gl_form.js144
-rw-r--r--app/assets/javascripts/group_avatar.js35
-rw-r--r--app/assets/javascripts/group_label_subscription.js97
-rw-r--r--app/assets/javascripts/groups_select.js124
-rw-r--r--app/assets/javascripts/header.js15
-rw-r--r--app/assets/javascripts/line_highlighter.js12
-rw-r--r--app/assets/javascripts/main.js358
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js6
-rw-r--r--app/assets/javascripts/new_commit_form.js12
-rw-r--r--app/assets/javascripts/todos.js244
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js2
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js4
-rw-r--r--app/assets/stylesheets/framework/highlight.scss9
-rw-r--r--app/assets/stylesheets/pages/environments.scss8
-rw-r--r--app/assets/stylesheets/pages/note_form.scss21
-rw-r--r--app/assets/stylesheets/pages/notes.scss22
-rw-r--r--app/assets/stylesheets/pages/projects.scss3
-rw-r--r--app/controllers/admin/users_controller.rb17
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb7
-rw-r--r--app/controllers/dashboard/milestones_controller.rb1
-rw-r--r--app/controllers/groups/milestones_controller.rb1
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb7
-rw-r--r--app/controllers/projects/branches_controller.rb13
-rw-r--r--app/controllers/projects/issues_controller.rb37
-rw-r--r--app/controllers/projects/raw_controller.rb2
-rw-r--r--app/controllers/projects/tags_controller.rb22
-rw-r--r--app/helpers/issuables_helper.rb19
-rw-r--r--app/helpers/issues_helper.rb14
-rw-r--r--app/models/ability.rb7
-rw-r--r--app/models/blob.rb8
-rw-r--r--app/models/global_milestone.rb22
-rw-r--r--app/models/guest.rb2
-rw-r--r--app/models/user.rb37
-rw-r--r--app/policies/base_policy.rb9
-rw-r--r--app/policies/global_policy.rb7
-rw-r--r--app/services/concerns/issues/resolve_discussions.rb32
-rw-r--r--app/services/issues/base_service.rb8
-rw-r--r--app/services/issues/build_service.rb34
-rw-r--r--app/services/issues/create_service.rb20
-rw-r--r--app/services/notification_service.rb2
-rw-r--r--app/services/tags/destroy_service.rb2
-rw-r--r--app/validators/namespace_validator.rb3
-rw-r--r--app/views/admin/users/_head.html.haml4
-rw-r--r--app/views/dashboard/issues.atom.builder2
-rw-r--r--app/views/dashboard/milestones/index.html.haml10
-rw-r--r--app/views/dashboard/todos/_todo.html.haml5
-rw-r--r--app/views/discussions/_new_issue_for_all_discussions.html.haml6
-rw-r--r--app/views/discussions/_new_issue_for_discussion.html.haml8
-rw-r--r--app/views/discussions/_notes.html.haml4
-rw-r--r--app/views/events/_event.atom.builder2
-rw-r--r--app/views/groups/issues.atom.builder2
-rw-r--r--app/views/groups/milestones/index.html.haml2
-rw-r--r--app/views/issues/_issue.atom.builder2
-rw-r--r--app/views/projects/blob/_actions.html.haml2
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/commit/_change.html.haml2
-rw-r--r--app/views/projects/diffs/_line.html.haml2
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml4
-rw-r--r--app/views/projects/issues/index.atom.builder2
-rw-r--r--app/views/projects/issues/verify.html.haml3
-rw-r--r--app/views/projects/merge_requests/_show.html.haml1
-rw-r--r--app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml2
-rw-r--r--app/views/projects/milestones/index.html.haml10
-rw-r--r--app/views/projects/tags/destroy.js.haml4
-rw-r--r--app/views/shared/_branch_switcher.html.haml8
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml2
-rw-r--r--app/views/shared/_milestones_filter.html.haml12
-rw-r--r--app/views/shared/_new_commit_form.html.haml2
-rw-r--r--app/views/shared/icons/_icon_mr_issue.svg1
-rw-r--r--app/views/shared/issuable/_form.html.haml27
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/projects/blob/_branch_page_create.html.haml8
-rw-r--r--app/views/shared/projects/blob/_branch_page_default.html.haml10
-rw-r--r--changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml4
-rw-r--r--changelogs/unreleased/24137-issuable-permalink.yml4
-rw-r--r--changelogs/unreleased/24421-personal-milestone-count-badges.yml4
-rw-r--r--changelogs/unreleased/24501-new-file-existing-branch.yml4
-rw-r--r--changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml4
-rw-r--r--changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml4
-rw-r--r--changelogs/unreleased/dz-blacklist--names.yml4
-rw-r--r--changelogs/unreleased/feature-custom-lfs.yml4
-rw-r--r--changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml4
-rw-r--r--changelogs/unreleased/fix_updated_field_in_issues-atom.yml4
-rw-r--r--changelogs/unreleased/handle-failure-when-deleting-tags.yml4
-rw-r--r--changelogs/unreleased/pages-0-4-0.yml4
-rw-r--r--changelogs/unreleased/refresh-permissions-recent-users.yml4
-rw-r--r--changelogs/unreleased/use-corejs-polyfills.yml4
-rw-r--r--config/application.rb3
-rw-r--r--config/initializers/8_metrics.rb18
-rw-r--r--config/initializers/fix_local_cache_middleware.rb24
-rw-r--r--db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb19
-rw-r--r--db/post_migrate/20170313133418_rename_more_reserved_project_names.rb101
-rw-r--r--db/schema.rb2
-rw-r--r--doc/api/issues.md25
-rw-r--r--doc/ci/docker/using_docker_images.md10
-rw-r--r--doc/ci/ssh_keys/README.md4
-rw-r--r--doc/development/changelog.md89
-rw-r--r--doc/development/merge_request_performance_guidelines.md4
-rw-r--r--doc/development/performance.md1
-rw-r--r--doc/development/profiling.md2
-rw-r--r--doc/development/query_recorder.md29
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/user/markdown.md2
-rw-r--r--doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.pngbin0 -> 29007 bytes
-rw-r--r--doc/user/project/merge_requests/img/new_issue_for_discussion.pngbin0 -> 39563 bytes
-rw-r--r--doc/user/project/merge_requests/img/preview_issue_for_discussion.pngbin0 -> 82412 bytes
-rw-r--r--doc/user/project/merge_requests/img/preview_issue_for_discussions.pngbin178361 -> 143871 bytes
-rw-r--r--doc/user/project/merge_requests/img/resolve_discussion_issue_notice.pngbin11123 -> 10307 bytes
-rw-r--r--doc/user/project/merge_requests/merge_request_discussion_resolution.md33
-rw-r--r--features/steps/project/source/browse_files.rb6
-rw-r--r--lib/api/helpers.rb6
-rw-r--r--lib/api/issues.rb10
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/api/v3/issues.rb7
-rw-r--r--lib/banzai/reference_parser/base_parser.rb2
-rw-r--r--lib/ci/api/runners.rb4
-rw-r--r--lib/gitlab/allowable.rb2
-rw-r--r--lib/gitlab/checks/change_access.rb8
-rw-r--r--lib/gitlab/diff/line.rb6
-rw-r--r--lib/gitlab/diff/parser.rb6
-rw-r--r--lib/gitlab/git_access.rb4
-rw-r--r--lib/gitlab/user_access.rb14
-rw-r--r--package.json4
-rw-r--r--qa/.rspec3
-rw-r--r--qa/Dockerfile14
-rw-r--r--qa/Gemfile7
-rw-r--r--qa/README.md18
-rwxr-xr-xqa/bin/qa7
-rwxr-xr-xqa/bin/test3
-rw-r--r--qa/qa.rb81
-rw-r--r--qa/qa/ce/strategy.rb15
-rw-r--r--qa/qa/git/repository.rb71
-rw-r--r--qa/qa/page/admin/menu.rb19
-rw-r--r--qa/qa/page/base.rb12
-rw-r--r--qa/qa/page/main/entry.rb26
-rw-r--r--qa/qa/page/main/groups.rb20
-rw-r--r--qa/qa/page/main/menu.rb46
-rw-r--r--qa/qa/page/main/projects.rb16
-rw-r--r--qa/qa/page/project/new.rb24
-rw-r--r--qa/qa/page/project/show.rb23
-rw-r--r--qa/qa/runtime/namespace.rb15
-rw-r--r--qa/qa/runtime/release.rb28
-rw-r--r--qa/qa/runtime/user.rb15
-rw-r--r--qa/qa/scenario/actable.rb23
-rw-r--r--qa/qa/scenario/gitlab/project/create.rb31
-rw-r--r--qa/qa/scenario/template.rb16
-rw-r--r--qa/qa/scenario/test/instance.rb26
-rw-r--r--qa/qa/specs/config.rb78
-rw-r--r--qa/qa/specs/features/login/standard_spec.rb14
-rw-r--r--qa/qa/specs/features/project/create_spec.rb19
-rw-r--r--qa/qa/specs/features/repository/clone_spec.rb57
-rw-r--r--qa/qa/specs/features/repository/push_spec.rb39
-rw-r--r--qa/qa/specs/runner.rb15
-rw-r--r--qa/spec/runtime/release_spec.rb50
-rw-r--r--qa/spec/scenario/actable_spec.rb47
-rw-r--r--qa/spec/spec_helper.rb19
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb14
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb38
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb61
-rw-r--r--spec/factories/notes.rb7
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb101
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb81
-rw-r--r--spec/features/issues/form_spec.rb10
-rw-r--r--spec/features/login_spec.rb12
-rw-r--r--spec/features/merge_requests/form_spec.rb11
-rw-r--r--spec/features/projects/blobs/blob_line_permalink_updater_spec.rb97
-rw-r--r--spec/features/projects/blobs/user_create_spec.rb107
-rw-r--r--spec/features/projects/environments/environments_spec.rb10
-rw-r--r--spec/features/projects/files/browse_files_spec.rb14
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb27
-rw-r--r--spec/features/todos/todos_spec.rb43
-rw-r--r--spec/helpers/issues_helper_spec.rb32
-rw-r--r--spec/javascripts/awards_handler_spec.js3
-rw-r--r--spec/javascripts/blob/create_branch_dropdown_spec.js107
-rw-r--r--spec/javascripts/blob/target_branch_dropdown_spec.js119
-rw-r--r--spec/javascripts/boards/board_new_issue_spec.js1
-rw-r--r--spec/javascripts/boards/boards_store_spec.js1
-rw-r--r--spec/javascripts/bootstrap_jquery_spec.js (renamed from spec/javascripts/extensions/jquery_spec.js)4
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js35
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js11
-rw-r--r--spec/javascripts/environments/environment_item_spec.js18
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js42
-rw-r--r--spec/javascripts/environments/environment_spec.js8
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js30
-rw-r--r--spec/javascripts/environments/environment_table_spec.js8
-rw-r--r--spec/javascripts/environments/environment_terminal_button_spec.js24
-rw-r--r--spec/javascripts/environments/environments_store_spec.js4
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js8
-rw-r--r--spec/javascripts/environments/mock_data.js12
-rw-r--r--spec/javascripts/extensions/array_spec.js23
-rw-r--r--spec/javascripts/extensions/element_spec.js38
-rw-r--r--spec/javascripts/extensions/object_spec.js25
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js5
-rw-r--r--spec/javascripts/fixtures/project_branches.json5
-rw-r--r--spec/javascripts/fixtures/target_branch_dropdown.html.haml28
-rw-r--r--spec/javascripts/gl_emoji_spec.js3
-rw-r--r--spec/javascripts/line_highlighter_spec.js18
-rw-r--r--spec/javascripts/monitoring/prometheus_graph_spec.js3
-rw-r--r--spec/javascripts/polyfills/element_spec.js36
-rw-r--r--spec/javascripts/right_sidebar_spec.js4
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js8
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb12
-rw-r--r--spec/lib/gitlab/diff/parser_spec.rb48
-rw-r--r--spec/migrations/rename_more_reserved_project_names_spec.rb47
-rw-r--r--spec/models/ability_spec.rb6
-rw-r--r--spec/models/blob_spec.rb25
-rw-r--r--spec/models/global_milestone_spec.rb63
-rw-r--r--spec/models/user_spec.rb16
-rw-r--r--spec/policies/base_policy_spec.rb10
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/issues_spec.rb31
-rw-r--r--spec/requests/api/projects_spec.rb4
-rw-r--r--spec/requests/api/runner_spec.rb3
-rw-r--r--spec/requests/ci/api/runners_spec.rb3
-rw-r--r--spec/services/issues/build_service_spec.rb56
-rw-r--r--spec/services/issues/create_service_spec.rb85
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb106
-rw-r--r--spec/spec_helper.rb13
-rw-r--r--spec/support/api/issues_resolving_discussions_shared_examples.rb15
-rw-r--r--spec/support/api_helpers.rb4
-rw-r--r--spec/support/features/resolving_discussions_in_issues_shared_examples.rb41
-rw-r--r--spec/support/json_response_helpers.rb9
-rw-r--r--vendor/licenses.csv945
-rw-r--r--yarn.lock12
296 files changed, 8649 insertions, 4763 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index deeb01f9a3c..db3d25195dc 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,8 +7,6 @@ cache:
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
- # retry tests only in CI environment
- RSPEC_RETRY_RETRY_COUNT: "3"
RAILS_ENV: "test"
SIMPLECOV: "true"
SETUP_DB: "true"
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index d15723fbe8d..1d0ba9ea182 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.3.2
+0.4.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 88c5fb891dc..347f5833ee6 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.4.0
+1.4.1
diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js
index 8a260aae1b1..346de4ad11e 100644
--- a/app/assets/javascripts/abuse_reports.js
+++ b/app/assets/javascripts/abuse_reports.js
@@ -1,40 +1,37 @@
-/* eslint-disable no-param-reassign */
+const MAX_MESSAGE_LENGTH = 500;
+const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
-((global) => {
- const MAX_MESSAGE_LENGTH = 500;
- const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
-
- class AbuseReports {
- constructor() {
- $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
- $(document)
- .off('click', MESSAGE_CELL_SELECTOR)
- .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
- }
+class AbuseReports {
+ constructor() {
+ $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
+ $(document)
+ .off('click', MESSAGE_CELL_SELECTOR)
+ .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
+ }
- truncateLongMessage() {
- const $messageCellElement = $(this);
- const reportMessage = $messageCellElement.text();
- if (reportMessage.length > MAX_MESSAGE_LENGTH) {
- $messageCellElement.data('original-message', reportMessage);
- $messageCellElement.data('message-truncated', 'true');
- $messageCellElement.text(global.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
- }
+ truncateLongMessage() {
+ const $messageCellElement = $(this);
+ const reportMessage = $messageCellElement.text();
+ if (reportMessage.length > MAX_MESSAGE_LENGTH) {
+ $messageCellElement.data('original-message', reportMessage);
+ $messageCellElement.data('message-truncated', 'true');
+ $messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
}
+ }
- toggleMessageTruncation() {
- const $messageCellElement = $(this);
- const originalMessage = $messageCellElement.data('original-message');
- if (!originalMessage) return;
- if ($messageCellElement.data('message-truncated') === 'true') {
- $messageCellElement.data('message-truncated', 'false');
- $messageCellElement.text(originalMessage);
- } else {
- $messageCellElement.data('message-truncated', 'true');
- $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
- }
+ toggleMessageTruncation() {
+ const $messageCellElement = $(this);
+ const originalMessage = $messageCellElement.data('original-message');
+ if (!originalMessage) return;
+ if ($messageCellElement.data('message-truncated') === 'true') {
+ $messageCellElement.data('message-truncated', 'false');
+ $messageCellElement.text(originalMessage);
+ } else {
+ $messageCellElement.data('message-truncated', 'true');
+ $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
}
}
+}
- global.AbuseReports = AbuseReports;
-})(window.gl || (window.gl = {}));
+window.gl = window.gl || {};
+window.gl.AbuseReports = AbuseReports;
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index 648cb4d5d85..aebda7780e1 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -2,36 +2,35 @@
/* global Pager */
/* global Cookies */
-((global) => {
- class Activities {
- constructor() {
- Pager.init(20, true, false, this.updateTooltips);
- $('.event-filter-link').on('click', (e) => {
- e.preventDefault();
- this.toggleFilter(e.currentTarget);
- this.reloadActivities();
- });
- }
+class Activities {
+ constructor() {
+ Pager.init(20, true, false, this.updateTooltips);
+ $('.event-filter-link').on('click', (e) => {
+ e.preventDefault();
+ this.toggleFilter(e.currentTarget);
+ this.reloadActivities();
+ });
+ }
- updateTooltips() {
- gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
- }
+ updateTooltips() {
+ gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
+ }
- reloadActivities() {
- $('.content_list').html('');
- Pager.init(20, true, false, this.updateTooltips);
- }
+ reloadActivities() {
+ $('.content_list').html('');
+ Pager.init(20, true, false, this.updateTooltips);
+ }
- toggleFilter(sender) {
- const $sender = $(sender);
- const filter = $sender.attr('id').split('_')[0];
+ toggleFilter(sender) {
+ const $sender = $(sender);
+ const filter = $sender.attr('id').split('_')[0];
- $('.event-filter .active').removeClass('active');
- Cookies.set('event_filter', filter);
+ $('.event-filter .active').removeClass('active');
+ Cookies.set('event_filter', filter);
- $sender.closest('li').toggleClass('active');
- }
+ $sender.closest('li').toggleClass('active');
}
+}
- global.Activities = Activities;
-})(window.gl || (window.gl = {}));
+window.gl = window.gl || {};
+window.gl.Activities = Activities;
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index aaed74d6073..34669dd13d6 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,64 +1,62 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */
-(function() {
- this.Admin = (function() {
- function Admin() {
- var modal, showBlacklistType;
- $('input#user_force_random_password').on('change', function(elem) {
- var elems;
- elems = $('#user_password, #user_password_confirmation');
- if ($(this).attr('checked')) {
- return elems.val('').attr('disabled', true);
- } else {
- return elems.removeAttr('disabled');
- }
- });
- $('body').on('click', '.js-toggle-colors-link', function(e) {
- e.preventDefault();
- return $('.js-toggle-colors-container').toggle();
- });
- $('.log-tabs a').click(function(e) {
- e.preventDefault();
- return $(this).tab('show');
- });
- $('.log-bottom').click(function(e) {
- var visible_log;
- e.preventDefault();
- visible_log = $(".file-content:visible");
- return visible_log.animate({
- scrollTop: visible_log.find('ol').height()
- }, "fast");
- });
- modal = $('.change-owner-holder');
- $('.change-owner-link').bind("click", function(e) {
- e.preventDefault();
- $(this).hide();
- return modal.show();
- });
- $('.change-owner-cancel-link').bind("click", function(e) {
- e.preventDefault();
- modal.hide();
- return $('.change-owner-link').show();
- });
- $('li.project_member').bind('ajax:success', function() {
- return gl.utils.refreshCurrentPage();
- });
- $('li.group_member').bind('ajax:success', function() {
- return gl.utils.refreshCurrentPage();
- });
- showBlacklistType = function() {
- if ($("input[name='blacklist_type']:checked").val() === 'file') {
- $('.blacklist-file').show();
- return $('.blacklist-raw').hide();
- } else {
- $('.blacklist-file').hide();
- return $('.blacklist-raw').show();
- }
- };
- $("input[name='blacklist_type']").click(showBlacklistType);
- showBlacklistType();
- }
+window.Admin = (function() {
+ function Admin() {
+ var modal, showBlacklistType;
+ $('input#user_force_random_password').on('change', function(elem) {
+ var elems;
+ elems = $('#user_password, #user_password_confirmation');
+ if ($(this).attr('checked')) {
+ return elems.val('').attr('disabled', true);
+ } else {
+ return elems.removeAttr('disabled');
+ }
+ });
+ $('body').on('click', '.js-toggle-colors-link', function(e) {
+ e.preventDefault();
+ return $('.js-toggle-colors-container').toggle();
+ });
+ $('.log-tabs a').click(function(e) {
+ e.preventDefault();
+ return $(this).tab('show');
+ });
+ $('.log-bottom').click(function(e) {
+ var visible_log;
+ e.preventDefault();
+ visible_log = $(".file-content:visible");
+ return visible_log.animate({
+ scrollTop: visible_log.find('ol').height()
+ }, "fast");
+ });
+ modal = $('.change-owner-holder');
+ $('.change-owner-link').bind("click", function(e) {
+ e.preventDefault();
+ $(this).hide();
+ return modal.show();
+ });
+ $('.change-owner-cancel-link').bind("click", function(e) {
+ e.preventDefault();
+ modal.hide();
+ return $('.change-owner-link').show();
+ });
+ $('li.project_member').bind('ajax:success', function() {
+ return gl.utils.refreshCurrentPage();
+ });
+ $('li.group_member').bind('ajax:success', function() {
+ return gl.utils.refreshCurrentPage();
+ });
+ showBlacklistType = function() {
+ if ($("input[name='blacklist_type']:checked").val() === 'file') {
+ $('.blacklist-file').show();
+ return $('.blacklist-raw').hide();
+ } else {
+ $('.blacklist-file').hide();
+ return $('.blacklist-raw').show();
+ }
+ };
+ $("input[name='blacklist_type']").click(showBlacklistType);
+ showBlacklistType();
+ }
- return Admin;
- })();
-}).call(window);
+ return Admin;
+})();
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 86e0ad89431..a0946eb392a 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,150 +1,148 @@
/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, quote-props, no-param-reassign, max-len */
-(function() {
- var Api = {
- groupsPath: "/api/:version/groups.json",
- groupPath: "/api/:version/groups/:id.json",
- namespacesPath: "/api/:version/namespaces.json",
- groupProjectsPath: "/api/:version/groups/:id/projects.json",
- projectsPath: "/api/:version/projects.json?simple=true",
- labelsPath: "/:namespace_path/:project_path/labels",
- licensePath: "/api/:version/templates/licenses/:key",
- gitignorePath: "/api/:version/templates/gitignores/:key",
- gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
- dockerfilePath: "/api/:version/templates/dockerfiles/:key",
- issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
- group: function(group_id, callback) {
- var url = Api.buildUrl(Api.groupPath)
- .replace(':id', group_id);
- return $.ajax({
- url: url,
- dataType: "json"
- }).done(function(group) {
- return callback(group);
- });
- },
- // Return groups list. Filtered by query
- groups: function(query, options, callback) {
- var url = Api.buildUrl(Api.groupsPath);
- return $.ajax({
- url: url,
- data: $.extend({
- search: query,
- per_page: 20
- }, options),
- dataType: "json"
- }).done(function(groups) {
- return callback(groups);
- });
- },
- // Return namespaces list. Filtered by query
- namespaces: function(query, callback) {
- var url = Api.buildUrl(Api.namespacesPath);
- return $.ajax({
- url: url,
- data: {
- search: query,
- per_page: 20
- },
- dataType: "json"
- }).done(function(namespaces) {
- return callback(namespaces);
- });
- },
- // Return projects list. Filtered by query
- projects: function(query, order, callback) {
- var url = Api.buildUrl(Api.projectsPath);
- return $.ajax({
- url: url,
- data: {
- search: query,
- order_by: order,
- per_page: 20
- },
- dataType: "json"
- }).done(function(projects) {
- return callback(projects);
- });
- },
- newLabel: function(namespace_path, project_path, data, callback) {
- var url = Api.buildUrl(Api.labelsPath)
- .replace(':namespace_path', namespace_path)
- .replace(':project_path', project_path);
- return $.ajax({
- url: url,
- type: "POST",
- data: { 'label': data },
- dataType: "json"
- }).done(function(label) {
- return callback(label);
- }).error(function(message) {
- return callback(message.responseJSON);
- });
- },
- // Return group projects list. Filtered by query
- groupProjects: function(group_id, query, callback) {
- var url = Api.buildUrl(Api.groupProjectsPath)
- .replace(':id', group_id);
- return $.ajax({
- url: url,
- data: {
- search: query,
- per_page: 20
- },
- dataType: "json"
- }).done(function(projects) {
- return callback(projects);
- });
- },
- // Return text for a specific license
- licenseText: function(key, data, callback) {
- var url = Api.buildUrl(Api.licensePath)
- .replace(':key', key);
- return $.ajax({
- url: url,
- data: data
- }).done(function(license) {
- return callback(license);
- });
- },
- gitignoreText: function(key, callback) {
- var url = Api.buildUrl(Api.gitignorePath)
- .replace(':key', key);
- return $.get(url, function(gitignore) {
- return callback(gitignore);
- });
- },
- gitlabCiYml: function(key, callback) {
- var url = Api.buildUrl(Api.gitlabCiYmlPath)
- .replace(':key', key);
- return $.get(url, function(file) {
- return callback(file);
- });
- },
- dockerfileYml: function(key, callback) {
- var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
- $.get(url, callback);
- },
- issueTemplate: function(namespacePath, projectPath, key, type, callback) {
- var url = Api.buildUrl(Api.issuableTemplatePath)
- .replace(':key', key)
- .replace(':type', type)
- .replace(':project_path', projectPath)
- .replace(':namespace_path', namespacePath);
- $.ajax({
- url: url,
- dataType: 'json'
- }).done(function(file) {
- callback(null, file);
- }).error(callback);
- },
- buildUrl: function(url) {
- if (gon.relative_url_root != null) {
- url = gon.relative_url_root + url;
- }
- return url.replace(':version', gon.api_version);
+var Api = {
+ groupsPath: "/api/:version/groups.json",
+ groupPath: "/api/:version/groups/:id.json",
+ namespacesPath: "/api/:version/namespaces.json",
+ groupProjectsPath: "/api/:version/groups/:id/projects.json",
+ projectsPath: "/api/:version/projects.json?simple=true",
+ labelsPath: "/:namespace_path/:project_path/labels",
+ licensePath: "/api/:version/templates/licenses/:key",
+ gitignorePath: "/api/:version/templates/gitignores/:key",
+ gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
+ dockerfilePath: "/api/:version/templates/dockerfiles/:key",
+ issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
+ group: function(group_id, callback) {
+ var url = Api.buildUrl(Api.groupPath)
+ .replace(':id', group_id);
+ return $.ajax({
+ url: url,
+ dataType: "json"
+ }).done(function(group) {
+ return callback(group);
+ });
+ },
+ // Return groups list. Filtered by query
+ groups: function(query, options, callback) {
+ var url = Api.buildUrl(Api.groupsPath);
+ return $.ajax({
+ url: url,
+ data: $.extend({
+ search: query,
+ per_page: 20
+ }, options),
+ dataType: "json"
+ }).done(function(groups) {
+ return callback(groups);
+ });
+ },
+ // Return namespaces list. Filtered by query
+ namespaces: function(query, callback) {
+ var url = Api.buildUrl(Api.namespacesPath);
+ return $.ajax({
+ url: url,
+ data: {
+ search: query,
+ per_page: 20
+ },
+ dataType: "json"
+ }).done(function(namespaces) {
+ return callback(namespaces);
+ });
+ },
+ // Return projects list. Filtered by query
+ projects: function(query, order, callback) {
+ var url = Api.buildUrl(Api.projectsPath);
+ return $.ajax({
+ url: url,
+ data: {
+ search: query,
+ order_by: order,
+ per_page: 20
+ },
+ dataType: "json"
+ }).done(function(projects) {
+ return callback(projects);
+ });
+ },
+ newLabel: function(namespace_path, project_path, data, callback) {
+ var url = Api.buildUrl(Api.labelsPath)
+ .replace(':namespace_path', namespace_path)
+ .replace(':project_path', project_path);
+ return $.ajax({
+ url: url,
+ type: "POST",
+ data: { 'label': data },
+ dataType: "json"
+ }).done(function(label) {
+ return callback(label);
+ }).error(function(message) {
+ return callback(message.responseJSON);
+ });
+ },
+ // Return group projects list. Filtered by query
+ groupProjects: function(group_id, query, callback) {
+ var url = Api.buildUrl(Api.groupProjectsPath)
+ .replace(':id', group_id);
+ return $.ajax({
+ url: url,
+ data: {
+ search: query,
+ per_page: 20
+ },
+ dataType: "json"
+ }).done(function(projects) {
+ return callback(projects);
+ });
+ },
+ // Return text for a specific license
+ licenseText: function(key, data, callback) {
+ var url = Api.buildUrl(Api.licensePath)
+ .replace(':key', key);
+ return $.ajax({
+ url: url,
+ data: data
+ }).done(function(license) {
+ return callback(license);
+ });
+ },
+ gitignoreText: function(key, callback) {
+ var url = Api.buildUrl(Api.gitignorePath)
+ .replace(':key', key);
+ return $.get(url, function(gitignore) {
+ return callback(gitignore);
+ });
+ },
+ gitlabCiYml: function(key, callback) {
+ var url = Api.buildUrl(Api.gitlabCiYmlPath)
+ .replace(':key', key);
+ return $.get(url, function(file) {
+ return callback(file);
+ });
+ },
+ dockerfileYml: function(key, callback) {
+ var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
+ $.get(url, callback);
+ },
+ issueTemplate: function(namespacePath, projectPath, key, type, callback) {
+ var url = Api.buildUrl(Api.issuableTemplatePath)
+ .replace(':key', key)
+ .replace(':type', type)
+ .replace(':project_path', projectPath)
+ .replace(':namespace_path', namespacePath);
+ $.ajax({
+ url: url,
+ dataType: 'json'
+ }).done(function(file) {
+ callback(null, file);
+ }).error(callback);
+ },
+ buildUrl: function(url) {
+ if (gon.relative_url_root != null) {
+ url = gon.relative_url_root + url;
}
- };
+ return url.replace(':version', gon.api_version);
+ }
+};
- window.Api = Api;
-}).call(window);
+window.Api = Api;
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
index 448e6e2cc78..88756884d16 100644
--- a/app/assets/javascripts/aside.js
+++ b/app/assets/javascripts/aside.js
@@ -1,25 +1,24 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */
-(function() {
- this.Aside = (function() {
- function Aside() {
- $(document).off("click", "a.show-aside");
- $(document).on("click", 'a.show-aside', function(e) {
- var btn, icon;
- e.preventDefault();
- btn = $(e.currentTarget);
- icon = btn.find('i');
- if (icon.hasClass('fa-angle-left')) {
- btn.parent().find('section').hide();
- btn.parent().find('aside').fadeIn();
- return icon.removeClass('fa-angle-left').addClass('fa-angle-right');
- } else {
- btn.parent().find('aside').hide();
- btn.parent().find('section').fadeIn();
- return icon.removeClass('fa-angle-right').addClass('fa-angle-left');
- }
- });
- }
- return Aside;
- })();
-}).call(window);
+window.Aside = (function() {
+ function Aside() {
+ $(document).off("click", "a.show-aside");
+ $(document).on("click", 'a.show-aside', function(e) {
+ var btn, icon;
+ e.preventDefault();
+ btn = $(e.currentTarget);
+ icon = btn.find('i');
+ if (icon.hasClass('fa-angle-left')) {
+ btn.parent().find('section').hide();
+ btn.parent().find('aside').fadeIn();
+ return icon.removeClass('fa-angle-left').addClass('fa-angle-right');
+ } else {
+ btn.parent().find('aside').hide();
+ btn.parent().find('section').fadeIn();
+ return icon.removeClass('fa-angle-right').addClass('fa-angle-left');
+ }
+ });
+ }
+
+ return Aside;
+})();
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index e55405135fb..8630b18a73f 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,62 +1,61 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
-(function() {
- this.Autosave = (function() {
- function Autosave(field, key) {
- this.field = field;
- if (key.join != null) {
- key = key.join("/");
- }
- this.key = "autosave/" + key;
- this.field.data("autosave", this);
- this.restore();
- this.field.on("input", (function(_this) {
- return function() {
- return _this.save();
- };
- })(this));
- }
- Autosave.prototype.restore = function() {
- var e, text;
- if (window.localStorage == null) {
- return;
- }
- try {
- text = window.localStorage.getItem(this.key);
- } catch (error) {
- e = error;
- return;
- }
- if ((text != null ? text.length : void 0) > 0) {
- this.field.val(text);
- }
- return this.field.trigger("input");
- };
+window.Autosave = (function() {
+ function Autosave(field, key) {
+ this.field = field;
+ if (key.join != null) {
+ key = key.join("/");
+ }
+ this.key = "autosave/" + key;
+ this.field.data("autosave", this);
+ this.restore();
+ this.field.on("input", (function(_this) {
+ return function() {
+ return _this.save();
+ };
+ })(this));
+ }
- Autosave.prototype.save = function() {
- var text;
- if (window.localStorage == null) {
- return;
- }
- text = this.field.val();
- if ((text != null ? text.length : void 0) > 0) {
- try {
- return window.localStorage.setItem(this.key, text);
- } catch (error) {}
- } else {
- return this.reset();
- }
- };
+ Autosave.prototype.restore = function() {
+ var e, text;
+ if (window.localStorage == null) {
+ return;
+ }
+ try {
+ text = window.localStorage.getItem(this.key);
+ } catch (error) {
+ e = error;
+ return;
+ }
+ if ((text != null ? text.length : void 0) > 0) {
+ this.field.val(text);
+ }
+ return this.field.trigger("input");
+ };
- Autosave.prototype.reset = function() {
- if (window.localStorage == null) {
- return;
- }
+ Autosave.prototype.save = function() {
+ var text;
+ if (window.localStorage == null) {
+ return;
+ }
+ text = this.field.val();
+ if ((text != null ? text.length : void 0) > 0) {
try {
- return window.localStorage.removeItem(this.key);
+ return window.localStorage.setItem(this.key, text);
} catch (error) {}
- };
+ } else {
+ return this.reset();
+ }
+ };
+
+ Autosave.prototype.reset = function() {
+ if (window.localStorage == null) {
+ return;
+ }
+ try {
+ return window.localStorage.removeItem(this.key);
+ } catch (error) {}
+ };
- return Autosave;
- })();
-}).call(window);
+ return Autosave;
+})();
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index a7e68ae5cb9..626f3503c91 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -6,7 +6,7 @@
// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
// is submitted.
//
-require('../extensions/jquery');
+import '../commons/bootstrap';
//
// ### Example Markup
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index 6b21695d082..eb7143f5b1a 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -4,7 +4,7 @@
// When called on a form with input fields with the `required` attribute, the
// form's submit button will be disabled until all required fields have values.
//
-require('../extensions/jquery');
+import '../commons/bootstrap';
//
// ### Example Markup
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 5f14ff40eee..8f6bf162d6e 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -36,7 +36,7 @@
this.removeFile(file);
});
return this.on('sending', function(file, xhr, formData) {
- formData.append('target_branch', form.find('.js-target-branch').val());
+ formData.append('target_branch', form.find('input[name="target_branch"]').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_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js
new file mode 100644
index 00000000000..c8f68860fbd
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js
@@ -0,0 +1,35 @@
+const lineNumberRe = /^L[0-9]+/;
+
+const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
+ const hash = gl.utils.getLocationHash();
+ if (hash && lineNumberRe.test(hash)) {
+ const hashUrlString = `#${hash}`;
+
+ [].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => {
+ const baseHref = permalinkButton.getAttribute('data-original-href') || (() => {
+ const href = permalinkButton.getAttribute('href');
+ permalinkButton.setAttribute('data-original-href', href);
+ return href;
+ })();
+ permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`);
+ });
+ }
+};
+
+function BlobLinePermalinkUpdater(blobContentHolder, lineNumberSelector, elementsToUpdate) {
+ const updateBlameAndBlobPermalinkCb = () => {
+ // Wait for the hash to update from the LineHighlighter callback
+ setTimeout(() => {
+ updateLineNumbersOnBlobPermalinks(elementsToUpdate);
+ }, 0);
+ };
+
+ blobContentHolder.addEventListener('click', (e) => {
+ if (e.target.matches(lineNumberSelector)) {
+ updateBlameAndBlobPermalinkCb();
+ }
+ });
+ updateBlameAndBlobPermalinkCb();
+}
+
+export default BlobLinePermalinkUpdater;
diff --git a/app/assets/javascripts/blob/create_branch_dropdown.js b/app/assets/javascripts/blob/create_branch_dropdown.js
new file mode 100644
index 00000000000..95517f51b1c
--- /dev/null
+++ b/app/assets/javascripts/blob/create_branch_dropdown.js
@@ -0,0 +1,88 @@
+class CreateBranchDropdown {
+ constructor(el, targetBranchDropdown) {
+ this.targetBranchDropdown = targetBranchDropdown;
+ this.el = el;
+ this.dropdownBack = this.el.closest('.dropdown').querySelector('.dropdown-menu-back');
+ this.cancelButton = this.el.querySelector('.js-cancel-branch-btn');
+ this.newBranchField = this.el.querySelector('#new_branch_name');
+ this.newBranchCreateButton = this.el.querySelector('.js-new-branch-btn');
+
+ this.newBranchCreateButton.setAttribute('disabled', '');
+
+ this.addBindings();
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ cleanup() {
+ this.cleanBindings();
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ cleanBindings() {
+ this.newBranchField.removeEventListener('keyup', this.enableBranchCreateButtonWrapper);
+ this.newBranchField.removeEventListener('change', this.enableBranchCreateButtonWrapper);
+ this.newBranchField.removeEventListener('keydown', this.handleNewBranchKeydownWrapper);
+ this.dropdownBack.removeEventListener('click', this.resetFormWrapper);
+ this.cancelButton.removeEventListener('click', this.handleCancelClickWrapper);
+ this.newBranchCreateButton.removeEventListener('click', this.createBranchWrapper);
+ }
+
+ addBindings() {
+ this.enableBranchCreateButtonWrapper = this.enableBranchCreateButton.bind(this);
+ this.handleNewBranchKeydownWrapper = this.handleNewBranchKeydown.bind(this);
+ this.resetFormWrapper = this.resetForm.bind(this);
+ this.handleCancelClickWrapper = this.handleCancelClick.bind(this);
+ this.createBranchWrapper = this.createBranch.bind(this);
+
+ this.newBranchField.addEventListener('keyup', this.enableBranchCreateButtonWrapper);
+ this.newBranchField.addEventListener('change', this.enableBranchCreateButtonWrapper);
+ this.newBranchField.addEventListener('keydown', this.handleNewBranchKeydownWrapper);
+ this.dropdownBack.addEventListener('click', this.resetFormWrapper);
+ this.cancelButton.addEventListener('click', this.handleCancelClickWrapper);
+ this.newBranchCreateButton.addEventListener('click', this.createBranchWrapper);
+ }
+
+ handleCancelClick(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.resetForm();
+ this.dropdownBack.click();
+ }
+
+ handleNewBranchKeydown(e) {
+ const keyCode = e.which;
+ const ENTER_KEYCODE = 13;
+ if (keyCode === ENTER_KEYCODE) {
+ this.createBranch(e);
+ }
+ }
+
+ enableBranchCreateButton() {
+ if (this.newBranchField.value !== '') {
+ this.newBranchCreateButton.removeAttribute('disabled');
+ } else {
+ this.newBranchCreateButton.setAttribute('disabled', '');
+ }
+ }
+
+ resetForm() {
+ this.newBranchField.value = '';
+ this.enableBranchCreateButtonWrapper();
+ }
+
+ createBranch(e) {
+ e.preventDefault();
+
+ if (this.newBranchCreateButton.getAttribute('disabled') === '') {
+ return;
+ }
+ const newBranchName = this.newBranchField.value;
+ this.targetBranchDropdown.setNewBranch(newBranchName);
+ this.resetForm();
+ }
+}
+
+window.gl = window.gl || {};
+gl.CreateBranchDropdown = CreateBranchDropdown;
diff --git a/app/assets/javascripts/blob/target_branch_dropdown.js b/app/assets/javascripts/blob/target_branch_dropdown.js
new file mode 100644
index 00000000000..216f069ef71
--- /dev/null
+++ b/app/assets/javascripts/blob/target_branch_dropdown.js
@@ -0,0 +1,152 @@
+/* eslint-disable class-methods-use-this */
+const SELECT_ITEM_MSG = 'Select';
+
+class TargetBranchDropDown {
+ constructor(dropdown) {
+ this.dropdown = dropdown;
+ this.$dropdown = $(dropdown);
+ this.fieldName = this.dropdown.getAttribute('data-field-name');
+ this.form = this.dropdown.closest('form');
+ this.createDropdown();
+ }
+
+ static bootstrap() {
+ const dropdowns = document.querySelectorAll('.js-project-branches-dropdown');
+ [].forEach.call(dropdowns, dropdown => new TargetBranchDropDown(dropdown));
+ }
+
+ createDropdown() {
+ const self = this;
+ this.$dropdown.glDropdown({
+ selectable: true,
+ filterable: true,
+ search: {
+ fields: ['title'],
+ },
+ data: (term, callback) => $.ajax({
+ url: self.dropdown.getAttribute('data-refs-url'),
+ data: {
+ ref: self.dropdown.getAttribute('data-ref'),
+ show_all: true,
+ },
+ dataType: 'json',
+ }).done(refs => callback(self.dropdownData(refs))),
+ toggleLabel(item, el) {
+ if (el.is('.is-active')) {
+ return item.text;
+ }
+ return SELECT_ITEM_MSG;
+ },
+ clicked(item, el, e) {
+ e.preventDefault();
+ self.onClick.call(self);
+ },
+ fieldName: self.fieldName,
+ });
+ return new gl.CreateBranchDropdown(this.form.querySelector('.dropdown-new-branch'), this);
+ }
+
+ onClick() {
+ this.enableSubmit();
+ this.$dropdown.trigger('change.branch');
+ }
+
+ enableSubmit() {
+ const submitBtn = this.form.querySelector('[type="submit"]');
+ if (this.branchInput && this.branchInput.value) {
+ submitBtn.removeAttribute('disabled');
+ } else {
+ submitBtn.setAttribute('disabled', '');
+ }
+ }
+
+ dropdownData(refs) {
+ const branchList = this.dropdownItems(refs);
+ this.cachedRefs = refs;
+ this.addDefaultBranch(branchList);
+ this.addNewBranch(branchList);
+ return { Branches: branchList };
+ }
+
+ dropdownItems(refs) {
+ return refs.map(this.dropdownItem);
+ }
+
+ dropdownItem(ref) {
+ return { id: ref, text: ref, title: ref };
+ }
+
+ addDefaultBranch(branchList) {
+ // when no branch is selected do nothing
+ if (!this.branchInput) {
+ return;
+ }
+
+ const branchInputVal = this.branchInput.value;
+ const currentBranchIndex = this.searchBranch(branchList, branchInputVal);
+
+ if (currentBranchIndex === -1) {
+ this.unshiftBranch(branchList, this.dropdownItem(branchInputVal));
+ }
+ }
+
+ addNewBranch(branchList) {
+ if (this.newBranch) {
+ this.unshiftBranch(branchList, this.newBranch);
+ }
+ }
+
+ searchBranch(branchList, branchName) {
+ return _.findIndex(branchList, el => branchName === el.id);
+ }
+
+ unshiftBranch(branchList, branch) {
+ const branchIndex = this.searchBranch(branchList, branch.id);
+
+ if (branchIndex === -1) {
+ branchList.unshift(branch);
+ }
+ }
+
+ setNewBranch(newBranchName) {
+ this.newBranch = this.dropdownItem(newBranchName);
+ this.refreshData();
+ this.selectBranch(this.searchBranch(this.glDropdown.fullData.Branches, newBranchName));
+ }
+
+ refreshData() {
+ this.glDropdown.fullData = this.dropdownData(this.cachedRefs);
+ this.clearFilter();
+ }
+
+ clearFilter() {
+ // apply an empty filter in order to refresh the data
+ this.glDropdown.filter.filter('');
+ this.dropdown.closest('.dropdown').querySelector('.dropdown-page-one .dropdown-input-field').value = '';
+ }
+
+ selectBranch(index) {
+ const branch = this.dropdown.closest('.dropdown').querySelectorAll('li a')[index];
+
+ if (!branch.classList.contains('is-active')) {
+ branch.click();
+ } else {
+ this.closeDropdown();
+ }
+ }
+
+ closeDropdown() {
+ this.dropdown.closest('.dropdown').querySelector('.dropdown-menu-close').click();
+ }
+
+ get branchInput() {
+ return this.form.querySelector(`input[name="${this.fieldName}"]`);
+ }
+
+ get glDropdown() {
+ return this.$dropdown.data('glDropdown');
+ }
+}
+
+window.gl = window.gl || {};
+gl.TargetBranchDropDown = TargetBranchDropDown;
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index 22e93328548..2c1f988d987 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,72 +1,66 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, no-return-assign, new-parens, no-param-reassign, max-len */
-(function() {
- var Breakpoints = (function() {
- var BreakpointInstance, instance;
+var Breakpoints = (function() {
+ var BreakpointInstance, instance;
- function Breakpoints() {}
+ function Breakpoints() {}
- instance = null;
+ instance = null;
- BreakpointInstance = (function() {
- var BREAKPOINTS;
+ BreakpointInstance = (function() {
+ var BREAKPOINTS;
- BREAKPOINTS = ["xs", "sm", "md", "lg"];
+ BREAKPOINTS = ["xs", "sm", "md", "lg"];
- function BreakpointInstance() {
- this.setup();
- }
-
- BreakpointInstance.prototype.setup = function() {
- var allDeviceSelector, els;
- allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
- return ".device-" + breakpoint;
- });
- if ($(allDeviceSelector.join(",")).length) {
- return;
- }
- // Create all the elements
- els = $.map(BREAKPOINTS, function(breakpoint) {
- return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
- });
- return $("body").append(els.join(''));
- };
+ function BreakpointInstance() {
+ this.setup();
+ }
- BreakpointInstance.prototype.visibleDevice = function() {
- var allDeviceSelector;
- allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
- return ".device-" + breakpoint;
- });
- return $(allDeviceSelector.join(",")).filter(":visible");
- };
-
- BreakpointInstance.prototype.getBreakpointSize = function() {
- var $visibleDevice;
- $visibleDevice = this.visibleDevice;
- // TODO: Consider refactoring in light of turbolinks removal.
- // the page refreshed via turbolinks
- if (!$visibleDevice().length) {
- this.setup();
- }
- $visibleDevice = this.visibleDevice();
- return $visibleDevice.attr("class").split("visible-")[1];
- };
+ BreakpointInstance.prototype.setup = function() {
+ var allDeviceSelector, els;
+ allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
+ return ".device-" + breakpoint;
+ });
+ if ($(allDeviceSelector.join(",")).length) {
+ return;
+ }
+ // Create all the elements
+ els = $.map(BREAKPOINTS, function(breakpoint) {
+ return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
+ });
+ return $("body").append(els.join(''));
+ };
- return BreakpointInstance;
- })();
+ BreakpointInstance.prototype.visibleDevice = function() {
+ var allDeviceSelector;
+ allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
+ return ".device-" + breakpoint;
+ });
+ return $(allDeviceSelector.join(",")).filter(":visible");
+ };
- Breakpoints.get = function() {
- return instance != null ? instance : instance = new BreakpointInstance;
+ BreakpointInstance.prototype.getBreakpointSize = function() {
+ var $visibleDevice;
+ $visibleDevice = this.visibleDevice;
+ // TODO: Consider refactoring in light of turbolinks removal.
+ // the page refreshed via turbolinks
+ if (!$visibleDevice().length) {
+ this.setup();
+ }
+ $visibleDevice = this.visibleDevice();
+ return $visibleDevice.attr("class").split("visible-")[1];
};
- return Breakpoints;
+ return BreakpointInstance;
})();
- $((function(_this) {
- return function() {
- return _this.bp = Breakpoints.get();
- };
- })(this));
+ Breakpoints.get = function() {
+ return instance != null ? instance : instance = new BreakpointInstance;
+ };
+
+ return Breakpoints;
+})();
+
+$(() => { window.bp = Breakpoints.get(); });
- window.Breakpoints = Breakpoints;
-}).call(window);
+window.Breakpoints = Breakpoints;
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index e8531c43b4b..f73e489e7b2 100644
--- a/app/assets/javascripts/broadcast_message.js
+++ b/app/assets/javascripts/broadcast_message.js
@@ -1,34 +1,33 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */
-(function() {
- $(function() {
- var previewPath;
- $('input#broadcast_message_color').on('input', function() {
- var previewColor;
- previewColor = $(this).val();
- return $('div.broadcast-message-preview').css('background-color', previewColor);
- });
- $('input#broadcast_message_font').on('input', function() {
- var previewColor;
- previewColor = $(this).val();
- return $('div.broadcast-message-preview').css('color', previewColor);
- });
- previewPath = $('textarea#broadcast_message_message').data('preview-path');
- return $('textarea#broadcast_message_message').on('input', function() {
- var message;
- message = $(this).val();
- if (message === '') {
- return $('.js-broadcast-message-preview').text("Your message here");
- } else {
- return $.ajax({
- url: previewPath,
- type: "POST",
- data: {
- broadcast_message: {
- message: message
- }
+
+$(function() {
+ var previewPath;
+ $('input#broadcast_message_color').on('input', function() {
+ var previewColor;
+ previewColor = $(this).val();
+ return $('div.broadcast-message-preview').css('background-color', previewColor);
+ });
+ $('input#broadcast_message_font').on('input', function() {
+ var previewColor;
+ previewColor = $(this).val();
+ return $('div.broadcast-message-preview').css('color', previewColor);
+ });
+ previewPath = $('textarea#broadcast_message_message').data('preview-path');
+ return $('textarea#broadcast_message_message').on('input', function() {
+ var message;
+ message = $(this).val();
+ if (message === '') {
+ return $('.js-broadcast-message-preview').text("Your message here");
+ } else {
+ return $.ajax({
+ url: previewPath,
+ type: "POST",
+ data: {
+ broadcast_message: {
+ message: message
}
- });
- }
- });
+ }
+ });
+ }
});
-}).call(window);
+});
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 6e6e9b18686..6efd26ccc37 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,285 +1,283 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */
/* global Breakpoints */
-(function() {
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
- var AUTO_SCROLL_OFFSET = 75;
- var DOWN_BUILD_TRACE = '#down-build-trace';
-
- this.Build = (function() {
- Build.timeout = null;
-
- Build.state = null;
-
- function Build(options) {
- options = options || $('.js-build-options').data();
- this.pageUrl = options.pageUrl;
- this.buildUrl = options.buildUrl;
- this.buildStatus = options.buildStatus;
- this.state = options.logState;
- this.buildStage = options.buildStage;
- this.updateDropdown = bind(this.updateDropdown, this);
- this.$document = $(document);
- this.$body = $('body');
- this.$buildTrace = $('#build-trace');
- this.$autoScrollContainer = $('.autoscroll-container');
- this.$autoScrollStatus = $('#autoscroll-status');
- this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
- this.$upBuildTrace = $('#up-build-trace');
- this.$downBuildTrace = $(DOWN_BUILD_TRACE);
- this.$scrollTopBtn = $('#scroll-top');
- this.$scrollBottomBtn = $('#scroll-bottom');
- this.$buildRefreshAnimation = $('.js-build-refresh');
-
- clearTimeout(Build.timeout);
- // Init breakpoint checker
- this.bp = Breakpoints.get();
-
- this.initSidebar();
- this.$buildScroll = $('#js-build-scroll');
-
- this.populateJobs(this.buildStage);
- this.updateStageDropdownText(this.buildStage);
- this.sidebarOnResize();
-
- this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
- this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
- this.$document.on('scroll', this.initScrollMonitor.bind(this));
- $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
- $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
- this.updateArtifactRemoveDate();
- if ($('#build-trace').length) {
- this.getInitialBuildTrace();
- this.initScrollButtonAffix();
- }
- this.invokeBuildTrace();
+var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+var AUTO_SCROLL_OFFSET = 75;
+var DOWN_BUILD_TRACE = '#down-build-trace';
+
+window.Build = (function() {
+ Build.timeout = null;
+
+ Build.state = null;
+
+ function Build(options) {
+ options = options || $('.js-build-options').data();
+ this.pageUrl = options.pageUrl;
+ this.buildUrl = options.buildUrl;
+ this.buildStatus = options.buildStatus;
+ this.state = options.logState;
+ this.buildStage = options.buildStage;
+ this.updateDropdown = bind(this.updateDropdown, this);
+ this.$document = $(document);
+ this.$body = $('body');
+ this.$buildTrace = $('#build-trace');
+ this.$autoScrollContainer = $('.autoscroll-container');
+ this.$autoScrollStatus = $('#autoscroll-status');
+ this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
+ this.$upBuildTrace = $('#up-build-trace');
+ this.$downBuildTrace = $(DOWN_BUILD_TRACE);
+ this.$scrollTopBtn = $('#scroll-top');
+ this.$scrollBottomBtn = $('#scroll-bottom');
+ this.$buildRefreshAnimation = $('.js-build-refresh');
+
+ clearTimeout(Build.timeout);
+ // Init breakpoint checker
+ this.bp = Breakpoints.get();
+
+ this.initSidebar();
+ this.$buildScroll = $('#js-build-scroll');
+
+ this.populateJobs(this.buildStage);
+ this.updateStageDropdownText(this.buildStage);
+ this.sidebarOnResize();
+
+ this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
+ this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
+ this.$document.on('scroll', this.initScrollMonitor.bind(this));
+ $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
+ $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
+ this.updateArtifactRemoveDate();
+ if ($('#build-trace').length) {
+ this.getInitialBuildTrace();
+ this.initScrollButtonAffix();
}
-
- Build.prototype.initSidebar = function() {
- this.$sidebar = $('.js-build-sidebar');
- this.$sidebar.niceScroll();
- this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
- };
-
- Build.prototype.location = function() {
- return window.location.href.split("#")[0];
- };
-
- Build.prototype.invokeBuildTrace = function() {
- var continueRefreshStatuses = ['running', 'pending'];
- // Continue to update build trace when build is running or pending
- if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) {
- // Check for new build output if user still watching build page
- // Only valid for runnig build when output changes during time
- Build.timeout = setTimeout((function(_this) {
- return function() {
- if (_this.location() === _this.pageUrl) {
- return _this.getBuildTrace();
- }
- };
- })(this), 4000);
- }
- };
-
- Build.prototype.getInitialBuildTrace = function() {
- var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
-
- return $.ajax({
- url: this.buildUrl,
- dataType: 'json',
- success: function(buildData) {
- $('.js-build-output').html(buildData.trace_html);
- if (window.location.hash === DOWN_BUILD_TRACE) {
- $("html,body").scrollTop(this.$buildTrace.height());
+ this.invokeBuildTrace();
+ }
+
+ Build.prototype.initSidebar = function() {
+ this.$sidebar = $('.js-build-sidebar');
+ this.$sidebar.niceScroll();
+ this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
+ };
+
+ Build.prototype.location = function() {
+ return window.location.href.split("#")[0];
+ };
+
+ Build.prototype.invokeBuildTrace = function() {
+ var continueRefreshStatuses = ['running', 'pending'];
+ // Continue to update build trace when build is running or pending
+ if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) {
+ // Check for new build output if user still watching build page
+ // Only valid for runnig build when output changes during time
+ Build.timeout = setTimeout((function(_this) {
+ return function() {
+ if (_this.location() === _this.pageUrl) {
+ return _this.getBuildTrace();
}
- if (removeRefreshStatuses.indexOf(buildData.status) !== -1) {
- this.$buildRefreshAnimation.remove();
- return this.initScrollMonitor();
+ };
+ })(this), 4000);
+ }
+ };
+
+ Build.prototype.getInitialBuildTrace = function() {
+ var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
+
+ return $.ajax({
+ url: this.buildUrl,
+ dataType: 'json',
+ success: function(buildData) {
+ $('.js-build-output').html(buildData.trace_html);
+ if (window.location.hash === DOWN_BUILD_TRACE) {
+ $("html,body").scrollTop(this.$buildTrace.height());
+ }
+ if (removeRefreshStatuses.indexOf(buildData.status) !== -1) {
+ this.$buildRefreshAnimation.remove();
+ return this.initScrollMonitor();
+ }
+ }.bind(this)
+ });
+ };
+
+ Build.prototype.getBuildTrace = function() {
+ return $.ajax({
+ url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
+ dataType: "json",
+ success: (function(_this) {
+ return function(log) {
+ var pageUrl;
+
+ if (log.state) {
+ _this.state = log.state;
}
- }.bind(this)
- });
- };
-
- Build.prototype.getBuildTrace = function() {
- return $.ajax({
- url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
- dataType: "json",
- success: (function(_this) {
- return function(log) {
- var pageUrl;
-
- if (log.state) {
- _this.state = log.state;
+ _this.invokeBuildTrace();
+ if (log.status === "running") {
+ if (log.append) {
+ $('.js-build-output').append(log.html);
+ } else {
+ $('.js-build-output').html(log.html);
}
- _this.invokeBuildTrace();
- if (log.status === "running") {
- if (log.append) {
- $('.js-build-output').append(log.html);
- } else {
- $('.js-build-output').html(log.html);
- }
- return _this.checkAutoscroll();
- } else if (log.status !== _this.buildStatus) {
- pageUrl = _this.pageUrl;
- if (_this.$autoScrollStatus.data('state') === 'enabled') {
- pageUrl += DOWN_BUILD_TRACE;
- }
-
- return gl.utils.visitUrl(pageUrl);
+ return _this.checkAutoscroll();
+ } else if (log.status !== _this.buildStatus) {
+ pageUrl = _this.pageUrl;
+ if (_this.$autoScrollStatus.data('state') === 'enabled') {
+ pageUrl += DOWN_BUILD_TRACE;
}
- };
- })(this)
- });
- };
-
- Build.prototype.checkAutoscroll = function() {
- if (this.$autoScrollStatus.data("state") === "enabled") {
- return $("html,body").scrollTop(this.$buildTrace.height());
- }
-
- // Handle a situation where user started new build
- // but never scrolled a page
- if (!this.$scrollTopBtn.is(':visible') &&
- !this.$scrollBottomBtn.is(':visible') &&
- !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
- this.$scrollBottomBtn.show();
- }
- };
- Build.prototype.initScrollButtonAffix = function() {
- // Hide everything initially
- this.$scrollTopBtn.hide();
- this.$scrollBottomBtn.hide();
- this.$autoScrollContainer.hide();
- };
-
- // Page scroll listener to detect if user has scrolling page
- // and handle following cases
- // 1) User is at Top of Build Log;
- // - Hide Top Arrow button
- // - Show Bottom Arrow button
- // - Disable Autoscroll and hide indicator (when build is running)
- // 2) User is at Bottom of Build Log;
- // - Show Top Arrow button
- // - Hide Bottom Arrow button
- // - Enable Autoscroll and show indicator (when build is running)
- // 3) User is somewhere in middle of Build Log;
- // - Show Top Arrow button
- // - Show Bottom Arrow button
- // - Disable Autoscroll and hide indicator (when build is running)
- Build.prototype.initScrollMonitor = function() {
- if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
- // User is somewhere in middle of Build Log
-
- this.$scrollTopBtn.show();
-
- if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
- this.$scrollBottomBtn.show();
- } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
- this.$scrollBottomBtn.show();
- } else {
- this.$scrollBottomBtn.hide();
- }
-
- // Hide Autoscroll Status Indicator
- if (this.$scrollBottomBtn.is(':visible')) {
- this.$autoScrollContainer.hide();
- this.$autoScrollStatusText.removeClass('animate');
- } else {
- this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
- this.$autoScrollStatusText.addClass('animate');
- }
- } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
- // User is at Top of Build Log
+ return gl.utils.visitUrl(pageUrl);
+ }
+ };
+ })(this)
+ });
+ };
+
+ Build.prototype.checkAutoscroll = function() {
+ if (this.$autoScrollStatus.data("state") === "enabled") {
+ return $("html,body").scrollTop(this.$buildTrace.height());
+ }
- this.$scrollTopBtn.hide();
+ // Handle a situation where user started new build
+ // but never scrolled a page
+ if (!this.$scrollTopBtn.is(':visible') &&
+ !this.$scrollBottomBtn.is(':visible') &&
+ !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ this.$scrollBottomBtn.show();
+ }
+ };
+
+ Build.prototype.initScrollButtonAffix = function() {
+ // Hide everything initially
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.hide();
+ this.$autoScrollContainer.hide();
+ };
+
+ // Page scroll listener to detect if user has scrolling page
+ // and handle following cases
+ // 1) User is at Top of Build Log;
+ // - Hide Top Arrow button
+ // - Show Bottom Arrow button
+ // - Disable Autoscroll and hide indicator (when build is running)
+ // 2) User is at Bottom of Build Log;
+ // - Show Top Arrow button
+ // - Hide Bottom Arrow button
+ // - Enable Autoscroll and show indicator (when build is running)
+ // 3) User is somewhere in middle of Build Log;
+ // - Show Top Arrow button
+ // - Show Bottom Arrow button
+ // - Disable Autoscroll and hide indicator (when build is running)
+ Build.prototype.initScrollMonitor = function() {
+ if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // User is somewhere in middle of Build Log
+
+ this.$scrollTopBtn.show();
+
+ if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
+ this.$scrollBottomBtn.show();
+ } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
this.$scrollBottomBtn.show();
+ } else {
+ this.$scrollBottomBtn.hide();
+ }
+ // Hide Autoscroll Status Indicator
+ if (this.$scrollBottomBtn.is(':visible')) {
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
- } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
- (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
- // User is at Bottom of Build Log
-
- this.$scrollTopBtn.show();
- this.$scrollBottomBtn.hide();
-
- // Show and Reposition Autoscroll Status Indicator
+ } else {
this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
this.$autoScrollStatusText.addClass('animate');
- } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
- // Build Log height is small
+ }
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // User is at Top of Build Log
- this.$scrollTopBtn.hide();
- this.$scrollBottomBtn.hide();
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.show();
- // Hide Autoscroll Status Indicator
- this.$autoScrollContainer.hide();
- this.$autoScrollStatusText.removeClass('animate');
- }
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
+ (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
+ // User is at Bottom of Build Log
- if (this.buildStatus === "running" || this.buildStatus === "pending") {
- // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
- this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
- }
- };
-
- Build.prototype.shouldHideSidebarForViewport = function() {
- var bootstrapBreakpoint;
- bootstrapBreakpoint = this.bp.getBreakpointSize();
- return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
- };
-
- Build.prototype.toggleSidebar = function(shouldHide) {
- var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
- this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
- .toggleClass('sidebar-collapsed', shouldHide);
- this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
- .toggleClass('right-sidebar-collapsed', shouldHide);
- };
-
- Build.prototype.sidebarOnResize = function() {
- this.toggleSidebar(this.shouldHideSidebarForViewport());
- };
-
- Build.prototype.sidebarOnClick = function() {
- if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
- };
-
- Build.prototype.updateArtifactRemoveDate = function() {
- var $date, date;
- $date = $('.js-artifacts-remove');
- if ($date.length) {
- date = $date.text();
- return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
- }
- };
-
- Build.prototype.populateJobs = function(stage) {
- $('.build-job').hide();
- $('.build-job[data-stage="' + stage + '"]').show();
- };
-
- Build.prototype.updateStageDropdownText = function(stage) {
- $('.stage-selection').text(stage);
- };
-
- Build.prototype.updateDropdown = function(e) {
- e.preventDefault();
- var stage = e.currentTarget.text;
- this.updateStageDropdownText(stage);
- this.populateJobs(stage);
- };
-
- Build.prototype.stepTrace = function(e) {
- var $currentTarget;
- e.preventDefault();
- $currentTarget = $(e.currentTarget);
- $.scrollTo($currentTarget.attr('href'), {
- offset: 0
- });
- };
-
- return Build;
- })();
-}).call(window);
+ this.$scrollTopBtn.show();
+ this.$scrollBottomBtn.hide();
+
+ // Show and Reposition Autoscroll Status Indicator
+ this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+ this.$autoScrollStatusText.addClass('animate');
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // Build Log height is small
+
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.hide();
+
+ // Hide Autoscroll Status Indicator
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ }
+
+ if (this.buildStatus === "running" || this.buildStatus === "pending") {
+ // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
+ this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
+ }
+ };
+
+ Build.prototype.shouldHideSidebarForViewport = function() {
+ var bootstrapBreakpoint;
+ bootstrapBreakpoint = this.bp.getBreakpointSize();
+ return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+ };
+
+ Build.prototype.toggleSidebar = function(shouldHide) {
+ var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
+ this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
+ .toggleClass('sidebar-collapsed', shouldHide);
+ this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
+ .toggleClass('right-sidebar-collapsed', shouldHide);
+ };
+
+ Build.prototype.sidebarOnResize = function() {
+ this.toggleSidebar(this.shouldHideSidebarForViewport());
+ };
+
+ Build.prototype.sidebarOnClick = function() {
+ if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
+ };
+
+ Build.prototype.updateArtifactRemoveDate = function() {
+ var $date, date;
+ $date = $('.js-artifacts-remove');
+ if ($date.length) {
+ date = $date.text();
+ return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
+ }
+ };
+
+ Build.prototype.populateJobs = function(stage) {
+ $('.build-job').hide();
+ $('.build-job[data-stage="' + stage + '"]').show();
+ };
+
+ Build.prototype.updateStageDropdownText = function(stage) {
+ $('.stage-selection').text(stage);
+ };
+
+ Build.prototype.updateDropdown = function(e) {
+ e.preventDefault();
+ var stage = e.currentTarget.text;
+ this.updateStageDropdownText(stage);
+ this.populateJobs(stage);
+ };
+
+ Build.prototype.stepTrace = function(e) {
+ var $currentTarget;
+ e.preventDefault();
+ $currentTarget = $(e.currentTarget);
+ $.scrollTo($currentTarget.attr('href'), {
+ offset: 0
+ });
+ };
+
+ return Build;
+})();
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index cae9a0ffca4..bd479700fd3 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,26 +1,25 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
-(function() {
- this.BuildArtifacts = (function() {
- function BuildArtifacts() {
- this.disablePropagation();
- this.setupEntryClick();
- }
- BuildArtifacts.prototype.disablePropagation = function() {
- $('.top-block').on('click', '.download', function(e) {
- return e.stopPropagation();
- });
- return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
- return e.stopImmediatePropagation();
- });
- };
+window.BuildArtifacts = (function() {
+ function BuildArtifacts() {
+ this.disablePropagation();
+ this.setupEntryClick();
+ }
- BuildArtifacts.prototype.setupEntryClick = function() {
- return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
- return window.location = this.dataset.link;
- });
- };
+ BuildArtifacts.prototype.disablePropagation = function() {
+ $('.top-block').on('click', '.download', function(e) {
+ return e.stopPropagation();
+ });
+ return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
+ return e.stopImmediatePropagation();
+ });
+ };
- return BuildArtifacts;
- })();
-}).call(window);
+ BuildArtifacts.prototype.setupEntryClick = function() {
+ return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
+ return window.location = this.dataset.link;
+ });
+ };
+
+ return BuildArtifacts;
+})();
diff --git a/app/assets/javascripts/ci_lint_editor.js b/app/assets/javascripts/ci_lint_editor.js
index 56ffaa765a8..dd4a08a2f31 100644
--- a/app/assets/javascripts/ci_lint_editor.js
+++ b/app/assets/javascripts/ci_lint_editor.js
@@ -1,18 +1,17 @@
-(() => {
- window.gl = window.gl || {};
- class CILintEditor {
- constructor() {
- this.editor = window.ace.edit('ci-editor');
- this.textarea = document.querySelector('#content');
+window.gl = window.gl || {};
- this.editor.getSession().setMode('ace/mode/yaml');
- this.editor.on('input', () => {
- const content = this.editor.getSession().getValue();
- this.textarea.value = content;
- });
- }
+class CILintEditor {
+ constructor() {
+ this.editor = window.ace.edit('ci-editor');
+ this.textarea = document.querySelector('#content');
+
+ this.editor.getSession().setMode('ace/mode/yaml');
+ this.editor.on('input', () => {
+ const content = this.editor.getSession().getValue();
+ this.textarea.value = content;
+ });
}
+}
- gl.CILintEditor = CILintEditor;
-})();
+gl.CILintEditor = CILintEditor;
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
index 566b322eb49..5f637524e30 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -1,14 +1,12 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife */
/* global CommitFile */
-(function() {
- this.Commit = (function() {
- function Commit() {
- $('.files .diff-file').each(function() {
- return new CommitFile(this);
- });
- }
+window.Commit = (function() {
+ function Commit() {
+ $('.files .diff-file').each(function() {
+ return new CommitFile(this);
+ });
+ }
- return Commit;
- })();
-}).call(window);
+ return Commit;
+})();
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index ccd895f3bf4..e3f9eaaf39c 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,68 +1,66 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */
/* global Pager */
-(function() {
- this.CommitsList = (function() {
- var CommitsList = {};
+window.CommitsList = (function() {
+ var CommitsList = {};
- CommitsList.timer = null;
+ CommitsList.timer = null;
- CommitsList.init = function(limit) {
- $("body").on("click", ".day-commits-table li.commit", function(e) {
- if (e.target.nodeName !== "A") {
- location.href = $(this).attr("url");
- e.stopPropagation();
- return false;
- }
- });
- Pager.init(limit, false, false, function() {
- gl.utils.localTimeAgo($('.js-timeago'));
- });
- this.content = $("#commits-list");
- this.searchField = $("#commits-search");
- this.lastSearch = this.searchField.val();
- return this.initSearch();
- };
+ CommitsList.init = function(limit) {
+ $("body").on("click", ".day-commits-table li.commit", function(e) {
+ if (e.target.nodeName !== "A") {
+ location.href = $(this).attr("url");
+ e.stopPropagation();
+ return false;
+ }
+ });
+ Pager.init(limit, false, false, function() {
+ gl.utils.localTimeAgo($('.js-timeago'));
+ });
+ this.content = $("#commits-list");
+ this.searchField = $("#commits-search");
+ this.lastSearch = this.searchField.val();
+ return this.initSearch();
+ };
- CommitsList.initSearch = function() {
- this.timer = null;
- return this.searchField.keyup((function(_this) {
- return function() {
- clearTimeout(_this.timer);
- return _this.timer = setTimeout(_this.filterResults, 500);
- };
- })(this));
- };
+ CommitsList.initSearch = function() {
+ this.timer = null;
+ return this.searchField.keyup((function(_this) {
+ return function() {
+ clearTimeout(_this.timer);
+ return _this.timer = setTimeout(_this.filterResults, 500);
+ };
+ })(this));
+ };
- CommitsList.filterResults = function() {
- var commitsUrl, form, search;
- form = $(".commits-search-form");
- search = CommitsList.searchField.val();
- if (search === CommitsList.lastSearch) return;
- commitsUrl = form.attr("action") + '?' + form.serialize();
- CommitsList.content.fadeTo('fast', 0.5);
- return $.ajax({
- type: "GET",
- url: form.attr("action"),
- data: form.serialize(),
- complete: function() {
- return CommitsList.content.fadeTo('fast', 1.0);
- },
- success: function(data) {
- CommitsList.lastSearch = search;
- CommitsList.content.html(data.html);
- return history.replaceState({
- page: commitsUrl
- // Change url so if user reload a page - search results are saved
- }, document.title, commitsUrl);
- },
- error: function() {
- CommitsList.lastSearch = null;
- },
- dataType: "json"
- });
- };
+ CommitsList.filterResults = function() {
+ var commitsUrl, form, search;
+ form = $(".commits-search-form");
+ search = CommitsList.searchField.val();
+ if (search === CommitsList.lastSearch) return;
+ commitsUrl = form.attr("action") + '?' + form.serialize();
+ CommitsList.content.fadeTo('fast', 0.5);
+ return $.ajax({
+ type: "GET",
+ url: form.attr("action"),
+ data: form.serialize(),
+ complete: function() {
+ return CommitsList.content.fadeTo('fast', 1.0);
+ },
+ success: function(data) {
+ CommitsList.lastSearch = search;
+ CommitsList.content.html(data.html);
+ return history.replaceState({
+ page: commitsUrl
+ // Change url so if user reload a page - search results are saved
+ }, document.title, commitsUrl);
+ },
+ error: function() {
+ CommitsList.lastSearch = null;
+ },
+ dataType: "json"
+ });
+ };
- return CommitsList;
- })();
-}).call(window);
+ return CommitsList;
+})();
diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js
index db0cbfd87c3..36bfe457be9 100644
--- a/app/assets/javascripts/commons/bootstrap.js
+++ b/app/assets/javascripts/commons/bootstrap.js
@@ -1,4 +1,4 @@
-import 'jquery';
+import $ from 'jquery';
// bootstrap jQuery plugins
import 'bootstrap-sass/assets/javascripts/bootstrap/affix';
@@ -8,3 +8,9 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
+
+// custom jQuery functions
+$.fn.extend({
+ disable() { return $(this).attr('disabled', 'disabled').addClass('disabled'); },
+ enable() { return $(this).removeAttr('disabled').removeClass('disabled'); },
+});
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
index 72ede1d621a..7063f59d446 100644
--- a/app/assets/javascripts/commons/index.js
+++ b/app/assets/javascripts/commons/index.js
@@ -1,2 +1,3 @@
+import './polyfills';
import './jquery';
import './bootstrap';
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
new file mode 100644
index 00000000000..fbd0db64ca7
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -0,0 +1,10 @@
+// ECMAScript polyfills
+import 'core-js/fn/array/find';
+import 'core-js/fn/object/assign';
+import 'core-js/fn/promise';
+import 'core-js/fn/string/code-point-at';
+import 'core-js/fn/string/from-code-point';
+
+// Browser polyfills
+import './polyfills/custom_event';
+import './polyfills/element';
diff --git a/app/assets/javascripts/commons/polyfills/custom_event.js b/app/assets/javascripts/commons/polyfills/custom_event.js
new file mode 100644
index 00000000000..aea61b82d03
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills/custom_event.js
@@ -0,0 +1,9 @@
+if (typeof window.CustomEvent !== 'function') {
+ window.CustomEvent = function CustomEvent(event, params) {
+ const evt = document.createEvent('CustomEvent');
+ const evtParams = params || { bubbles: false, cancelable: false, detail: undefined };
+ evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail);
+ return evt;
+ };
+ window.CustomEvent.prototype = Event;
+}
diff --git a/app/assets/javascripts/commons/polyfills/element.js b/app/assets/javascripts/commons/polyfills/element.js
new file mode 100644
index 00000000000..9a1f73bf2ac
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills/element.js
@@ -0,0 +1,20 @@
+Element.prototype.closest = Element.prototype.closest ||
+ function closest(selector, selectedElement = this) {
+ if (!selectedElement) return null;
+ return selectedElement.matches(selector) ?
+ selectedElement :
+ Element.prototype.closest(selector, selectedElement.parentElement);
+ };
+
+Element.prototype.matches = Element.prototype.matches ||
+ Element.prototype.matchesSelector ||
+ Element.prototype.mozMatchesSelector ||
+ Element.prototype.msMatchesSelector ||
+ Element.prototype.oMatchesSelector ||
+ Element.prototype.webkitMatchesSelector ||
+ function matches(selector) {
+ const elms = (this.document || this.ownerDocument).querySelectorAll(selector);
+ let i = elms.length - 1;
+ while (i >= 0 && elms.item(i) !== this) { i -= 1; }
+ return i > -1;
+ };
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index 15df105d4cc..9e5dbd64a7e 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -1,91 +1,90 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
-(function() {
- this.Compare = (function() {
- function Compare(opts) {
- this.opts = opts;
- this.source_loading = $(".js-source-loading");
- this.target_loading = $(".js-target-loading");
- $('.js-compare-dropdown').each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return $dropdown.glDropdown({
- selectable: true,
- fieldName: $dropdown.data('field-name'),
- filterable: true,
- id: function(obj, $el) {
- return $el.data('id');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
- },
- clicked: function(e, el) {
- if ($dropdown.is('.js-target-branch')) {
- return _this.getTargetHtml();
- } else if ($dropdown.is('.js-source-branch')) {
- return _this.getSourceHtml();
- } else if ($dropdown.is('.js-target-project')) {
- return _this.getTargetProject();
- }
+
+window.Compare = (function() {
+ function Compare(opts) {
+ this.opts = opts;
+ this.source_loading = $(".js-source-loading");
+ this.target_loading = $(".js-target-loading");
+ $('.js-compare-dropdown').each((function(_this) {
+ return function(i, dropdown) {
+ var $dropdown;
+ $dropdown = $(dropdown);
+ return $dropdown.glDropdown({
+ selectable: true,
+ fieldName: $dropdown.data('field-name'),
+ filterable: true,
+ id: function(obj, $el) {
+ return $el.data('id');
+ },
+ toggleLabel: function(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked: function(e, el) {
+ if ($dropdown.is('.js-target-branch')) {
+ return _this.getTargetHtml();
+ } else if ($dropdown.is('.js-source-branch')) {
+ return _this.getSourceHtml();
+ } else if ($dropdown.is('.js-target-project')) {
+ return _this.getTargetProject();
}
- });
- };
- })(this));
- this.initialState();
- }
+ }
+ });
+ };
+ })(this));
+ this.initialState();
+ }
- Compare.prototype.initialState = function() {
- this.getSourceHtml();
- return this.getTargetHtml();
- };
+ Compare.prototype.initialState = function() {
+ this.getSourceHtml();
+ return this.getTargetHtml();
+ };
- Compare.prototype.getTargetProject = function() {
- return $.ajax({
- url: this.opts.targetProjectUrl,
- data: {
- target_project_id: $("input[name='merge_request[target_project_id]']").val()
- },
- beforeSend: function() {
- return $('.mr_target_commit').empty();
- },
- success: function(html) {
- return $('.js-target-branch-dropdown .dropdown-content').html(html);
- }
- });
- };
+ Compare.prototype.getTargetProject = function() {
+ return $.ajax({
+ url: this.opts.targetProjectUrl,
+ data: {
+ target_project_id: $("input[name='merge_request[target_project_id]']").val()
+ },
+ beforeSend: function() {
+ return $('.mr_target_commit').empty();
+ },
+ success: function(html) {
+ return $('.js-target-branch-dropdown .dropdown-content').html(html);
+ }
+ });
+ };
- Compare.prototype.getSourceHtml = function() {
- return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
- ref: $("input[name='merge_request[source_branch]']").val()
- });
- };
+ Compare.prototype.getSourceHtml = function() {
+ return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
+ ref: $("input[name='merge_request[source_branch]']").val()
+ });
+ };
- Compare.prototype.getTargetHtml = function() {
- return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
- target_project_id: $("input[name='merge_request[target_project_id]']").val(),
- ref: $("input[name='merge_request[target_branch]']").val()
- });
- };
+ Compare.prototype.getTargetHtml = function() {
+ return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
+ target_project_id: $("input[name='merge_request[target_project_id]']").val(),
+ ref: $("input[name='merge_request[target_branch]']").val()
+ });
+ };
- Compare.prototype.sendAjax = function(url, loading, target, data) {
- var $target;
- $target = $(target);
- return $.ajax({
- url: url,
- data: data,
- beforeSend: function() {
- loading.show();
- return $target.empty();
- },
- success: function(html) {
- loading.hide();
- $target.html(html);
- var className = '.' + $target[0].className.replace(' ', '.');
- gl.utils.localTimeAgo($('.js-timeago', className));
- }
- });
- };
+ Compare.prototype.sendAjax = function(url, loading, target, data) {
+ var $target;
+ $target = $(target);
+ return $.ajax({
+ url: url,
+ data: data,
+ beforeSend: function() {
+ loading.show();
+ return $target.empty();
+ },
+ success: function(html) {
+ loading.hide();
+ $target.html(html);
+ var className = '.' + $target[0].className.replace(' ', '.');
+ gl.utils.localTimeAgo($('.js-timeago', className));
+ }
+ });
+ };
- return Compare;
- })();
-}).call(window);
+ return Compare;
+})();
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 1eca973e069..d91bfb1ccbd 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -1,69 +1,67 @@
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
-(function() {
- this.CompareAutocomplete = (function() {
- function CompareAutocomplete() {
- this.initDropdown();
- }
+window.CompareAutocomplete = (function() {
+ function CompareAutocomplete() {
+ this.initDropdown();
+ }
- CompareAutocomplete.prototype.initDropdown = function() {
- return $('.js-compare-dropdown').each(function() {
- var $dropdown, selected;
- $dropdown = $(this);
- selected = $dropdown.data('selected');
- const $dropdownContainer = $dropdown.closest('.dropdown');
- const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
- const $filterInput = $('input[type="search"]', $dropdownContainer);
- $dropdown.glDropdown({
- data: function(term, callback) {
- return $.ajax({
- url: $dropdown.data('refs-url'),
- data: {
- ref: $dropdown.data('ref')
- }
- }).done(function(refs) {
- return callback(refs);
- });
- },
- selectable: true,
- filterable: true,
- filterByText: true,
- fieldName: $dropdown.data('field-name'),
- filterInput: 'input[type="search"]',
- renderRow: function(ref) {
- var link;
- if (ref.header != null) {
- return $('<li />').addClass('dropdown-header').text(ref.header);
- } else {
- link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
- return $('<li />').append(link);
+ CompareAutocomplete.prototype.initDropdown = function() {
+ return $('.js-compare-dropdown').each(function() {
+ var $dropdown, selected;
+ $dropdown = $(this);
+ selected = $dropdown.data('selected');
+ const $dropdownContainer = $dropdown.closest('.dropdown');
+ const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
+ const $filterInput = $('input[type="search"]', $dropdownContainer);
+ $dropdown.glDropdown({
+ data: function(term, callback) {
+ return $.ajax({
+ url: $dropdown.data('refs-url'),
+ data: {
+ ref: $dropdown.data('ref')
}
- },
- id: function(obj, $el) {
- return $el.attr('data-ref');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
+ }).done(function(refs) {
+ return callback(refs);
+ });
+ },
+ selectable: true,
+ filterable: true,
+ filterByText: true,
+ fieldName: $dropdown.data('field-name'),
+ filterInput: 'input[type="search"]',
+ renderRow: function(ref) {
+ var link;
+ if (ref.header != null) {
+ return $('<li />').addClass('dropdown-header').text(ref.header);
+ } else {
+ link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+ return $('<li />').append(link);
}
- });
- $filterInput.on('keyup', (e) => {
- const keyCode = e.keyCode || e.which;
- if (keyCode !== 13) return;
- const text = $filterInput.val();
- $fieldInput.val(text);
- $('.dropdown-toggle-text', $dropdown).text(text);
- $dropdownContainer.removeClass('open');
- });
+ },
+ id: function(obj, $el) {
+ return $el.attr('data-ref');
+ },
+ toggleLabel: function(obj, $el) {
+ return $el.text().trim();
+ }
+ });
+ $filterInput.on('keyup', (e) => {
+ const keyCode = e.keyCode || e.which;
+ if (keyCode !== 13) return;
+ const text = $filterInput.val();
+ $fieldInput.val(text);
+ $('.dropdown-toggle-text', $dropdown).text(text);
+ $dropdownContainer.removeClass('open');
+ });
- $dropdownContainer.on('click', '.dropdown-content a', (e) => {
- $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
- if ($dropdown.hasClass('has-tooltip')) {
- $dropdown.tooltip('fixTitle');
- }
- });
+ $dropdownContainer.on('click', '.dropdown-content a', (e) => {
+ $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
+ if ($dropdown.hasClass('has-tooltip')) {
+ $dropdown.tooltip('fixTitle');
+ }
});
- };
+ });
+ };
- return CompareAutocomplete;
- })();
-}).call(window);
+ return CompareAutocomplete;
+})();
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index a1c1b721228..b375b61202e 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,31 +1,30 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */
-(function() {
- this.ConfirmDangerModal = (function() {
- function ConfirmDangerModal(form, text) {
- var project_path, submit;
- this.form = form;
- $('.js-confirm-text').text(text || '');
- $('.js-confirm-danger-input').val('');
- $('#modal-confirm-danger').modal('show');
- project_path = $('.js-confirm-danger-match').text();
- submit = $('.js-confirm-danger-submit');
- submit.disable();
- $('.js-confirm-danger-input').off('input');
- $('.js-confirm-danger-input').on('input', function() {
- if (gl.utils.rstrip($(this).val()) === project_path) {
- return submit.enable();
- } else {
- return submit.disable();
- }
- });
- $('.js-confirm-danger-submit').off('click');
- $('.js-confirm-danger-submit').on('click', (function(_this) {
- return function() {
- return _this.form.submit();
- };
- })(this));
- }
- return ConfirmDangerModal;
- })();
-}).call(window);
+window.ConfirmDangerModal = (function() {
+ function ConfirmDangerModal(form, text) {
+ var project_path, submit;
+ this.form = form;
+ $('.js-confirm-text').text(text || '');
+ $('.js-confirm-danger-input').val('');
+ $('#modal-confirm-danger').modal('show');
+ project_path = $('.js-confirm-danger-match').text();
+ submit = $('.js-confirm-danger-submit');
+ submit.disable();
+ $('.js-confirm-danger-input').off('input');
+ $('.js-confirm-danger-input').on('input', function() {
+ if (gl.utils.rstrip($(this).val()) === project_path) {
+ return submit.enable();
+ } else {
+ return submit.disable();
+ }
+ });
+ $('.js-confirm-danger-submit').off('click');
+ $('.js-confirm-danger-submit').on('click', (function(_this) {
+ return function() {
+ return _this.form.submit();
+ };
+ })(this));
+ }
+
+ return ConfirmDangerModal;
+})();
diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js
index 8883c339335..0fb7bde1fd6 100644
--- a/app/assets/javascripts/copy_as_gfm.js
+++ b/app/assets/javascripts/copy_as_gfm.js
@@ -1,364 +1,361 @@
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
-/* jshint esversion: 6 */
require('./lib/utils/common_utils');
-(() => {
- const gfmRules = {
- // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
- // GitLab Flavored Markdown (GFM) to HTML.
- // These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
- // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
- // from GFM should have a handler here, in reverse order.
- // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
- InlineDiffFilter: {
- 'span.idiff.addition'(el, text) {
- return `{+${text}+}`;
- },
- 'span.idiff.deletion'(el, text) {
- return `{-${text}-}`;
- },
- },
- TaskListFilter: {
- 'input[type=checkbox].task-list-item-checkbox'(el, text) {
- return `[${el.checked ? 'x' : ' '}]`;
- },
- },
- ReferenceFilter: {
- '.tooltip'(el, text) {
- return '';
- },
- 'a.gfm:not([data-link=true])'(el, text) {
- return el.dataset.original || text;
- },
- },
- AutolinkFilter: {
- 'a'(el, text) {
- // Fallback on the regular MarkdownFilter's `a` handler.
- if (text !== el.getAttribute('href')) return false;
-
- return text;
- },
- },
- TableOfContentsFilter: {
- 'ul.section-nav'(el, text) {
- return '[[_TOC_]]';
- },
- },
- EmojiFilter: {
- 'img.emoji'(el, text) {
- return el.getAttribute('alt');
- },
- 'gl-emoji'(el, text) {
- return `:${el.getAttribute('data-name')}:`;
- },
- },
- ImageLinkFilter: {
- 'a.no-attachment-icon'(el, text) {
- return text;
- },
- },
- VideoLinkFilter: {
- '.video-container'(el, text) {
- const videoEl = el.querySelector('video');
- if (!videoEl) return false;
-
- return CopyAsGFM.nodeToGFM(videoEl);
- },
- 'video'(el, text) {
- return `![${el.dataset.title}](${el.getAttribute('src')})`;
- },
- },
- MathFilter: {
- 'pre.code.math[data-math-style=display]'(el, text) {
- return `\`\`\`math\n${text.trim()}\n\`\`\``;
- },
- 'code.code.math[data-math-style=inline]'(el, text) {
- return `$\`${text}\`$`;
- },
- 'span.katex-display span.katex-mathml'(el, text) {
- const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
- if (!mathAnnotation) return false;
-
- return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
- },
- 'span.katex-mathml'(el, text) {
- const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
- if (!mathAnnotation) return false;
-
- return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
- },
- 'span.katex-html'(el, text) {
- // We don't want to include the content of this element in the copied text.
- return '';
- },
- 'annotation[encoding="application/x-tex"]'(el, text) {
- return text.trim();
- },
- },
- SanitizationFilter: {
- 'a[name]:not([href]):empty'(el, text) {
- return el.outerHTML;
- },
- 'dl'(el, text) {
- let lines = text.trim().split('\n');
- // Add two spaces to the front of subsequent list items lines,
- // or leave the line entirely blank.
- lines = lines.map((l) => {
+const gfmRules = {
+ // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
+ // GitLab Flavored Markdown (GFM) to HTML.
+ // These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
+ // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
+ // from GFM should have a handler here, in reverse order.
+ // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
+ InlineDiffFilter: {
+ 'span.idiff.addition'(el, text) {
+ return `{+${text}+}`;
+ },
+ 'span.idiff.deletion'(el, text) {
+ return `{-${text}-}`;
+ },
+ },
+ TaskListFilter: {
+ 'input[type=checkbox].task-list-item-checkbox'(el, text) {
+ return `[${el.checked ? 'x' : ' '}]`;
+ },
+ },
+ ReferenceFilter: {
+ '.tooltip'(el, text) {
+ return '';
+ },
+ 'a.gfm:not([data-link=true])'(el, text) {
+ return el.dataset.original || text;
+ },
+ },
+ AutolinkFilter: {
+ 'a'(el, text) {
+ // Fallback on the regular MarkdownFilter's `a` handler.
+ if (text !== el.getAttribute('href')) return false;
+
+ return text;
+ },
+ },
+ TableOfContentsFilter: {
+ 'ul.section-nav'(el, text) {
+ return '[[_TOC_]]';
+ },
+ },
+ EmojiFilter: {
+ 'img.emoji'(el, text) {
+ return el.getAttribute('alt');
+ },
+ 'gl-emoji'(el, text) {
+ return `:${el.getAttribute('data-name')}:`;
+ },
+ },
+ ImageLinkFilter: {
+ 'a.no-attachment-icon'(el, text) {
+ return text;
+ },
+ },
+ VideoLinkFilter: {
+ '.video-container'(el, text) {
+ const videoEl = el.querySelector('video');
+ if (!videoEl) return false;
+
+ return CopyAsGFM.nodeToGFM(videoEl);
+ },
+ 'video'(el, text) {
+ return `![${el.dataset.title}](${el.getAttribute('src')})`;
+ },
+ },
+ MathFilter: {
+ 'pre.code.math[data-math-style=display]'(el, text) {
+ return `\`\`\`math\n${text.trim()}\n\`\`\``;
+ },
+ 'code.code.math[data-math-style=inline]'(el, text) {
+ return `$\`${text}\`$`;
+ },
+ 'span.katex-display span.katex-mathml'(el, text) {
+ const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
+ if (!mathAnnotation) return false;
+
+ return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
+ },
+ 'span.katex-mathml'(el, text) {
+ const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
+ if (!mathAnnotation) return false;
+
+ return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
+ },
+ 'span.katex-html'(el, text) {
+ // We don't want to include the content of this element in the copied text.
+ return '';
+ },
+ 'annotation[encoding="application/x-tex"]'(el, text) {
+ return text.trim();
+ },
+ },
+ SanitizationFilter: {
+ 'a[name]:not([href]):empty'(el, text) {
+ return el.outerHTML;
+ },
+ 'dl'(el, text) {
+ let lines = text.trim().split('\n');
+ // Add two spaces to the front of subsequent list items lines,
+ // or leave the line entirely blank.
+ lines = lines.map((l) => {
+ const line = l.trim();
+ if (line.length === 0) return '';
+
+ return ` ${line}`;
+ });
+
+ return `<dl>\n${lines.join('\n')}\n</dl>`;
+ },
+ 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) {
+ const tag = el.nodeName.toLowerCase();
+ return `<${tag}>${text}</${tag}>`;
+ },
+ },
+ SyntaxHighlightFilter: {
+ 'pre.code.highlight'(el, t) {
+ const text = t.trim();
+
+ let lang = el.getAttribute('lang');
+ if (lang === 'plaintext') {
+ lang = '';
+ }
+
+ // Prefixes lines with 4 spaces if the code contains triple backticks
+ if (lang === '' && text.match(/^```/gm)) {
+ return text.split('\n').map((l) => {
const line = l.trim();
if (line.length === 0) return '';
- return ` ${line}`;
- });
+ return ` ${line}`;
+ }).join('\n');
+ }
- return `<dl>\n${lines.join('\n')}\n</dl>`;
- },
- 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) {
- const tag = el.nodeName.toLowerCase();
- return `<${tag}>${text}</${tag}>`;
- },
+ return `\`\`\`${lang}\n${text}\n\`\`\``;
+ },
+ 'pre > code'(el, text) {
+ // Don't wrap code blocks in ``
+ return text;
+ },
+ },
+ MarkdownFilter: {
+ 'br'(el, text) {
+ // Two spaces at the end of a line are turned into a BR
+ return ' ';
},
- SyntaxHighlightFilter: {
- 'pre.code.highlight'(el, t) {
- const text = t.trim();
+ 'code'(el, text) {
+ let backtickCount = 1;
+ const backtickMatch = text.match(/`+/);
+ if (backtickMatch) {
+ backtickCount = backtickMatch[0].length + 1;
+ }
- let lang = el.getAttribute('lang');
- if (lang === 'plaintext') {
- lang = '';
- }
+ const backticks = Array(backtickCount + 1).join('`');
+ const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
- // Prefixes lines with 4 spaces if the code contains triple backticks
- if (lang === '' && text.match(/^```/gm)) {
- return text.split('\n').map((l) => {
- const line = l.trim();
- if (line.length === 0) return '';
+ return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks;
+ },
+ 'blockquote'(el, text) {
+ return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
+ },
+ 'img'(el, text) {
+ return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
+ },
+ 'a.anchor'(el, text) {
+ // Don't render a Markdown link for the anchor link inside a heading
+ return text;
+ },
+ 'a'(el, text) {
+ return `[${text}](${el.getAttribute('href')})`;
+ },
+ 'li'(el, text) {
+ const lines = text.trim().split('\n');
+ const firstLine = `- ${lines.shift()}`;
+ // Add four spaces to the front of subsequent list items lines,
+ // or leave the line entirely blank.
+ const nextLines = lines.map((s) => {
+ if (s.trim().length === 0) return '';
+
+ return ` ${s}`;
+ });
+
+ return `${firstLine}\n${nextLines.join('\n')}`;
+ },
+ 'ul'(el, text) {
+ return text;
+ },
+ 'ol'(el, text) {
+ // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
+ return text.replace(/^- /mg, '1. ');
+ },
+ 'h1'(el, text) {
+ return `# ${text.trim()}`;
+ },
+ 'h2'(el, text) {
+ return `## ${text.trim()}`;
+ },
+ 'h3'(el, text) {
+ return `### ${text.trim()}`;
+ },
+ 'h4'(el, text) {
+ return `#### ${text.trim()}`;
+ },
+ 'h5'(el, text) {
+ return `##### ${text.trim()}`;
+ },
+ 'h6'(el, text) {
+ return `###### ${text.trim()}`;
+ },
+ 'strong'(el, text) {
+ return `**${text}**`;
+ },
+ 'em'(el, text) {
+ return `_${text}_`;
+ },
+ 'del'(el, text) {
+ return `~~${text}~~`;
+ },
+ 'sup'(el, text) {
+ return `^${text}`;
+ },
+ 'hr'(el, text) {
+ return '-----';
+ },
+ 'table'(el, text) {
+ const theadEl = el.querySelector('thead');
+ const tbodyEl = el.querySelector('tbody');
+ if (!theadEl || !tbodyEl) return false;
- return ` ${line}`;
- }).join('\n');
- }
+ const theadText = CopyAsGFM.nodeToGFM(theadEl);
+ const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
- return `\`\`\`${lang}\n${text}\n\`\`\``;
- },
- 'pre > code'(el, text) {
- // Don't wrap code blocks in ``
- return text;
- },
- },
- MarkdownFilter: {
- 'br'(el, text) {
- // Two spaces at the end of a line are turned into a BR
- return ' ';
- },
- 'code'(el, text) {
- let backtickCount = 1;
- const backtickMatch = text.match(/`+/);
- if (backtickMatch) {
- backtickCount = backtickMatch[0].length + 1;
+ return theadText + tbodyText;
+ },
+ 'thead'(el, text) {
+ const cells = _.map(el.querySelectorAll('th'), (cell) => {
+ let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2;
+
+ let before = '';
+ let after = '';
+ switch (cell.style.textAlign) {
+ case 'center':
+ before = ':';
+ after = ':';
+ chars -= 2;
+ break;
+ case 'right':
+ after = ':';
+ chars -= 1;
+ break;
+ default:
+ break;
}
- const backticks = Array(backtickCount + 1).join('`');
- const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
-
- return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks;
- },
- 'blockquote'(el, text) {
- return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
- },
- 'img'(el, text) {
- return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
- },
- 'a.anchor'(el, text) {
- // Don't render a Markdown link for the anchor link inside a heading
- return text;
- },
- 'a'(el, text) {
- return `[${text}](${el.getAttribute('href')})`;
- },
- 'li'(el, text) {
- const lines = text.trim().split('\n');
- const firstLine = `- ${lines.shift()}`;
- // Add four spaces to the front of subsequent list items lines,
- // or leave the line entirely blank.
- const nextLines = lines.map((s) => {
- if (s.trim().length === 0) return '';
-
- return ` ${s}`;
- });
-
- return `${firstLine}\n${nextLines.join('\n')}`;
- },
- 'ul'(el, text) {
- return text;
- },
- 'ol'(el, text) {
- // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
- return text.replace(/^- /mg, '1. ');
- },
- 'h1'(el, text) {
- return `# ${text.trim()}`;
- },
- 'h2'(el, text) {
- return `## ${text.trim()}`;
- },
- 'h3'(el, text) {
- return `### ${text.trim()}`;
- },
- 'h4'(el, text) {
- return `#### ${text.trim()}`;
- },
- 'h5'(el, text) {
- return `##### ${text.trim()}`;
- },
- 'h6'(el, text) {
- return `###### ${text.trim()}`;
- },
- 'strong'(el, text) {
- return `**${text}**`;
- },
- 'em'(el, text) {
- return `_${text}_`;
- },
- 'del'(el, text) {
- return `~~${text}~~`;
- },
- 'sup'(el, text) {
- return `^${text}`;
- },
- 'hr'(el, text) {
- return '-----';
- },
- 'table'(el, text) {
- const theadEl = el.querySelector('thead');
- const tbodyEl = el.querySelector('tbody');
- if (!theadEl || !tbodyEl) return false;
-
- const theadText = CopyAsGFM.nodeToGFM(theadEl);
- const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
-
- return theadText + tbodyText;
- },
- 'thead'(el, text) {
- const cells = _.map(el.querySelectorAll('th'), (cell) => {
- let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2;
-
- let before = '';
- let after = '';
- switch (cell.style.textAlign) {
- case 'center':
- before = ':';
- after = ':';
- chars -= 2;
- break;
- case 'right':
- after = ':';
- chars -= 1;
- break;
- default:
- break;
- }
-
- chars = Math.max(chars, 3);
-
- const middle = Array(chars + 1).join('-');
-
- return before + middle + after;
- });
-
- return `${text}|${cells.join('|')}|`;
- },
- 'tr'(el, text) {
- const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim());
- return `| ${cells.join(' | ')} |`;
- },
- },
- };
-
- class CopyAsGFM {
- constructor() {
- $(document).on('copy', '.md, .wiki', this.handleCopy);
- $(document).on('paste', '.js-gfm-input', this.handlePaste);
- }
-
- handleCopy(e) {
- const clipboardData = e.originalEvent.clipboardData;
- if (!clipboardData) return;
-
- const documentFragment = window.gl.utils.getSelectedFragment();
- if (!documentFragment) return;
+ chars = Math.max(chars, 3);
- // If the documentFragment contains more than just Markdown, don't copy as GFM.
- if (documentFragment.querySelector('.md, .wiki')) return;
+ const middle = Array(chars + 1).join('-');
- e.preventDefault();
- clipboardData.setData('text/plain', documentFragment.textContent);
+ return before + middle + after;
+ });
- const gfm = CopyAsGFM.nodeToGFM(documentFragment);
- clipboardData.setData('text/x-gfm', gfm);
- }
+ return `${text}|${cells.join('|')}|`;
+ },
+ 'tr'(el, text) {
+ const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim());
+ return `| ${cells.join(' | ')} |`;
+ },
+ },
+};
- handlePaste(e) {
- const clipboardData = e.originalEvent.clipboardData;
- if (!clipboardData) return;
+class CopyAsGFM {
+ constructor() {
+ $(document).on('copy', '.md, .wiki', this.handleCopy);
+ $(document).on('paste', '.js-gfm-input', this.handlePaste);
+ }
- const gfm = clipboardData.getData('text/x-gfm');
- if (!gfm) return;
+ handleCopy(e) {
+ const clipboardData = e.originalEvent.clipboardData;
+ if (!clipboardData) return;
- e.preventDefault();
+ const documentFragment = window.gl.utils.getSelectedFragment();
+ if (!documentFragment) return;
- window.gl.utils.insertText(e.target, gfm);
- }
+ // If the documentFragment contains more than just Markdown, don't copy as GFM.
+ if (documentFragment.querySelector('.md, .wiki')) return;
- static nodeToGFM(node) {
- if (node.nodeType === Node.TEXT_NODE) {
- return node.textContent;
- }
+ e.preventDefault();
+ clipboardData.setData('text/plain', documentFragment.textContent);
- const text = this.innerGFM(node);
+ const gfm = CopyAsGFM.nodeToGFM(documentFragment);
+ clipboardData.setData('text/x-gfm', gfm);
+ }
- if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
- return text;
- }
+ handlePaste(e) {
+ const clipboardData = e.originalEvent.clipboardData;
+ if (!clipboardData) return;
- for (const filter in gfmRules) {
- const rules = gfmRules[filter];
+ const gfm = clipboardData.getData('text/x-gfm');
+ if (!gfm) return;
- for (const selector in rules) {
- const func = rules[selector];
+ e.preventDefault();
- if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue;
+ window.gl.utils.insertText(e.target, gfm);
+ }
- const result = func(node, text);
- if (result === false) continue;
+ static nodeToGFM(node) {
+ if (node.nodeType === Node.TEXT_NODE) {
+ return node.textContent;
+ }
- return result;
- }
- }
+ const text = this.innerGFM(node);
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
return text;
}
- static innerGFM(parentNode) {
- const nodes = parentNode.childNodes;
+ for (const filter in gfmRules) {
+ const rules = gfmRules[filter];
- const clonedParentNode = parentNode.cloneNode(true);
- const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
+ for (const selector in rules) {
+ const func = rules[selector];
- for (let i = 0; i < nodes.length; i += 1) {
- const node = nodes[i];
- const clonedNode = clonedNodes[i];
+ if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue;
- const text = this.nodeToGFM(node);
+ const result = func(node, text);
+ if (result === false) continue;
- // `clonedNode.replaceWith(text)` is not yet widely supported
- clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
+ return result;
}
+ }
+
+ return text;
+ }
- return clonedParentNode.innerText || clonedParentNode.textContent;
+ static innerGFM(parentNode) {
+ const nodes = parentNode.childNodes;
+
+ const clonedParentNode = parentNode.cloneNode(true);
+ const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
+
+ for (let i = 0; i < nodes.length; i += 1) {
+ const node = nodes[i];
+ const clonedNode = clonedNodes[i];
+
+ const text = this.nodeToGFM(node);
+
+ // `clonedNode.replaceWith(text)` is not yet widely supported
+ clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
}
+
+ return clonedParentNode.innerText || clonedParentNode.textContent;
}
+}
- window.gl = window.gl || {};
- window.gl.CopyAsGFM = CopyAsGFM;
+window.gl = window.gl || {};
+window.gl.CopyAsGFM = CopyAsGFM;
- new CopyAsGFM();
-})();
+new CopyAsGFM();
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 615f485e18a..6dbec50b890 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,49 +1,46 @@
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
-/* global Clipboard */
-
-window.Clipboard = require('vendor/clipboard');
-
-(function() {
- var genericError, genericSuccess, showTooltip;
-
- genericSuccess = function(e) {
- showTooltip(e.trigger, 'Copied');
- // Clear the selection and blur the trigger so it loses its border
- e.clearSelection();
- return $(e.trigger).blur();
- };
-
- // Safari doesn't support `execCommand`, so instead we inform the user to
- // copy manually.
- //
- // See http://clipboardjs.com/#browser-support
- genericError = function(e) {
- var key;
- if (/Mac/i.test(navigator.userAgent)) {
- key = '&#8984;'; // Command
- } else {
- key = 'Ctrl';
- }
- return showTooltip(e.trigger, "Press " + key + "-C to copy");
- };
-
- showTooltip = function(target, title) {
- var $target = $(target);
- var originalTitle = $target.data('original-title');
-
- $target
- .attr('title', 'Copied')
- .tooltip('fixTitle')
- .tooltip('show')
- .attr('title', originalTitle)
- .tooltip('fixTitle');
- };
-
- $(function() {
- var clipboard;
-
- clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
- clipboard.on('success', genericSuccess);
- return clipboard.on('error', genericError);
- });
-}).call(window);
+
+import Clipboard from 'vendor/clipboard';
+
+var genericError, genericSuccess, showTooltip;
+
+genericSuccess = function(e) {
+ showTooltip(e.trigger, 'Copied');
+ // Clear the selection and blur the trigger so it loses its border
+ e.clearSelection();
+ return $(e.trigger).blur();
+};
+
+// Safari doesn't support `execCommand`, so instead we inform the user to
+// copy manually.
+//
+// See http://clipboardjs.com/#browser-support
+genericError = function(e) {
+ var key;
+ if (/Mac/i.test(navigator.userAgent)) {
+ key = '&#8984;'; // Command
+ } else {
+ key = 'Ctrl';
+ }
+ return showTooltip(e.trigger, "Press " + key + "-C to copy");
+};
+
+showTooltip = function(target, title) {
+ var $target = $(target);
+ var originalTitle = $target.data('original-title');
+
+ $target
+ .attr('title', 'Copied')
+ .tooltip('fixTitle')
+ .tooltip('show')
+ .attr('title', originalTitle)
+ .tooltip('fixTitle');
+};
+
+$(function() {
+ var clipboard;
+
+ clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
+ clipboard.on('success', genericSuccess);
+ return clipboard.on('error', genericError);
+});
diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js
index 85384d98126..121d64db789 100644
--- a/app/assets/javascripts/create_label.js
+++ b/app/assets/javascripts/create_label.js
@@ -1,132 +1,127 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */
/* global Api */
-(function (w) {
- class CreateLabelDropdown {
- constructor ($el, namespacePath, projectPath) {
- this.$el = $el;
- this.namespacePath = namespacePath;
- this.projectPath = projectPath;
- this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
- this.$cancelButton = $('.js-cancel-label-btn', this.$el);
- this.$newLabelField = $('#new_label_name', this.$el);
- this.$newColorField = $('#new_label_color', this.$el);
- this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
- this.$newLabelError = $('.js-label-error', this.$el);
- this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
- this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
-
- this.$newLabelError.hide();
- this.$newLabelCreateButton.disable();
+class CreateLabelDropdown {
+ constructor ($el, namespacePath, projectPath) {
+ this.$el = $el;
+ this.namespacePath = namespacePath;
+ this.projectPath = projectPath;
+ this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
+ this.$cancelButton = $('.js-cancel-label-btn', this.$el);
+ this.$newLabelField = $('#new_label_name', this.$el);
+ this.$newColorField = $('#new_label_color', this.$el);
+ this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
+ this.$newLabelError = $('.js-label-error', this.$el);
+ this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
+ this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
+
+ this.$newLabelError.hide();
+ this.$newLabelCreateButton.disable();
+
+ this.cleanBinding();
+ this.addBinding();
+ }
- this.cleanBinding();
- this.addBinding();
- }
+ cleanBinding () {
+ this.$colorSuggestions.off('click');
+ this.$newLabelField.off('keyup change');
+ this.$newColorField.off('keyup change');
+ this.$dropdownBack.off('click');
+ this.$cancelButton.off('click');
+ this.$newLabelCreateButton.off('click');
+ }
- cleanBinding () {
- this.$colorSuggestions.off('click');
- this.$newLabelField.off('keyup change');
- this.$newColorField.off('keyup change');
- this.$dropdownBack.off('click');
- this.$cancelButton.off('click');
- this.$newLabelCreateButton.off('click');
- }
+ addBinding () {
+ const self = this;
- addBinding () {
- const self = this;
+ this.$colorSuggestions.on('click', function (e) {
+ const $this = $(this);
+ self.addColorValue(e, $this);
+ });
- this.$colorSuggestions.on('click', function (e) {
- const $this = $(this);
- self.addColorValue(e, $this);
- });
+ this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this));
+ this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this));
- this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this));
- this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this));
+ this.$dropdownBack.on('click', this.resetForm.bind(this));
- this.$dropdownBack.on('click', this.resetForm.bind(this));
+ this.$cancelButton.on('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
- this.$cancelButton.on('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
+ self.resetForm();
+ self.$dropdownBack.trigger('click');
+ });
- self.resetForm();
- self.$dropdownBack.trigger('click');
- });
+ this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
+ }
- this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
- }
+ addColorValue (e, $this) {
+ e.preventDefault();
+ e.stopPropagation();
- addColorValue (e, $this) {
- e.preventDefault();
- e.stopPropagation();
+ this.$newColorField.val($this.data('color')).trigger('change');
+ this.$colorPreview
+ .css('background-color', $this.data('color'))
+ .parent()
+ .addClass('is-active');
+ }
- this.$newColorField.val($this.data('color')).trigger('change');
- this.$colorPreview
- .css('background-color', $this.data('color'))
- .parent()
- .addClass('is-active');
+ enableLabelCreateButton () {
+ if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
+ this.$newLabelError.hide();
+ this.$newLabelCreateButton.enable();
+ } else {
+ this.$newLabelCreateButton.disable();
}
+ }
- enableLabelCreateButton () {
- if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
- this.$newLabelError.hide();
- this.$newLabelCreateButton.enable();
- } else {
- this.$newLabelCreateButton.disable();
- }
- }
+ resetForm () {
+ this.$newLabelField
+ .val('')
+ .trigger('change');
- resetForm () {
- this.$newLabelField
- .val('')
- .trigger('change');
+ this.$newColorField
+ .val('')
+ .trigger('change');
- this.$newColorField
- .val('')
- .trigger('change');
+ this.$colorPreview
+ .css('background-color', '')
+ .parent()
+ .removeClass('is-active');
+ }
- this.$colorPreview
- .css('background-color', '')
- .parent()
- .removeClass('is-active');
- }
+ saveLabel (e) {
+ e.preventDefault();
+ e.stopPropagation();
- saveLabel (e) {
- e.preventDefault();
- e.stopPropagation();
+ Api.newLabel(this.namespacePath, this.projectPath, {
+ title: this.$newLabelField.val(),
+ color: this.$newColorField.val()
+ }, (label) => {
+ this.$newLabelCreateButton.enable();
- Api.newLabel(this.namespacePath, this.projectPath, {
- title: this.$newLabelField.val(),
- color: this.$newColorField.val()
- }, (label) => {
- this.$newLabelCreateButton.enable();
-
- if (label.message) {
- let errors;
-
- if (typeof label.message === 'string') {
- errors = label.message;
- } else {
- errors = Object.keys(label.message).map(key =>
- `${gl.text.humanize(key)} ${label.message[key].join(', ')}`
- ).join("<br/>");
- }
-
- this.$newLabelError
- .html(errors)
- .show();
- } else {
- this.$dropdownBack.trigger('click');
+ if (label.message) {
+ let errors;
- $(document).trigger('created.label', label);
+ if (typeof label.message === 'string') {
+ errors = label.message;
+ } else {
+ errors = Object.keys(label.message).map(key =>
+ `${gl.text.humanize(key)} ${label.message[key].join(', ')}`
+ ).join("<br/>");
}
- });
- }
- }
- if (!w.gl) {
- w.gl = {};
+ this.$newLabelError
+ .html(errors)
+ .show();
+ } else {
+ this.$dropdownBack.trigger('click');
+
+ $(document).trigger('created.label', label);
+ }
+ });
}
+}
- gl.CreateLabelDropdown = CreateLabelDropdown;
-})(window);
+window.gl = window.gl || {};
+gl.CreateLabelDropdown = CreateLabelDropdown;
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 6829e8aeaea..cfa60325fcc 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -2,129 +2,127 @@
require('./lib/utils/url_utility');
-(() => {
- const UNFOLD_COUNT = 20;
- let isBound = false;
+const UNFOLD_COUNT = 20;
+let isBound = false;
- class Diff {
- constructor() {
- const $diffFile = $('.files .diff-file');
- $diffFile.singleFileDiff();
- $diffFile.filesCommentButton();
+class Diff {
+ constructor() {
+ const $diffFile = $('.files .diff-file');
+ $diffFile.singleFileDiff();
+ $diffFile.filesCommentButton();
- $diffFile.each((index, file) => new gl.ImageFile(file));
+ $diffFile.each((index, file) => new gl.ImageFile(file));
- if (this.diffViewType() === 'parallel') {
- $('.content-wrapper .container-fluid').removeClass('container-limited');
- }
-
- if (!isBound) {
- $(document)
- .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
- .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
- isBound = true;
- }
+ if (this.diffViewType() === 'parallel') {
+ $('.content-wrapper .container-fluid').removeClass('container-limited');
+ }
- if (gl.utils.getLocationHash()) {
- this.highlightSelectedLine();
- }
+ if (!isBound) {
+ $(document)
+ .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
+ .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
+ isBound = true;
+ }
- this.openAnchoredDiff();
+ if (gl.utils.getLocationHash()) {
+ this.highlightSelectedLine();
}
- handleClickUnfold(e) {
- const $target = $(e.target);
- // current babel config relies on iterators implementation, so we cannot simply do:
- // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
- const ref = this.lineNumbers($target.parent());
- const oldLineNumber = ref[0];
- const newLineNumber = ref[1];
- const offset = newLineNumber - oldLineNumber;
- const bottom = $target.hasClass('js-unfold-bottom');
- let since;
- let to;
- let unfold = true;
-
- if (bottom) {
- const lineNumber = newLineNumber + 1;
- since = lineNumber;
- to = lineNumber + UNFOLD_COUNT;
- } else {
- const lineNumber = newLineNumber - 1;
- since = lineNumber - UNFOLD_COUNT;
- to = lineNumber;
-
- // make sure we aren't loading more than we need
- const prevNewLine = this.lineNumbers($target.parent().prev())[1];
- if (since <= prevNewLine + 1) {
- since = prevNewLine + 1;
- unfold = false;
- }
+ this.openAnchoredDiff();
+ }
+
+ handleClickUnfold(e) {
+ const $target = $(e.target);
+ // current babel config relies on iterators implementation, so we cannot simply do:
+ // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
+ const ref = this.lineNumbers($target.parent());
+ const oldLineNumber = ref[0];
+ const newLineNumber = ref[1];
+ const offset = newLineNumber - oldLineNumber;
+ const bottom = $target.hasClass('js-unfold-bottom');
+ let since;
+ let to;
+ let unfold = true;
+
+ if (bottom) {
+ const lineNumber = newLineNumber + 1;
+ since = lineNumber;
+ to = lineNumber + UNFOLD_COUNT;
+ } else {
+ const lineNumber = newLineNumber - 1;
+ since = lineNumber - UNFOLD_COUNT;
+ to = lineNumber;
+
+ // make sure we aren't loading more than we need
+ const prevNewLine = this.lineNumbers($target.parent().prev())[1];
+ if (since <= prevNewLine + 1) {
+ since = prevNewLine + 1;
+ unfold = false;
}
+ }
- const file = $target.parents('.diff-file');
- const link = file.data('blob-diff-path');
- const view = file.data('view');
+ const file = $target.parents('.diff-file');
+ const link = file.data('blob-diff-path');
+ const view = file.data('view');
- const params = { since, to, bottom, offset, unfold, view };
- $.get(link, params, response => $target.parent().replaceWith(response));
- }
+ const params = { since, to, bottom, offset, unfold, view };
+ $.get(link, params, response => $target.parent().replaceWith(response));
+ }
- openAnchoredDiff(cb) {
- const locationHash = gl.utils.getLocationHash();
- const anchoredDiff = locationHash && locationHash.split('_')[0];
-
- if (!anchoredDiff) return;
-
- const diffTitle = $(`#${anchoredDiff}`);
- const diffFile = diffTitle.closest('.diff-file');
- const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
- if (nothingHereBlock.length) {
- const clickTarget = $('.js-file-title, .click-to-expand', diffFile);
- diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => {
- this.highlightSelectedLine();
- if (cb) cb();
- });
- } else if (cb) {
- cb();
- }
- }
+ openAnchoredDiff(cb) {
+ const locationHash = gl.utils.getLocationHash();
+ const anchoredDiff = locationHash && locationHash.split('_')[0];
- handleClickLineNum(e) {
- const hash = $(e.currentTarget).attr('href');
- e.preventDefault();
- if (window.history.pushState) {
- window.history.pushState(null, null, hash);
- } else {
- window.location.hash = hash;
- }
- this.highlightSelectedLine();
+ if (!anchoredDiff) return;
+
+ const diffTitle = $(`#${anchoredDiff}`);
+ const diffFile = diffTitle.closest('.diff-file');
+ const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
+ if (nothingHereBlock.length) {
+ const clickTarget = $('.js-file-title, .click-to-expand', diffFile);
+ diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => {
+ this.highlightSelectedLine();
+ if (cb) cb();
+ });
+ } else if (cb) {
+ cb();
}
+ }
- diffViewType() {
- return $('.inline-parallel-buttons a.active').data('view-type');
+ handleClickLineNum(e) {
+ const hash = $(e.currentTarget).attr('href');
+ e.preventDefault();
+ if (window.history.pushState) {
+ window.history.pushState(null, null, hash);
+ } else {
+ window.location.hash = hash;
}
+ this.highlightSelectedLine();
+ }
- lineNumbers(line) {
- if (!line.children().length) {
- return [0, 0];
- }
- return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10));
+ diffViewType() {
+ return $('.inline-parallel-buttons a.active').data('view-type');
+ }
+
+ lineNumbers(line) {
+ if (!line.children().length) {
+ return [0, 0];
}
+ return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10));
+ }
- highlightSelectedLine() {
- const hash = gl.utils.getLocationHash();
- const $diffFiles = $('.diff-file');
- $diffFiles.find('.hll').removeClass('hll');
+ highlightSelectedLine() {
+ const hash = gl.utils.getLocationHash();
+ const $diffFiles = $('.diff-file');
+ $diffFiles.find('.hll').removeClass('hll');
- if (hash) {
- $diffFiles
- .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`)
- .addClass('hll');
- }
+ if (hash) {
+ $diffFiles
+ .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`)
+ .addClass('hll');
}
}
+}
- window.gl = window.gl || {};
- window.gl.Diff = Diff;
-})();
+window.gl = window.gl || {};
+window.gl.Diff = Diff;
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
new file mode 100644
index 00000000000..e86bef47172
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
@@ -0,0 +1,29 @@
+/* global Vue */
+/* global CommentsStore */
+
+(() => {
+ const NewIssueForDiscussion = Vue.extend({
+ props: {
+ discussionId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ discussions: CommentsStore.state,
+ };
+ },
+ computed: {
+ discussion() {
+ return this.discussions[this.discussionId];
+ },
+ showButton() {
+ if (this.discussion) return !this.discussion.isResolved();
+ return false;
+ },
+ },
+ });
+
+ Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
+})();
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index 7d8316dfd63..4f6b86a917c 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -14,10 +14,11 @@ require('./components/resolve_btn');
require('./components/resolve_count');
require('./components/resolve_discussion_btn');
require('./components/diff_note_avatars');
+require('./components/new_issue_for_discussion');
$(() => {
const projectPath = document.querySelector('.merge-request').dataset.projectPath;
- const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn';
+ const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
window.gl = window.gl || {};
window.gl.diffNoteApps = {};
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 7b9b9123c31..6d8174e199e 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -40,6 +40,7 @@ import BindInOut from './behaviors/bind_in_out';
import GroupsList from './groups_list';
import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
+import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
const ShortcutsBlob = require('./shortcuts_blob');
const UserCallout = require('./user_callout');
@@ -59,13 +60,32 @@ const UserCallout = require('./user_callout');
}
Dispatcher.prototype.initPageScripts = function() {
- var page, path, shortcut_handler;
+ var page, path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
page = $('body').attr('data-page');
if (!page) {
return false;
}
path = page.split(':');
shortcut_handler = null;
+
+ function initBlob() {
+ new LineHighlighter();
+
+ new BlobLinePermalinkUpdater(
+ document.querySelector('#blob-content-holder'),
+ '.diff-line-num[data-line-number]',
+ document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
+ );
+
+ shortcut_handler = new ShortcutsNavigation();
+ fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
+ fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+ new ShortcutsBlob({
+ skipResetBindings: true,
+ fileBlobPermalinkUrl,
+ });
+ }
+
switch (page) {
case 'sessions:new':
new UsernameValidator();
@@ -245,20 +265,26 @@ const UserCallout = require('./user_callout');
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
new TreeView();
+ gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:find_file:show':
shortcut_handler = true;
break;
+ case 'projects:blob:new':
+ gl.TargetBranchDropDown.bootstrap();
+ break;
+ case 'projects:blob:create':
+ gl.TargetBranchDropDown.bootstrap();
+ break;
case 'projects:blob:show':
+ gl.TargetBranchDropDown.bootstrap();
+ initBlob();
+ break;
+ case 'projects:blob:edit':
+ gl.TargetBranchDropDown.bootstrap();
+ break;
case 'projects:blame:show':
- new LineHighlighter();
- shortcut_handler = new ShortcutsNavigation();
- const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
- const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
- new ShortcutsBlob({
- skipResetBindings: true,
- fileBlobPermalinkUrl,
- });
+ initBlob();
break;
case 'groups:labels:new':
case 'groups:labels:edit':
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index f61be741b4a..020f8b4ac65 100644
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -74,6 +74,9 @@ require('../window')(function(w){
this._loadUrlData(config.endpoint)
.then(function(d) {
self._loadData(d, config, self);
+ }, function(xhrError) {
+ // TODO: properly handle errors due to XHR cancellation
+ return;
}).catch(function(e) {
throw new droplabAjaxException(e.message || e);
});
diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js
index b63d73066cb..05eba7aef56 100644
--- a/app/assets/javascripts/droplab/droplab_ajax_filter.js
+++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js
@@ -82,6 +82,9 @@ require('../window')(function(w){
this._loadUrlData(url)
.then(function(data) {
self._loadData(data, config, self);
+ }, function(xhrError) {
+ // TODO: properly handle errors due to XHR cancellation
+ return;
});
}
},
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 646f836aff0..f2963a5eb19 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -3,218 +3,216 @@
require('./preview_markdown');
-(function() {
- this.DropzoneInput = (function() {
- function DropzoneInput(form) {
- var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress;
- Dropzone.autoDiscover = false;
- alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
- alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
- divHover = "<div class=\"div-dropzone-hover\"></div>";
- divSpinner = "<div class=\"div-dropzone-spinner\"></div>";
- divAlert = "<div class=\"" + alertClass + "\"></div>";
- iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>";
- iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
- uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
- btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
- project_uploads_path = window.project_uploads_path || null;
- max_file_size = gon.max_file_size || 10;
- form_textarea = $(form).find(".js-gfm-input");
- form_textarea.wrap("<div class=\"div-dropzone\"></div>");
- form_textarea.on('paste', (function(_this) {
- return function(event) {
- return handlePaste(event);
- };
- })(this));
- $mdArea = $(form_textarea).closest('.md-area');
- $(form).setupMarkdownPreview();
- form_dropzone = $(form).find('.div-dropzone');
- form_dropzone.parent().addClass("div-dropzone-wrapper");
- form_dropzone.append(divHover);
- form_dropzone.find(".div-dropzone-hover").append(iconPaperclip);
- form_dropzone.append(divSpinner);
- form_dropzone.find(".div-dropzone-spinner").append(iconSpinner);
- form_dropzone.find(".div-dropzone-spinner").append(uploadProgress);
- form_dropzone.find(".div-dropzone-spinner").css({
- "opacity": 0,
- "display": "none"
- });
- dropzone = form_dropzone.dropzone({
- url: project_uploads_path,
- dictDefaultMessage: "",
- clickable: true,
- paramName: "file",
- maxFilesize: max_file_size,
- uploadMultiple: false,
- headers: {
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
- },
- previewContainer: false,
- processing: function() {
- return $(".div-dropzone-alert").alert("close");
- },
- dragover: function() {
- $mdArea.addClass('is-dropzone-hover');
- form.find(".div-dropzone-hover").css("opacity", 0.7);
- },
- dragleave: function() {
- $mdArea.removeClass('is-dropzone-hover');
- form.find(".div-dropzone-hover").css("opacity", 0);
- },
- drop: function() {
- $mdArea.removeClass('is-dropzone-hover');
- form.find(".div-dropzone-hover").css("opacity", 0);
- form_textarea.focus();
- },
- success: function(header, response) {
- pasteText(response.link.markdown);
- },
- error: function(temp) {
- var checkIfMsgExists, errorAlert;
- errorAlert = $(form).find('.error-alert');
- checkIfMsgExists = errorAlert.children().length;
- if (checkIfMsgExists === 0) {
- errorAlert.append(divAlert);
- $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed.");
- }
- },
- totaluploadprogress: function(totalUploadProgress) {
- uploadProgress.text(Math.round(totalUploadProgress) + "%");
- },
- sending: function() {
- form_dropzone.find(".div-dropzone-spinner").css({
- "opacity": 0.7,
- "display": "inherit"
- });
- },
- queuecomplete: function() {
- uploadProgress.text("");
- $(".dz-preview").remove();
- $(".markdown-area").trigger("input");
- $(".div-dropzone-spinner").css({
- "opacity": 0,
- "display": "none"
- });
- }
- });
- child = $(dropzone[0]).children("textarea");
- handlePaste = function(event) {
- var filename, image, pasteEvent, text;
- pasteEvent = event.originalEvent;
- if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
- image = isImage(pasteEvent);
- if (image) {
- event.preventDefault();
- filename = getFilename(pasteEvent) || "image.png";
- text = "{{" + filename + "}}";
- pasteText(text);
- return uploadFile(image.getAsFile(), filename);
- }
- }
+window.DropzoneInput = (function() {
+ function DropzoneInput(form) {
+ var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress;
+ Dropzone.autoDiscover = false;
+ alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
+ alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
+ divHover = "<div class=\"div-dropzone-hover\"></div>";
+ divSpinner = "<div class=\"div-dropzone-spinner\"></div>";
+ divAlert = "<div class=\"" + alertClass + "\"></div>";
+ iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>";
+ iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
+ uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
+ btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
+ project_uploads_path = window.project_uploads_path || null;
+ max_file_size = gon.max_file_size || 10;
+ form_textarea = $(form).find(".js-gfm-input");
+ form_textarea.wrap("<div class=\"div-dropzone\"></div>");
+ form_textarea.on('paste', (function(_this) {
+ return function(event) {
+ return handlePaste(event);
};
- isImage = function(data) {
- var i, item;
- i = 0;
- while (i < data.clipboardData.items.length) {
- item = data.clipboardData.items[i];
- if (item.type.indexOf("image") !== -1) {
- return item;
- }
- i += 1;
- }
- return false;
- };
- pasteText = function(text) {
- var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
- var formattedText = text + "\n\n";
- caretStart = $(child)[0].selectionStart;
- caretEnd = $(child)[0].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);
- return form_textarea.trigger("input");
- };
- getFilename = function(e) {
- var value;
- if (window.clipboardData && window.clipboardData.getData) {
- value = window.clipboardData.getData("Text");
- } else if (e.clipboardData && e.clipboardData.getData) {
- value = e.clipboardData.getData("text/plain");
+ })(this));
+ $mdArea = $(form_textarea).closest('.md-area');
+ $(form).setupMarkdownPreview();
+ form_dropzone = $(form).find('.div-dropzone');
+ form_dropzone.parent().addClass("div-dropzone-wrapper");
+ form_dropzone.append(divHover);
+ form_dropzone.find(".div-dropzone-hover").append(iconPaperclip);
+ form_dropzone.append(divSpinner);
+ form_dropzone.find(".div-dropzone-spinner").append(iconSpinner);
+ form_dropzone.find(".div-dropzone-spinner").append(uploadProgress);
+ form_dropzone.find(".div-dropzone-spinner").css({
+ "opacity": 0,
+ "display": "none"
+ });
+ dropzone = form_dropzone.dropzone({
+ url: project_uploads_path,
+ dictDefaultMessage: "",
+ clickable: true,
+ paramName: "file",
+ maxFilesize: max_file_size,
+ uploadMultiple: false,
+ headers: {
+ "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+ },
+ previewContainer: false,
+ processing: function() {
+ return $(".div-dropzone-alert").alert("close");
+ },
+ dragover: function() {
+ $mdArea.addClass('is-dropzone-hover');
+ form.find(".div-dropzone-hover").css("opacity", 0.7);
+ },
+ dragleave: function() {
+ $mdArea.removeClass('is-dropzone-hover');
+ form.find(".div-dropzone-hover").css("opacity", 0);
+ },
+ drop: function() {
+ $mdArea.removeClass('is-dropzone-hover');
+ form.find(".div-dropzone-hover").css("opacity", 0);
+ form_textarea.focus();
+ },
+ success: function(header, response) {
+ pasteText(response.link.markdown);
+ },
+ error: function(temp) {
+ var checkIfMsgExists, errorAlert;
+ errorAlert = $(form).find('.error-alert');
+ checkIfMsgExists = errorAlert.children().length;
+ if (checkIfMsgExists === 0) {
+ errorAlert.append(divAlert);
+ $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed.");
}
- value = value.split("\r");
- return value.first();
- };
- uploadFile = function(item, filename) {
- var formData;
- formData = new FormData();
- formData.append("file", item, filename);
- return $.ajax({
- url: project_uploads_path,
- type: "POST",
- data: formData,
- dataType: "json",
- processData: false,
- contentType: false,
- headers: {
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
- },
- beforeSend: function() {
- showSpinner();
- return closeAlertMessage();
- },
- success: function(e, textStatus, response) {
- return insertToTextArea(filename, response.responseJSON.link.markdown);
- },
- error: function(response) {
- return showError(response.responseJSON.message);
- },
- complete: function() {
- return closeSpinner();
- }
- });
- };
- insertToTextArea = function(filename, url) {
- return $(child).val(function(index, val) {
- return val.replace("{{" + filename + "}}", url + "\n");
- });
- };
- appendToTextArea = function(url) {
- return $(child).val(function(index, val) {
- return val + url + "\n";
- });
- };
- showSpinner = function(e) {
- return form.find(".div-dropzone-spinner").css({
+ },
+ totaluploadprogress: function(totalUploadProgress) {
+ uploadProgress.text(Math.round(totalUploadProgress) + "%");
+ },
+ sending: function() {
+ form_dropzone.find(".div-dropzone-spinner").css({
"opacity": 0.7,
"display": "inherit"
});
- };
- closeSpinner = function() {
- return form.find(".div-dropzone-spinner").css({
+ },
+ queuecomplete: function() {
+ uploadProgress.text("");
+ $(".dz-preview").remove();
+ $(".markdown-area").trigger("input");
+ $(".div-dropzone-spinner").css({
"opacity": 0,
"display": "none"
});
- };
- showError = function(message) {
- var checkIfMsgExists, errorAlert;
- errorAlert = $(form).find('.error-alert');
- checkIfMsgExists = errorAlert.children().length;
- if (checkIfMsgExists === 0) {
- errorAlert.append(divAlert);
- return $(".div-dropzone-alert").append(btnAlert + message);
+ }
+ });
+ child = $(dropzone[0]).children("textarea");
+ handlePaste = function(event) {
+ var filename, image, pasteEvent, text;
+ pasteEvent = event.originalEvent;
+ if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
+ image = isImage(pasteEvent);
+ if (image) {
+ event.preventDefault();
+ filename = getFilename(pasteEvent) || "image.png";
+ text = "{{" + filename + "}}";
+ pasteText(text);
+ return uploadFile(image.getAsFile(), filename);
}
- };
- closeAlertMessage = function() {
- return form.find(".div-dropzone-alert").alert("close");
- };
- form.find(".markdown-selector").click(function(e) {
- e.preventDefault();
- $(this).closest('.gfm-form').find('.div-dropzone').click();
+ }
+ };
+ isImage = function(data) {
+ var i, item;
+ i = 0;
+ while (i < data.clipboardData.items.length) {
+ item = data.clipboardData.items[i];
+ if (item.type.indexOf("image") !== -1) {
+ return item;
+ }
+ i += 1;
+ }
+ return false;
+ };
+ pasteText = function(text) {
+ var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
+ var formattedText = text + "\n\n";
+ caretStart = $(child)[0].selectionStart;
+ caretEnd = $(child)[0].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);
+ return form_textarea.trigger("input");
+ };
+ getFilename = function(e) {
+ var value;
+ if (window.clipboardData && window.clipboardData.getData) {
+ value = window.clipboardData.getData("Text");
+ } else if (e.clipboardData && e.clipboardData.getData) {
+ value = e.clipboardData.getData("text/plain");
+ }
+ value = value.split("\r");
+ return value.first();
+ };
+ uploadFile = function(item, filename) {
+ var formData;
+ formData = new FormData();
+ formData.append("file", item, filename);
+ return $.ajax({
+ url: project_uploads_path,
+ type: "POST",
+ data: formData,
+ dataType: "json",
+ processData: false,
+ contentType: false,
+ headers: {
+ "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+ },
+ beforeSend: function() {
+ showSpinner();
+ return closeAlertMessage();
+ },
+ success: function(e, textStatus, response) {
+ return insertToTextArea(filename, response.responseJSON.link.markdown);
+ },
+ error: function(response) {
+ return showError(response.responseJSON.message);
+ },
+ complete: function() {
+ return closeSpinner();
+ }
+ });
+ };
+ insertToTextArea = function(filename, url) {
+ return $(child).val(function(index, val) {
+ return val.replace("{{" + filename + "}}", url + "\n");
+ });
+ };
+ appendToTextArea = function(url) {
+ return $(child).val(function(index, val) {
+ return val + url + "\n";
+ });
+ };
+ showSpinner = function(e) {
+ return form.find(".div-dropzone-spinner").css({
+ "opacity": 0.7,
+ "display": "inherit"
+ });
+ };
+ closeSpinner = function() {
+ return form.find(".div-dropzone-spinner").css({
+ "opacity": 0,
+ "display": "none"
});
- }
+ };
+ showError = function(message) {
+ var checkIfMsgExists, errorAlert;
+ errorAlert = $(form).find('.error-alert');
+ checkIfMsgExists = errorAlert.children().length;
+ if (checkIfMsgExists === 0) {
+ errorAlert.append(divAlert);
+ return $(".div-dropzone-alert").append(btnAlert + message);
+ }
+ };
+ closeAlertMessage = function() {
+ return form.find(".div-dropzone-alert").alert("close");
+ };
+ form.find(".markdown-selector").click(function(e) {
+ e.preventDefault();
+ $(this).closest('.gfm-form').find('.div-dropzone').click();
+ });
+ }
- return DropzoneInput;
- })();
-}).call(window);
+ return DropzoneInput;
+})();
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 9169fcd7328..fdbb4644971 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -2,203 +2,202 @@
/* global dateFormat */
/* global Pikaday */
-(function(global) {
- class DueDateSelect {
- constructor({ $dropdown, $loading } = {}) {
- const $dropdownParent = $dropdown.closest('.dropdown');
- const $block = $dropdown.closest('.block');
- this.$loading = $loading;
- this.$dropdown = $dropdown;
- this.$dropdownParent = $dropdownParent;
- this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
- this.$block = $block;
- this.$selectbox = $dropdown.closest('.selectbox');
- this.$value = $block.find('.value');
- this.$valueContent = $block.find('.value-content');
- this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
- this.fieldName = $dropdown.data('field-name'),
- this.abilityName = $dropdown.data('ability-name'),
- this.issueUpdateURL = $dropdown.data('issue-update');
-
- this.rawSelectedDate = null;
- this.displayedDate = null;
- this.datePayload = null;
-
- this.initGlDropdown();
- this.initRemoveDueDate();
- this.initDatePicker();
- }
-
- initGlDropdown() {
- this.$dropdown.glDropdown({
- opened: () => {
- const calendar = this.$datePicker.data('pikaday');
- calendar.show();
- },
- hidden: () => {
- this.$selectbox.hide();
- this.$value.css('display', '');
- }
- });
- }
-
- initDatePicker() {
- const $dueDateInput = $(`input[name='${this.fieldName}']`);
-
- const calendar = new Pikaday({
- field: $dueDateInput.get(0),
- theme: 'gitlab-theme',
- format: 'yyyy-mm-dd',
- onSelect: (dateText) => {
- const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
-
- $dueDateInput.val(formattedDate);
+class DueDateSelect {
+ constructor({ $dropdown, $loading } = {}) {
+ const $dropdownParent = $dropdown.closest('.dropdown');
+ const $block = $dropdown.closest('.block');
+ this.$loading = $loading;
+ this.$dropdown = $dropdown;
+ this.$dropdownParent = $dropdownParent;
+ this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
+ this.$block = $block;
+ this.$selectbox = $dropdown.closest('.selectbox');
+ this.$value = $block.find('.value');
+ this.$valueContent = $block.find('.value-content');
+ this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
+ this.fieldName = $dropdown.data('field-name'),
+ this.abilityName = $dropdown.data('ability-name'),
+ this.issueUpdateURL = $dropdown.data('issue-update');
+
+ this.rawSelectedDate = null;
+ this.displayedDate = null;
+ this.datePayload = null;
+
+ this.initGlDropdown();
+ this.initRemoveDueDate();
+ this.initDatePicker();
+ }
- if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
- gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
- this.updateIssueBoardIssue();
- } else {
- this.saveDueDate(true);
- }
- }
- });
+ initGlDropdown() {
+ this.$dropdown.glDropdown({
+ opened: () => {
+ const calendar = this.$datePicker.data('pikaday');
+ calendar.show();
+ },
+ hidden: () => {
+ this.$selectbox.hide();
+ this.$value.css('display', '');
+ }
+ });
+ }
- calendar.setDate(new Date($dueDateInput.val()));
- this.$datePicker.append(calendar.el);
- this.$datePicker.data('pikaday', calendar);
- }
+ initDatePicker() {
+ const $dueDateInput = $(`input[name='${this.fieldName}']`);
- initRemoveDueDate() {
- this.$block.on('click', '.js-remove-due-date', (e) => {
- const calendar = this.$datePicker.data('pikaday');
- e.preventDefault();
+ const calendar = new Pikaday({
+ field: $dueDateInput.get(0),
+ theme: 'gitlab-theme',
+ format: 'yyyy-mm-dd',
+ onSelect: (dateText) => {
+ const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
- calendar.setDate(null);
+ $dueDateInput.val(formattedDate);
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
- gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
+ gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
this.updateIssueBoardIssue();
} else {
- $("input[name='" + this.fieldName + "']").val('');
- return this.saveDueDate(false);
+ this.saveDueDate(true);
}
- });
- }
+ }
+ });
- saveDueDate(isDropdown) {
- this.parseSelectedDate();
- this.prepSelectedDate();
- this.submitSelectedDate(isDropdown);
- }
+ calendar.setDate(new Date($dueDateInput.val()));
+ this.$datePicker.append(calendar.el);
+ this.$datePicker.data('pikaday', calendar);
+ }
+
+ initRemoveDueDate() {
+ this.$block.on('click', '.js-remove-due-date', (e) => {
+ const calendar = this.$datePicker.data('pikaday');
+ e.preventDefault();
- parseSelectedDate() {
- this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val();
+ calendar.setDate(null);
- if (this.rawSelectedDate.length) {
- // Construct Date object manually to avoid buggy dateString support within Date constructor
- const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10));
- const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
- this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy');
+ if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
+ gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
+ this.updateIssueBoardIssue();
} else {
- this.displayedDate = 'No due date';
+ $("input[name='" + this.fieldName + "']").val('');
+ return this.saveDueDate(false);
}
- }
+ });
+ }
- prepSelectedDate() {
- const datePayload = {};
- datePayload[this.abilityName] = {};
- datePayload[this.abilityName].due_date = this.rawSelectedDate;
- this.datePayload = datePayload;
- }
+ saveDueDate(isDropdown) {
+ this.parseSelectedDate();
+ this.prepSelectedDate();
+ this.submitSelectedDate(isDropdown);
+ }
- updateIssueBoardIssue () {
- this.$loading.fadeIn();
- this.$dropdown.trigger('loading.gl.dropdown');
- this.$selectbox.hide();
- this.$value.css('display', '');
+ parseSelectedDate() {
+ this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val();
- gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
- .then(() => {
- this.$loading.fadeOut();
- });
+ if (this.rawSelectedDate.length) {
+ // Construct Date object manually to avoid buggy dateString support within Date constructor
+ const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10));
+ const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
+ this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy');
+ } else {
+ this.displayedDate = 'No due date';
}
+ }
+
+ prepSelectedDate() {
+ const datePayload = {};
+ datePayload[this.abilityName] = {};
+ datePayload[this.abilityName].due_date = this.rawSelectedDate;
+ this.datePayload = datePayload;
+ }
+
+ updateIssueBoardIssue () {
+ this.$loading.fadeIn();
+ this.$dropdown.trigger('loading.gl.dropdown');
+ this.$selectbox.hide();
+ this.$value.css('display', '');
+
+ gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
+ .then(() => {
+ this.$loading.fadeOut();
+ });
+ }
+
+ submitSelectedDate(isDropdown) {
+ return $.ajax({
+ type: 'PUT',
+ url: this.issueUpdateURL,
+ data: this.datePayload,
+ dataType: 'json',
+ beforeSend: () => {
+ const selectedDateValue = this.datePayload[this.abilityName].due_date;
+ const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
+
+ this.$loading.fadeIn();
- submitSelectedDate(isDropdown) {
- return $.ajax({
- type: 'PUT',
- url: this.issueUpdateURL,
- data: this.datePayload,
- dataType: 'json',
- beforeSend: () => {
- const selectedDateValue = this.datePayload[this.abilityName].due_date;
- const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
-
- this.$loading.fadeIn();
-
- if (isDropdown) {
- this.$dropdown.trigger('loading.gl.dropdown');
- this.$selectbox.hide();
- }
-
- this.$value.css('display', '');
- this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
- this.$sidebarValue.html(this.displayedDate);
-
- return selectedDateValue.length ?
- $('.js-remove-due-date-holder').removeClass('hidden') :
- $('.js-remove-due-date-holder').addClass('hidden');
- }
- }).done((data) => {
if (isDropdown) {
- this.$dropdown.trigger('loaded.gl.dropdown');
- this.$dropdown.dropdown('toggle');
+ this.$dropdown.trigger('loading.gl.dropdown');
+ this.$selectbox.hide();
}
- return this.$loading.fadeOut();
- });
- }
+
+ this.$value.css('display', '');
+ this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
+ this.$sidebarValue.html(this.displayedDate);
+
+ return selectedDateValue.length ?
+ $('.js-remove-due-date-holder').removeClass('hidden') :
+ $('.js-remove-due-date-holder').addClass('hidden');
+ }
+ }).done((data) => {
+ if (isDropdown) {
+ this.$dropdown.trigger('loaded.gl.dropdown');
+ this.$dropdown.dropdown('toggle');
+ }
+ return this.$loading.fadeOut();
+ });
}
+}
- class DueDateSelectors {
- constructor() {
- this.initMilestoneDatePicker();
- this.initIssuableSelect();
- }
+class DueDateSelectors {
+ constructor() {
+ this.initMilestoneDatePicker();
+ this.initIssuableSelect();
+ }
- initMilestoneDatePicker() {
- $('.datepicker').each(function() {
- const $datePicker = $(this);
- const calendar = new Pikaday({
- field: $datePicker.get(0),
- theme: 'gitlab-theme',
- format: 'yyyy-mm-dd',
- onSelect(dateText) {
- $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
- }
- });
- calendar.setDate(new Date($datePicker.val()));
-
- $datePicker.data('pikaday', calendar);
+ initMilestoneDatePicker() {
+ $('.datepicker').each(function() {
+ const $datePicker = $(this);
+ const calendar = new Pikaday({
+ field: $datePicker.get(0),
+ theme: 'gitlab-theme',
+ format: 'yyyy-mm-dd',
+ onSelect(dateText) {
+ $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
+ }
});
+ calendar.setDate(new Date($datePicker.val()));
- $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
- e.preventDefault();
- const calendar = $(e.target).siblings('.datepicker').data('pikaday');
- calendar.setDate(null);
- });
- }
+ $datePicker.data('pikaday', calendar);
+ });
- initIssuableSelect() {
- const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
+ $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
+ e.preventDefault();
+ const calendar = $(e.target).siblings('.datepicker').data('pikaday');
+ calendar.setDate(null);
+ });
+ }
+
+ initIssuableSelect() {
+ const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
- $('.js-due-date-select').each((i, dropdown) => {
- const $dropdown = $(dropdown);
- new DueDateSelect({
- $dropdown,
- $loading
- });
+ $('.js-due-date-select').each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ new DueDateSelect({
+ $dropdown,
+ $loading
});
- }
+ });
}
+}
- global.DueDateSelectors = DueDateSelectors;
-})(window.gl || (window.gl = {}));
+window.gl = window.gl || {};
+window.gl.DueDateSelectors = DueDateSelectors;
diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js
index 2cb48dde628..0923ce6b550 100644
--- a/app/assets/javascripts/environments/components/environment.js
+++ b/app/assets/javascripts/environments/components/environment.js
@@ -1,16 +1,17 @@
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
+import EnvironmentsService from '../services/environments_service';
+import EnvironmentTable from './environments_table';
+import EnvironmentsStore from '../stores/environments_store';
+import eventHub from '../event_hub';
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
-const EnvironmentsService = require('../services/environments_service');
-const EnvironmentTable = require('./environments_table');
-const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
-module.exports = Vue.component('environment-component', {
+export default Vue.component('environment-component', {
components: {
'environment-table': EnvironmentTable,
@@ -66,33 +67,15 @@ module.exports = Vue.component('environment-component', {
* Toggles loading property.
*/
created() {
- const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
-
- const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
-
- const service = new EnvironmentsService(endpoint);
-
- this.isLoading = true;
-
- return service.get()
- .then(resp => ({
- headers: resp.headers,
- body: resp.json(),
- }))
- .then((response) => {
- this.store.storeAvailableCount(response.body.available_count);
- this.store.storeStoppedCount(response.body.stopped_count);
- this.store.storeEnvironments(response.body.environments);
- this.store.setPagination(response.headers);
- })
- .then(() => {
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- new Flash('An error occurred while fetching the environments.', 'alert');
- });
+ this.service = new EnvironmentsService(this.endpoint);
+
+ this.fetchEnvironments();
+
+ eventHub.$on('refreshEnvironments', this.fetchEnvironments);
+ },
+
+ beforeDestroyed() {
+ eventHub.$off('refreshEnvironments');
},
methods: {
@@ -112,6 +95,32 @@ module.exports = Vue.component('environment-component', {
gl.utils.visitUrl(param);
return param;
},
+
+ fetchEnvironments() {
+ const scope = gl.utils.getParameterByName('scope') || this.visibility;
+ const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
+
+ this.isLoading = true;
+
+ return this.service.get(scope, pageNumber)
+ .then(resp => ({
+ headers: resp.headers,
+ body: resp.json(),
+ }))
+ .then((response) => {
+ this.store.storeAvailableCount(response.body.available_count);
+ this.store.storeStoppedCount(response.body.stopped_count);
+ this.store.storeEnvironments(response.body.environments);
+ this.store.setPagination(response.headers);
+ })
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occurred while fetching the environments.');
+ });
+ },
},
template: `
@@ -144,7 +153,7 @@ module.exports = Vue.component('environment-component', {
<div class="content-list environments-container">
<div class="environments-list-loading text-center" v-if="isLoading">
- <i class="fa fa-spinner fa-spin"></i>
+ <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div>
<div class="blank-state blank-state-no-icon"
@@ -173,7 +182,8 @@ module.exports = Vue.component('environment-component', {
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"/>
+ :can-read-environment="canReadEnvironmentParsed"
+ :service="service"/>
</div>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js
index 15e3f8823d2..455a8819549 100644
--- a/app/assets/javascripts/environments/components/environment_actions.js
+++ b/app/assets/javascripts/environments/components/environment_actions.js
@@ -1,41 +1,71 @@
-const Vue = require('vue');
-const playIconSvg = require('icons/_icon_play.svg');
+/* global Flash */
+/* eslint-disable no-new */
-module.exports = Vue.component('actions-component', {
+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 };
+ return {
+ playIconSvg,
+ isLoading: false,
+ };
+ },
+
+ 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.');
+ });
+ },
},
template: `
<div class="btn-group" role="group">
- <button class="dropdown btn btn-default dropdown-new" data-toggle="dropdown">
+ <button
+ class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
+ data-toggle="dropdown"
+ :disabled="isLoading">
<span>
- <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
- <i class="fa fa-caret-down"></i>
+ <span v-html="playIconSvg"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</span>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
- <a :href="action.play_path"
- data-method="post"
- rel="nofollow"
- class="js-manual-action-link">
+ <button
+ @click="onClickAction(action.play_path)"
+ class="js-manual-action-link no-btn">
${playIconSvg}
<span>
{{action.name}}
</span>
- </a>
+ </button>
</li>
</ul>
</button>
</div>
`,
-});
+};
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js
index 2599bba3c59..a554998f52c 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.js
+++ b/app/assets/javascripts/environments/components/environment_external_url.js
@@ -1,9 +1,7 @@
/**
* Renders the external url link in environments table.
*/
-const Vue = require('vue');
-
-module.exports = Vue.component('external-url-component', {
+export default {
props: {
externalUrl: {
type: String,
@@ -12,8 +10,12 @@ module.exports = Vue.component('external-url-component', {
},
template: `
- <a class="btn external_url" :href="externalUrl" target="_blank">
- <i class="fa fa-external-link"></i>
+ <a
+ class="btn external_url"
+ :href="externalUrl"
+ target="_blank"
+ title="Environment external URL">
+ <i class="fa fa-external-link" aria-hidden="true"></i>
</a>
`,
-});
+};
diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js
index 7f4e070b229..93919d41c60 100644
--- a/app/assets/javascripts/environments/components/environment_item.js
+++ b/app/assets/javascripts/environments/components/environment_item.js
@@ -1,13 +1,11 @@
-const Vue = require('vue');
-const Timeago = require('timeago.js');
-
-require('../../lib/utils/text_utility');
-require('../../vue_shared/components/commit');
-const ActionsComponent = require('./environment_actions');
-const ExternalUrlComponent = require('./environment_external_url');
-const StopComponent = require('./environment_stop');
-const RollbackComponent = require('./environment_rollback');
-const TerminalButtonComponent = require('./environment_terminal_button');
+import Timeago from 'timeago.js';
+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 '../../lib/utils/text_utility';
+import '../../vue_shared/components/commit';
/**
* Envrionment Item Component
@@ -17,7 +15,7 @@ const TerminalButtonComponent = require('./environment_terminal_button');
const timeagoInstance = new Timeago();
-module.exports = Vue.component('environment-item', {
+export default {
components: {
'commit-component': gl.CommitComponent,
@@ -46,6 +44,11 @@ module.exports = Vue.component('environment-item', {
required: false,
default: false,
},
+
+ service: {
+ type: Object,
+ required: true,
+ },
},
computed: {
@@ -489,22 +492,25 @@ module.exports = Vue.component('environment-item', {
<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"/>
<stop-component v-if="hasStopAction && canCreateDeployment"
- :stop-url="model.stop_path"/>
+ :stop-url="model.stop_path"
+ :service="service"/>
<terminal-button-component v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/>
<rollback-component v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
- :retry-url="retryUrl"/>
+ :retry-url="retryUrl"
+ :service="service"/>
</div>
</td>
</tr>
`,
-});
+};
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js b/app/assets/javascripts/environments/components/environment_rollback.js
index daf126eb4e8..baa15d9e5b5 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.js
+++ b/app/assets/javascripts/environments/components/environment_rollback.js
@@ -1,10 +1,14 @@
+/* global Flash */
+/* eslint-disable no-new */
/**
* Renders Rollback or Re deploy button in environments table depending
- * of the provided property `isLastDeployment`
+ * of the provided property `isLastDeployment`.
+ *
+ * Makes a post request when the button is clicked.
*/
-const Vue = require('vue');
+import eventHub from '../event_hub';
-module.exports = Vue.component('rollback-component', {
+export default {
props: {
retryUrl: {
type: String,
@@ -15,16 +19,49 @@ module.exports = Vue.component('rollback-component', {
type: Boolean,
default: true,
},
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+
+ methods: {
+ onClick() {
+ this.isLoading = true;
+
+ this.service.postAction(this.retryUrl)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshEnvironments');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
},
template: `
- <a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
+ <button type="button"
+ class="btn"
+ @click="onClick"
+ :disabled="isLoading">
+
<span v-if="isLastDeployment">
Re-deploy
</span>
<span v-else>
Rollback
</span>
- </a>
+
+ <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </button>
`,
-});
+};
diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js
index 96983a19568..5404d647745 100644
--- a/app/assets/javascripts/environments/components/environment_stop.js
+++ b/app/assets/javascripts/environments/components/environment_stop.js
@@ -1,24 +1,56 @@
+/* global Flash */
+/* eslint-disable no-new, no-alert */
/**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
*/
-const Vue = require('vue');
+import eventHub from '../event_hub';
-module.exports = Vue.component('stop-component', {
+export default {
props: {
stopUrl: {
type: String,
default: '',
},
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+
+ methods: {
+ onClick() {
+ if (confirm('Are you sure you want to stop this environment?')) {
+ this.isLoading = true;
+
+ this.service.postAction(this.retryUrl)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshEnvironments');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.', 'alert');
+ });
+ }
+ },
},
template: `
- <a class="btn stop-env-link"
- :href="stopUrl"
- data-confirm="Are you sure you want to stop this environment?"
- data-method="post"
- rel="nofollow">
+ <button type="button"
+ class="btn stop-env-link"
+ @click="onClick"
+ :disabled="isLoading"
+ title="Stop Environment">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
- </a>
+ <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </button>
`,
-});
+};
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.js
index e86607e78f4..66a71faa02f 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.js
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.js
@@ -2,13 +2,13 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
-const Vue = require('vue');
-const terminalIconSvg = require('icons/_icon_terminal.svg');
+import terminalIconSvg from 'icons/_icon_terminal.svg';
-module.exports = Vue.component('terminal-button-component', {
+export default {
props: {
terminalPath: {
type: String,
+ required: false,
default: '',
},
},
@@ -19,8 +19,9 @@ module.exports = Vue.component('terminal-button-component', {
template: `
<a class="btn terminal-button"
+ title="Open web terminal"
:href="terminalPath">
${terminalIconSvg}
</a>
`,
-});
+};
diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js
index 4088d63be80..5f07b612b91 100644
--- a/app/assets/javascripts/environments/components/environments_table.js
+++ b/app/assets/javascripts/environments/components/environments_table.js
@@ -1,11 +1,9 @@
/**
* Render environments table.
*/
-const Vue = require('vue');
-const EnvironmentItem = require('./environment_item');
-
-module.exports = Vue.component('environment-table-component', {
+import EnvironmentItem from './environment_item';
+export default {
components: {
'environment-item': EnvironmentItem,
},
@@ -28,6 +26,11 @@ module.exports = Vue.component('environment-table-component', {
required: false,
default: false,
},
+
+ service: {
+ type: Object,
+ required: true,
+ },
},
template: `
@@ -48,9 +51,10 @@ module.exports = Vue.component('environment-table-component', {
<tr is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
- :can-read-environment="canReadEnvironment"></tr>
+ :can-read-environment="canReadEnvironment"
+ :service="service"></tr>
</template>
</tbody>
</table>
`,
-});
+};
diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/environments_bundle.js
index 7bbba91bc10..8d963b335cf 100644
--- a/app/assets/javascripts/environments/environments_bundle.js
+++ b/app/assets/javascripts/environments/environments_bundle.js
@@ -1,4 +1,4 @@
-const EnvironmentsComponent = require('./components/environment');
+import EnvironmentsComponent from './components/environment';
$(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/environments/event_hub.js b/app/assets/javascripts/environments/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/environments/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index d2ca465351a..f939eccf246 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,4 +1,4 @@
-const EnvironmentsFolderComponent = require('./environments_folder_view');
+import EnvironmentsFolderComponent from './environments_folder_view';
$(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js
index 2a9d0492d7a..7abcf6dbbea 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js
@@ -1,16 +1,16 @@
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
+import EnvironmentsService from '../services/environments_service';
+import EnvironmentTable from '../components/environments_table';
+import EnvironmentsStore from '../stores/environments_store';
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
-const EnvironmentsService = require('../services/environments_service');
-const EnvironmentTable = require('../components/environments_table');
-const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
-module.exports = Vue.component('environment-folder-view', {
+export default Vue.component('environment-folder-view', {
components: {
'environment-table': EnvironmentTable,
@@ -88,11 +88,11 @@ module.exports = Vue.component('environment-folder-view', {
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
- const service = new EnvironmentsService(endpoint);
+ this.service = new EnvironmentsService(endpoint);
this.isLoading = true;
- return service.get()
+ return this.service.get()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
@@ -168,13 +168,12 @@ module.exports = Vue.component('environment-folder-view', {
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
- :commit-icon-svg="commitIconSvg">
- </environment-table>
+ :commit-icon-svg="commitIconSvg"
+ :service="service"/>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
- :pageInfo="state.paginationInformation">
- </table-pagination>
+ :pageInfo="state.paginationInformation"/>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index effc6c4c838..76296c83d11 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -1,13 +1,16 @@
-const Vue = require('vue');
+/* eslint-disable class-methods-use-this */
+import Vue from 'vue';
-class EnvironmentsService {
+export default class EnvironmentsService {
constructor(endpoint) {
this.environments = Vue.resource(endpoint);
}
- get() {
- return this.environments.get();
+ get(scope, page) {
+ return this.environments.get({ scope, page });
}
-}
-module.exports = EnvironmentsService;
+ postAction(endpoint) {
+ return Vue.http.post(endpoint, {}, { emulateJSON: true });
+ }
+}
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 15cd9bde08e..d3fe3872c56 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -1,11 +1,12 @@
-require('~/lib/utils/common_utils');
+import '~/lib/utils/common_utils';
+
/**
* Environments Store.
*
* Stores received environments, count of stopped environments and count of
* available environments.
*/
-class EnvironmentsStore {
+export default class EnvironmentsStore {
constructor() {
this.state = {};
this.state.environments = [];
@@ -86,5 +87,3 @@ class EnvironmentsStore {
return count;
}
}
-
-module.exports = EnvironmentsStore;
diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js
index f8256a8d26d..027222f804d 100644
--- a/app/assets/javascripts/extensions/array.js
+++ b/app/assets/javascripts/extensions/array.js
@@ -1,27 +1,11 @@
-/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, strict, max-len */
+// TODO: remove this
-'use strict';
-
-Array.prototype.first = function() {
+// eslint-disable-next-line no-extend-native
+Array.prototype.first = function first() {
return this[0];
};
-Array.prototype.last = function() {
- return this[this.length-1];
-};
-
-Array.prototype.find = Array.prototype.find || function(predicate, ...args) {
- if (!this) throw new TypeError('Array.prototype.find called on null or undefined');
- if (typeof predicate !== 'function') throw new TypeError('predicate must be a function');
-
- const list = Object(this);
- const thisArg = args[1];
- let value = {};
-
- for (let i = 0; i < list.length; i += 1) {
- value = list[i];
- if (predicate.call(thisArg, value, i, list)) return value;
- }
-
- return undefined;
+// eslint-disable-next-line no-extend-native
+Array.prototype.last = function last() {
+ return this[this.length - 1];
};
diff --git a/app/assets/javascripts/extensions/custom_event.js b/app/assets/javascripts/extensions/custom_event.js
deleted file mode 100644
index abedae4c1c7..00000000000
--- a/app/assets/javascripts/extensions/custom_event.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/* global CustomEvent */
-/* eslint-disable no-global-assign */
-
-// Custom event support for IE
-CustomEvent = function CustomEvent(event, parameters) {
- const params = parameters || { bubbles: false, cancelable: false, detail: undefined };
- const evt = document.createEvent('CustomEvent');
- evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
- return evt;
-};
-
-CustomEvent.prototype = window.Event.prototype;
diff --git a/app/assets/javascripts/extensions/element.js b/app/assets/javascripts/extensions/element.js
deleted file mode 100644
index 90ab79305a7..00000000000
--- a/app/assets/javascripts/extensions/element.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/* global Element */
-/* eslint-disable consistent-return, max-len, no-empty, func-names */
-
-Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) {
- if (!selectedElement) return;
- return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement);
-};
-
-Element.prototype.matches = Element.prototype.matches ||
- Element.prototype.matchesSelector ||
- Element.prototype.mozMatchesSelector ||
- Element.prototype.msMatchesSelector ||
- Element.prototype.oMatchesSelector ||
- Element.prototype.webkitMatchesSelector ||
- function (s) {
- const matches = (this.document || this.ownerDocument).querySelectorAll(s);
- let i = matches.length - 1;
- while (i >= 0 && matches.item(i) !== this) { i -= 1; }
- return i > -1;
- };
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
deleted file mode 100644
index 1a489b859e8..00000000000
--- a/app/assets/javascripts/extensions/jquery.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, max-len */
-// Disable an element and add the 'disabled' Bootstrap class
-(function() {
- $.fn.extend({
- disable: function() {
- return $(this).attr('disabled', 'disabled').addClass('disabled');
- }
- });
-
- // Enable an element and remove the 'disabled' Bootstrap class
- $.fn.extend({
- enable: function() {
- return $(this).removeAttr('disabled').removeClass('disabled');
- }
- });
-}).call(window);
diff --git a/app/assets/javascripts/extensions/object.js b/app/assets/javascripts/extensions/object.js
deleted file mode 100644
index 70a2d765abd..00000000000
--- a/app/assets/javascripts/extensions/object.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/* eslint-disable no-restricted-syntax */
-
-// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
-if (typeof Object.assign !== 'function') {
- Object.assign = function assign(target, ...args) {
- if (target == null) { // TypeError if undefined or null
- throw new TypeError('Cannot convert undefined or null to object');
- }
-
- const to = Object(target);
-
- for (let index = 0; index < args.length; index += 1) {
- const nextSource = args[index];
-
- if (nextSource != null) { // Skip over if undefined or null
- for (const nextKey in nextSource) {
- // Avoid bugs when hasOwnProperty is shadowed
- if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
- to[nextKey] = nextSource[nextKey];
- }
- }
- }
- }
- return to;
- };
-}
diff --git a/app/assets/javascripts/extensions/string.js b/app/assets/javascripts/extensions/string.js
deleted file mode 100644
index ae9662444b0..00000000000
--- a/app/assets/javascripts/extensions/string.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import 'string.prototype.codepointat';
-import 'string.fromcodepoint';
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index bf84f2a0a8f..3f041172ff3 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -2,142 +2,140 @@
/* global FilesCommentButton */
/* global notes */
-(function() {
- let $commentButtonTemplate;
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+let $commentButtonTemplate;
+var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
- this.FilesCommentButton = (function() {
- var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
+window.FilesCommentButton = (function() {
+ var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
- COMMENT_BUTTON_CLASS = '.add-diff-note';
+ COMMENT_BUTTON_CLASS = '.add-diff-note';
- LINE_HOLDER_CLASS = '.line_holder';
+ LINE_HOLDER_CLASS = '.line_holder';
- LINE_NUMBER_CLASS = 'diff-line-num';
+ LINE_NUMBER_CLASS = 'diff-line-num';
- LINE_CONTENT_CLASS = 'line_content';
+ LINE_CONTENT_CLASS = 'line_content';
- UNFOLDABLE_LINE_CLASS = 'js-unfold';
+ UNFOLDABLE_LINE_CLASS = 'js-unfold';
- EMPTY_CELL_CLASS = 'empty-cell';
+ EMPTY_CELL_CLASS = 'empty-cell';
- OLD_LINE_CLASS = 'old_line';
+ OLD_LINE_CLASS = 'old_line';
- LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content";
+ LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content";
- TEXT_FILE_SELECTOR = '.text-file';
+ TEXT_FILE_SELECTOR = '.text-file';
- function FilesCommentButton(filesContainerElement) {
- this.render = bind(this.render, this);
- this.hideButton = bind(this.hideButton, this);
- this.isParallelView = notes.isParallelView();
- filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
- .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
+ function FilesCommentButton(filesContainerElement) {
+ this.render = bind(this.render, this);
+ this.hideButton = bind(this.hideButton, this);
+ this.isParallelView = notes.isParallelView();
+ filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
+ .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
+ }
+
+ FilesCommentButton.prototype.render = function(e) {
+ var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
+ $currentTarget = $(e.currentTarget);
+
+ if ($currentTarget.hasClass('js-no-comment-btn')) return;
+
+ lineContentElement = this.getLineContent($currentTarget);
+ buttonParentElement = this.getButtonParent($currentTarget);
+
+ if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
+
+ $button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
+ buttonParentElement.addClass('is-over')
+ .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
+
+ if ($button.length) {
+ return;
}
- FilesCommentButton.prototype.render = function(e) {
- var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
- $currentTarget = $(e.currentTarget);
+ textFileElement = this.getTextFileElement($currentTarget);
+ buttonParentElement.append(this.buildButton({
+ noteableType: textFileElement.attr('data-noteable-type'),
+ noteableID: textFileElement.attr('data-noteable-id'),
+ commitID: textFileElement.attr('data-commit-id'),
+ noteType: lineContentElement.attr('data-note-type'),
+ position: lineContentElement.attr('data-position'),
+ lineType: lineContentElement.attr('data-line-type'),
+ discussionID: lineContentElement.attr('data-discussion-id'),
+ lineCode: lineContentElement.attr('data-line-code')
+ }));
+ };
- if ($currentTarget.hasClass('js-no-comment-btn')) return;
+ FilesCommentButton.prototype.hideButton = function(e) {
+ var $currentTarget = $(e.currentTarget);
+ var buttonParentElement = this.getButtonParent($currentTarget);
- lineContentElement = this.getLineContent($currentTarget);
- buttonParentElement = this.getButtonParent($currentTarget);
+ buttonParentElement.removeClass('is-over')
+ .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
+ };
- if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
+ FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
+ return $commentButtonTemplate.clone().attr({
+ 'data-noteable-type': buttonAttributes.noteableType,
+ 'data-noteable-id': buttonAttributes.noteableID,
+ 'data-commit-id': buttonAttributes.commitID,
+ 'data-note-type': buttonAttributes.noteType,
+ 'data-line-code': buttonAttributes.lineCode,
+ 'data-position': buttonAttributes.position,
+ 'data-discussion-id': buttonAttributes.discussionID,
+ 'data-line-type': buttonAttributes.lineType
+ });
+ };
- $button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
- buttonParentElement.addClass('is-over')
- .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
+ FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
+ return hoveredElement.closest(TEXT_FILE_SELECTOR);
+ };
- if ($button.length) {
- return;
- }
+ FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
+ if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
+ return hoveredElement;
+ }
+ if (!this.isParallelView) {
+ return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
+ } else {
+ return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
+ }
+ };
- textFileElement = this.getTextFileElement($currentTarget);
- buttonParentElement.append(this.buildButton({
- noteableType: textFileElement.attr('data-noteable-type'),
- noteableID: textFileElement.attr('data-noteable-id'),
- commitID: textFileElement.attr('data-commit-id'),
- noteType: lineContentElement.attr('data-note-type'),
- position: lineContentElement.attr('data-position'),
- lineType: lineContentElement.attr('data-line-type'),
- discussionID: lineContentElement.attr('data-discussion-id'),
- lineCode: lineContentElement.attr('data-line-code')
- }));
- };
-
- FilesCommentButton.prototype.hideButton = function(e) {
- var $currentTarget = $(e.currentTarget);
- var buttonParentElement = this.getButtonParent($currentTarget);
-
- buttonParentElement.removeClass('is-over')
- .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
- };
-
- FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
- return $commentButtonTemplate.clone().attr({
- 'data-noteable-type': buttonAttributes.noteableType,
- 'data-noteable-id': buttonAttributes.noteableID,
- 'data-commit-id': buttonAttributes.commitID,
- 'data-note-type': buttonAttributes.noteType,
- 'data-line-code': buttonAttributes.lineCode,
- 'data-position': buttonAttributes.position,
- 'data-discussion-id': buttonAttributes.discussionID,
- 'data-line-type': buttonAttributes.lineType
- });
- };
-
- FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
- return hoveredElement.closest(TEXT_FILE_SELECTOR);
- };
-
- FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
- if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
+ FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
+ if (!this.isParallelView) {
+ if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
return hoveredElement;
}
- if (!this.isParallelView) {
- return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
- } else {
- return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
- }
- };
-
- FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
- if (!this.isParallelView) {
- if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
- return hoveredElement;
- }
- return hoveredElement.parent().find("." + OLD_LINE_CLASS);
- } else {
- if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
- return hoveredElement;
- }
- return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
+ return hoveredElement.parent().find("." + OLD_LINE_CLASS);
+ } else {
+ if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
+ return hoveredElement;
}
- };
+ return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
+ }
+ };
- FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
- return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
- };
+ FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
+ return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
+ };
- FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
- return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== '';
- };
+ FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
+ return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== '';
+ };
- return FilesCommentButton;
- })();
+ return FilesCommentButton;
+})();
- $.fn.filesCommentButton = function() {
- $commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
+$.fn.filesCommentButton = function() {
+ $commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
- if (!(this && (this.parent().data('can-create-note') != null))) {
- return;
+ if (!(this && (this.parent().data('can-create-note') != null))) {
+ return;
+ }
+ return this.each(function() {
+ if (!$.data(this, 'filesCommentButton')) {
+ return $.data(this, 'filesCommentButton', new FilesCommentButton($(this)));
}
- return this.each(function() {
- if (!$.data(this, 'filesCommentButton')) {
- return $.data(this, 'filesCommentButton', new FilesCommentButton($(this)));
- }
- });
- };
-}).call(window);
+ });
+};
diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js
index 47a40e28461..aaaeb9bddb1 100644
--- a/app/assets/javascripts/filterable_list.js
+++ b/app/assets/javascripts/filterable_list.js
@@ -2,6 +2,7 @@
* Makes search request for content when user types a value in the search input.
* Updates the html content of the page with the received one.
*/
+
export default class FilterableList {
constructor(form, filter, holder) {
this.filterForm = form;
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 e1a97070439..d37c812c1f7 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -162,6 +162,10 @@
}
resetDropdowns() {
+ if (!this.currentDropdown) {
+ return;
+ }
+
// Force current dropdown to hide
this.mapping[this.currentDropdown].reference.hideDropdown();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 638fe744668..835e87a28d7 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -38,7 +38,8 @@
this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this);
- this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit);
+ this.filteredSearchInputForm = this.filteredSearchInput.form;
+ this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
@@ -56,7 +57,7 @@
}
unbindEvents() {
- this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit);
+ this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 730104b89f9..eec30624ff2 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,42 +1,41 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */
-(function() {
- this.Flash = (function() {
- var hideFlash;
- hideFlash = function() {
- return $(this).fadeOut();
- };
+window.Flash = (function() {
+ var hideFlash;
- function Flash(message, type, parent) {
- var flash, textDiv;
- if (type == null) {
- type = 'alert';
- }
- if (parent == null) {
- parent = null;
- }
- if (parent) {
- this.flashContainer = parent.find('.flash-container');
- } else {
- this.flashContainer = $('.flash-container-page');
- }
- this.flashContainer.html('');
- flash = $('<div/>', {
- "class": "flash-" + type
- });
- flash.on('click', hideFlash);
- textDiv = $('<div/>', {
- "class": 'flash-text',
- text: message
- });
- textDiv.appendTo(flash);
- if (this.flashContainer.parent().hasClass('content-wrapper')) {
- textDiv.addClass('container-fluid container-limited');
- }
- flash.appendTo(this.flashContainer);
- this.flashContainer.show();
+ hideFlash = function() {
+ return $(this).fadeOut();
+ };
+
+ function Flash(message, type, parent) {
+ var flash, textDiv;
+ if (type == null) {
+ type = 'alert';
+ }
+ if (parent == null) {
+ parent = null;
+ }
+ if (parent) {
+ this.flashContainer = parent.find('.flash-container');
+ } else {
+ this.flashContainer = $('.flash-container-page');
+ }
+ this.flashContainer.html('');
+ flash = $('<div/>', {
+ "class": "flash-" + type
+ });
+ flash.on('click', hideFlash);
+ textDiv = $('<div/>', {
+ "class": 'flash-text',
+ text: message
+ });
+ textDiv.appendTo(flash);
+ if (this.flashContainer.parent().hasClass('content-wrapper')) {
+ textDiv.addClass('container-fluid container-limited');
}
+ flash.appendTo(this.flashContainer);
+ this.flashContainer.show();
+ }
- return Flash;
- })();
-}).call(window);
+ return Flash;
+})();
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 4f7ce1fa197..9ac4c49d697 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -5,390 +5,386 @@ import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji';
// Creates the variables for setting up GFM auto-completion
-(function() {
- if (window.gl == null) {
- window.gl = {};
- }
+window.gl = window.gl || {};
- function sanitize(str) {
- return str.replace(/<(?:.|\n)*?>/gm, '');
- }
+function sanitize(str) {
+ return str.replace(/<(?:.|\n)*?>/gm, '');
+}
- window.gl.GfmAutoComplete = {
- dataSources: {},
- defaultLoadingData: ['loading'],
- cachedData: {},
- isLoadingData: {},
- atTypeMap: {
- ':': 'emojis',
- '@': 'members',
- '#': 'issues',
- '!': 'mergeRequests',
- '~': 'labels',
- '%': 'milestones',
- '/': 'commands'
- },
- // Emoji
- Emoji: {
- templateFunction: function(name) {
- return `<li>
- ${name} ${glEmojiTag(name)}
- </li>
- `;
+window.gl.GfmAutoComplete = {
+ dataSources: {},
+ defaultLoadingData: ['loading'],
+ cachedData: {},
+ isLoadingData: {},
+ atTypeMap: {
+ ':': 'emojis',
+ '@': 'members',
+ '#': 'issues',
+ '!': 'mergeRequests',
+ '~': 'labels',
+ '%': 'milestones',
+ '/': 'commands'
+ },
+ // Emoji
+ Emoji: {
+ templateFunction: function(name) {
+ return `<li>
+ ${name} ${glEmojiTag(name)}
+ </li>
+ `;
+ }
+ },
+ // Team Members
+ Members: {
+ template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
+ },
+ Labels: {
+ template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
+ },
+ // Issues and MergeRequests
+ Issues: {
+ template: '<li><small>${id}</small> ${title}</li>'
+ },
+ // Milestones
+ Milestones: {
+ template: '<li>${title}</li>'
+ },
+ Loading: {
+ template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
+ },
+ DefaultOptions: {
+ sorter: function(query, items, searchKey) {
+ this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
+ if (gl.GfmAutoComplete.isLoading(items)) {
+ this.setting.highlightFirst = false;
+ return items;
}
+ return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
},
- // Team Members
- Members: {
- template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
- },
- Labels: {
- template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
- },
- // Issues and MergeRequests
- Issues: {
- template: '<li><small>${id}</small> ${title}</li>'
- },
- // Milestones
- Milestones: {
- template: '<li>${title}</li>'
+ filter: function(query, data, searchKey) {
+ if (gl.GfmAutoComplete.isLoading(data)) {
+ gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
+ return data;
+ } else {
+ return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
+ }
},
- Loading: {
- template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
+ beforeInsert: function(value) {
+ if (value && !this.setting.skipSpecialCharacterTest) {
+ var withoutAt = value.substring(1);
+ if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
+ }
+ return value;
},
- DefaultOptions: {
- sorter: function(query, items, searchKey) {
- this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
- if (gl.GfmAutoComplete.isLoading(items)) {
- this.setting.highlightFirst = false;
- return items;
- }
- return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
- },
- filter: function(query, data, searchKey) {
- if (gl.GfmAutoComplete.isLoading(data)) {
- gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
- return data;
- } else {
- return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
- }
- },
- beforeInsert: function(value) {
- if (value && !this.setting.skipSpecialCharacterTest) {
- var withoutAt = value.substring(1);
- if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
- }
- return value;
- },
- matcher: function (flag, subtext) {
- // The below is taken from At.js source
- // Tweaked to commands to start without a space only if char before is a non-word character
- // https://github.com/ichord/At.js
- var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar;
- atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
- atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
- subtext = subtext.split(/\s+/g).pop();
- flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ matcher: function (flag, subtext) {
+ // The below is taken from At.js source
+ // Tweaked to commands to start without a space only if char before is a non-word character
+ // https://github.com/ichord/At.js
+ var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar;
+ atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
+ atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
+ subtext = subtext.split(/\s+/g).pop();
+ flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
- _a = decodeURI("%C3%80");
- _y = decodeURI("%C3%BF");
+ _a = decodeURI("%C3%80");
+ _y = decodeURI("%C3%BF");
- regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
+ regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
- match = regexp.exec(subtext);
+ match = regexp.exec(subtext);
- if (match) {
- return match[1];
- } else {
- return null;
- }
+ if (match) {
+ return match[1];
+ } else {
+ return null;
}
- },
- setup: function(input) {
- // Add GFM auto-completion to all input fields, that accept GFM input.
- this.input = input || $('.js-gfm-input');
- this.setupLifecycle();
- },
- setupLifecycle() {
- this.input.each((i, input) => {
- const $input = $(input);
- $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
- // This triggers at.js again
- // Needed for slash commands with suffixes (ex: /label ~)
- $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
- });
- },
- setupAtWho: function($input) {
- // Emoji
- $input.atwho({
- at: ':',
- displayTpl: function(value) {
- return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template;
- }.bind(this),
- insertTpl: ':${name}:',
- skipSpecialCharacterTest: true,
- data: this.defaultLoadingData,
- callbacks: {
- sorter: this.DefaultOptions.sorter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter
- }
- });
- // Team Members
- $input.atwho({
- at: '@',
- displayTpl: function(value) {
- return value.username != null ? this.Members.template : this.Loading.template;
- }.bind(this),
- insertTpl: '${atwho-at}${username}',
- searchKey: 'search',
- alwaysHighlightFirst: true,
- skipSpecialCharacterTest: true,
- data: this.defaultLoadingData,
- callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher,
- beforeSave: function(members) {
- return $.map(members, function(m) {
- let title = '';
- if (m.username == null) {
- return m;
- }
- title = m.name;
- if (m.count) {
- title += " (" + m.count + ")";
- }
+ }
+ },
+ setup: function(input) {
+ // Add GFM auto-completion to all input fields, that accept GFM input.
+ this.input = input || $('.js-gfm-input');
+ this.setupLifecycle();
+ },
+ setupLifecycle() {
+ this.input.each((i, input) => {
+ const $input = $(input);
+ $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
+ // This triggers at.js again
+ // Needed for slash commands with suffixes (ex: /label ~)
+ $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
+ });
+ },
+ setupAtWho: function($input) {
+ // Emoji
+ $input.atwho({
+ at: ':',
+ displayTpl: function(value) {
+ return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template;
+ }.bind(this),
+ insertTpl: ':${name}:',
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ filter: this.DefaultOptions.filter
+ }
+ });
+ // Team Members
+ $input.atwho({
+ at: '@',
+ displayTpl: function(value) {
+ return value.username != null ? this.Members.template : this.Loading.template;
+ }.bind(this),
+ insertTpl: '${atwho-at}${username}',
+ searchKey: 'search',
+ alwaysHighlightFirst: true,
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ matcher: this.DefaultOptions.matcher,
+ beforeSave: function(members) {
+ return $.map(members, function(m) {
+ let title = '';
+ if (m.username == null) {
+ return m;
+ }
+ title = m.name;
+ if (m.count) {
+ title += " (" + m.count + ")";
+ }
- const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
- const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
- const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
+ const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
+ const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
+ const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
- return {
- username: m.username,
- avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
- title: sanitize(title),
- search: sanitize(m.username + " " + m.name)
- };
- });
- }
+ return {
+ username: m.username,
+ avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
+ title: sanitize(title),
+ search: sanitize(m.username + " " + m.name)
+ };
+ });
}
- });
- $input.atwho({
- at: '#',
- alias: 'issues',
- searchKey: 'search',
- displayTpl: function(value) {
- return value.title != null ? this.Issues.template : this.Loading.template;
- }.bind(this),
- data: this.defaultLoadingData,
- insertTpl: '${atwho-at}${id}',
- callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher,
- beforeSave: function(issues) {
- return $.map(issues, function(i) {
- if (i.title == null) {
- return i;
- }
- return {
- id: i.iid,
- title: sanitize(i.title),
- search: i.iid + " " + i.title
- };
- });
- }
+ }
+ });
+ $input.atwho({
+ at: '#',
+ alias: 'issues',
+ searchKey: 'search',
+ displayTpl: function(value) {
+ return value.title != null ? this.Issues.template : this.Loading.template;
+ }.bind(this),
+ data: this.defaultLoadingData,
+ insertTpl: '${atwho-at}${id}',
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ matcher: this.DefaultOptions.matcher,
+ beforeSave: function(issues) {
+ return $.map(issues, function(i) {
+ if (i.title == null) {
+ return i;
+ }
+ return {
+ id: i.iid,
+ title: sanitize(i.title),
+ search: i.iid + " " + i.title
+ };
+ });
}
- });
- $input.atwho({
- at: '%',
- alias: 'milestones',
- searchKey: 'search',
- insertTpl: '${atwho-at}${title}',
- displayTpl: function(value) {
- return value.title != null ? this.Milestones.template : this.Loading.template;
- }.bind(this),
- data: this.defaultLoadingData,
- callbacks: {
- matcher: this.DefaultOptions.matcher,
- sorter: this.DefaultOptions.sorter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter,
- beforeSave: function(milestones) {
- return $.map(milestones, function(m) {
- if (m.title == null) {
- return m;
- }
- return {
- id: m.iid,
- title: sanitize(m.title),
- search: "" + m.title
- };
- });
- }
+ }
+ });
+ $input.atwho({
+ at: '%',
+ alias: 'milestones',
+ searchKey: 'search',
+ insertTpl: '${atwho-at}${title}',
+ displayTpl: function(value) {
+ return value.title != null ? this.Milestones.template : this.Loading.template;
+ }.bind(this),
+ data: this.defaultLoadingData,
+ callbacks: {
+ matcher: this.DefaultOptions.matcher,
+ sorter: this.DefaultOptions.sorter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ filter: this.DefaultOptions.filter,
+ beforeSave: function(milestones) {
+ return $.map(milestones, function(m) {
+ if (m.title == null) {
+ return m;
+ }
+ return {
+ id: m.iid,
+ title: sanitize(m.title),
+ search: "" + m.title
+ };
+ });
}
- });
- $input.atwho({
- at: '!',
- alias: 'mergerequests',
- searchKey: 'search',
- displayTpl: function(value) {
- return value.title != null ? this.Issues.template : this.Loading.template;
- }.bind(this),
- data: this.defaultLoadingData,
- insertTpl: '${atwho-at}${id}',
- callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher,
- beforeSave: function(merges) {
- return $.map(merges, function(m) {
- if (m.title == null) {
- return m;
- }
- return {
- id: m.iid,
- title: sanitize(m.title),
- search: m.iid + " " + m.title
- };
- });
- }
+ }
+ });
+ $input.atwho({
+ at: '!',
+ alias: 'mergerequests',
+ searchKey: 'search',
+ displayTpl: function(value) {
+ return value.title != null ? this.Issues.template : this.Loading.template;
+ }.bind(this),
+ data: this.defaultLoadingData,
+ insertTpl: '${atwho-at}${id}',
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ matcher: this.DefaultOptions.matcher,
+ beforeSave: function(merges) {
+ return $.map(merges, function(m) {
+ if (m.title == null) {
+ return m;
+ }
+ return {
+ id: m.iid,
+ title: sanitize(m.title),
+ search: m.iid + " " + m.title
+ };
+ });
}
- });
- $input.atwho({
- at: '~',
- alias: 'labels',
- searchKey: 'search',
- data: this.defaultLoadingData,
- displayTpl: function(value) {
- return this.isLoading(value) ? this.Loading.template : this.Labels.template;
- }.bind(this),
- insertTpl: '${atwho-at}${title}',
- callbacks: {
- matcher: this.DefaultOptions.matcher,
- beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter,
- sorter: this.DefaultOptions.sorter,
- beforeSave: function(merges) {
- if (gl.GfmAutoComplete.isLoading(merges)) return merges;
- var sanitizeLabelTitle;
- sanitizeLabelTitle = function(title) {
- if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
- return "\"" + (sanitize(title)) + "\"";
- } else {
- return sanitize(title);
- }
+ }
+ });
+ $input.atwho({
+ at: '~',
+ alias: 'labels',
+ searchKey: 'search',
+ data: this.defaultLoadingData,
+ displayTpl: function(value) {
+ return this.isLoading(value) ? this.Loading.template : this.Labels.template;
+ }.bind(this),
+ insertTpl: '${atwho-at}${title}',
+ callbacks: {
+ matcher: this.DefaultOptions.matcher,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ filter: this.DefaultOptions.filter,
+ sorter: this.DefaultOptions.sorter,
+ beforeSave: function(merges) {
+ if (gl.GfmAutoComplete.isLoading(merges)) return merges;
+ var sanitizeLabelTitle;
+ sanitizeLabelTitle = function(title) {
+ if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
+ return "\"" + (sanitize(title)) + "\"";
+ } else {
+ return sanitize(title);
+ }
+ };
+ return $.map(merges, function(m) {
+ return {
+ title: sanitize(m.title),
+ color: m.color,
+ search: "" + m.title
};
- return $.map(merges, function(m) {
- return {
- title: sanitize(m.title),
- color: m.color,
- search: "" + m.title
- };
- });
- }
+ });
}
- });
- // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
- $input.filter('[data-supports-slash-commands="true"]').atwho({
- at: '/',
- alias: 'commands',
- searchKey: 'search',
- skipSpecialCharacterTest: true,
- data: this.defaultLoadingData,
- displayTpl: function(value) {
- if (this.isLoading(value)) return this.Loading.template;
- var tpl = '<li>/${name}';
- if (value.aliases.length > 0) {
- tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
- }
- if (value.params.length > 0) {
- tpl += ' <small><%- params.join(" ") %></small>';
- }
- if (value.description !== '') {
- tpl += '<small class="description"><i><%- description %></i></small>';
+ }
+ });
+ // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
+ $input.filter('[data-supports-slash-commands="true"]').atwho({
+ at: '/',
+ alias: 'commands',
+ searchKey: 'search',
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
+ displayTpl: function(value) {
+ if (this.isLoading(value)) return this.Loading.template;
+ var tpl = '<li>/${name}';
+ if (value.aliases.length > 0) {
+ tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
+ }
+ if (value.params.length > 0) {
+ tpl += ' <small><%- params.join(" ") %></small>';
+ }
+ if (value.description !== '') {
+ tpl += '<small class="description"><i><%- description %></i></small>';
+ }
+ tpl += '</li>';
+ return _.template(tpl)(value);
+ }.bind(this),
+ insertTpl: function(value) {
+ var tpl = "/${name} ";
+ var reference_prefix = null;
+ if (value.params.length > 0) {
+ reference_prefix = value.params[0][0];
+ if (/^[@%~]/.test(reference_prefix)) {
+ tpl += '<%- reference_prefix %>';
}
- tpl += '</li>';
- return _.template(tpl)(value);
- }.bind(this),
- insertTpl: function(value) {
- var tpl = "/${name} ";
- var reference_prefix = null;
- if (value.params.length > 0) {
- reference_prefix = value.params[0][0];
- if (/^[@%~]/.test(reference_prefix)) {
- tpl += '<%- reference_prefix %>';
+ }
+ return _.template(tpl)({ reference_prefix: reference_prefix });
+ },
+ suffix: '',
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ beforeSave: function(commands) {
+ if (gl.GfmAutoComplete.isLoading(commands)) return commands;
+ return $.map(commands, function(c) {
+ var search = c.name;
+ if (c.aliases.length > 0) {
+ search = search + " " + c.aliases.join(" ");
}
- }
- return _.template(tpl)({ reference_prefix: reference_prefix });
+ return {
+ name: c.name,
+ aliases: c.aliases,
+ params: c.params,
+ description: c.description,
+ search: search
+ };
+ });
},
- suffix: '',
- callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- beforeSave: function(commands) {
- if (gl.GfmAutoComplete.isLoading(commands)) return commands;
- return $.map(commands, function(c) {
- var search = c.name;
- if (c.aliases.length > 0) {
- search = search + " " + c.aliases.join(" ");
- }
- return {
- name: c.name,
- aliases: c.aliases,
- params: c.params,
- description: c.description,
- search: search
- };
- });
- },
- matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
- var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
- var match = regexp.exec(subtext);
- if (match) {
- return match[1];
- } else {
- return null;
- }
+ matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
+ var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
+ var match = regexp.exec(subtext);
+ if (match) {
+ return match[1];
+ } else {
+ return null;
}
}
- });
- return;
- },
- fetchData: function($input, at) {
- if (this.isLoadingData[at]) return;
- this.isLoadingData[at] = true;
- if (this.cachedData[at]) {
- this.loadData($input, at, this.cachedData[at]);
- } else if (this.atTypeMap[at] === 'emojis') {
- this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
- } else {
- $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
- this.loadData($input, at, data);
- }).fail(() => { this.isLoadingData[at] = false; });
- }
- },
- loadData: function($input, at, data) {
- this.isLoadingData[at] = false;
- this.cachedData[at] = data;
- $input.atwho('load', at, data);
- // This trigger at.js again
- // otherwise we would be stuck with loading until the user types
- return $input.trigger('keyup');
- },
- isLoading(data) {
- var dataToInspect = data;
- if (data && data.length > 0) {
- dataToInspect = data[0];
}
-
- var loadingState = this.defaultLoadingData[0];
- return dataToInspect &&
- (dataToInspect === loadingState || dataToInspect.name === loadingState);
+ });
+ return;
+ },
+ fetchData: function($input, at) {
+ if (this.isLoadingData[at]) return;
+ this.isLoadingData[at] = true;
+ if (this.cachedData[at]) {
+ this.loadData($input, at, this.cachedData[at]);
+ } else if (this.atTypeMap[at] === 'emojis') {
+ this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
+ } else {
+ $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
+ this.loadData($input, at, data);
+ }).fail(() => { this.isLoadingData[at] = false; });
+ }
+ },
+ loadData: function($input, at, data) {
+ this.isLoadingData[at] = false;
+ this.cachedData[at] = data;
+ $input.atwho('load', at, data);
+ // This trigger at.js again
+ // otherwise we would be stuck with loading until the user types
+ return $input.trigger('keyup');
+ },
+ isLoading(data) {
+ var dataToInspect = data;
+ if (data && data.length > 0) {
+ dataToInspect = data[0];
}
- };
-}).call(window);
+
+ var loadingState = this.defaultLoadingData[0];
+ return dataToInspect &&
+ (dataToInspect === loadingState || dataToInspect.name === loadingState);
+ }
+};
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 9e6ed06054b..a03f1202a6d 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,850 +1,848 @@
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
-(function() {
- var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
- bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
- indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
-
- GitLabDropdownFilter = (function() {
- var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
-
- BLUR_KEYCODES = [27, 40];
-
- ARROW_KEY_CODES = [38, 40];
-
- HAS_VALUE_CLASS = "has-value";
-
- function GitLabDropdownFilter(input, options) {
- var $clearButton, $inputContainer, ref, timeout;
- this.input = input;
- this.options = options;
- this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
- $inputContainer = this.input.parent();
- $clearButton = $inputContainer.find('.js-dropdown-input-clear');
- $clearButton.on('click', (function(_this) {
- // Clear click
- return function(e) {
+var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
+ bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
+ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
+
+GitLabDropdownFilter = (function() {
+ var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
+
+ BLUR_KEYCODES = [27, 40];
+
+ ARROW_KEY_CODES = [38, 40];
+
+ HAS_VALUE_CLASS = "has-value";
+
+ function GitLabDropdownFilter(input, options) {
+ var $clearButton, $inputContainer, ref, timeout;
+ this.input = input;
+ this.options = options;
+ this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
+ $inputContainer = this.input.parent();
+ $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+ $clearButton.on('click', (function(_this) {
+ // Clear click
+ return function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ return _this.input.val('').trigger('input').focus();
+ };
+ })(this));
+ // Key events
+ timeout = "";
+ this.input
+ .on('keydown', function (e) {
+ var keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
- e.stopPropagation();
- return _this.input.val('').trigger('input').focus();
- };
- })(this));
- // Key events
- timeout = "";
- this.input
- .on('keydown', function (e) {
- var keyCode = e.which;
- if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault();
- }
- })
- .on('input', function() {
- if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.addClass(HAS_VALUE_CLASS);
- } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.removeClass(HAS_VALUE_CLASS);
- }
- // Only filter asynchronously only if option remote is set
- if (this.options.remote) {
- clearTimeout(timeout);
- return timeout = setTimeout(function() {
- $inputContainer.parent().addClass('is-loading');
-
- return this.options.query(this.input.val(), function(data) {
- $inputContainer.parent().removeClass('is-loading');
- return this.options.callback(data);
- }.bind(this));
- }.bind(this), 250);
- } else {
- return this.filter(this.input.val());
- }
- }.bind(this));
- }
+ }
+ })
+ .on('input', function() {
+ if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.addClass(HAS_VALUE_CLASS);
+ } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.removeClass(HAS_VALUE_CLASS);
+ }
+ // Only filter asynchronously only if option remote is set
+ if (this.options.remote) {
+ clearTimeout(timeout);
+ return timeout = setTimeout(function() {
+ $inputContainer.parent().addClass('is-loading');
+
+ return this.options.query(this.input.val(), function(data) {
+ $inputContainer.parent().removeClass('is-loading');
+ return this.options.callback(data);
+ }.bind(this));
+ }.bind(this), 250);
+ } else {
+ return this.filter(this.input.val());
+ }
+ }.bind(this));
+ }
- GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
- return BLUR_KEYCODES.indexOf(keyCode) !== -1;
- };
+ GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
+ return BLUR_KEYCODES.indexOf(keyCode) !== -1;
+ };
- GitLabDropdownFilter.prototype.filter = function(search_text) {
- var data, elements, group, key, results, tmp;
- if (this.options.onFilter) {
- this.options.onFilter(search_text);
- }
- data = this.options.data();
- if ((data != null) && !this.options.filterByText) {
- results = data;
- if (search_text !== '') {
- // When data is an array of objects therefore [object Array] e.g.
- // [
- // { prop: 'foo' },
- // { prop: 'baz' }
- // ]
- if (_.isArray(data)) {
- results = fuzzaldrinPlus.filter(data, search_text, {
- key: this.options.keys
- });
- } else {
- // If data is grouped therefore an [object Object]. e.g.
- // {
- // groupName1: [
- // { prop: 'foo' },
- // { prop: 'baz' }
- // ],
- // groupName2: [
- // { prop: 'abc' },
- // { prop: 'def' }
- // ]
- // }
- if (gl.utils.isObject(data)) {
- results = {};
- for (key in data) {
- group = data[key];
- tmp = fuzzaldrinPlus.filter(group, search_text, {
- key: this.options.keys
+ GitLabDropdownFilter.prototype.filter = function(search_text) {
+ var data, elements, group, key, results, tmp;
+ if (this.options.onFilter) {
+ this.options.onFilter(search_text);
+ }
+ data = this.options.data();
+ if ((data != null) && !this.options.filterByText) {
+ results = data;
+ if (search_text !== '') {
+ // When data is an array of objects therefore [object Array] e.g.
+ // [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ]
+ if (_.isArray(data)) {
+ results = fuzzaldrinPlus.filter(data, search_text, {
+ key: this.options.keys
+ });
+ } else {
+ // If data is grouped therefore an [object Object]. e.g.
+ // {
+ // groupName1: [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ],
+ // groupName2: [
+ // { prop: 'abc' },
+ // { prop: 'def' }
+ // ]
+ // }
+ if (gl.utils.isObject(data)) {
+ results = {};
+ for (key in data) {
+ group = data[key];
+ tmp = fuzzaldrinPlus.filter(group, search_text, {
+ key: this.options.keys
+ });
+ if (tmp.length) {
+ results[key] = tmp.map(function(item) {
+ return item;
});
- if (tmp.length) {
- results[key] = tmp.map(function(item) {
- return item;
- });
- }
}
}
}
}
- return this.options.callback(results);
- } else {
- elements = this.options.elements();
- if (search_text) {
- return elements.each(function() {
- var $el, matches;
- $el = $(this);
- matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
- if (!$el.is('.dropdown-header')) {
- if (matches.length) {
- return $el.show().removeClass('option-hidden');
- } else {
- return $el.hide().addClass('option-hidden');
- }
+ }
+ return this.options.callback(results);
+ } else {
+ elements = this.options.elements();
+ if (search_text) {
+ return elements.each(function() {
+ var $el, matches;
+ $el = $(this);
+ matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
+ if (!$el.is('.dropdown-header')) {
+ if (matches.length) {
+ return $el.show().removeClass('option-hidden');
+ } else {
+ return $el.hide().addClass('option-hidden');
}
- });
- } else {
- return elements.show().removeClass('option-hidden');
- }
+ }
+ });
+ } else {
+ return elements.show().removeClass('option-hidden');
}
- };
-
- return GitLabDropdownFilter;
- })();
+ }
+ };
- GitLabDropdownRemote = (function() {
- function GitLabDropdownRemote(dataEndpoint, options) {
- this.dataEndpoint = dataEndpoint;
- this.options = options;
+ return GitLabDropdownFilter;
+})();
+
+GitLabDropdownRemote = (function() {
+ function GitLabDropdownRemote(dataEndpoint, options) {
+ this.dataEndpoint = dataEndpoint;
+ this.options = options;
+ }
+
+ GitLabDropdownRemote.prototype.execute = function() {
+ if (typeof this.dataEndpoint === "string") {
+ return this.fetchData();
+ } else if (typeof this.dataEndpoint === "function") {
+ if (this.options.beforeSend) {
+ this.options.beforeSend();
+ }
+ return this.dataEndpoint("", (function(_this) {
+ // Fetch the data by calling the data funcfion
+ return function(data) {
+ if (_this.options.success) {
+ _this.options.success(data);
+ }
+ if (_this.options.beforeSend) {
+ return _this.options.beforeSend();
+ }
+ };
+ })(this));
}
+ };
- GitLabDropdownRemote.prototype.execute = function() {
- if (typeof this.dataEndpoint === "string") {
- return this.fetchData();
- } else if (typeof this.dataEndpoint === "function") {
- if (this.options.beforeSend) {
- this.options.beforeSend();
- }
- return this.dataEndpoint("", (function(_this) {
- // Fetch the data by calling the data funcfion
- return function(data) {
- if (_this.options.success) {
- _this.options.success(data);
- }
- if (_this.options.beforeSend) {
- return _this.options.beforeSend();
- }
- };
- })(this));
- }
- };
+ GitLabDropdownRemote.prototype.fetchData = function() {
+ return $.ajax({
+ url: this.dataEndpoint,
+ dataType: this.options.dataType,
+ beforeSend: (function(_this) {
+ return function() {
+ if (_this.options.beforeSend) {
+ return _this.options.beforeSend();
+ }
+ };
+ })(this),
+ success: (function(_this) {
+ return function(data) {
+ if (_this.options.success) {
+ return _this.options.success(data);
+ }
+ };
+ })(this)
+ });
+ // Fetch the data through ajax if the data is a string
+ };
- GitLabDropdownRemote.prototype.fetchData = function() {
- return $.ajax({
- url: this.dataEndpoint,
- dataType: this.options.dataType,
- beforeSend: (function(_this) {
- return function() {
- if (_this.options.beforeSend) {
- return _this.options.beforeSend();
- }
- };
- })(this),
- success: (function(_this) {
- return function(data) {
- if (_this.options.success) {
- return _this.options.success(data);
- }
- };
- })(this)
- });
- // Fetch the data through ajax if the data is a string
- };
+ return GitLabDropdownRemote;
+})();
- return GitLabDropdownRemote;
- })();
+GitLabDropdown = (function() {
+ var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
- GitLabDropdown = (function() {
- var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
+ LOADING_CLASS = "is-loading";
- LOADING_CLASS = "is-loading";
+ PAGE_TWO_CLASS = "is-page-two";
- PAGE_TWO_CLASS = "is-page-two";
+ ACTIVE_CLASS = "is-active";
- ACTIVE_CLASS = "is-active";
+ INDETERMINATE_CLASS = "is-indeterminate";
- INDETERMINATE_CLASS = "is-indeterminate";
+ currentIndex = -1;
- currentIndex = -1;
+ NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
- NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
-
- SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
-
- CURSOR_SELECT_SCROLL_PADDING = 5;
-
- FILTER_INPUT = '.dropdown-input .dropdown-input-field';
-
- function GitLabDropdown(el1, options) {
- var searchFields, selector, self;
- this.el = el1;
- this.options = options;
- this.updateLabel = bind(this.updateLabel, this);
- this.hidden = bind(this.hidden, this);
- this.opened = bind(this.opened, this);
- this.shouldPropagate = bind(this.shouldPropagate, this);
- self = this;
- selector = $(this.el).data("target");
- this.dropdown = selector != null ? $(selector) : $(this.el).parent();
- // Set Defaults
- this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
- this.highlight = !!this.options.highlight;
- this.filterInputBlur = this.options.filterInputBlur != null
- ? this.options.filterInputBlur
- : true;
- // If no input is passed create a default one
- self = this;
- // If selector was passed
- if (_.isString(this.filterInput)) {
- this.filterInput = this.getElement(this.filterInput);
- }
- searchFields = this.options.search ? this.options.search.fields : [];
- if (this.options.data) {
- // If we provided data
- // data could be an array of objects or a group of arrays
- if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
- this.fullData = this.options.data;
- currentIndex = -1;
- this.parseData(this.options.data);
- this.focusTextInput();
- } else {
- this.remote = new GitLabDropdownRemote(this.options.data, {
- dataType: this.options.dataType,
- beforeSend: this.toggleLoading.bind(this),
- success: (function(_this) {
- return function(data) {
- _this.fullData = data;
- _this.parseData(_this.fullData);
- _this.focusTextInput();
- if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
- return _this.filter.input.trigger('input');
- }
- };
- // Remote data
- })(this)
- });
- }
- }
- // Init filterable
- if (this.options.filterable) {
- this.filter = new GitLabDropdownFilter(this.filterInput, {
- elIsInput: $(this.el).is('input'),
- filterInputBlur: this.filterInputBlur,
- filterByText: this.options.filterByText,
- onFilter: this.options.onFilter,
- remote: this.options.filterRemote,
- query: this.options.data,
- keys: searchFields,
- elements: (function(_this) {
- return function() {
- selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
- if (_this.dropdown.find('.dropdown-toggle-page').length) {
- selector = ".dropdown-page-one " + selector;
- }
- return $(selector);
- };
- })(this),
- data: (function(_this) {
- return function() {
- return _this.fullData;
- };
- })(this),
- callback: (function(_this) {
+ SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
+
+ CURSOR_SELECT_SCROLL_PADDING = 5;
+
+ FILTER_INPUT = '.dropdown-input .dropdown-input-field';
+
+ function GitLabDropdown(el1, options) {
+ var searchFields, selector, self;
+ this.el = el1;
+ this.options = options;
+ this.updateLabel = bind(this.updateLabel, this);
+ this.hidden = bind(this.hidden, this);
+ this.opened = bind(this.opened, this);
+ this.shouldPropagate = bind(this.shouldPropagate, this);
+ self = this;
+ selector = $(this.el).data("target");
+ this.dropdown = selector != null ? $(selector) : $(this.el).parent();
+ // Set Defaults
+ this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
+ this.highlight = !!this.options.highlight;
+ this.filterInputBlur = this.options.filterInputBlur != null
+ ? this.options.filterInputBlur
+ : true;
+ // If no input is passed create a default one
+ self = this;
+ // If selector was passed
+ if (_.isString(this.filterInput)) {
+ this.filterInput = this.getElement(this.filterInput);
+ }
+ searchFields = this.options.search ? this.options.search.fields : [];
+ if (this.options.data) {
+ // If we provided data
+ // data could be an array of objects or a group of arrays
+ if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
+ this.fullData = this.options.data;
+ currentIndex = -1;
+ this.parseData(this.options.data);
+ this.focusTextInput();
+ } else {
+ this.remote = new GitLabDropdownRemote(this.options.data, {
+ dataType: this.options.dataType,
+ beforeSend: this.toggleLoading.bind(this),
+ success: (function(_this) {
return function(data) {
- _this.parseData(data);
- if (_this.filterInput.val() !== '') {
- selector = SELECTABLE_CLASSES;
- if (_this.dropdown.find('.dropdown-toggle-page').length) {
- selector = ".dropdown-page-one " + selector;
- }
- if ($(_this.el).is('input')) {
- currentIndex = -1;
- } else {
- $(selector, _this.dropdown).first().find('a').addClass('is-focused');
- currentIndex = 0;
- }
+ _this.fullData = data;
+ _this.parseData(_this.fullData);
+ _this.focusTextInput();
+ if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
+ return _this.filter.input.trigger('input');
}
};
+ // Remote data
})(this)
});
}
- // Event listeners
- this.dropdown.on("shown.bs.dropdown", this.opened);
- this.dropdown.on("hidden.bs.dropdown", this.hidden);
- $(this.el).on("update.label", this.updateLabel);
- this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate);
- this.dropdown.on('keyup', (function(_this) {
- return function(e) {
- // Escape key
- if (e.which === 27) {
- return $('.dropdown-menu-close', _this.dropdown).trigger('click');
- }
- };
- })(this));
- this.dropdown.on('blur', 'a', (function(_this) {
- return function(e) {
- var $dropdownMenu, $relatedTarget;
- if (e.relatedTarget != null) {
- $relatedTarget = $(e.relatedTarget);
- $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
- if ($dropdownMenu.length === 0) {
- return _this.dropdown.removeClass('open');
+ }
+ // Init filterable
+ if (this.options.filterable) {
+ this.filter = new GitLabDropdownFilter(this.filterInput, {
+ elIsInput: $(this.el).is('input'),
+ filterInputBlur: this.filterInputBlur,
+ filterByText: this.options.filterByText,
+ onFilter: this.options.onFilter,
+ remote: this.options.filterRemote,
+ query: this.options.data,
+ keys: searchFields,
+ elements: (function(_this) {
+ return function() {
+ selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
+ if (_this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = ".dropdown-page-one " + selector;
+ }
+ return $(selector);
+ };
+ })(this),
+ data: (function(_this) {
+ return function() {
+ return _this.fullData;
+ };
+ })(this),
+ callback: (function(_this) {
+ return function(data) {
+ _this.parseData(data);
+ if (_this.filterInput.val() !== '') {
+ selector = SELECTABLE_CLASSES;
+ if (_this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = ".dropdown-page-one " + selector;
+ }
+ if ($(_this.el).is('input')) {
+ currentIndex = -1;
+ } else {
+ $(selector, _this.dropdown).first().find('a').addClass('is-focused');
+ currentIndex = 0;
+ }
}
+ };
+ })(this)
+ });
+ }
+ // Event listeners
+ this.dropdown.on("shown.bs.dropdown", this.opened);
+ this.dropdown.on("hidden.bs.dropdown", this.hidden);
+ $(this.el).on("update.label", this.updateLabel);
+ this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate);
+ this.dropdown.on('keyup', (function(_this) {
+ return function(e) {
+ // Escape key
+ if (e.which === 27) {
+ return $('.dropdown-menu-close', _this.dropdown).trigger('click');
+ }
+ };
+ })(this));
+ this.dropdown.on('blur', 'a', (function(_this) {
+ return function(e) {
+ var $dropdownMenu, $relatedTarget;
+ if (e.relatedTarget != null) {
+ $relatedTarget = $(e.relatedTarget);
+ $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
+ if ($dropdownMenu.length === 0) {
+ return _this.dropdown.removeClass('open');
}
+ }
+ };
+ })(this));
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) {
+ return function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ return _this.togglePage();
};
})(this));
+ }
+ if (this.options.selectable) {
+ selector = ".dropdown-content a";
if (this.dropdown.find(".dropdown-toggle-page").length) {
- this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) {
- return function(e) {
- e.preventDefault();
- e.stopPropagation();
- return _this.togglePage();
- };
- })(this));
- }
- if (this.options.selectable) {
- selector = ".dropdown-content a";
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one .dropdown-content a";
+ selector = ".dropdown-page-one .dropdown-content a";
+ }
+ this.dropdown.on("click", selector, function(e) {
+ var $el, selected, selectedObj, isMarking;
+ $el = $(this);
+ selected = self.rowClicked($el);
+ selectedObj = selected ? selected[0] : null;
+ isMarking = selected ? selected[1] : null;
+ if (self.options.clicked) {
+ self.options.clicked(selectedObj, $el, e, isMarking);
}
- this.dropdown.on("click", selector, function(e) {
- var $el, selected, selectedObj, isMarking;
- $el = $(this);
- selected = self.rowClicked($el);
- selectedObj = selected ? selected[0] : null;
- isMarking = selected ? selected[1] : null;
- if (self.options.clicked) {
- self.options.clicked(selectedObj, $el, e, isMarking);
- }
- // Update label right after all modifications in dropdown has been done
- if (self.options.toggleLabel) {
- self.updateLabel(selectedObj, $el, self);
- }
+ // Update label right after all modifications in dropdown has been done
+ if (self.options.toggleLabel) {
+ self.updateLabel(selectedObj, $el, self);
+ }
- $el.trigger('blur');
- });
- }
+ $el.trigger('blur');
+ });
}
+ }
- // Finds an element inside wrapper element
- GitLabDropdown.prototype.getElement = function(selector) {
- return this.dropdown.find(selector);
- };
+ // Finds an element inside wrapper element
+ GitLabDropdown.prototype.getElement = function(selector) {
+ return this.dropdown.find(selector);
+ };
- GitLabDropdown.prototype.toggleLoading = function() {
- return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
- };
+ GitLabDropdown.prototype.toggleLoading = function() {
+ return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
+ };
- GitLabDropdown.prototype.togglePage = function() {
- var menu;
- menu = $('.dropdown-menu', this.dropdown);
- if (menu.hasClass(PAGE_TWO_CLASS)) {
- if (this.remote) {
- this.remote.execute();
- }
- }
- menu.toggleClass(PAGE_TWO_CLASS);
- // Focus first visible input on active page
- return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
- };
-
- GitLabDropdown.prototype.parseData = function(data) {
- var full_html, groupData, html, name;
- this.renderedData = data;
- if (this.options.filterable && data.length === 0) {
- // render no matching results
- html = [this.noResults()];
- } else {
- // Handle array groups
- if (gl.utils.isObject(data)) {
- html = [];
- for (name in data) {
- groupData = data[name];
- html.push(this.renderItem({
- header: name
- // Add header for each group
- }, name));
- this.renderData(groupData, name).map(function(item) {
- return html.push(item);
- });
- }
- } else {
- // Render each row
- html = this.renderData(data);
- }
- }
- // Render the full menu
- full_html = this.renderMenu(html);
- return this.appendMenu(full_html);
- };
-
- GitLabDropdown.prototype.renderData = function(data, group) {
- if (group == null) {
- group = false;
+ GitLabDropdown.prototype.togglePage = function() {
+ var menu;
+ menu = $('.dropdown-menu', this.dropdown);
+ if (menu.hasClass(PAGE_TWO_CLASS)) {
+ if (this.remote) {
+ this.remote.execute();
}
- return data.map((function(_this) {
- return function(obj, index) {
- return _this.renderItem(obj, group, index);
- };
- })(this));
- };
-
- GitLabDropdown.prototype.shouldPropagate = function(e) {
- var $target;
- if (this.options.multiSelect) {
- $target = $(e.target);
- if ($target && !$target.hasClass('dropdown-menu-close') &&
- !$target.hasClass('dropdown-menu-close-icon') &&
- !$target.data('is-link')) {
- e.stopPropagation();
- return false;
- } else {
- return true;
+ }
+ menu.toggleClass(PAGE_TWO_CLASS);
+ // Focus first visible input on active page
+ return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
+ };
+
+ GitLabDropdown.prototype.parseData = function(data) {
+ var full_html, groupData, html, name;
+ this.renderedData = data;
+ if (this.options.filterable && data.length === 0) {
+ // render no matching results
+ html = [this.noResults()];
+ } else {
+ // Handle array groups
+ if (gl.utils.isObject(data)) {
+ html = [];
+ for (name in data) {
+ groupData = data[name];
+ html.push(this.renderItem({
+ header: name
+ // Add header for each group
+ }, name));
+ this.renderData(groupData, name).map(function(item) {
+ return html.push(item);
+ });
}
+ } else {
+ // Render each row
+ html = this.renderData(data);
}
- };
+ }
+ // Render the full menu
+ full_html = this.renderMenu(html);
+ return this.appendMenu(full_html);
+ };
- GitLabDropdown.prototype.opened = function(e) {
- var contentHtml;
- this.resetRows();
- this.addArrowKeyEvent();
+ GitLabDropdown.prototype.renderData = function(data, group) {
+ if (group == null) {
+ group = false;
+ }
+ return data.map((function(_this) {
+ return function(obj, index) {
+ return _this.renderItem(obj, group, index);
+ };
+ })(this));
+ };
- // Makes indeterminate items effective
- if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
- this.parseData(this.fullData);
- }
- contentHtml = $('.dropdown-content', this.dropdown).html();
- if (this.remote && contentHtml === "") {
- this.remote.execute();
+ GitLabDropdown.prototype.shouldPropagate = function(e) {
+ var $target;
+ if (this.options.multiSelect) {
+ $target = $(e.target);
+ if ($target && !$target.hasClass('dropdown-menu-close') &&
+ !$target.hasClass('dropdown-menu-close-icon') &&
+ !$target.data('is-link')) {
+ e.stopPropagation();
+ return false;
} else {
- this.focusTextInput();
+ return true;
}
+ }
+ };
- if (this.options.showMenuAbove) {
- this.positionMenuAbove();
- }
+ GitLabDropdown.prototype.opened = function(e) {
+ var contentHtml;
+ this.resetRows();
+ this.addArrowKeyEvent();
- if (this.options.opened) {
- this.options.opened.call(this, e);
- }
+ // Makes indeterminate items effective
+ if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
+ this.parseData(this.fullData);
+ }
+ contentHtml = $('.dropdown-content', this.dropdown).html();
+ if (this.remote && contentHtml === "") {
+ this.remote.execute();
+ } else {
+ this.focusTextInput();
+ }
+
+ if (this.options.showMenuAbove) {
+ this.positionMenuAbove();
+ }
- return this.dropdown.trigger('shown.gl.dropdown');
- };
+ if (this.options.opened) {
+ this.options.opened.call(this, e);
+ }
- GitLabDropdown.prototype.positionMenuAbove = function() {
- var $button = $(this.el);
- var $menu = this.dropdown.find('.dropdown-menu');
+ return this.dropdown.trigger('shown.gl.dropdown');
+ };
- $menu.css('top', ($button.height() + $menu.height()) * -1);
- };
+ GitLabDropdown.prototype.positionMenuAbove = function() {
+ var $button = $(this.el);
+ var $menu = this.dropdown.find('.dropdown-menu');
- GitLabDropdown.prototype.hidden = function(e) {
- var $input;
- this.resetRows();
- this.removeArrayKeyEvent();
- $input = this.dropdown.find(".dropdown-input-field");
- if (this.options.filterable) {
- $input.blur();
- }
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
- }
- if (this.options.hidden) {
- this.options.hidden.call(this, e);
- }
- return this.dropdown.trigger('hidden.gl.dropdown');
- };
+ $menu.css('top', ($button.height() + $menu.height()) * -1);
+ };
- // Render the full menu
- GitLabDropdown.prototype.renderMenu = function(html) {
- if (this.options.renderMenu) {
- return this.options.renderMenu(html);
- } else {
- var ul = document.createElement('ul');
+ GitLabDropdown.prototype.hidden = function(e) {
+ var $input;
+ this.resetRows();
+ this.removeArrayKeyEvent();
+ $input = this.dropdown.find(".dropdown-input-field");
+ if (this.options.filterable) {
+ $input.blur();
+ }
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
+ }
+ if (this.options.hidden) {
+ this.options.hidden.call(this, e);
+ }
+ return this.dropdown.trigger('hidden.gl.dropdown');
+ };
- for (var i = 0; i < html.length; i += 1) {
- var el = html[i];
+ // Render the full menu
+ GitLabDropdown.prototype.renderMenu = function(html) {
+ if (this.options.renderMenu) {
+ return this.options.renderMenu(html);
+ } else {
+ var ul = document.createElement('ul');
- if (el instanceof jQuery) {
- el = el.get(0);
- }
+ for (var i = 0; i < html.length; i += 1) {
+ var el = html[i];
- if (typeof el === 'string') {
- ul.innerHTML += el;
- } else {
- ul.appendChild(el);
- }
+ if (el instanceof jQuery) {
+ el = el.get(0);
}
- return ul;
+ if (typeof el === 'string') {
+ ul.innerHTML += el;
+ } else {
+ ul.appendChild(el);
+ }
}
- };
- // Append the menu into the dropdown
- GitLabDropdown.prototype.appendMenu = function(html) {
- return this.clearMenu().append(html);
- };
+ return ul;
+ }
+ };
+
+ // Append the menu into the dropdown
+ GitLabDropdown.prototype.appendMenu = function(html) {
+ return this.clearMenu().append(html);
+ };
- GitLabDropdown.prototype.clearMenu = function() {
- var selector;
- selector = '.dropdown-content';
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one .dropdown-content";
- }
+ GitLabDropdown.prototype.clearMenu = function() {
+ var selector;
+ selector = '.dropdown-content';
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ selector = ".dropdown-page-one .dropdown-content";
+ }
- return $(selector, this.dropdown).empty();
- };
+ return $(selector, this.dropdown).empty();
+ };
- GitLabDropdown.prototype.renderItem = function(data, group, index) {
- var field, fieldName, html, selected, text, url, value;
- if (group == null) {
- group = false;
+ GitLabDropdown.prototype.renderItem = function(data, group, index) {
+ var field, fieldName, html, selected, text, url, value;
+ if (group == null) {
+ group = false;
+ }
+ if (index == null) {
+ // Render the row
+ index = false;
+ }
+ html = document.createElement('li');
+ if (data === 'divider' || data === 'separator') {
+ html.className = data;
+ return html;
+ }
+ // Header
+ if (data.header != null) {
+ html.className = 'dropdown-header';
+ html.innerHTML = data.header;
+ return html;
+ }
+ if (this.options.renderRow) {
+ // Call the render function
+ html = this.options.renderRow.call(this.options, data, this);
+ } else {
+ if (!selected) {
+ value = this.options.id ? this.options.id(data) : data.id;
+ fieldName = this.options.fieldName;
+
+ if (value) { value = value.toString().replace(/'/g, '\\\''); }
+
+ field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
+ if (field.length) {
+ selected = true;
+ }
}
- if (index == null) {
- // Render the row
- index = false;
+ // Set URL
+ if (this.options.url != null) {
+ url = this.options.url(data);
+ } else {
+ url = data.url != null ? data.url : '#';
}
- html = document.createElement('li');
- if (data === 'divider' || data === 'separator') {
- html.className = data;
- return html;
+ // Set Text
+ if (this.options.text != null) {
+ text = this.options.text(data);
+ } else {
+ text = data.text != null ? data.text : '';
}
- // Header
- if (data.header != null) {
- html.className = 'dropdown-header';
- html.innerHTML = data.header;
- return html;
+ if (this.highlight) {
+ text = this.highlightTextMatches(text, this.filterInput.val());
}
- if (this.options.renderRow) {
- // Call the render function
- html = this.options.renderRow.call(this.options, data, this);
- } else {
- if (!selected) {
- value = this.options.id ? this.options.id(data) : data.id;
- fieldName = this.options.fieldName;
-
- if (value) { value = value.toString().replace(/'/g, '\\\''); }
-
- field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
- if (field.length) {
- selected = true;
- }
- }
- // Set URL
- if (this.options.url != null) {
- url = this.options.url(data);
- } else {
- url = data.url != null ? data.url : '#';
- }
- // Set Text
- if (this.options.text != null) {
- text = this.options.text(data);
- } else {
- text = data.text != null ? data.text : '';
- }
- if (this.highlight) {
- text = this.highlightTextMatches(text, this.filterInput.val());
- }
- // Create the list item & the link
- var link = document.createElement('a');
-
- link.href = url;
- link.innerHTML = text;
+ // Create the list item & the link
+ var link = document.createElement('a');
- if (selected) {
- link.className = 'is-active';
- }
-
- if (group) {
- link.dataset.group = group;
- link.dataset.index = index;
- }
+ link.href = url;
+ link.innerHTML = text;
- html.appendChild(link);
+ if (selected) {
+ link.className = 'is-active';
}
- return html;
- };
-
- GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
- var occurrences;
- occurrences = fuzzaldrinPlus.match(text, term);
- return text.split('').map(function(character, i) {
- if (indexOf.call(occurrences, i) !== -1) {
- return "<b>" + character + "</b>";
- } else {
- return character;
- }
- }).join('');
- };
-
- GitLabDropdown.prototype.noResults = function() {
- var html;
- return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
- };
-
- GitLabDropdown.prototype.rowClicked = function(el) {
- var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
-
- fieldName = this.options.fieldName;
- isInput = $(this.el).is('input');
- if (this.renderedData) {
- groupName = el.data('group');
- if (groupName) {
- selectedIndex = el.data('index');
- selectedObject = this.renderedData[groupName][selectedIndex];
- } else {
- selectedIndex = el.closest('li').index();
- selectedObject = this.renderedData[selectedIndex];
- }
+
+ if (group) {
+ link.dataset.group = group;
+ link.dataset.index = index;
}
- if (this.options.vue) {
- if (el.hasClass(ACTIVE_CLASS)) {
- el.removeClass(ACTIVE_CLASS);
- } else {
- el.addClass(ACTIVE_CLASS);
- }
+ html.appendChild(link);
+ }
+ return html;
+ };
- return [selectedObject];
+ GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
+ var occurrences;
+ occurrences = fuzzaldrinPlus.match(text, term);
+ return text.split('').map(function(character, i) {
+ if (indexOf.call(occurrences, i) !== -1) {
+ return "<b>" + character + "</b>";
+ } else {
+ return character;
}
+ }).join('');
+ };
- field = [];
- value = this.options.id
- ? this.options.id(selectedObject, el)
- : selectedObject.id;
- if (isInput) {
- field = $(this.el);
- } else if (value) {
- field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
- }
+ GitLabDropdown.prototype.noResults = function() {
+ var html;
+ return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
+ };
+
+ GitLabDropdown.prototype.rowClicked = function(el) {
+ var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
- if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
- return;
+ fieldName = this.options.fieldName;
+ isInput = $(this.el).is('input');
+ if (this.renderedData) {
+ groupName = el.data('group');
+ if (groupName) {
+ selectedIndex = el.data('index');
+ selectedObject = this.renderedData[groupName][selectedIndex];
+ } else {
+ selectedIndex = el.closest('li').index();
+ selectedObject = this.renderedData[selectedIndex];
}
+ }
+ if (this.options.vue) {
if (el.hasClass(ACTIVE_CLASS)) {
- isMarking = false;
el.removeClass(ACTIVE_CLASS);
- if (field && field.length) {
- this.clearField(field, isInput);
- }
- } else if (el.hasClass(INDETERMINATE_CLASS)) {
- isMarking = true;
- el.addClass(ACTIVE_CLASS);
- el.removeClass(INDETERMINATE_CLASS);
- if (field && field.length && value == null) {
- this.clearField(field, isInput);
- }
- if ((!field || !field.length) && fieldName) {
- this.addInput(fieldName, value, selectedObject);
- }
} else {
- isMarking = true;
- if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
- this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
- if (!isInput) {
- this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
- }
- }
- if (field && field.length && value == null) {
- this.clearField(field, isInput);
- }
- // Toggle active class for the tick mark
el.addClass(ACTIVE_CLASS);
- if (value != null) {
- if ((!field || !field.length) && fieldName) {
- this.addInput(fieldName, value, selectedObject);
- } else if (field && field.length) {
- field.val(value).trigger('change');
- }
- }
}
- return [selectedObject, isMarking];
- };
+ return [selectedObject];
+ }
- GitLabDropdown.prototype.focusTextInput = function() {
- if (this.options.filterable) { this.filterInput.focus(); }
- };
+ field = [];
+ value = this.options.id
+ ? this.options.id(selectedObject, el)
+ : selectedObject.id;
+ if (isInput) {
+ field = $(this.el);
+ } else if (value) {
+ field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
+ }
- GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
- var $input;
- // Create hidden input for form
- $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
- if (this.options.inputId != null) {
- $input.attr('id', this.options.inputId);
- }
- return this.dropdown.before($input);
- };
-
- GitLabDropdown.prototype.selectRowAtIndex = function(index) {
- var $el, selector;
- // If we pass an option index
- if (typeof index !== "undefined") {
- selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
- } else {
- selector = ".dropdown-content .is-focused";
+ if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
+ return;
+ }
+
+ if (el.hasClass(ACTIVE_CLASS)) {
+ isMarking = false;
+ el.removeClass(ACTIVE_CLASS);
+ if (field && field.length) {
+ this.clearField(field, isInput);
+ }
+ } else if (el.hasClass(INDETERMINATE_CLASS)) {
+ isMarking = true;
+ el.addClass(ACTIVE_CLASS);
+ el.removeClass(INDETERMINATE_CLASS);
+ if (field && field.length && value == null) {
+ this.clearField(field, isInput);
+ }
+ if ((!field || !field.length) && fieldName) {
+ this.addInput(fieldName, value, selectedObject);
+ }
+ } else {
+ isMarking = true;
+ if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
+ this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
+ if (!isInput) {
+ this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
+ }
}
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one " + selector;
+ if (field && field.length && value == null) {
+ this.clearField(field, isInput);
}
- // simulate a click on the first link
- $el = $(selector, this.dropdown);
- if ($el.length) {
- var href = $el.attr('href');
- if (href && href !== '#') {
- gl.utils.visitUrl(href);
- } else {
- $el.first().trigger('click');
+ // Toggle active class for the tick mark
+ el.addClass(ACTIVE_CLASS);
+ if (value != null) {
+ if ((!field || !field.length) && fieldName) {
+ this.addInput(fieldName, value, selectedObject);
+ } else if (field && field.length) {
+ field.val(value).trigger('change');
}
}
- };
+ }
- GitLabDropdown.prototype.addArrowKeyEvent = function() {
- var $input, ARROW_KEY_CODES, selector;
- ARROW_KEY_CODES = [38, 40];
- $input = this.dropdown.find(".dropdown-input-field");
- selector = SELECTABLE_CLASSES;
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one " + selector;
+ return [selectedObject, isMarking];
+ };
+
+ GitLabDropdown.prototype.focusTextInput = function() {
+ if (this.options.filterable) { this.filterInput.focus(); }
+ };
+
+ GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
+ var $input;
+ // Create hidden input for form
+ $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
+ if (this.options.inputId != null) {
+ $input.attr('id', this.options.inputId);
+ }
+ return this.dropdown.before($input);
+ };
+
+ GitLabDropdown.prototype.selectRowAtIndex = function(index) {
+ var $el, selector;
+ // If we pass an option index
+ if (typeof index !== "undefined") {
+ selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
+ } else {
+ selector = ".dropdown-content .is-focused";
+ }
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ selector = ".dropdown-page-one " + selector;
+ }
+ // simulate a click on the first link
+ $el = $(selector, this.dropdown);
+ if ($el.length) {
+ var href = $el.attr('href');
+ if (href && href !== '#') {
+ gl.utils.visitUrl(href);
+ } else {
+ $el.first().trigger('click');
}
- return $('body').on('keydown', (function(_this) {
- return function(e) {
- var $listItems, PREV_INDEX, currentKeyCode;
- currentKeyCode = e.which;
- if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
- e.preventDefault();
- e.stopImmediatePropagation();
- PREV_INDEX = currentIndex;
- $listItems = $(selector, _this.dropdown);
- // if @options.filterable
- // $input.blur()
- if (currentKeyCode === 40) {
- // Move down
- if (currentIndex < ($listItems.length - 1)) {
- currentIndex += 1;
- }
- } else if (currentKeyCode === 38) {
- // Move up
- if (currentIndex > 0) {
- currentIndex -= 1;
- }
+ }
+ };
+
+ GitLabDropdown.prototype.addArrowKeyEvent = function() {
+ var $input, ARROW_KEY_CODES, selector;
+ ARROW_KEY_CODES = [38, 40];
+ $input = this.dropdown.find(".dropdown-input-field");
+ selector = SELECTABLE_CLASSES;
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ selector = ".dropdown-page-one " + selector;
+ }
+ return $('body').on('keydown', (function(_this) {
+ return function(e) {
+ var $listItems, PREV_INDEX, currentKeyCode;
+ currentKeyCode = e.which;
+ if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ PREV_INDEX = currentIndex;
+ $listItems = $(selector, _this.dropdown);
+ // if @options.filterable
+ // $input.blur()
+ if (currentKeyCode === 40) {
+ // Move down
+ if (currentIndex < ($listItems.length - 1)) {
+ currentIndex += 1;
}
- if (currentIndex !== PREV_INDEX) {
- _this.highlightRowAtIndex($listItems, currentIndex);
+ } else if (currentKeyCode === 38) {
+ // Move up
+ if (currentIndex > 0) {
+ currentIndex -= 1;
}
- return false;
}
- if (currentKeyCode === 13 && currentIndex !== -1) {
- e.preventDefault();
- _this.selectRowAtIndex();
+ if (currentIndex !== PREV_INDEX) {
+ _this.highlightRowAtIndex($listItems, currentIndex);
}
- };
- })(this));
- };
-
- GitLabDropdown.prototype.removeArrayKeyEvent = function() {
- return $('body').off('keydown');
- };
-
- GitLabDropdown.prototype.resetRows = function resetRows() {
- currentIndex = -1;
- $('.is-focused', this.dropdown).removeClass('is-focused');
- };
-
- GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
- var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
- // Remove the class for the previously focused row
- $('.is-focused', this.dropdown).removeClass('is-focused');
- // Update the class for the row at the specific index
- $listItem = $listItems.eq(index);
- $listItem.find('a:first-child').addClass("is-focused");
- // Dropdown content scroll area
- $dropdownContent = $listItem.closest('.dropdown-content');
- dropdownScrollTop = $dropdownContent.scrollTop();
- dropdownContentHeight = $dropdownContent.outerHeight();
- dropdownContentTop = $dropdownContent.prop('offsetTop');
- dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
- // Get the offset bottom of the list item
- listItemHeight = $listItem.outerHeight();
- listItemTop = $listItem.prop('offsetTop');
- listItemBottom = listItemTop + listItemHeight;
- if (!index) {
- // Scroll the dropdown content to the top
- $dropdownContent.scrollTop(0);
- } else if (index === ($listItems.length - 1)) {
- // Scroll the dropdown content to the bottom
- $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
- } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
- // Scroll the dropdown content down
- $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
- } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
- // Scroll the dropdown content up
- return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
- }
- };
+ return false;
+ }
+ if (currentKeyCode === 13 && currentIndex !== -1) {
+ e.preventDefault();
+ _this.selectRowAtIndex();
+ }
+ };
+ })(this));
+ };
- GitLabDropdown.prototype.updateLabel = function(selected, el, instance) {
- if (selected == null) {
- selected = null;
- }
- if (el == null) {
- el = null;
- }
- if (instance == null) {
- instance = null;
- }
- return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance));
- };
+ GitLabDropdown.prototype.removeArrayKeyEvent = function() {
+ return $('body').off('keydown');
+ };
- GitLabDropdown.prototype.clearField = function(field, isInput) {
- return isInput ? field.val('') : field.remove();
- };
+ GitLabDropdown.prototype.resetRows = function resetRows() {
+ currentIndex = -1;
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ };
- return GitLabDropdown;
- })();
+ GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
+ var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
+ // Remove the class for the previously focused row
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ // Update the class for the row at the specific index
+ $listItem = $listItems.eq(index);
+ $listItem.find('a:first-child').addClass("is-focused");
+ // Dropdown content scroll area
+ $dropdownContent = $listItem.closest('.dropdown-content');
+ dropdownScrollTop = $dropdownContent.scrollTop();
+ dropdownContentHeight = $dropdownContent.outerHeight();
+ dropdownContentTop = $dropdownContent.prop('offsetTop');
+ dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
+ // Get the offset bottom of the list item
+ listItemHeight = $listItem.outerHeight();
+ listItemTop = $listItem.prop('offsetTop');
+ listItemBottom = listItemTop + listItemHeight;
+ if (!index) {
+ // Scroll the dropdown content to the top
+ $dropdownContent.scrollTop(0);
+ } else if (index === ($listItems.length - 1)) {
+ // Scroll the dropdown content to the bottom
+ $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
+ } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
+ // Scroll the dropdown content down
+ $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
+ } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
+ // Scroll the dropdown content up
+ return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
+ }
+ };
- $.fn.glDropdown = function(opts) {
- return this.each(function() {
- if (!$.data(this, 'glDropdown')) {
- return $.data(this, 'glDropdown', new GitLabDropdown(this, opts));
- }
- });
+ GitLabDropdown.prototype.updateLabel = function(selected, el, instance) {
+ if (selected == null) {
+ selected = null;
+ }
+ if (el == null) {
+ el = null;
+ }
+ if (instance == null) {
+ instance = null;
+ }
+ return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance));
+ };
+
+ GitLabDropdown.prototype.clearField = function(field, isInput) {
+ return isInput ? field.val('') : field.remove();
};
-}).call(window);
+
+ return GitLabDropdown;
+})();
+
+$.fn.glDropdown = function(opts) {
+ return this.each(function() {
+ if (!$.data(this, 'glDropdown')) {
+ return $.data(this, 'glDropdown', new GitLabDropdown(this, opts));
+ }
+ });
+};
diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js
index f7cbecc0385..76de249ac3b 100644
--- a/app/assets/javascripts/gl_field_error.js
+++ b/app/assets/javascripts/gl_field_error.js
@@ -1,164 +1,162 @@
-/* eslint-disable no-param-reassign */
-((global) => {
- /*
- * This class overrides the browser's validation error bubbles, displaying custom
- * error messages for invalid fields instead. To begin validating any form, add the
- * class `gl-show-field-errors` to the form element, and ensure error messages are
- * declared in each inputs' `title` attribute. If no title is declared for an invalid
- * field the user attempts to submit, "This field is required." will be shown by default.
- *
- * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input.
- *
- * Set a custom error anchor for error message to be injected after with the
- * class `gl-field-error-anchor`
- *
- * Examples:
- *
- * Basic:
- *
- * <form class='gl-show-field-errors'>
- * <input type='text' name='username' title='Username is required.'/>
- * </form>
- *
- * Ignore specific inputs (e.g. UsernameValidator):
- *
- * <form class='gl-show-field-errors'>
- * <div class="form-group>
- * <input type='text' class='gl-field-errors-ignore' pattern='[a-zA-Z0-9-_]+'/>
- * </div>
- * <div class="form-group">
- * <input type='text' name='username' title='Username is required.'/>
- * </div>
- * </form>
- *
- * Custom Error Anchor (allows error message to be injected after specified element):
- *
- * <form class='gl-show-field-errors'>
- * <div class="form-group gl-field-error-anchor">
- * <input type='text' name='username' title='Username is required.'/>
- * // Error message typically injected here
- * </div>
- * // Error message now injected here
- * </form>
- *
- * */
-
- /*
- * Regex Patterns in use:
- *
- * Only alphanumeric: : "[a-zA-Z0-9]+"
- * No special characters : "[a-zA-Z0-9-_]+",
- *
- * */
-
- const errorMessageClass = 'gl-field-error';
- const inputErrorClass = 'gl-field-error-outline';
- const errorAnchorSelector = '.gl-field-error-anchor';
- const ignoreInputSelector = '.gl-field-error-ignore';
-
- class GlFieldError {
- constructor({ input, formErrors }) {
- this.inputElement = $(input);
- this.inputDomElement = this.inputElement.get(0);
- this.form = formErrors;
- this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
- this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`);
-
- this.state = {
- valid: false,
- empty: true,
- };
-
- this.initFieldValidation();
- }
+/**
+ * This class overrides the browser's validation error bubbles, displaying custom
+ * error messages for invalid fields instead. To begin validating any form, add the
+ * class `gl-show-field-errors` to the form element, and ensure error messages are
+ * declared in each inputs' `title` attribute. If no title is declared for an invalid
+ * field the user attempts to submit, "This field is required." will be shown by default.
+ *
+ * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input.
+ *
+ * Set a custom error anchor for error message to be injected after with the
+ * class `gl-field-error-anchor`
+ *
+ * Examples:
+ *
+ * Basic:
+ *
+ * <form class='gl-show-field-errors'>
+ * <input type='text' name='username' title='Username is required.'/>
+ * </form>
+ *
+ * Ignore specific inputs (e.g. UsernameValidator):
+ *
+ * <form class='gl-show-field-errors'>
+ * <div class="form-group>
+ * <input type='text' class='gl-field-errors-ignore' pattern='[a-zA-Z0-9-_]+'/>
+ * </div>
+ * <div class="form-group">
+ * <input type='text' name='username' title='Username is required.'/>
+ * </div>
+ * </form>
+ *
+ * Custom Error Anchor (allows error message to be injected after specified element):
+ *
+ * <form class='gl-show-field-errors'>
+ * <div class="form-group gl-field-error-anchor">
+ * <input type='text' name='username' title='Username is required.'/>
+ * // Error message typically injected here
+ * </div>
+ * // Error message now injected here
+ * </form>
+ *
+ */
+
+/**
+ * Regex Patterns in use:
+ *
+ * Only alphanumeric: : "[a-zA-Z0-9]+"
+ * No special characters : "[a-zA-Z0-9-_]+",
+ *
+ */
+
+const errorMessageClass = 'gl-field-error';
+const inputErrorClass = 'gl-field-error-outline';
+const errorAnchorSelector = '.gl-field-error-anchor';
+const ignoreInputSelector = '.gl-field-error-ignore';
+
+class GlFieldError {
+ constructor({ input, formErrors }) {
+ this.inputElement = $(input);
+ this.inputDomElement = this.inputElement.get(0);
+ this.form = formErrors;
+ this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
+ this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`);
+
+ this.state = {
+ valid: false,
+ empty: true,
+ };
+
+ this.initFieldValidation();
+ }
- initFieldValidation() {
- const customErrorAnchor = this.inputElement.parents(errorAnchorSelector);
- const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement;
+ initFieldValidation() {
+ const customErrorAnchor = this.inputElement.parents(errorAnchorSelector);
+ const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement;
- // hidden when injected into DOM
- errorAnchor.after(this.fieldErrorElement);
- this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this));
- this.scopedSiblings = this.safelySelectSiblings();
- }
+ // hidden when injected into DOM
+ errorAnchor.after(this.fieldErrorElement);
+ this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this));
+ this.scopedSiblings = this.safelySelectSiblings();
+ }
- safelySelectSiblings() {
- // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled
- const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`);
- const parentContainer = this.inputElement.parent('.form-group');
+ safelySelectSiblings() {
+ // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled
+ const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`);
+ const parentContainer = this.inputElement.parent('.form-group');
- // Only select siblings when they're scoped within a form-group with one input
- const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1;
+ // Only select siblings when they're scoped within a form-group with one input
+ const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1;
- return safelyScoped ? unignoredSiblings : this.fieldErrorElement;
- }
+ return safelyScoped ? unignoredSiblings : this.fieldErrorElement;
+ }
- renderValidity() {
- this.renderClear();
+ renderValidity() {
+ this.renderClear();
- if (this.state.valid) {
- this.renderValid();
- } else if (this.state.empty) {
- this.renderEmpty();
- } else if (!this.state.valid) {
- this.renderInvalid();
- }
+ if (this.state.valid) {
+ this.renderValid();
+ } else if (this.state.empty) {
+ this.renderEmpty();
+ } else if (!this.state.valid) {
+ this.renderInvalid();
}
+ }
- handleInvalidSubmit(event) {
- event.preventDefault();
- const currentValue = this.accessCurrentValue();
- this.state.valid = false;
- this.state.empty = currentValue === '';
-
- this.renderValidity();
- this.form.focusOnFirstInvalid.apply(this.form);
- // For UX, wait til after first invalid submission to check each keyup
- this.inputElement.off('keyup.fieldValidator')
- .on('keyup.fieldValidator', this.updateValidity.bind(this));
- }
+ handleInvalidSubmit(event) {
+ event.preventDefault();
+ const currentValue = this.accessCurrentValue();
+ this.state.valid = false;
+ this.state.empty = currentValue === '';
+
+ this.renderValidity();
+ this.form.focusOnFirstInvalid.apply(this.form);
+ // For UX, wait til after first invalid submission to check each keyup
+ this.inputElement.off('keyup.fieldValidator')
+ .on('keyup.fieldValidator', this.updateValidity.bind(this));
+ }
- /* Get or set current input value */
- accessCurrentValue(newVal) {
- return newVal ? this.inputElement.val(newVal) : this.inputElement.val();
- }
+ /* Get or set current input value */
+ accessCurrentValue(newVal) {
+ return newVal ? this.inputElement.val(newVal) : this.inputElement.val();
+ }
- getInputValidity() {
- return this.inputDomElement.validity.valid;
- }
+ getInputValidity() {
+ return this.inputDomElement.validity.valid;
+ }
- updateValidity() {
- const inputVal = this.accessCurrentValue();
- this.state.empty = !inputVal.length;
- this.state.valid = this.getInputValidity();
- this.renderValidity();
- }
+ updateValidity() {
+ const inputVal = this.accessCurrentValue();
+ this.state.empty = !inputVal.length;
+ this.state.valid = this.getInputValidity();
+ this.renderValidity();
+ }
- renderValid() {
- return this.renderClear();
- }
+ renderValid() {
+ return this.renderClear();
+ }
- renderEmpty() {
- return this.renderInvalid();
- }
+ renderEmpty() {
+ return this.renderInvalid();
+ }
- renderInvalid() {
- this.inputElement.addClass(inputErrorClass);
- this.scopedSiblings.hide();
- return this.fieldErrorElement.show();
- }
+ renderInvalid() {
+ this.inputElement.addClass(inputErrorClass);
+ this.scopedSiblings.hide();
+ return this.fieldErrorElement.show();
+ }
- renderClear() {
- const inputVal = this.accessCurrentValue();
- if (!inputVal.split(' ').length) {
- const trimmedInput = inputVal.trim();
- this.accessCurrentValue(trimmedInput);
- }
- this.inputElement.removeClass(inputErrorClass);
- this.scopedSiblings.hide();
- this.fieldErrorElement.hide();
+ renderClear() {
+ const inputVal = this.accessCurrentValue();
+ if (!inputVal.split(' ').length) {
+ const trimmedInput = inputVal.trim();
+ this.accessCurrentValue(trimmedInput);
}
+ this.inputElement.removeClass(inputErrorClass);
+ this.scopedSiblings.hide();
+ this.fieldErrorElement.hide();
}
+}
- global.GlFieldError = GlFieldError;
-})(window.gl || (window.gl = {}));
+window.gl = window.gl || {};
+window.gl.GlFieldError = GlFieldError;
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index e9add115429..636258ec555 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -2,47 +2,46 @@
require('./gl_field_error');
-((global) => {
- const customValidationFlag = 'gl-field-error-ignore';
-
- class GlFieldErrors {
- constructor(form) {
- this.form = $(form);
- this.state = {
- inputs: [],
- valid: false
- };
- this.initValidators();
- }
+const customValidationFlag = 'gl-field-error-ignore';
+
+class GlFieldErrors {
+ constructor(form) {
+ this.form = $(form);
+ this.state = {
+ inputs: [],
+ valid: false
+ };
+ this.initValidators();
+ }
- initValidators () {
- // register selectors here as needed
- const validateSelectors = [':text', ':password', '[type=email]']
- .map((selector) => `input${selector}`).join(',');
+ initValidators () {
+ // register selectors here as needed
+ const validateSelectors = [':text', ':password', '[type=email]']
+ .map((selector) => `input${selector}`).join(',');
- this.state.inputs = this.form.find(validateSelectors).toArray()
- .filter((input) => !input.classList.contains(customValidationFlag))
- .map((input) => new global.GlFieldError({ input, formErrors: this }));
+ this.state.inputs = this.form.find(validateSelectors).toArray()
+ .filter((input) => !input.classList.contains(customValidationFlag))
+ .map((input) => new window.gl.GlFieldError({ input, formErrors: this }));
- this.form.on('submit', this.catchInvalidFormSubmit);
- }
+ this.form.on('submit', this.catchInvalidFormSubmit);
+ }
- /* Neccessary to prevent intercept and override invalid form submit
- * because Safari & iOS quietly allow form submission when form is invalid
- * and prevents disabling of invalid submit button by application.js */
+ /* Neccessary to prevent intercept and override invalid form submit
+ * because Safari & iOS quietly allow form submission when form is invalid
+ * and prevents disabling of invalid submit button by application.js */
- catchInvalidFormSubmit (event) {
- if (!event.currentTarget.checkValidity()) {
- event.preventDefault();
- event.stopPropagation();
- }
+ catchInvalidFormSubmit (event) {
+ if (!event.currentTarget.checkValidity()) {
+ event.preventDefault();
+ event.stopPropagation();
}
+ }
- focusOnFirstInvalid () {
- const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
- firstInvalid.inputElement.focus();
- }
+ focusOnFirstInvalid () {
+ const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
+ firstInvalid.inputElement.focus();
}
+}
- global.GlFieldErrors = GlFieldErrors;
-})(window.gl || (window.gl = {}));
+window.gl = window.gl || {};
+window.gl.GlFieldErrors = GlFieldErrors;
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 0b446ff364a..e7c98e16581 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -3,90 +3,88 @@
/* global DropzoneInput */
/* global autosize */
-(() => {
- const global = window.gl || (window.gl = {});
+window.gl = window.gl || {};
- function GLForm(form) {
- this.form = form;
- this.textarea = this.form.find('textarea.js-gfm-input');
- // Before we start, we should clean up any previous data for this form
- this.destroy();
- // Setup the form
- this.setupForm();
- this.form.data('gl-form', this);
- }
+function GLForm(form) {
+ this.form = form;
+ this.textarea = this.form.find('textarea.js-gfm-input');
+ // Before we start, we should clean up any previous data for this form
+ this.destroy();
+ // Setup the form
+ this.setupForm();
+ this.form.data('gl-form', this);
+}
- GLForm.prototype.destroy = function() {
- // Clean form listeners
- this.clearEventListeners();
- return this.form.data('gl-form', null);
- };
+GLForm.prototype.destroy = function() {
+ // Clean form listeners
+ this.clearEventListeners();
+ return this.form.data('gl-form', null);
+};
- GLForm.prototype.setupForm = function() {
- var isNewForm;
- isNewForm = this.form.is(':not(.gfm-form)');
- this.form.removeClass('js-new-note-form');
- if (isNewForm) {
- this.form.find('.div-dropzone').remove();
- this.form.addClass('gfm-form');
- // remove notify commit author checkbox for non-commit notes
- gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
- gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
- new DropzoneInput(this.form);
- autosize(this.textarea);
- // form and textarea event listeners
- this.addEventListeners();
- }
- gl.text.init(this.form);
- // hide discard button
- this.form.find('.js-note-discard').hide();
- this.form.show();
- if (this.isAutosizeable) this.setupAutosize();
- };
+GLForm.prototype.setupForm = function() {
+ var isNewForm;
+ isNewForm = this.form.is(':not(.gfm-form)');
+ this.form.removeClass('js-new-note-form');
+ if (isNewForm) {
+ this.form.find('.div-dropzone').remove();
+ this.form.addClass('gfm-form');
+ // remove notify commit author checkbox for non-commit notes
+ gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
+ gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+ new DropzoneInput(this.form);
+ autosize(this.textarea);
+ // form and textarea event listeners
+ this.addEventListeners();
+ }
+ gl.text.init(this.form);
+ // hide discard button
+ this.form.find('.js-note-discard').hide();
+ this.form.show();
+ if (this.isAutosizeable) this.setupAutosize();
+};
- GLForm.prototype.setupAutosize = function () {
- this.textarea.off('autosize:resized')
- .on('autosize:resized', this.setHeightData.bind(this));
+GLForm.prototype.setupAutosize = function () {
+ this.textarea.off('autosize:resized')
+ .on('autosize:resized', this.setHeightData.bind(this));
- this.textarea.off('mouseup.autosize')
- .on('mouseup.autosize', this.destroyAutosize.bind(this));
+ this.textarea.off('mouseup.autosize')
+ .on('mouseup.autosize', this.destroyAutosize.bind(this));
- setTimeout(() => {
- autosize(this.textarea);
- this.textarea.css('resize', 'vertical');
- }, 0);
- };
+ setTimeout(() => {
+ autosize(this.textarea);
+ this.textarea.css('resize', 'vertical');
+ }, 0);
+};
- GLForm.prototype.setHeightData = function () {
- this.textarea.data('height', this.textarea.outerHeight());
- };
+GLForm.prototype.setHeightData = function () {
+ this.textarea.data('height', this.textarea.outerHeight());
+};
- GLForm.prototype.destroyAutosize = function () {
- const outerHeight = this.textarea.outerHeight();
+GLForm.prototype.destroyAutosize = function () {
+ const outerHeight = this.textarea.outerHeight();
- if (this.textarea.data('height') === outerHeight) return;
+ if (this.textarea.data('height') === outerHeight) return;
- autosize.destroy(this.textarea);
+ autosize.destroy(this.textarea);
- this.textarea.data('height', outerHeight);
- this.textarea.outerHeight(outerHeight);
- this.textarea.css('max-height', window.outerHeight);
- };
+ this.textarea.data('height', outerHeight);
+ this.textarea.outerHeight(outerHeight);
+ this.textarea.css('max-height', window.outerHeight);
+};
- GLForm.prototype.clearEventListeners = function() {
- this.textarea.off('focus');
- this.textarea.off('blur');
- return gl.text.removeListeners(this.form);
- };
+GLForm.prototype.clearEventListeners = function() {
+ this.textarea.off('focus');
+ this.textarea.off('blur');
+ return gl.text.removeListeners(this.form);
+};
- GLForm.prototype.addEventListeners = function() {
- this.textarea.on('focus', function() {
- return $(this).closest('.md-area').addClass('is-focused');
- });
- return this.textarea.on('blur', function() {
- return $(this).closest('.md-area').removeClass('is-focused');
- });
- };
+GLForm.prototype.addEventListeners = function() {
+ this.textarea.on('focus', function() {
+ return $(this).closest('.md-area').addClass('is-focused');
+ });
+ return this.textarea.on('blur', function() {
+ return $(this).closest('.md-area').removeClass('is-focused');
+ });
+};
- global.GLForm = GLForm;
-})();
+window.gl.GLForm = GLForm;
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index c5cb273c5b2..f03b47b1c1d 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -1,20 +1,19 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
-(function() {
- this.GroupAvatar = (function() {
- function GroupAvatar() {
- $('.js-choose-group-avatar-button').on("click", function() {
- var form;
- form = $(this).closest("form");
- return form.find(".js-group-avatar-input").click();
- });
- $('.js-group-avatar-input').on("change", function() {
- var filename, form;
- form = $(this).closest("form");
- filename = $(this).val().replace(/^.*[\\\/]/, '');
- return form.find(".js-avatar-filename").text(filename);
- });
- }
- return GroupAvatar;
- })();
-}).call(window);
+window.GroupAvatar = (function() {
+ function GroupAvatar() {
+ $('.js-choose-group-avatar-button').on("click", function() {
+ var form;
+ form = $(this).closest("form");
+ return form.find(".js-group-avatar-input").click();
+ });
+ $('.js-group-avatar-input').on("change", function() {
+ var filename, form;
+ form = $(this).closest("form");
+ filename = $(this).val().replace(/^.*[\\\/]/, '');
+ return form.find(".js-avatar-filename").text(filename);
+ });
+ }
+
+ return GroupAvatar;
+})();
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
index 15e695e81cf..7dc9ce898e8 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -1,53 +1,52 @@
/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */
-(function(global) {
- class GroupLabelSubscription {
- constructor(container) {
- const $container = $(container);
- this.$dropdown = $container.find('.dropdown');
- this.$subscribeButtons = $container.find('.js-subscribe-button');
- this.$unsubscribeButtons = $container.find('.js-unsubscribe-button');
-
- this.$subscribeButtons.on('click', this.subscribe.bind(this));
- this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this));
- }
-
- unsubscribe(event) {
- event.preventDefault();
-
- const url = this.$unsubscribeButtons.attr('data-url');
-
- $.ajax({
- type: 'POST',
- url: url
- }).done(() => {
- this.toggleSubscriptionButtons();
- this.$unsubscribeButtons.removeAttr('data-url');
- });
- }
-
- subscribe(event) {
- event.preventDefault();
-
- const $btn = $(event.currentTarget);
- const url = $btn.attr('data-url');
-
- this.$unsubscribeButtons.attr('data-url', url);
-
- $.ajax({
- type: 'POST',
- url: url
- }).done(() => {
- this.toggleSubscriptionButtons();
- });
- }
-
- toggleSubscriptionButtons() {
- this.$dropdown.toggleClass('hidden');
- this.$subscribeButtons.toggleClass('hidden');
- this.$unsubscribeButtons.toggleClass('hidden');
- }
+class GroupLabelSubscription {
+ constructor(container) {
+ const $container = $(container);
+ this.$dropdown = $container.find('.dropdown');
+ this.$subscribeButtons = $container.find('.js-subscribe-button');
+ this.$unsubscribeButtons = $container.find('.js-unsubscribe-button');
+
+ this.$subscribeButtons.on('click', this.subscribe.bind(this));
+ this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this));
}
- global.GroupLabelSubscription = GroupLabelSubscription;
-})(window.gl || (window.gl = {}));
+ unsubscribe(event) {
+ event.preventDefault();
+
+ const url = this.$unsubscribeButtons.attr('data-url');
+
+ $.ajax({
+ type: 'POST',
+ url: url
+ }).done(() => {
+ this.toggleSubscriptionButtons();
+ this.$unsubscribeButtons.removeAttr('data-url');
+ });
+ }
+
+ subscribe(event) {
+ event.preventDefault();
+
+ const $btn = $(event.currentTarget);
+ const url = $btn.attr('data-url');
+
+ this.$unsubscribeButtons.attr('data-url', url);
+
+ $.ajax({
+ type: 'POST',
+ url: url
+ }).done(() => {
+ this.toggleSubscriptionButtons();
+ });
+ }
+
+ toggleSubscriptionButtons() {
+ this.$dropdown.toggleClass('hidden');
+ this.$subscribeButtons.toggleClass('hidden');
+ this.$unsubscribeButtons.toggleClass('hidden');
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.GroupLabelSubscription = GroupLabelSubscription;
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 6b937e7fa0f..e5dfa30edab 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,71 +1,69 @@
/* 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 */
/* global Api */
-(function() {
- var slice = [].slice;
+var slice = [].slice;
- this.GroupsSelect = (function() {
- function GroupsSelect() {
- $('.ajax-groups-select').each((function(_this) {
- return function(i, select) {
- var all_available, skip_groups;
- all_available = $(select).data('all-available');
- skip_groups = $(select).data('skip-groups') || [];
- return $(select).select2({
- placeholder: "Search for a group",
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query: function(query) {
- var options = { all_available: all_available, skip_groups: skip_groups };
- return Api.groups(query.term, options, function(groups) {
- var data;
- data = {
- results: groups
- };
- return query.callback(data);
- });
- },
- initSelection: function(element, callback) {
- var id;
- id = $(element).val();
- if (id !== "") {
- return Api.group(id, callback);
- }
- },
- formatResult: function() {
- var args;
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.formatResult.apply(_this, args);
- },
- formatSelection: function() {
- var args;
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.formatSelection.apply(_this, args);
- },
- dropdownCssClass: "ajax-groups-dropdown",
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup: function(m) {
- return m;
+window.GroupsSelect = (function() {
+ function GroupsSelect() {
+ $('.ajax-groups-select').each((function(_this) {
+ return function(i, select) {
+ var all_available, skip_groups;
+ all_available = $(select).data('all-available');
+ skip_groups = $(select).data('skip-groups') || [];
+ return $(select).select2({
+ placeholder: "Search for a group",
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query: function(query) {
+ var options = { all_available: all_available, skip_groups: skip_groups };
+ return Api.groups(query.term, options, function(groups) {
+ var data;
+ data = {
+ results: groups
+ };
+ return query.callback(data);
+ });
+ },
+ initSelection: function(element, callback) {
+ var id;
+ id = $(element).val();
+ if (id !== "") {
+ return Api.group(id, callback);
}
- });
- };
- })(this));
- }
+ },
+ formatResult: function() {
+ var args;
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+ return _this.formatResult.apply(_this, args);
+ },
+ formatSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+ return _this.formatSelection.apply(_this, args);
+ },
+ dropdownCssClass: "ajax-groups-dropdown",
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup: function(m) {
+ return m;
+ }
+ });
+ };
+ })(this));
+ }
- GroupsSelect.prototype.formatResult = function(group) {
- var avatar;
- if (group.avatar_url) {
- avatar = group.avatar_url;
- } else {
- avatar = gon.default_avatar_url;
- }
- return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
- };
+ GroupsSelect.prototype.formatResult = function(group) {
+ var avatar;
+ if (group.avatar_url) {
+ avatar = group.avatar_url;
+ } else {
+ avatar = gon.default_avatar_url;
+ }
+ return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
+ };
- GroupsSelect.prototype.formatSelection = function(group) {
- return group.full_name;
- };
+ GroupsSelect.prototype.formatSelection = function(group) {
+ return group.full_name;
+ };
- return GroupsSelect;
- })();
-}).call(window);
+ return GroupsSelect;
+})();
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index a853c3aeb1f..34f44dad7a5 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,8 +1,7 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, no-var, max-len */
-(function() {
- $(document).on('todo:toggle', function(e, count) {
- var $todoPendingCount = $('.todos-pending-count');
- $todoPendingCount.text(gl.text.highCountTrim(count));
- $todoPendingCount.toggleClass('hidden', count === 0);
- });
-})();
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */
+
+$(document).on('todo:toggle', function(e, count) {
+ var $todoPendingCount = $('.todos-pending-count');
+ $todoPendingCount.text(gl.text.highCountTrim(count));
+ $todoPendingCount.toggleClass('hidden', count === 0);
+});
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index 966fcd8ec47..1821ca18053 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -67,17 +67,7 @@ require('vendor/jquery.scrollTo');
}
LineHighlighter.prototype.bindEvents = function() {
- $('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.clickHandler);
- // While it may seem odd to bind to the mousedown event and then throw away
- // the click event, there is a method to our madness.
- //
- // If not done this way, the line number anchor will sometimes keep its
- // active state even when the event is cancelled, resulting in an ugly border
- // around the link and/or a persisted underline text decoration.
- $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) {
- event.preventDefault();
- event.stopPropagation();
- });
+ $('#blob-content-holder').on('click', 'a[data-line-number]', this.clickHandler);
};
LineHighlighter.prototype.clickHandler = function(event) {
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 689a6c3a93a..81d5748191d 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -16,17 +16,9 @@ import Sortable from 'vendor/Sortable';
import 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause';
import 'vendor/fuzzaldrin-plus';
-import promisePolyfill from 'es6-promise';
// extensions
-import './extensions/string';
import './extensions/array';
-import './extensions/custom_event';
-import './extensions/element';
-import './extensions/jquery';
-import './extensions/object';
-
-promisePolyfill.polyfill();
// expose common libraries as globals (TODO: remove these)
window.jQuery = jQuery;
@@ -66,6 +58,8 @@ import './blob/blob_gitignore_selectors';
import './blob/blob_license_selector';
import './blob/blob_license_selectors';
import './blob/template_selector';
+import './blob/create_branch_dropdown';
+import './blob/target_branch_dropdown';
// templates
import './templates/issuable_template_selector';
@@ -204,189 +198,187 @@ import './visibility_select';
import './wikis';
import './zen_mode';
-(function () {
- document.addEventListener('beforeunload', function () {
- // Unbind scroll events
- $(document).off('scroll');
- // Close any open tooltips
- $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
- });
-
- window.addEventListener('hashchange', gl.utils.handleLocationHash);
- window.addEventListener('load', function onLoad() {
- window.removeEventListener('load', onLoad, false);
- gl.utils.handleLocationHash();
- }, false);
+document.addEventListener('beforeunload', function () {
+ // Unbind scroll events
+ $(document).off('scroll');
+ // Close any open tooltips
+ $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
+});
- $(function () {
- var $body = $('body');
- var $document = $(document);
- var $window = $(window);
- var $sidebarGutterToggle = $('.js-sidebar-toggle');
- var $flash = $('.flash-container');
- var bootstrapBreakpoint = bp.getBreakpointSize();
- var fitSidebarForSize;
+window.addEventListener('hashchange', gl.utils.handleLocationHash);
+window.addEventListener('load', function onLoad() {
+ window.removeEventListener('load', onLoad, false);
+ gl.utils.handleLocationHash();
+}, false);
- // Set the default path for all cookies to GitLab's root directory
- Cookies.defaults.path = gon.relative_url_root || '/';
+$(function () {
+ var $body = $('body');
+ var $document = $(document);
+ var $window = $(window);
+ var $sidebarGutterToggle = $('.js-sidebar-toggle');
+ var $flash = $('.flash-container');
+ var bootstrapBreakpoint = bp.getBreakpointSize();
+ var fitSidebarForSize;
- // `hashchange` is not triggered when link target is already in window.location
- $body.on('click', 'a[href^="#"]', function() {
- var href = this.getAttribute('href');
- if (href.substr(1) === gl.utils.getLocationHash()) {
- setTimeout(gl.utils.handleLocationHash, 1);
- }
- });
+ // Set the default path for all cookies to GitLab's root directory
+ Cookies.defaults.path = gon.relative_url_root || '/';
- // prevent default action for disabled buttons
- $('.btn').click(function(e) {
- if ($(this).hasClass('disabled')) {
- e.preventDefault();
- e.stopImmediatePropagation();
- return false;
- }
- });
+ // `hashchange` is not triggered when link target is already in window.location
+ $body.on('click', 'a[href^="#"]', function() {
+ var href = this.getAttribute('href');
+ if (href.substr(1) === gl.utils.getLocationHash()) {
+ setTimeout(gl.utils.handleLocationHash, 1);
+ }
+ });
- $('.js-select-on-focus').on('focusin', function () {
- return $(this).select().one('mouseup', function (e) {
- return e.preventDefault();
- });
- // Click a .js-select-on-focus field, select the contents
- // Prevent a mouseup event from deselecting the input
- });
- $('.remove-row').bind('ajax:success', function () {
- $(this).tooltip('destroy')
- .closest('li')
- .fadeOut();
- });
- $('.js-remove-tr').bind('ajax:before', function () {
- return $(this).hide();
- });
- $('.js-remove-tr').bind('ajax:success', function () {
- return $(this).closest('tr').fadeOut();
- });
- $('select.select2').select2({
- width: 'resolve',
- // Initialize select2 selects
- dropdownAutoWidth: true
- });
- $('.js-select2').bind('select2-close', function () {
- return setTimeout((function () {
- $('.select2-container-active').removeClass('select2-container-active');
- return $(':focus').blur();
- }), 1);
- // Close select2 on escape
- });
- // Initialize tooltips
- $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover';
- $body.tooltip({
- selector: '.has-tooltip, [data-toggle="tooltip"]',
- placement: function (tip, el) {
- return $(el).data('placement') || 'bottom';
- }
- });
- $('.trigger-submit').on('change', function () {
- return $(this).parents('form').submit();
- // Form submitter
- });
- gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
- // Flash
- if ($flash.length > 0) {
- $flash.click(function () {
- return $(this).fadeOut();
- });
- $flash.show();
+ // prevent default action for disabled buttons
+ $('.btn').click(function(e) {
+ if ($(this).hasClass('disabled')) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ return false;
}
- // Disable form buttons while a form is submitting
- $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
- var buttons;
- buttons = $('[type="submit"]', this);
- switch (e.type) {
- case 'ajax:beforeSend':
- case 'submit':
- return buttons.disable();
- default:
- return buttons.enable();
- }
- });
- $(document).ajaxError(function (e, xhrObj) {
- var ref = xhrObj.status;
- if (xhrObj.status === 401) {
- return new Flash('You need to be logged in.', 'alert');
- } else if (ref === 404 || ref === 500) {
- return new Flash('Something went wrong on our end.', 'alert');
- }
- });
- $('.account-box').hover(function () {
- // Show/Hide the profile menu when hovering the account box
- return $(this).toggleClass('hover');
- });
- $document.on('click', '.diff-content .js-show-suppressed-diff', function () {
- var $container;
- $container = $(this).parent();
- $container.next('table').show();
- return $container.remove();
- // Commit show suppressed diff
- });
- $('.navbar-toggle').on('click', function () {
- $('.header-content .title').toggle();
- $('.header-content .header-logo').toggle();
- $('.header-content .navbar-collapse').toggle();
- return $('.navbar-toggle').toggleClass('active');
- });
- // Show/hide comments on diff
- $body.on('click', '.js-toggle-diff-comments', function (e) {
- var $this = $(this);
- var notesHolders = $this.closest('.diff-file').find('.notes_holder');
- $this.toggleClass('active');
- if ($this.hasClass('active')) {
- notesHolders.show().find('.hide, .content').show();
- } else {
- notesHolders.hide().find('.content').hide();
- }
- $(document).trigger('toggle.comments');
+ });
+
+ $('.js-select-on-focus').on('focusin', function () {
+ return $(this).select().one('mouseup', function (e) {
return e.preventDefault();
});
- $document.off('click', '.js-confirm-danger');
- $document.on('click', '.js-confirm-danger', function (e) {
- var btn = $(e.target);
- var form = btn.closest('form');
- var text = btn.data('confirm-danger-message');
- e.preventDefault();
- return new ConfirmDangerModal(form, text);
- });
- $('input[type="search"]').each(function () {
- var $this = $(this);
- $this.attr('value', $this.val());
- });
- $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () {
- var $this;
- $this = $(this);
- return $this.attr('value', $this.val());
- });
- $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) {
- var $gutterIcon;
- if (breakpoint === 'sm' || breakpoint === 'xs') {
- $gutterIcon = $sidebarGutterToggle.find('i');
- if ($gutterIcon.hasClass('fa-angle-double-right')) {
- return $sidebarGutterToggle.trigger('click');
- }
- }
+ // Click a .js-select-on-focus field, select the contents
+ // Prevent a mouseup event from deselecting the input
+ });
+ $('.remove-row').bind('ajax:success', function () {
+ $(this).tooltip('destroy')
+ .closest('li')
+ .fadeOut();
+ });
+ $('.js-remove-tr').bind('ajax:before', function () {
+ return $(this).hide();
+ });
+ $('.js-remove-tr').bind('ajax:success', function () {
+ return $(this).closest('tr').fadeOut();
+ });
+ $('select.select2').select2({
+ width: 'resolve',
+ // Initialize select2 selects
+ dropdownAutoWidth: true
+ });
+ $('.js-select2').bind('select2-close', function () {
+ return setTimeout((function () {
+ $('.select2-container-active').removeClass('select2-container-active');
+ return $(':focus').blur();
+ }), 1);
+ // Close select2 on escape
+ });
+ // Initialize tooltips
+ $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover';
+ $body.tooltip({
+ selector: '.has-tooltip, [data-toggle="tooltip"]',
+ placement: function (tip, el) {
+ return $(el).data('placement') || 'bottom';
+ }
+ });
+ $('.trigger-submit').on('change', function () {
+ return $(this).parents('form').submit();
+ // Form submitter
+ });
+ gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
+ // Flash
+ if ($flash.length > 0) {
+ $flash.click(function () {
+ return $(this).fadeOut();
});
- fitSidebarForSize = function () {
- var oldBootstrapBreakpoint;
- oldBootstrapBreakpoint = bootstrapBreakpoint;
- bootstrapBreakpoint = bp.getBreakpointSize();
- if (bootstrapBreakpoint !== oldBootstrapBreakpoint) {
- return $document.trigger('breakpoint:change', [bootstrapBreakpoint]);
+ $flash.show();
+ }
+ // Disable form buttons while a form is submitting
+ $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
+ var buttons;
+ buttons = $('[type="submit"]', this);
+ switch (e.type) {
+ case 'ajax:beforeSend':
+ case 'submit':
+ return buttons.disable();
+ default:
+ return buttons.enable();
+ }
+ });
+ $(document).ajaxError(function (e, xhrObj) {
+ var ref = xhrObj.status;
+ if (xhrObj.status === 401) {
+ return new Flash('You need to be logged in.', 'alert');
+ } else if (ref === 404 || ref === 500) {
+ return new Flash('Something went wrong on our end.', 'alert');
+ }
+ });
+ $('.account-box').hover(function () {
+ // Show/Hide the profile menu when hovering the account box
+ return $(this).toggleClass('hover');
+ });
+ $document.on('click', '.diff-content .js-show-suppressed-diff', function () {
+ var $container;
+ $container = $(this).parent();
+ $container.next('table').show();
+ return $container.remove();
+ // Commit show suppressed diff
+ });
+ $('.navbar-toggle').on('click', function () {
+ $('.header-content .title').toggle();
+ $('.header-content .header-logo').toggle();
+ $('.header-content .navbar-collapse').toggle();
+ return $('.navbar-toggle').toggleClass('active');
+ });
+ // Show/hide comments on diff
+ $body.on('click', '.js-toggle-diff-comments', function (e) {
+ var $this = $(this);
+ var notesHolders = $this.closest('.diff-file').find('.notes_holder');
+ $this.toggleClass('active');
+ if ($this.hasClass('active')) {
+ notesHolders.show().find('.hide, .content').show();
+ } else {
+ notesHolders.hide().find('.content').hide();
+ }
+ $(document).trigger('toggle.comments');
+ return e.preventDefault();
+ });
+ $document.off('click', '.js-confirm-danger');
+ $document.on('click', '.js-confirm-danger', function (e) {
+ var btn = $(e.target);
+ var form = btn.closest('form');
+ var text = btn.data('confirm-danger-message');
+ e.preventDefault();
+ return new ConfirmDangerModal(form, text);
+ });
+ $('input[type="search"]').each(function () {
+ var $this = $(this);
+ $this.attr('value', $this.val());
+ });
+ $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () {
+ var $this;
+ $this = $(this);
+ return $this.attr('value', $this.val());
+ });
+ $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) {
+ var $gutterIcon;
+ if (breakpoint === 'sm' || breakpoint === 'xs') {
+ $gutterIcon = $sidebarGutterToggle.find('i');
+ if ($gutterIcon.hasClass('fa-angle-double-right')) {
+ return $sidebarGutterToggle.trigger('click');
}
- };
- $window.off('resize.app').on('resize.app', function () {
- return fitSidebarForSize();
- });
- gl.awardsHandler = new AwardsHandler();
- new Aside();
-
- gl.utils.initTimeagoTimeout();
+ }
});
-}).call(window);
+ fitSidebarForSize = function () {
+ var oldBootstrapBreakpoint;
+ oldBootstrapBreakpoint = bootstrapBreakpoint;
+ bootstrapBreakpoint = bp.getBreakpointSize();
+ if (bootstrapBreakpoint !== oldBootstrapBreakpoint) {
+ return $document.trigger('breakpoint:change', [bootstrapBreakpoint]);
+ }
+ };
+ $window.off('resize.app').on('resize.app', function () {
+ return fitSidebarForSize();
+ });
+ gl.awardsHandler = new AwardsHandler();
+ new Aside();
+
+ gl.utils.initTimeagoTimeout();
+});
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index 9384fe3f276..71eb746edac 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -1,9 +1,11 @@
-/* eslint-disable no-new*/
+/* eslint-disable no-new */
+/* global Flash */
+
import d3 from 'd3';
import _ from 'underscore';
import statusCodes from '~/lib/utils/http_status';
import '~/lib/utils/common_utils';
-import Flash from '~/flash';
+import '~/flash';
const prometheusGraphsContainer = '.prometheus-graph';
const metricsEndpoint = 'metrics.json';
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 747f693726e..ad36f08840d 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -3,19 +3,23 @@
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.NewCommitForm = (function() {
- function NewCommitForm(form) {
+ function NewCommitForm(form, targetBranchName = 'target_branch') {
+ this.form = form;
+ this.targetBranchName = targetBranchName;
this.renderDestination = bind(this.renderDestination, this);
- this.newBranch = form.find('.js-target-branch');
+ this.targetBranchDropdown = form.find('button.js-target-branch');
this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
+ this.targetBranchDropdown.on('change.branch', this.renderDestination);
this.renderDestination();
- this.newBranch.keyup(this.renderDestination);
}
NewCommitForm.prototype.renderDestination = function() {
var different;
- different = this.newBranch.val() !== this.originalBranch.val();
+ var targetBranch = this.form.find(`input[name="${this.targetBranchName}"]`);
+
+ different = targetBranch.val() !== this.originalBranch.val();
if (different) {
this.createMergeRequestContainer.show();
if (!this.wasDifferent) {
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
index e9513725d9d..caaf6484a34 100644
--- a/app/assets/javascripts/todos.js
+++ b/app/assets/javascripts/todos.js
@@ -1,146 +1,146 @@
-/* eslint-disable class-methods-use-this, no-new, func-names, no-unneeded-ternary, object-shorthand, quote-props, no-param-reassign, max-len */
+/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
/* global UsersSelect */
-((global) => {
- class Todos {
- constructor() {
- this.initFilters();
- this.bindEvents();
+class Todos {
+ constructor() {
+ this.initFilters();
+ this.bindEvents();
- this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('beforeunload', this.cleanupWrapper);
- }
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
+ }
- cleanup() {
- this.unbindEvents();
- document.removeEventListener('beforeunload', this.cleanupWrapper);
- }
+ cleanup() {
+ this.unbindEvents();
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
+ }
- unbindEvents() {
- $('.js-done-todo, .js-undo-todo').off('click', this.updateStateClickedWrapper);
- $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper);
- $('.todo').off('click', this.goToTodoUrl);
- }
+ unbindEvents() {
+ $('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper);
+ $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper);
+ $('.todo').off('click', this.goToTodoUrl);
+ }
- bindEvents() {
- this.updateStateClickedWrapper = this.updateStateClicked.bind(this);
- this.allDoneClickedWrapper = this.allDoneClicked.bind(this);
+ bindEvents() {
+ this.updateRowStateClickedWrapper = this.updateRowStateClicked.bind(this);
+ this.allDoneClickedWrapper = this.allDoneClicked.bind(this);
- $('.js-done-todo, .js-undo-todo').on('click', this.updateStateClickedWrapper);
- $('.js-todos-mark-all').on('click', this.allDoneClickedWrapper);
- $('.todo').on('click', this.goToTodoUrl);
- }
+ $('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper);
+ $('.js-todos-mark-all').on('click', this.allDoneClickedWrapper);
+ $('.todo').on('click', this.goToTodoUrl);
+ }
- initFilters() {
- new UsersSelect();
- this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
- this.initFilterDropdown($('.js-type-search'), 'type');
- this.initFilterDropdown($('.js-action-search'), 'action_id');
+ initFilters() {
+ this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
+ this.initFilterDropdown($('.js-type-search'), 'type');
+ this.initFilterDropdown($('.js-action-search'), 'action_id');
- $('form.filter-form').on('submit', function (event) {
- event.preventDefault();
- gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
- });
- }
+ $('form.filter-form').on('submit', function applyFilters(event) {
+ event.preventDefault();
+ gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
+ });
+ return new UsersSelect();
+ }
- initFilterDropdown($dropdown, fieldName, searchFields) {
- $dropdown.glDropdown({
- fieldName,
- selectable: true,
- filterable: searchFields ? true : false,
- search: { fields: searchFields },
- data: $dropdown.data('data'),
- clicked: function () {
- return $dropdown.closest('form.filter-form').submit();
- },
- });
- }
+ initFilterDropdown($dropdown, fieldName, searchFields) {
+ $dropdown.glDropdown({
+ fieldName,
+ selectable: true,
+ filterable: searchFields ? true : false,
+ search: { fields: searchFields },
+ data: $dropdown.data('data'),
+ clicked: () => $dropdown.closest('form.filter-form').submit(),
+ });
+ }
- updateStateClicked(e) {
- e.preventDefault();
- const target = e.target;
- target.setAttribute('disabled', '');
- target.classList.add('disabled');
- $.ajax({
- type: 'POST',
- url: target.getAttribute('href'),
- dataType: 'json',
- data: {
- '_method': target.getAttribute('data-method'),
- },
- success: (data) => {
- this.updateState(target);
- this.updateBadges(data);
- },
- });
- }
+ updateRowStateClicked(e) {
+ e.preventDefault();
+
+ const target = e.target;
+ target.setAttribute('disabled', '');
+ target.classList.add('disabled');
+ $.ajax({
+ type: 'POST',
+ url: target.getAttribute('href'),
+ dataType: 'json',
+ data: {
+ '_method': target.getAttribute('data-method'),
+ },
+ success: (data) => {
+ this.updateRowState(target);
+ return this.updateBadges(data);
+ },
+ });
+ }
- allDoneClicked(e) {
- e.preventDefault();
- const $target = $(e.currentTarget);
- $target.disable();
- $.ajax({
- type: 'POST',
- url: $target.attr('href'),
- dataType: 'json',
- data: {
- '_method': 'delete',
- },
- success: (data) => {
- $target.remove();
- $('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
- this.updateBadges(data);
- },
- });
+ allDoneClicked(e) {
+ e.preventDefault();
+ const $target = $(e.currentTarget);
+ $target.disable();
+ $.ajax({
+ type: 'POST',
+ url: $target.attr('href'),
+ dataType: 'json',
+ data: {
+ '_method': 'delete',
+ },
+ success: (data) => {
+ $target.remove();
+ $('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
+ this.updateBadges(data);
+ },
+ });
+ }
+
+ updateRowState(target) {
+ const row = target.closest('li');
+ const restoreBtn = row.querySelector('.js-undo-todo');
+ const doneBtn = row.querySelector('.js-done-todo');
+
+ target.classList.add('hidden');
+ target.removeAttribute('disabled');
+ target.classList.remove('disabled');
+
+ if (target === doneBtn) {
+ row.classList.add('done-reversible');
+ restoreBtn.classList.remove('hidden');
+ } else if (target === restoreBtn) {
+ row.classList.remove('done-reversible');
+ doneBtn.classList.remove('hidden');
+ } else {
+ row.parentNode.removeChild(row);
}
+ }
- updateState(target) {
- const row = target.closest('li');
- const restoreBtn = row.querySelector('.js-undo-todo');
- const doneBtn = row.querySelector('.js-done-todo');
+ updateBadges(data) {
+ $(document).trigger('todo:toggle', data.count);
+ document.querySelector('.todos-pending .badge').innerHTML = data.count;
+ document.querySelector('.todos-done .badge').innerHTML = data.done_count;
+ }
- target.removeAttribute('disabled');
- target.classList.remove('disabled');
- target.classList.add('hidden');
+ goToTodoUrl(e) {
+ const todoLink = this.dataset.url;
- if (target === doneBtn) {
- row.classList.add('done-reversible');
- restoreBtn.classList.remove('hidden');
- } else {
- row.classList.remove('done-reversible');
- doneBtn.classList.remove('hidden');
- }
- }
-
- updateBadges(data) {
- $(document).trigger('todo:toggle', data.count);
- $('.todos-pending .badge').text(data.count);
- $('.todos-done .badge').text(data.done_count);
+ if (!todoLink) {
+ return;
}
- goToTodoUrl(e) {
- const todoLink = this.dataset.url;
-
- if (!todoLink) {
- return;
- }
+ if (gl.utils.isMetaClick(e)) {
+ const windowTarget = '_blank';
+ const selected = e.target;
+ e.preventDefault();
- if (gl.utils.isMetaClick(e)) {
- const windowTarget = '_blank';
- const selected = e.target;
- e.preventDefault();
-
- if (selected.tagName === 'IMG') {
- const avatarUrl = selected.parentElement.getAttribute('href');
- window.open(avatarUrl, windowTarget);
- } else {
- window.open(todoLink, windowTarget);
- }
+ if (selected.tagName === 'IMG') {
+ const avatarUrl = selected.parentElement.getAttribute('href');
+ window.open(avatarUrl, windowTarget);
} else {
- gl.utils.visitUrl(todoLink);
+ window.open(todoLink, windowTarget);
}
+ } else {
+ gl.utils.visitUrl(todoLink);
}
}
+}
- global.Todos = Todos;
-})(window.gl || (window.gl = {}));
+window.gl = window.gl || {};
+gl.Todos = Todos;
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js b/app/assets/javascripts/vue_pipelines_index/stage.js
index f67ebd6a265..ae4f0b4a53b 100644
--- a/app/assets/javascripts/vue_pipelines_index/stage.js
+++ b/app/assets/javascripts/vue_pipelines_index/stage.js
@@ -69,7 +69,7 @@ import warningSvg from 'icons/_icon_status_warning_borderless.svg';
* target the click event of this component.
*/
stopDropdownClickPropagation() {
- $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
+ $(this.$el).on('click', '.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', (e) => {
e.stopPropagation();
});
},
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index d3229f9f730..4157fefddc9 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -6,10 +6,6 @@ Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
next((response) => {
- if (typeof response.data === 'string') {
- response.data = JSON.parse(response.data);
- }
-
Vue.activeResources--;
});
});
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 909a0f4afda..6d27d7568cf 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -57,8 +57,13 @@
visibility: hidden;
}
- &:hover i {
- visibility: visible;
+ &:hover,
+ &:focus {
+ outline: none;
+
+ & i {
+ visibility: visible;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 0e2b8dba780..73a5da715f2 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -141,6 +141,14 @@
margin-right: 0;
}
}
+
+ .no-btn {
+ border: none;
+ background: none;
+ outline: none;
+ width: 100%;
+ text-align: left;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index f984b469609..c2156a5ac69 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -178,8 +178,25 @@
padding-right: 5px;
}
- &:last-child {
- padding-left: 5px;
+ }
+
+ .discussion-actions {
+ display: table;
+
+ .new-issue-for-discussion path {
+ fill: $gray-darkest;
+ }
+
+ .btn-group {
+ display: table-cell;
+
+ &:first-child {
+ padding-right: 0;
+ }
+
+ &:first-child:not(:last-child) > div {
+ border-right: 0;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index c8ad88e9351..e238f0865f6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -509,6 +509,7 @@ ul.notes {
}
.line-resolve-all-container {
+
.btn-group {
margin-left: -4px;
}
@@ -517,6 +518,27 @@ ul.notes {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
+
+ .btn.discussion-create-issue-btn {
+ margin-left: -4px;
+ border-radius: 0;
+ border-right: 0;
+
+ a {
+ padding: 0;
+ line-height: 0;
+
+ &:hover {
+ text-decoration: none;
+ border: 0;
+ }
+ }
+
+ .new-issue-for-discussion path {
+ fill: $gray-darkest;
+ }
+ }
+
}
.line-resolve-all {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 4914933430f..efa47be9a73 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -795,7 +795,8 @@ pre.light-well {
}
.project-refs-form .dropdown-menu,
-.dropdown-menu-projects {
+.dropdown-menu-projects,
+.dropdown-menu-branches {
width: 300px;
@media (min-width: $screen-sm-min) {
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 7ffde71c3b1..24504685e48 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -29,11 +29,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def impersonate
- if user.blocked?
- flash[:alert] = "You cannot impersonate a blocked user"
-
- redirect_to admin_user_path(user)
- else
+ if can?(user, :log_in)
session[:impersonator_id] = current_user.id
warden.set_user(user, scope: :user)
@@ -43,6 +39,17 @@ class Admin::UsersController < Admin::ApplicationController
flash[:alert] = "You are now impersonating #{user.username}"
redirect_to root_path
+ else
+ flash[:alert] =
+ if user.blocked?
+ "You cannot impersonate a blocked user"
+ elsif user.internal?
+ "You cannot impersonate an internal user"
+ else
+ "You cannot impersonate a user who cannot log in"
+ end
+
+ redirect_to admin_user_path(user)
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 1c66c530cd2..b7ce081a5cd 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -67,7 +67,7 @@ class ApplicationController < ActionController::Base
token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
- if user
+ if user && can?(user, :log_in)
# Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed
# for every request. If you want the token to work as a
@@ -90,7 +90,7 @@ class ApplicationController < ActionController::Base
current_application_settings.after_sign_out_path.presence || new_user_session_path
end
- def can?(object, action, subject)
+ def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 4c497711fc0..ea441b1736b 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -23,7 +23,7 @@ module AuthenticatesWithTwoFactor
#
# Returns nil
def prompt_for_two_factor(user)
- return locked_user_redirect(user) if user.access_locked?
+ return locked_user_redirect(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id
setup_u2f_authentication(user)
@@ -37,10 +37,9 @@ module AuthenticatesWithTwoFactor
def authenticate_with_two_factor
user = self.resource = find_user
+ return locked_user_redirect(user) unless user.can?(:log_in)
- if user.access_locked?
- locked_user_redirect(user)
- elsif user_params[:otp_attempt].present? && session[:otp_user_id]
+ if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user)
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 7f506db583f..df528d10f6e 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -5,6 +5,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
def index
respond_to do |format|
format.html do
+ @milestone_states = GlobalMilestone.states_count(@projects)
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 0d872c86c8a..43102596201 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -6,6 +6,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def index
respond_to do |format|
format.html do
+ @milestone_states = GlobalMilestone.states_count(@projects)
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 4663b6e7fc6..05f9ee1ee90 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -118,7 +118,7 @@ class GroupsController < Groups::ApplicationController
end
def authorize_create_group!
- unless can?(current_user, :create_group, nil)
+ unless can?(current_user, :create_group)
return render_404
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 21ed0660762..52fc67d162c 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -23,6 +23,8 @@ class Projects::BlobController < Projects::ApplicationController
end
def create
+ update_ref
+
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)) },
failure_view: :new,
@@ -87,6 +89,11 @@ class Projects::BlobController < Projects::ApplicationController
private
+ def update_ref
+ branch_exists = @repository.find_branch(@target_branch)
+ @ref = @target_branch if branch_exists
+ end
+
def blob
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index c40f9b7f75f..22714d9c5a4 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -10,15 +10,16 @@ class Projects::BranchesController < Projects::ApplicationController
def index
@sort = params[:sort].presence || sort_value_name
@branches = BranchesFinder.new(@repository, params).execute
- @branches = Kaminari.paginate_array(@branches).page(params[:page])
- @max_commits = @branches.reduce(0) do |memo, branch|
- diverging_commit_counts = repository.diverging_commit_counts(branch)
- [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
- end
+ @branches = Kaminari.paginate_array(@branches).page(params[:page]) unless params[:show_all].present?
respond_to do |format|
- format.html
+ format.html do
+ @max_commits = @branches.reduce(0) do |memo, branch|
+ diverging_commit_counts = repository.diverging_commit_counts(branch)
+ [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+ end
+ end
format.json do
render json: @branches.map(&:name)
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 1151555b8fa..f2fee62ebd6 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -64,8 +64,15 @@ class Projects::IssuesController < Projects::ApplicationController
params[:issue] ||= ActionController::Parameters.new(
assignee_id: ""
)
- build_params = issue_params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
- @issue = @noteable = Issues::BuildService.new(project, current_user, build_params).execute
+ build_params = issue_params.merge(
+ merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
+ discussion_to_resolve: params[:discussion_to_resolve]
+ )
+ service = Issues::BuildService.new(project, current_user, build_params)
+
+ @issue = @noteable = service.execute
+ @merge_request_to_resolve_discussions_of = service.merge_request_to_resolve_discussions_of
+ @discussion_to_resolve = service.discussions_to_resolve.first if params[:discussion_to_resolve]
respond_with(@issue)
end
@@ -94,11 +101,21 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create
- create_params = issue_params
- .merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
- .merge(spammable_params)
+ create_params = issue_params.merge(spammable_params).merge(
+ merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
+ discussion_to_resolve: params[:discussion_to_resolve]
+ )
+
+ service = Issues::CreateService.new(project, current_user, create_params)
+ @issue = service.execute
- @issue = Issues::CreateService.new(project, current_user, create_params).execute
+ if service.discussions_to_resolve.count(&:resolved?) > 0
+ flash[:notice] = if service.discussion_to_resolve_id
+ "Resolved 1 discussion."
+ else
+ "Resolved all discussions."
+ end
+ end
respond_to do |format|
format.html do
@@ -185,14 +202,6 @@ class Projects::IssuesController < Projects::ApplicationController
alias_method :awardable, :issue
alias_method :spammable, :issue
- def merge_request_for_resolving_discussions
- return unless merge_request_iid = params[:merge_request_for_resolving_discussions]
-
- @merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: project.id).
- execute.
- find_by(iid: merge_request_iid)
- end
-
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
end
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 10d24da16d7..c55b37ae0dd 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
return if cached_blob?
- if @blob.lfs_pointer?
+ if @blob.lfs_pointer? && project.lfs_enabled?
send_lfs_object
else
send_git_blob @repository, @blob
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 33379659d73..ea7e4d9f663 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -41,13 +41,27 @@ class Projects::TagsController < Projects::ApplicationController
end
def destroy
- Tags::DestroyService.new(project, current_user).execute(params[:id])
+ result = Tags::DestroyService.new(project, current_user).execute(params[:id])
respond_to do |format|
- format.html do
- redirect_to namespace_project_tags_path(@project.namespace, @project)
+ if result[:status] == :success
+ format.html do
+ redirect_to namespace_project_tags_path(@project.namespace, @project)
+ end
+
+ format.js
+ else
+ @error = result[:message]
+
+ format.html do
+ redirect_to namespace_project_tags_path(@project.namespace, @project),
+ alert: @error
+ end
+
+ format.js do
+ render status: :unprocessable_entity
+ end
end
- format.js
end
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index c2b399041c6..aad83731b87 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -1,4 +1,6 @@
module IssuablesHelper
+ include GitlabRoutingHelper
+
def sidebar_gutter_toggle_icon
sidebar_gutter_collapsed? ? icon('angle-double-left', { 'aria-hidden': 'true' }) : icon('angle-double-right', { 'aria-hidden': 'true' })
end
@@ -95,8 +97,23 @@ module IssuablesHelper
h(milestone_title.presence || default_label)
end
+ def to_url_reference(issuable)
+ case issuable
+ when Issue
+ link_to issuable.to_reference, issue_url(issuable)
+ when MergeRequest
+ link_to issuable.to_reference, merge_request_url(issuable)
+ else
+ issuable.to_reference
+ end
+ end
+
def issuable_meta(issuable, project, text)
- output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier"
+ output = content_tag(:strong, class: "identifier") do
+ concat("#{text} ")
+ concat(to_url_reference(issuable))
+ end
+
output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true)
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 4bdf07fe1ad..6978b0c89fd 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -134,6 +134,20 @@ module IssuesHelper
options_from_collection_for_select(options, 'name', 'title', params[:due_date])
end
+ def link_to_discussions_to_resolve(merge_request, single_discussion = nil)
+ link_text = merge_request.to_reference
+ link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion
+
+ path = if single_discussion
+ Gitlab::UrlBuilder.build(single_discussion.first_note)
+ else
+ project = merge_request.project
+ namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ link_to link_text, path
+ end
+
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index ad6c588202e..f3692a5a067 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -56,15 +56,16 @@ class Ability
end
end
- def allowed?(user, action, subject)
+ def allowed?(user, action, subject = :global)
allowed(user, subject).include?(action)
end
- def allowed(user, subject)
+ def allowed(user, subject = :global)
+ return BasePolicy::RuleSet.none if subject.nil?
return uncached_allowed(user, subject) unless RequestStore.active?
user_key = user ? user.id : 'anonymous'
- subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global'
+ subject_key = subject == :global ? 'global' : "#{subject.class.name}/#{subject.id}"
key = "/ability/#{user_key}/#{subject_key}"
RequestStore[key] ||= uncached_allowed(user, subject).freeze
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index ab92e820335..1376b86fdad 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -54,9 +54,13 @@ class Blob < SimpleDelegator
UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.'))
end
- def to_partial_path
+ def to_partial_path(project)
if lfs_pointer?
- 'download'
+ if project.lfs_enabled?
+ 'download'
+ else
+ 'text'
+ end
elsif image? || svg?
'image'
elsif text?
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index b991d78e27f..0afbca2cb32 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -28,6 +28,28 @@ class GlobalMilestone
new(title, child_milestones)
end
+ def self.states_count(projects)
+ relation = MilestonesFinder.new.execute(projects, state: 'all')
+ milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count
+
+ opened = count_by_state(milestones_by_state_and_title, 'active')
+ closed = count_by_state(milestones_by_state_and_title, 'closed')
+ all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
+
+ {
+ opened: opened,
+ closed: closed,
+ all: all
+ }
+ end
+
+ def self.count_by_state(milestones_by_state_and_title, state)
+ milestones_by_state_and_title.count do |(milestone_state, _), _|
+ milestone_state == state
+ end
+ end
+ private_class_method :count_by_state
+
def initialize(title, milestones)
@title = title
@name = title
diff --git a/app/models/guest.rb b/app/models/guest.rb
index 01285ca1264..df287c277a7 100644
--- a/app/models/guest.rb
+++ b/app/models/guest.rb
@@ -1,6 +1,6 @@
class Guest
class << self
- def can?(action, subject)
+ def can?(action, subject = :global)
Ability.allowed?(nil, action, subject)
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 76fb4cd470e..39c1281179b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -126,7 +126,6 @@ class User < ActiveRecord::Base
validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
validate :owns_public_email, if: ->(user) { user.public_email_changed? }
- validate :ghost_users_must_be_blocked
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create
@@ -350,12 +349,27 @@ class User < ActiveRecord::Base
def ghost
unique_internal(where(ghost: true), 'ghost', 'ghost%s@example.com') do |u|
u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
- u.state = :blocked
u.name = 'Ghost User'
end
end
end
+ def self.internal_attributes
+ [:ghost]
+ end
+
+ def internal?
+ self.class.internal_attributes.any? { |a| self[a] }
+ end
+
+ def self.internal
+ where(Hash[internal_attributes.zip([true] * internal_attributes.size)])
+ end
+
+ def self.non_internal
+ where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
+ end
+
#
# Instance methods
#
@@ -452,12 +466,6 @@ class User < ActiveRecord::Base
errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end
- def ghost_users_must_be_blocked
- if ghost? && !blocked?
- errors.add(:ghost, 'cannot be enabled for a user who is not blocked.')
- end
- end
-
def update_emails_with_primary_email
primary_email_record = emails.find_by(email: email)
if primary_email_record
@@ -563,14 +571,14 @@ class User < ActiveRecord::Base
end
def can_create_group?
- can?(:create_group, nil)
+ can?(:create_group)
end
def can_select_namespace?
several_namespaces? || admin
end
- def can?(action, subject)
+ def can?(action, subject = :global)
Ability.allowed?(self, action, subject)
end
@@ -955,6 +963,14 @@ class User < ActiveRecord::Base
self.admin = (new_level == 'admin')
end
+ protected
+
+ # override, from Devise::Validatable
+ def password_required?
+ return false if internal?
+ super
+ end
+
private
def ci_projects_union
@@ -1055,7 +1071,6 @@ class User < ActiveRecord::Base
scope.create(
username: username,
- password: Devise.friendly_token,
email: email,
&creation_block
)
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index e07b144355a..8890409d056 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -12,6 +12,10 @@ class BasePolicy
new(Set.new, Set.new)
end
+ def self.none
+ empty.freeze
+ end
+
def can?(ability)
@can_set.include?(ability) && !@cannot_set.include?(ability)
end
@@ -49,7 +53,8 @@ class BasePolicy
end
def self.class_for(subject)
- return GlobalPolicy if subject.nil?
+ return GlobalPolicy if subject == :global
+ raise ArgumentError, 'no policy for nil' if subject.nil?
if subject.class.try(:presenter?)
subject = subject.subject
@@ -79,7 +84,7 @@ class BasePolicy
end
def abilities
- return RuleSet.empty if @user && @user.blocked?
+ return RuleSet.none if @user && @user.blocked?
return anonymous_abilities if @user.nil?
collect_rules { rules }
end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 3c2fbe6b56b..cb72c2b4590 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -4,5 +4,12 @@ class GlobalPolicy < BasePolicy
can! :create_group if @user.can_create_group
can! :read_users_list
+
+ unless @user.blocked? || @user.internal?
+ can! :log_in unless @user.access_locked?
+ can! :access_api
+ can! :access_git
+ can! :receive_notifications
+ end
end
end
diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb
new file mode 100644
index 00000000000..297c7d696c3
--- /dev/null
+++ b/app/services/concerns/issues/resolve_discussions.rb
@@ -0,0 +1,32 @@
+module Issues
+ module ResolveDiscussions
+ attr_reader :merge_request_to_resolve_discussions_of_iid, :discussion_to_resolve_id
+
+ def filter_resolve_discussion_params
+ @merge_request_to_resolve_discussions_of_iid ||= params.delete(:merge_request_to_resolve_discussions_of)
+ @discussion_to_resolve_id ||= params.delete(:discussion_to_resolve)
+ end
+
+ def merge_request_to_resolve_discussions_of
+ return @merge_request_to_resolve_discussions_of if defined?(@merge_request_to_resolve_discussions_of)
+
+ @merge_request_to_resolve_discussions_of = MergeRequestsFinder.new(current_user, project_id: project.id).
+ execute.
+ find_by(iid: merge_request_to_resolve_discussions_of_iid)
+ end
+
+ def discussions_to_resolve
+ return [] unless merge_request_to_resolve_discussions_of
+
+ @discussions_to_resolve ||=
+ if discussion_to_resolve_id
+ discussion_or_nil = merge_request_to_resolve_discussions_of
+ .find_diff_discussion(discussion_to_resolve_id)
+ Array(discussion_or_nil)
+ else
+ merge_request_to_resolve_discussions_of
+ .resolvable_discussions
+ end
+ end
+ end
+end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 35af867a098..ee1b40db718 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -1,13 +1,5 @@
module Issues
class BaseService < ::IssuableBaseService
- attr_reader :merge_request_for_resolving_discussions
-
- def initialize(*args)
- super
-
- @merge_request_for_resolving_discussions ||= params.delete(:merge_request_for_resolving_discussions)
- end
-
def hook_data(issue, action)
issue_data = issue.to_hook_data(current_user)
issue_url = Gitlab::UrlBuilder.build(issue)
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index 7cd927d8005..77bced4bd5c 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -1,50 +1,56 @@
module Issues
class BuildService < Issues::BaseService
+ include ResolveDiscussions
+
def execute
+ filter_resolve_discussion_params
@issue = project.issues.new(issue_params)
end
- def issue_params_with_info_from_merge_request
- return {} unless merge_request_for_resolving_discussions
+ def issue_params_with_info_from_discussions
+ return {} unless merge_request_to_resolve_discussions_of
- { title: title_from_merge_request, description: description_from_merge_request }
+ { title: title_from_merge_request, description: description_for_discussions }
end
def title_from_merge_request
- "Follow-up from \"#{merge_request_for_resolving_discussions.title}\""
+ "Follow-up from \"#{merge_request_to_resolve_discussions_of.title}\""
end
- def description_from_merge_request
- if merge_request_for_resolving_discussions.resolvable_discussions.empty?
+ def description_for_discussions
+ if discussions_to_resolve.empty?
return "There are no unresolved discussions. "\
- "Review the conversation in #{merge_request_for_resolving_discussions.to_reference}"
+ "Review the conversation in #{merge_request_to_resolve_discussions_of.to_reference}"
end
- description = "The following discussions from #{merge_request_for_resolving_discussions.to_reference} should be addressed:"
+ description = "The following #{'discussion'.pluralize(discussions_to_resolve.size)} "\
+ "from #{merge_request_to_resolve_discussions_of.to_reference} "\
+ "should be addressed:"
+
[description, *items_for_discussions].join("\n\n")
end
def items_for_discussions
- merge_request_for_resolving_discussions.resolvable_discussions.map { |discussion| item_for_discussion(discussion) }
+ discussions_to_resolve.map { |discussion| item_for_discussion(discussion) }
end
def item_for_discussion(discussion)
- first_note = discussion.first_note_to_resolve
+ first_note = discussion.first_note_to_resolve || discussion.first_note
other_note_count = discussion.notes.size - 1
- creation_time = first_note.created_at.to_s(:medium)
note_url = Gitlab::UrlBuilder.build(first_note)
- discussion_info = "- [ ] #{first_note.author.to_reference} commented in a discussion on [#{creation_time}](#{note_url}): "
+ discussion_info = "- [ ] #{first_note.author.to_reference} commented on a [discussion](#{note_url}): "
discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0
note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call
- quote = ">>>\n#{note_without_block_quotes}\n>>>"
+ spaces = ' ' * 4
+ quote = note_without_block_quotes.lines.map { |line| "#{spaces}> #{line}" }.join
[discussion_info, quote].join("\n\n")
end
def issue_params
- @issue_params ||= issue_params_with_info_from_merge_request.merge(whitelisted_issue_params)
+ @issue_params ||= issue_params_with_info_from_discussions.merge(whitelisted_issue_params)
end
def whitelisted_issue_params
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 85b6eb3fe3d..3cf4b82b9f2 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -1,12 +1,13 @@
module Issues
class CreateService < Issues::BaseService
include SpamCheckService
+ include ResolveDiscussions
def execute
- filter_spam_check_params
+ @issue = BuildService.new(project, current_user, params).execute
- issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
- @issue = BuildService.new(project, current_user, issue_attributes).execute
+ filter_spam_check_params
+ filter_resolve_discussion_params
create(@issue)
end
@@ -21,17 +22,16 @@ module Issues
notification_service.new_issue(issuable, current_user)
todo_service.new_issue(issuable, current_user)
user_agent_detail_service.create
-
- if merge_request_for_resolving_discussions.try(:discussions_can_be_resolved_by?, current_user)
- resolve_discussions_in_merge_request(issuable)
- end
+ resolve_discussions_with_issue(issuable)
end
- def resolve_discussions_in_merge_request(issue)
+ def resolve_discussions_with_issue(issue)
+ return if discussions_to_resolve.empty?
+
Discussions::ResolveService.new(project, current_user,
- merge_request: merge_request_for_resolving_discussions,
+ merge_request: merge_request_to_resolve_discussions_of,
follow_up_issue: issue).
- execute(merge_request_for_resolving_discussions.resolvable_discussions)
+ execute(discussions_to_resolve)
end
private
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index fbad85d310e..d12692ecc90 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -465,7 +465,7 @@ class NotificationService
end
users = users.to_a.compact.uniq
- users = users.reject(&:blocked?)
+ users = users.select { |u| u.can?(:receive_notifications) }
users.reject do |user|
global_notification_setting = user.global_notification_setting
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index 910b4f5e361..a368f4f5b61 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -21,6 +21,8 @@ module Tags
else
error('Failed to remove tag')
end
+ rescue GitHooksService::PreReceiveError => ex
+ error(ex.message)
end
def error(message, return_code = 400)
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index 03921db6947..77ca033e97f 100644
--- a/app/validators/namespace_validator.rb
+++ b/app/validators/namespace_validator.rb
@@ -36,7 +36,8 @@ class NamespaceValidator < ActiveModel::EachValidator
].freeze
WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree
- preview blob blame raw files create_dir find_file].freeze
+ preview blob blame raw files create_dir find_file
+ artifacts graphs refs badges].freeze
STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index d20be373564..be41c33b853 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -2,11 +2,13 @@
= @user.name
- if @user.blocked?
%span.cred (Blocked)
+ - if @user.internal?
+ %span.cred (Internal)
- if @user.admin
%span.cred (Admin)
.pull-right
- - unless @user == current_user || @user.blocked?
+ - if @user != current_user && @user.can?(:log_in)
= link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
= link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
%i.fa.fa-pencil-square-o
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index bdea1064096..06fb531b546 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
- xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
+ xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
end
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index 917bfbd47e9..505b475f55b 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -1,11 +1,11 @@
-- page_title "Milestones"
-- header_title "Milestones", dashboard_milestones_path
+- page_title 'Milestones'
+- header_title 'Milestones', dashboard_milestones_path
.top-area
- = render 'shared/milestones_filter'
+ = render 'shared/milestones_filter', counts: @milestone_states
.nav-controls
- = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true
+ = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New Milestone', include_groups: true
.milestones
%ul.content-list
@@ -15,4 +15,4 @@
- else
- @milestones.each do |milestone|
= render 'milestone', milestone: milestone
- = paginate @milestones, theme: "gitlab"
+ = paginate @milestones, theme: 'gitlab'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index a3993d5ef16..388190642aa 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -42,3 +42,8 @@
= link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden' do
Undo
= icon('spinner spin')
+ - else
+ .todo-actions
+ = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo' do
+ Add todo
+ = icon('spinner spin')
diff --git a/app/views/discussions/_new_issue_for_all_discussions.html.haml b/app/views/discussions/_new_issue_for_all_discussions.html.haml
new file mode 100644
index 00000000000..ca9e0e8728a
--- /dev/null
+++ b/app/views/discussions/_new_issue_for_all_discussions.html.haml
@@ -0,0 +1,6 @@
+- if merge_request.discussions_can_be_resolved_by?(current_user) && can?(current_user, :create_issue, @project)
+ .btn-group{ role: "group", "v-if" => "unresolvedDiscussionCount > 0" }
+ .btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve all discussions in new issue",
+ "aria-label" => "Resolve all discussions in a new issue",
+ "data-container" => "body" }
+ = link_to custom_icon('icon_mr_issue'), new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: merge_request.iid), title: "Resolve all discussions in new issue", class: 'new-issue-for-discussion'
diff --git a/app/views/discussions/_new_issue_for_discussion.html.haml b/app/views/discussions/_new_issue_for_discussion.html.haml
new file mode 100644
index 00000000000..df5546a1e32
--- /dev/null
+++ b/app/views/discussions/_new_issue_for_discussion.html.haml
@@ -0,0 +1,8 @@
+- if discussion.can_resolve?(current_user) && can?(current_user, :create_issue, @project)
+ %new-issue-for-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
+ "inline-template" => true }
+ .btn-group{ role: "group", "v-if" => "showButton" }
+ .btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve this discussion in a new issue",
+ "aria-label" => "Resolve this discussion in a new issue",
+ "data-container" => "body" }
+ = link_to custom_icon('icon_mr_issue'), new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id), title: "Resolve this discussion in a new issue", class: 'new-issue-for-discussion'
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index dfdbdf1f969..2789391819c 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -11,6 +11,8 @@
= link_to_reply_discussion(discussion, line_type)
= render "discussions/resolve_all", discussion: discussion
- if discussion.for_merge_request?
- = render "discussions/jump_to_next", discussion: discussion
+ .btn-group.discussion-actions
+ = render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable
+ = render "discussions/jump_to_next", discussion: discussion
- else
= link_to_reply_discussion(discussion)
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index 7890e717aa7..43a52cf3002 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -4,7 +4,7 @@ xml.entry do
xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
xml.link href: event_feed_url(event)
xml.title truncate(event_feed_title(event), length: 80)
- xml.updated event.created_at.xmlschema
+ xml.updated event.updated_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
xml.author do
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index 0cc6466d34e..469768d83f2 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
xml.link href: issues_group_url, rel: "alternate", type: "text/html"
xml.id issues_group_url
- xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
+ xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
end
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index 644895c56a1..6893168f039 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -2,7 +2,7 @@
= render "groups/head_issues"
.top-area
- = render 'shared/milestones_filter'
+ = render 'shared/milestones_filter', counts: @milestone_states
.nav-controls
- if can?(current_user, :admin_milestones, @group)
diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder
index 96831874144..fcd30c8c765 100644
--- a/app/views/issues/_issue.atom.builder
+++ b/app/views/issues/_issue.atom.builder
@@ -2,7 +2,7 @@ xml.entry do
xml.id namespace_project_issue_url(issue.project.namespace, issue.project, issue)
xml.link href: namespace_project_issue_url(issue.project.namespace, issue.project, issue)
xml.title truncate(issue.title, length: 80)
- xml.updated issue.created_at.xmlschema
+ xml.updated issue.updated_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email))
xml.author do
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index c44d8fcd430..14d42f7d9ec 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -12,7 +12,7 @@
class: 'btn btn-sm'
- else
= link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id),
- class: 'btn btn-sm' unless @blob.empty?
+ class: 'btn btn-sm js-blob-blame-link' unless @blob.empty?
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
class: 'btn btn-sm'
= link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 24ff74ecb3b..bf8801bb1e3 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -33,4 +33,4 @@
= number_to_human_size(blob_size(blob))
.file-actions.hidden-xs
= render "actions"
- = render blob, blob: blob
+ = render blob.to_partial_path(@project), blob: blob
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 2ebd4f9069a..b5f67cae341 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -37,4 +37,4 @@
= commit_in_fork_help
:javascript
- new NewCommitForm($('.js-#{type}-form'))
+ new NewCommitForm($('.js-#{type}-form'), 'start_branch')
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 62135d3ae32..c09c7b87e24 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -9,7 +9,7 @@
- case type
- when 'match'
= diff_match_line line.old_pos, line.new_pos, text: line.text
- - when 'nonewline'
+ - when 'old-nonewline', 'new-nonewline'
%td.old_line.diff-line-num
%td.new_line.diff-line-num
%td.line_content.match= line.text
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index e7758c8bdfa..b7346f27ddb 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -12,7 +12,7 @@
- case left.type
- when 'match'
= diff_match_line left.old_pos, nil, text: left.text, view: :parallel
- - when 'nonewline'
+ - when 'old-nonewline', 'new-nonewline'
%td.old_line.diff-line-num
%td.line_content.match= left.text
- else
@@ -31,7 +31,7 @@
- case right.type
- when 'match'
= diff_match_line nil, right.new_pos, text: left.text, view: :parallel
- - when 'nonewline'
+ - when 'old-nonewline', 'new-nonewline'
%td.new_line.diff-line-num
%td.line_content.match= right.text
- else
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index a0df0db77c5..4feec09bb5d 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_issues_url(@project.namespace, @project)
- xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
+ xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
end
diff --git a/app/views/projects/issues/verify.html.haml b/app/views/projects/issues/verify.html.haml
index 09aa401e44a..6da7c317f3a 100644
--- a/app/views/projects/issues/verify.html.haml
+++ b/app/views/projects/issues/verify.html.haml
@@ -1,4 +1,5 @@
- form = [@project.namespace.becomes(Namespace), @project, @issue]
= render layout: 'layouts/recaptcha_verification', locals: { spammable: @issue, form: form } do
- = hidden_field_tag(:merge_request_for_resolving_discussions, params[:merge_request_for_resolving_discussions])
+ = hidden_field_tag(:merge_request_to_resolve_discussions_of, params[:merge_request_to_resolve_discussions_of])
+ = hidden_field_tag(:discussion_to_resolve, params[:discussion_to_resolve])
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 17be0490a86..c8f097c69da 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -82,6 +82,7 @@
= render "shared/icons/icon_status_success.svg"
%span.line-resolve-text
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
+ = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
= render "discussions/jump_to_next"
.tab-content#diff-notes-app
diff --git a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
index e094f97f3b6..ec9346ce89b 100644
--- a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
@@ -6,5 +6,5 @@
Please resolve these discussions
- if @project.issues_enabled? && can?(current_user, :create_issue, @project)
or
- = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_for_resolving_discussions: @merge_request.iid)
+ = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: @merge_request.iid)
to allow this merge request to be merged.
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index ad2bfbec915..918f5d161bb 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,14 +1,14 @@
- @no_container = true
-- page_title "Milestones"
-= render "projects/issues/head"
+- page_title 'Milestones'
+= render 'projects/issues/head'
%div{ class: container_class }
.top-area
- = render 'shared/milestones_filter'
+ = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
.nav-controls
- if can?(current_user, :admin_milestone, @project)
- = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do
New Milestone
.milestones
@@ -19,4 +19,4 @@
%li
.nothing-here-block No milestones to show
- = paginate @milestones, theme: "gitlab"
+ = paginate @milestones, theme: 'gitlab'
diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml
index e4a78fadbeb..cde23e03d54 100644
--- a/app/views/projects/tags/destroy.js.haml
+++ b/app/views/projects/tags/destroy.js.haml
@@ -1,2 +1,4 @@
-- if @repository.tags.empty?
+- if @error.present?
+ new Flash('#{escape_javascript(@error)}', 'alert');
+- elsif @repository.tags.empty?
$('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/shared/_branch_switcher.html.haml b/app/views/shared/_branch_switcher.html.haml
new file mode 100644
index 00000000000..7799aff6b5b
--- /dev/null
+++ b/app/views/shared/_branch_switcher.html.haml
@@ -0,0 +1,8 @@
+- dropdown_toggle_text = @target_branch || tree_edit_branch
+= hidden_field_tag 'target_branch', 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-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/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index 66310da5cd6..1d4fd71522d 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -6,7 +6,7 @@
- if issuable_mr > 0
%li
- = image_tag('icon-merge-request-unmerged', class: 'icon-merge-request-unmerged')
+ = image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged')
= issuable_mr
- if upvotes > 0
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 704893b4d5b..57a0eaa919e 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,19 +1,13 @@
-- if @project
- - counts = milestone_counts(@project.milestones)
-
%ul.nav-links
%li{ class: milestone_class_for_state(params[:state], 'opened', true) }>
= link_to milestones_filter_path(state: 'opened') do
Open
- - if @project
- %span.badge= counts[:opened]
+ %span.badge= counts[:opened]
%li{ class: milestone_class_for_state(params[:state], 'closed') }>
= link_to milestones_filter_path(state: 'closed') do
Closed
- - if @project
- %span.badge= counts[:closed]
+ %span.badge= counts[:closed]
%li{ class: milestone_class_for_state(params[:state], 'all') }>
= link_to milestones_filter_path(state: 'all') do
All
- - if @project
- %span.badge= counts[:all]
+ %span.badge= counts[:all]
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
index 0c8ac48bb58..3ac5e15d1c4 100644
--- a/app/views/shared/_new_commit_form.html.haml
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -7,7 +7,7 @@
.form-group.branch
= label_tag 'target_branch', 'Target branch', class: 'control-label'
.col-sm-10
- = text_field_tag 'target_branch', @target_branch || tree_edit_branch, required: true, class: "form-control js-target-branch"
+ = render 'shared/branch_switcher'
.js-create-merge-request-container
.checkbox
diff --git a/app/views/shared/icons/_icon_mr_issue.svg b/app/views/shared/icons/_icon_mr_issue.svg
new file mode 100644
index 00000000000..ae219a3ded2
--- /dev/null
+++ b/app/views/shared/icons/_icon_mr_issue.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="m8.411 1.012c-.136-.008-.273-.012-.411-.012-3.866 0-7 3.134-7 7 0 3.866 3.134 7 7 7 3.866 0 7-3.134 7-7 0-.138-.004-.275-.012-.411-.464.201-.964.334-1.488.386 0 .008 0 .016 0 .025 0 3.038-2.462 5.5-5.5 5.5-3.038 0-5.5-2.462-5.5-5.5 0-3.038 2.462-5.5 5.5-5.5.008 0 .016 0 .025 0 .052-.524.185-1.024.386-1.488"/><path d="m12 2h-1.01c-.54 0-.991.448-.991 1 0 .556.444 1 .991 1h1.01v1.01c0 .54.448.991 1 .991.556 0 1-.444 1-.991v-1.01h1.01c.54 0 .991-.448.991-1 0-.556-.444-1-.991-1h-1.01v-1.01c0-.54-.448-.991-1-.991-.556 0-1 .444-1 .991v1.01m-5 4.01c0-.557.444-1.01 1-1.01.552 0 1 .443 1 1.01v1.981c0 .557-.444 1.01-1 1.01-.552 0-1-.443-1-1.01v-1.981m1 5.991c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1"/></g></svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 70470c83c51..0b0f2c9cd1a 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -45,20 +45,25 @@
= render 'shared/issuable/form/merge_params', issuable: issuable
-- if @merge_request_for_resolving_discussions
+- if @merge_request_to_resolve_discussions_of
.form-group
.col-sm-10.col-sm-offset-2
- - if @merge_request_for_resolving_discussions.discussions_can_be_resolved_by?(current_user)
- = icon('exclamation-triangle')
- Creating this issue will mark all discussions in
- = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions)
- as resolved.
- = hidden_field_tag 'merge_request_for_resolving_discussions', @merge_request_for_resolving_discussions.iid
+ = icon('info-circle')
+ - if @merge_request_to_resolve_discussions_of.discussions_can_be_resolved_by?(current_user)
+ = hidden_field_tag 'merge_request_to_resolve_discussions_of', @merge_request_to_resolve_discussions_of.iid
+ - if @discussion_to_resolve
+ = hidden_field_tag 'discussion_to_resolve', @discussion_to_resolve.id
+ Creating this issue will resolve the discussion in
+ - else
+ Creating this issue will resolve all discussions in
+ = link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve)
- else
- = icon('exclamation-triangle')
- You can't automatically mark all discussions in
- = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions)
- as resolved. Ask someone with sufficient rights to resolve the them.
+ The
+ = @discussion_to_resolve ? 'discussion' : 'discussions'
+ at
+ = link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve)
+ will stay unresolved. Ask someone with permission to resolve
+ = @discussion_to_resolve ? 'it.' : 'them.'
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
.row-content-block{ class: (is_footer ? "footer-block" : "middle-block") }
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 4a27965754d..7e9fb7bb4d3 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -7,7 +7,7 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3']
-- cache_key.push(project.commit.status) if project.commit.try(:status)
+- cache_key.push(project.commit&.sha, project.commit&.status)
%li.project-row{ class: css_class }
= cache(cache_key) do
diff --git a/app/views/shared/projects/blob/_branch_page_create.html.haml b/app/views/shared/projects/blob/_branch_page_create.html.haml
new file mode 100644
index 00000000000..c279a0d8846
--- /dev/null
+++ b/app/views/shared/projects/blob/_branch_page_create.html.haml
@@ -0,0 +1,8 @@
+.dropdown-page-two.dropdown-new-branch
+ = dropdown_title('Create new branch', back: true)
+ = dropdown_content do
+ %input#new_branch_name.default-dropdown-input.append-bottom-10{ type: "text", placeholder: "Name new branch" }
+ %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" }
+ Create
+ %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" }
+ Cancel
diff --git a/app/views/shared/projects/blob/_branch_page_default.html.haml b/app/views/shared/projects/blob/_branch_page_default.html.haml
new file mode 100644
index 00000000000..9bf78d10878
--- /dev/null
+++ b/app/views/shared/projects/blob/_branch_page_default.html.haml
@@ -0,0 +1,10 @@
+.dropdown-page-one
+ = dropdown_title "Select branch"
+ = dropdown_filter "Search branches"
+ = dropdown_content
+ = dropdown_loading
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ %li
+ %a.create-new-branch.dropdown-toggle-page{ href: "#" }
+ Create new branch
diff --git a/changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml b/changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml
new file mode 100644
index 00000000000..199f1edec8b
--- /dev/null
+++ b/changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml
@@ -0,0 +1,4 @@
+---
+title: Update permalink/blame buttons with line number fragment hash
+merge_request:
+author:
diff --git a/changelogs/unreleased/24137-issuable-permalink.yml b/changelogs/unreleased/24137-issuable-permalink.yml
new file mode 100644
index 00000000000..bcc6c6957a1
--- /dev/null
+++ b/changelogs/unreleased/24137-issuable-permalink.yml
@@ -0,0 +1,4 @@
+---
+title: Link issuable reference to itself in meta-header
+merge_request: 9641
+author: mhasbini
diff --git a/changelogs/unreleased/24421-personal-milestone-count-badges.yml b/changelogs/unreleased/24421-personal-milestone-count-badges.yml
new file mode 100644
index 00000000000..8bbc1ed2dde
--- /dev/null
+++ b/changelogs/unreleased/24421-personal-milestone-count-badges.yml
@@ -0,0 +1,4 @@
+---
+title: Add dashboard and group milestones count badges
+merge_request: 9836
+author: Alex Braha Stoll
diff --git a/changelogs/unreleased/24501-new-file-existing-branch.yml b/changelogs/unreleased/24501-new-file-existing-branch.yml
new file mode 100644
index 00000000000..31c66b2a978
--- /dev/null
+++ b/changelogs/unreleased/24501-new-file-existing-branch.yml
@@ -0,0 +1,4 @@
+---
+title: New file from interface on existing branch
+merge_request: 8427
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml b/changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml
new file mode 100644
index 00000000000..5b755a8bc32
--- /dev/null
+++ b/changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml
@@ -0,0 +1,4 @@
+---
+title: Create a new issue for a single discussion in a Merge Request
+merge_request: 8266
+author: Bob Van Landuyt
diff --git a/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml b/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml
new file mode 100644
index 00000000000..2e6c10a6bfe
--- /dev/null
+++ b/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Add Undo to Todos in the Done tab
+merge_request: 8782
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/dz-blacklist--names.yml b/changelogs/unreleased/dz-blacklist--names.yml
new file mode 100644
index 00000000000..2941965002d
--- /dev/null
+++ b/changelogs/unreleased/dz-blacklist--names.yml
@@ -0,0 +1,4 @@
+---
+title: Reserve few project and nested group paths that have wildcard routes associated
+merge_request: 9898
+author:
diff --git a/changelogs/unreleased/feature-custom-lfs.yml b/changelogs/unreleased/feature-custom-lfs.yml
new file mode 100644
index 00000000000..ec968386a6f
--- /dev/null
+++ b/changelogs/unreleased/feature-custom-lfs.yml
@@ -0,0 +1,4 @@
+---
+title: Do not show LFS object when LFS is disabled
+merge_request: 9779
+author: Christopher Bartz
diff --git a/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml
new file mode 100644
index 00000000000..4db684c40b2
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve project pipeline status caching problem on dashboard
+merge_request: 9895
+author:
diff --git a/changelogs/unreleased/fix_updated_field_in_issues-atom.yml b/changelogs/unreleased/fix_updated_field_in_issues-atom.yml
new file mode 100644
index 00000000000..414facdf779
--- /dev/null
+++ b/changelogs/unreleased/fix_updated_field_in_issues-atom.yml
@@ -0,0 +1,4 @@
+---
+title: Fix xml.updated field in rss/atom feeds
+merge_request: 9889
+author: blackst0ne
diff --git a/changelogs/unreleased/handle-failure-when-deleting-tags.yml b/changelogs/unreleased/handle-failure-when-deleting-tags.yml
new file mode 100644
index 00000000000..99b07c5fb5f
--- /dev/null
+++ b/changelogs/unreleased/handle-failure-when-deleting-tags.yml
@@ -0,0 +1,4 @@
+---
+title: Display error message when deleting tag in web UI fails
+merge_request: 9906
+author:
diff --git a/changelogs/unreleased/pages-0-4-0.yml b/changelogs/unreleased/pages-0-4-0.yml
new file mode 100644
index 00000000000..7286b25125e
--- /dev/null
+++ b/changelogs/unreleased/pages-0-4-0.yml
@@ -0,0 +1,4 @@
+---
+title: Use GitLab Pages v0.4.0
+merge_request: 9896
+author:
diff --git a/changelogs/unreleased/refresh-permissions-recent-users.yml b/changelogs/unreleased/refresh-permissions-recent-users.yml
new file mode 100644
index 00000000000..4d08be6ed5c
--- /dev/null
+++ b/changelogs/unreleased/refresh-permissions-recent-users.yml
@@ -0,0 +1,4 @@
+---
+title: Reset users.authorized_projects_populated to automatically refresh user permissions
+merge_request:
+author:
diff --git a/changelogs/unreleased/use-corejs-polyfills.yml b/changelogs/unreleased/use-corejs-polyfills.yml
new file mode 100644
index 00000000000..381f80c5c0d
--- /dev/null
+++ b/changelogs/unreleased/use-corejs-polyfills.yml
@@ -0,0 +1,4 @@
+---
+title: Standardize on core-js for es2015 polyfills
+merge_request: 9749
+author:
diff --git a/config/application.rb b/config/application.rb
index 1cc092c4da1..98b2759a8a7 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -26,7 +26,8 @@ module Gitlab
#{config.root}/app/models/hooks
#{config.root}/app/models/members
#{config.root}/app/models/project_services
- #{config.root}/app/workers/concerns))
+ #{config.root}/app/workers/concerns
+ #{config.root}/app/services/concerns))
config.generators.templates.push("#{config.root}/generator_templates")
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index a1517e6afc8..3e1657b8382 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -20,13 +20,17 @@ def instrument_classes(instrumentation)
# Path to search => prefix to strip from constant
paths_to_instrument = {
- %w(app finders) => %w(app finders),
- %w(app mailers emails) => %w(app mailers),
- %w(app services **) => %w(app services),
- %w(lib gitlab conflicts) => ['lib'],
- %w(lib gitlab diff) => ['lib'],
- %w(lib gitlab email message) => ['lib'],
- %w(lib gitlab checks) => ['lib']
+ %w(app finders) => %w(app finders),
+ %w(app mailers emails) => %w(app mailers),
+ # Don't instrument `app/services/concerns`
+ # It contains modules that are included in the services.
+ # The services themselves are instrumented so the methods from the modules
+ # are included.
+ %w(app services [^concerns]**) => %w(app services),
+ %w(lib gitlab conflicts) => ['lib'],
+ %w(lib gitlab diff) => ['lib'],
+ %w(lib gitlab email message) => ['lib'],
+ %w(lib gitlab checks) => ['lib']
}
paths_to_instrument.each do |(path, prefix)|
diff --git a/config/initializers/fix_local_cache_middleware.rb b/config/initializers/fix_local_cache_middleware.rb
new file mode 100644
index 00000000000..cb37f9ed22c
--- /dev/null
+++ b/config/initializers/fix_local_cache_middleware.rb
@@ -0,0 +1,24 @@
+module LocalCacheRegistryCleanupWithEnsure
+ LocalCacheRegistry =
+ ActiveSupport::Cache::Strategy::LocalCache::LocalCacheRegistry
+ LocalStore =
+ ActiveSupport::Cache::Strategy::LocalCache::LocalStore
+
+ def call(env)
+ LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
+ response = @app.call(env)
+ response[2] = ::Rack::BodyProxy.new(response[2]) do
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
+ end
+ cleanup_after_response = true # ADDED THIS LINE
+ response
+ rescue Rack::Utils::InvalidParameterError
+ [400, {}, []]
+ ensure # ADDED ensure CLAUSE to cleanup when something is thrown
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil) unless
+ cleanup_after_response
+ end
+end
+
+ActiveSupport::Cache::Strategy::LocalCache::Middleware
+ .prepend(LocalCacheRegistryCleanupWithEnsure)
diff --git a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
new file mode 100644
index 00000000000..b518038e93a
--- /dev/null
+++ b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ResetUsersAuthorizedProjectsPopulated < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # This ensures we don't lock all users for the duration of the migration.
+ update_column_in_batches(:users, :authorized_projects_populated, nil)
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
new file mode 100644
index 00000000000..9dfe77bedb7
--- /dev/null
+++ b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
@@ -0,0 +1,101 @@
+require 'thread'
+
+class RenameMoreReservedProjectNames < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::ShellAdapter
+
+ DOWNTIME = false
+
+ THREAD_COUNT = 8
+
+ KNOWN_PATHS = %w(artifacts graphs refs badges).freeze
+
+ def up
+ queues = Array.new(THREAD_COUNT) { Queue.new }
+ start = false
+
+ threads = Array.new(THREAD_COUNT) do |index|
+ Thread.new do
+ queue = queues[index]
+
+ # Wait until we have input to process.
+ until start; end
+
+ rename_projects(queue.pop) until queue.empty?
+ end
+ end
+
+ enum = queues.each
+
+ reserved_projects.each_slice(100) do |slice|
+ begin
+ queue = enum.next
+ rescue StopIteration
+ enum.rewind
+ retry
+ end
+
+ queue << slice
+ end
+
+ start = true
+
+ threads.each(&:join)
+ end
+
+ def down
+ # nothing to do here
+ end
+
+ private
+
+ def reserved_projects
+ Project.unscoped.
+ includes(:namespace).
+ where('EXISTS (SELECT 1 FROM namespaces WHERE projects.namespace_id = namespaces.id)').
+ where('projects.path' => KNOWN_PATHS)
+ end
+
+ def route_exists?(full_path)
+ quoted_path = ActiveRecord::Base.connection.quote_string(full_path)
+
+ ActiveRecord::Base.connection.
+ select_all("SELECT id, path FROM routes WHERE path = '#{quoted_path}'").present?
+ end
+
+ # Adds number to the end of the path that is not taken by other route
+ def rename_path(namespace_path, path_was)
+ counter = 0
+ path = "#{path_was}#{counter}"
+
+ while route_exists?("#{namespace_path}/#{path}")
+ counter += 1
+ path = "#{path_was}#{counter}"
+ end
+
+ path
+ end
+
+ def rename_projects(projects)
+ projects.each do |project|
+ id = project.id
+ path_was = project.path
+ namespace_path = project.namespace.path
+ path = rename_path(namespace_path, path_was)
+
+ begin
+ # Because project path update is quite complex operation we can't safely
+ # copy-paste all code from GitLab. As exception we use Rails code here
+ project.rename_repo if rename_project_row(project, path)
+ rescue Exception => e # rubocop: disable Lint/RescueException
+ Rails.logger.error "Exception when renaming project #{id}: #{e.message}"
+ end
+ end
+ end
+
+ def rename_project_row(project, path)
+ project.respond_to?(:update_attributes) &&
+ project.update_attributes(path: path) &&
+ project.respond_to?(:rename_repo)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 3ec5461f600..ca88198079f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170306170512) do
+ActiveRecord::Schema.define(version: 20170313133418) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
diff --git a/doc/api/issues.md b/doc/api/issues.md
index e25841926f8..cb437ffb174 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -329,18 +329,19 @@ Creates a new project issue.
POST /projects/:id/issues
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `title` | string | yes | The title of an issue |
-| `description` | string | no | The description of an issue |
-| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
-| `assignee_id` | integer | no | The ID of a user to assign issue |
-| `milestone_id` | integer | no | The ID of a milestone to assign issue |
-| `labels` | string | no | Comma-separated label names for an issue |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
-| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
-| `merge_request_for_resolving_discussions` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values. |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `title` | string | yes | The title of an issue |
+| `description` | string | no | The description of an issue |
+| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
+| `assignee_id` | integer | no | The ID of a user to assign issue |
+| `milestone_id` | integer | no | The ID of a milestone to assign issue |
+| `labels` | string | no | Comma-separated label names for an issue |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
+| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values. |
+| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues?title=Issues%20with%20auth&labels=bug
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 00787323b6b..f025a7e3496 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -170,13 +170,17 @@ services:
```
When the job is run, `tutum/wordpress` will be started and you will have
-access to it from your build container under the hostname `tutum__wordpress`.
+access to it from your build container under the hostnames `tutum-wordpress`
+(requires GitLab Runner v1.1.0 or newer) and `tutum__wordpress`.
-The alias hostname for the service is made from the image name following these
+*Note: hostname with underscores is not RFC valid and may cause problems in 3rd party applications.*
+
+The alias hostnames for the service are made from the image name following these
rules:
1. Everything after `:` is stripped
-2. Slash (`/`) is replaced with double underscores (`__`)
+2. Slash (`/`) is replaced with double underscores (`__`) - primary alias
+3. Slash (`/`) is replaced with dash (`-`) - secondary alias, requires GitLab Runner v1.1.0 or newer
## Configuring services
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index 49e7ac38b26..d00faaadc8b 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -30,8 +30,8 @@ This is the universal solution which works with any type of executor
## SSH keys when using the Docker executor
You will first need to create an SSH key pair. For more information, follow the
-instructions to [generate an SSH key](../../ssh/README.md). Do not add a comment
-to the SSH key, or the `before_script` will prompt for a passphrase.
+instructions to [generate an SSH key](../../ssh/README.md). Do not add a
+passphrase to the SSH key, or the `before_script` will prompt for it.
Then, create a new **Secret Variable** in your project settings on GitLab
following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY`
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index c71858c6a24..ff9a4fc4fec 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -1,7 +1,7 @@
-# Generate a changelog entry
+# Changelog entries
-This guide contains instructions for generating a changelog entry data file, as
-well as information and history about our changelog process.
+This guide contains instructions for when and how to generate a changelog entry
+file, as well as information and history about our changelog process.
## Overview
@@ -19,19 +19,51 @@ author: Ozzy Osbourne
The `merge_request` value is a reference to a merge request that adds this
entry, and the `author` key is used to give attribution to community
-contributors. Both are optional.
+contributors. **Both are optional**.
Community contributors and core team members are encouraged to add their name to
-the `author` field. GitLab team members should not.
-
-If you're working on the GitLab EE repository, the entry will be added to
-`changelogs/unreleased-ee/` instead.
+the `author` field. GitLab team members **should not**.
[changelog.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md
[unreleased]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/changelogs/
[YAML]: https://en.wikipedia.org/wiki/YAML
-## Instructions
+## What warrants a changelog entry?
+
+- Any user-facing change **should** have a changelog entry. Example: "GitLab now
+ uses system fonts for all text."
+- A fix for a regression introduced and then fixed in the same release (i.e.,
+ fixing a bug introduced during a monthly release candidate) **should not**
+ have a changelog entry.
+- Any developer-facing change (e.g., refactoring, technical debt remediation,
+ test suite changes) **should not** have a changelog entry. Example: "Reduce
+ database records created during Cycle Analytics model spec."
+- _Any_ contribution from a community member, no matter how small, **may** have
+ a changelog entry regardless of these guidelines if the contributor wants one.
+ Example: "Fixed a typo on the search results page. (Jane Smith)"
+
+## Writing good changelog entries
+
+A good changelog entry should be descriptive and concise. It should explain the
+change to a reader who has _zero context_ about the change. If you have trouble
+making it both concise and descriptive, err on the side of descriptive.
+
+- **Bad:** Go to a project order.
+- **Good:** Show a user's starred projects at the top of the "Go to project"
+ dropdown.
+- **Bad:** Copy [some text] to clipboard.
+- **Good:** Update the "Copy to clipboard" tooltip to indicate what's being
+ copied.
+- **Bad:** Fixes and Improves CSS and HTML problems in mini pipeline graph and
+ builds dropdown.
+- **Good:** Fix tooltips and hover states in mini pipeline graph and builds
+ dropdown.
+
+Use your best judgement and try to put yourself in the mindset of someone
+reading the compiled changelog. Does this entry add value? Does it offer context
+about _where_ and _why_ the change was made?
+
+## How to generate a changelog entry
A `bin/changelog` script is available to generate the changelog entry file
automatically.
@@ -55,19 +87,28 @@ title: Hey DZ, I added a feature to GitLab!
merge_request:
author:
```
+If you're working on the GitLab EE repository, the entry will be added to
+`changelogs/unreleased-ee/` instead.
+
+#### Arguments
-### Arguments
+| Argument | Shorthand | Purpose |
+| ----------------- | --------- | --------------------------------------------- |
+| [`--amend`] | | Amend the previous commit |
+| [`--force`] | `-f` | Overwrite an existing entry |
+| [`--merge-request`] | `-m` | Set merge request ID |
+| [`--dry-run`] | `-n` | Don't actually write anything, just print |
+| [`--git-username`] | `-u` | Use Git user.name configuration as the author |
+| [`--help`] | `-h` | Print help message |
-| Argument | Shorthand | Purpose |
-| ----------------- | --------- | --------------------------------------------- |
-| `--amend` | | Amend the previous commit |
-| `--force` | `-f` | Overwrite an existing entry |
-| `--merge-request` | `-m` | Merge Request ID |
-| `--dry-run` | `-n` | Don't actually write anything, just print |
-| `--git-username` | `-u` | Use Git user.name configuration as the author |
-| `--help` | `-h` | Print help message |
+[`--amend`]: #-amend
+[`--force`]: #-force-or-f
+[`--merge-request`]: #-merge-request-or-m
+[`--dry-run`]: #-dry-run-or-n
+[`--git-username`]: #-git-username-or-u
+[`--help`]: #-help
-#### `--amend`
+##### `--amend`
You can pass the **`--amend`** argument to automatically stage the generated
file and amend it to the previous commit.
@@ -88,7 +129,7 @@ merge_request:
author:
```
-#### `--force` or `-f`
+##### `--force` or `-f`
Use **`--force`** or **`-f`** to overwrite an existing changelog entry if it
already exists.
@@ -105,7 +146,7 @@ merge_request: 1983
author:
```
-#### `--merge-request` or `-m`
+##### `--merge-request` or `-m`
Use the **`--merge-request`** or **`-m`** argument to provide the
`merge_request` value:
@@ -119,7 +160,7 @@ merge_request: 1983
author:
```
-#### `--dry-run` or `-n`
+##### `--dry-run` or `-n`
Use the **`--dry-run`** or **`-n`** argument to prevent actually writing or
committing anything:
@@ -135,7 +176,7 @@ author:
$ ls changelogs/unreleased/
```
-#### `--git-username` or `-u`
+##### `--git-username` or `-u`
Use the **`--git-username`** or **`-u`** argument to automatically fill in the
`author` value with your configured Git `user.name` value:
@@ -152,7 +193,7 @@ merge_request:
author: Jane Doe
```
-## History and Reasoning
+### History and Reasoning
Our `CHANGELOG` file was previously updated manually by each contributor that
felt their change warranted an entry. When two merge requests added their own
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
index 8232a0a113c..2b4126b43ef 100644
--- a/doc/development/merge_request_performance_guidelines.md
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -68,7 +68,7 @@ end
This will end up running one query for every object to update. This code can
easily overload a database given enough rows to update or many instances of this
code running in parallel. This particular problem is known as the
-["N+1 query problem"](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations).
+["N+1 query problem"](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations). You can write a test with [QueryRecoder](query_recorder.md) to detect this and prevent regressions.
In this particular case the workaround is fairly easy:
@@ -117,6 +117,8 @@ Post.all.includes(:author).each do |post|
end
```
+Also consider using [QueryRecoder tests](query_recorder.md) to prevent a regression when eager loading.
+
## Memory Usage
**Summary:** merge requests **must not** increase memory usage unless absolutely
diff --git a/doc/development/performance.md b/doc/development/performance.md
index c1f129e576c..04419650b12 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -39,6 +39,7 @@ GitLab provides built-in tools to aid the process of improving performance:
* [Sherlock](profiling.md#sherlock)
* [GitLab Performance Monitoring](../administration/monitoring/performance/introduction.md)
* [Request Profiling](../administration/monitoring/performance/request_profiling.md)
+* [QueryRecoder](query_recorder.md) for preventing `N+1` regressions
GitLab employees can use GitLab.com's performance monitoring systems located at
<http://performance.gitlab.net>, this requires you to log in using your
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index e244ad4e881..933033a09e0 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -25,3 +25,5 @@ starting GitLab. For example:
Bullet will log query problems to both the Rails log as well as the Chrome
console.
+
+As a follow up to finding `N+1` queries with Bullet, consider writing a [QueryRecoder test](query_recorder.md) to prevent a regression.
diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md
new file mode 100644
index 00000000000..e0127aaed4c
--- /dev/null
+++ b/doc/development/query_recorder.md
@@ -0,0 +1,29 @@
+# QueryRecorder
+
+QueryRecorder is a tool for detecting the [N+1 queries problem](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations) from tests.
+
+> Implemented in [spec/support/query_recorder.rb](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/support/query_recorder.rb) via [9c623e3e](https://gitlab.com/gitlab-org/gitlab-ce/commit/9c623e3e5d7434f2e30f7c389d13e5af4ede770a)
+
+As a rule, merge requests [should not increase query counts](merge_request_performance_guidelines.md#query-counts). If you find yourself adding something like `.includes(:author, :assignee)` to avoid having `N+1` queries, consider using QueryRecorder to enforce this with a test. Without this, a new feature which causes an additional model to be accessed will silently reintroduce the problem.
+
+## How it works
+
+This style of test works by counting the number of SQL queries executed by ActiveRecord. First a control count is taken, then you add new records to the database and rerun the count. If the number of queries has significantly increased then an `N+1` queries problem exists.
+
+```ruby
+it "avoids N+1 database queries" do
+ control_count = ActiveRecord::QueryRecorder.new { visit_some_page }.count
+ create_list(:issue, 5)
+ expect { visit_some_page }.not_to exceed_query_limit(control_count)
+end
+```
+
+As an example you might create 5 issues in between counts, which would cause the query count to increase by 5 if an N+1 problem exists.
+
+> **Note:** In some cases the query count might change slightly between runs for unrelated reasons. In this case you might need to test `exceed_query_limit(control_count + acceptable_change)`, but this should be avoided if possible.
+
+## See also
+
+- [Bullet](profiling.md#Bullet) For finding `N+1` query problems
+- [Performance guidelines](performance.md)
+- [Merge request performance guidelines](merge_request_performance_guidelines.md#query-counts) \ No newline at end of file
diff --git a/doc/integration/github.md b/doc/integration/github.md
index cea85f073cc..4b0d33334bd 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -19,7 +19,7 @@ GitHub will generate an application ID and secret key for you to use.
- Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com'
- Application description: Fill this in if you wish.
- - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'
+ - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'. Please make sure the port is included if your Gitlab instance is not configured on default port.
1. Select "Register application".
1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index db06224bac2..97de428d11d 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -431,7 +431,7 @@ Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
-Combined emphasis with **asterisks and _underscores_**.
+Combined emphasis with **_asterisks and underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
```
diff --git a/doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.png b/doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.png
new file mode 100644
index 00000000000..b15447ec290
--- /dev/null
+++ b/doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/new_issue_for_discussion.png b/doc/user/project/merge_requests/img/new_issue_for_discussion.png
new file mode 100644
index 00000000000..93c9dad8921
--- /dev/null
+++ b/doc/user/project/merge_requests/img/new_issue_for_discussion.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/preview_issue_for_discussion.png b/doc/user/project/merge_requests/img/preview_issue_for_discussion.png
new file mode 100644
index 00000000000..2ee0653b2ba
--- /dev/null
+++ b/doc/user/project/merge_requests/img/preview_issue_for_discussion.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/preview_issue_for_discussions.png b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png
index 9fdd387676c..3fe0a666678 100644
--- a/doc/user/project/merge_requests/img/preview_issue_for_discussions.png
+++ b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png b/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png
index 8c7ce215ae0..e0ee6a39ffd 100644
--- a/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png
+++ b/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png
Binary files differ
diff --git a/doc/user/project/merge_requests/merge_request_discussion_resolution.md b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
index d4b85676d19..230e957f045 100644
--- a/doc/user/project/merge_requests/merge_request_discussion_resolution.md
+++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
@@ -53,12 +53,18 @@ are resolved.
## Move all unresolved discussions in a merge request to an issue
-> [Introduced][ce-7180] in GitLab 8.15.
+> [Introduced][ce-8266]
-To delegate unresolved discussions to a new issue you can click the link **open
-an issue to resolve them later**.
+To continue all open discussions in a merge request, click the button **Resolve
+all discussions in new issue**
-![Open new issue from unresolved discussions](img/resolve_discussion_open_issue.png)
+![Open new issue for all unresolved discussions](img/btn_new_issue_for_all_discussions.png)
+
+Alternatively, when your project only accepts merge requests when all discussions
+are resolved, there will be an **open an issue to resolve them later** link in
+the merge request-widget.
+
+![Link in merge request widget](img/resolve_discussion_open_issue.png)
This will prepare an issue with content referring to the merge request and
discussions.
@@ -72,9 +78,28 @@ add a note referring to the newly created issue.
You can now proceed to merge the merge request from the UI.
+## Moving a single discussion to a new issue
+
+> [Introduced][ce-8266]
+
+To create a new issue for a single discussion, you can use the **Resolve this
+discussion in a new issue** button.
+
+![Create issue for discussion](img/new_issue_for_discussion.png)
+
+This will direct you to a new issue prefilled with the content of the
+discussion, similar to the issues created for delegating multiple
+discussions at once.
+
+![New issue for a single discussion](img/preview_issue_for_discussion.png)
+
+Saving the issue will mark the discussion as resolved and add a note
+to the discussion referencing the new issue.
+
[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
+[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
[resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_button.png
[discussion-view]: img/discussion_view.png
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index f18adcadcce..6845f75f22f 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -82,7 +82,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I fill the new branch name' do
- fill_in :target_branch, with: 'new_branch_name', visible: true
+ first('button.js-target-branch', visible: true).click
+ first('.create-new-branch', visible: true).click
+ first('#new_branch_name', visible: true).set('new_branch_name')
+ first('.js-new-branch-btn', visible: true).click
end
step 'I fill the new file name with an illegal name' do
@@ -334,6 +337,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I click on "files/lfs/lfs_object.iso" file in repo' do
+ allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
visit namespace_project_tree_path(@project.namespace, @project, "lfs")
click_link 'files'
click_link "lfs"
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index a9b364da9e1..bd22b82476b 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -97,7 +97,7 @@ module API
end
def authenticate!
- unauthorized! unless current_user
+ unauthorized! unless current_user && can?(current_user, :access_api)
end
def authenticate_non_get!
@@ -116,7 +116,7 @@ module API
forbidden! unless current_user.is_admin?
end
- def authorize!(action, subject = nil)
+ def authorize!(action, subject = :global)
forbidden! unless can?(current_user, action, subject)
end
@@ -134,7 +134,7 @@ module API
end
end
- def can?(object, action, subject)
+ def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 4a9f2b26fb2..1abe8639445 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -116,8 +116,10 @@ module API
requires :title, type: String, desc: 'The title of an issue'
optional :created_at, type: DateTime,
desc: 'Date time when the issue was created. Available only for admins and project owners.'
- optional :merge_request_for_resolving_discussions, type: Integer,
+ optional :merge_request_to_resolve_discussions_of, type: Integer,
desc: 'The IID of a merge request for which to resolve discussions'
+ optional :discussion_to_resolve, type: String,
+ desc: 'The ID of a discussion to resolve, also pass `merge_request_to_resolve_discussions_of`'
use :issue_params
end
post ':id/issues' do
@@ -128,12 +130,6 @@ module API
issue_params = declared_params(include_missing: false)
- if merge_request_iid = params[:merge_request_for_resolving_discussions]
- issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id).
- execute.
- find_by(iid: merge_request_iid)
- end
-
issue = ::Issues::CreateService.new(user_project,
current_user,
issue_params.merge(request: request, api: true)).execute
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 549003f576a..2d4d5a25221 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -45,7 +45,7 @@ module API
use :pagination
end
get do
- unless can?(current_user, :read_users_list, nil)
+ unless can?(current_user, :read_users_list)
render_api_error!("Not authorized.", 403)
end
diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb
index 5d7dfabfcd6..258cbfed022 100644
--- a/lib/api/v3/issues.rb
+++ b/lib/api/v3/issues.rb
@@ -139,12 +139,7 @@ module API
end
issue_params = declared_params(include_missing: false)
-
- if merge_request_iid = params[:merge_request_for_resolving_discussions]
- issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id).
- execute.
- find_by(iid: merge_request_iid)
- end
+ issue_params = issue_params.merge(merge_request_to_resolve_discussions_of: issue_params.delete(:merge_request_for_resolving_discussions))
issue = ::Issues::CreateService.new(user_project,
current_user,
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index 2058a58d0ae..b121c37c5d0 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -210,7 +210,7 @@ module Banzai
grouped_objects_for_nodes(nodes, Project, 'data-project')
end
- def can?(user, permission, subject)
+ def can?(user, permission, subject = :global)
Ability.allowed?(user, permission, subject)
end
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
index c1fd959ef14..45aa2adccf5 100644
--- a/lib/ci/api/runners.rb
+++ b/lib/ci/api/runners.rb
@@ -24,13 +24,13 @@ module Ci
optional :locked, type: Boolean, desc: 'Lock this runner for this specific project'
end
post "register" do
- runner_params = declared(params, include_missing: false)
+ runner_params = declared(params, include_missing: false).except(:token)
runner =
if runner_registration_token_valid?
# Create shared runner. Requires admin access
Ci::Runner.create(runner_params.merge(is_shared: true))
- elsif project = Project.find_by(runners_token: runner_params[:token])
+ elsif project = Project.find_by(runners_token: params[:token])
# Create a specific runner for project.
project.runners.create(runner_params)
end
diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb
index f48abcc86d5..e4f7cad2b79 100644
--- a/lib/gitlab/allowable.rb
+++ b/lib/gitlab/allowable.rb
@@ -1,6 +1,6 @@
module Gitlab
module Allowable
- def can?(user, action, subject)
+ def can?(user, action, subject = :global)
Ability.allowed?(user, action, subject)
end
end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 273118135a9..c85f79127bc 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -1,16 +1,20 @@
module Gitlab
module Checks
class ChangeAccess
- attr_reader :user_access, :project, :skip_authorization
+ # protocol is currently used only in EE
+ attr_reader :user_access, :project, :skip_authorization, :protocol
def initialize(
- change, user_access:, project:, env: {}, skip_authorization: false)
+ change, user_access:, project:, env: {}, skip_authorization: false,
+ protocol:
+ )
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@user_access = user_access
@project = project
@env = env
@skip_authorization = skip_authorization
+ @protocol = protocol
end
def exec
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 80a146b4a5a..114656958e3 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -38,11 +38,11 @@ module Gitlab
end
def added?
- type == 'new'
+ type == 'new' || type == 'new-nonewline'
end
def removed?
- type == 'old'
+ type == 'old' || type == 'old-nonewline'
end
def rich_text
@@ -52,7 +52,7 @@ module Gitlab
end
def meta?
- type == 'match' || type == 'nonewline'
+ type == 'match'
end
def as_json(opts = nil)
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 8f844224a7a..742f989c50b 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -11,6 +11,7 @@ module Gitlab
line_old = 1
line_new = 1
type = nil
+ context = nil
# By returning an Enumerator we make it possible to search for a single line (with #find)
# without having to instantiate all the others that come after it.
@@ -31,7 +32,8 @@ module Gitlab
line_obj_index += 1
next
elsif line[0] == '\\'
- type = 'nonewline'
+ type = "#{context}-nonewline"
+
yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
line_obj_index += 1
else
@@ -43,8 +45,10 @@ module Gitlab
case line[0]
when "+"
line_new += 1
+ context = :new
when "-"
line_old += 1
+ context = :old
when "\\" # rubocop:disable Lint/EmptyWhen
# No increment
else
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index ffb178334bc..eea2f206902 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -153,7 +153,9 @@ module Gitlab
user_access: user_access,
project: project,
env: @env,
- skip_authorization: deploy_key?).exec
+ skip_authorization: deploy_key?,
+ protocol: protocol
+ ).exec
end
def matching_merge_request?(newrev, branch_name)
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 6ce9b229294..f260c0c535f 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def can_do_action?(action)
- return false if no_user_or_blocked?
+ return false unless can_access_git?
@permission_cache ||= {}
@permission_cache[action] ||= user.can?(action, project)
@@ -19,7 +19,7 @@ module Gitlab
end
def allowed?
- return false if no_user_or_blocked?
+ return false unless can_access_git?
if user.requires_ldap_check? && user.try_obtain_ldap_lease
return false unless Gitlab::LDAP::Access.allowed?(user)
@@ -29,7 +29,7 @@ module Gitlab
end
def can_push_to_branch?(ref)
- return false if no_user_or_blocked?
+ return false unless can_access_git?
if project.protected_branch?(ref)
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
@@ -44,7 +44,7 @@ module Gitlab
end
def can_merge_to_branch?(ref)
- return false if no_user_or_blocked?
+ return false unless can_access_git?
if project.protected_branch?(ref)
access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
@@ -55,15 +55,15 @@ module Gitlab
end
def can_read_project?
- return false if no_user_or_blocked?
+ return false unless can_access_git?
user.can?(:read_project, project)
end
private
- def no_user_or_blocked?
- user.nil? || user.blocked?
+ def can_access_git?
+ user && user.can?(:access_git)
end
end
end
diff --git a/package.json b/package.json
index efa3a63e693..9652dd8f972 100644
--- a/package.json
+++ b/package.json
@@ -17,11 +17,11 @@
"babel-preset-stage-2": "^6.22.0",
"bootstrap-sass": "^3.3.6",
"compression-webpack-plugin": "^0.3.2",
+ "core-js": "^2.4.1",
"d3": "^3.5.11",
"document-register-element": "^1.3.0",
"dropzone": "^4.2.0",
"emoji-unicode-version": "^0.2.1",
- "es6-promise": "^4.0.5",
"jquery": "^2.2.1",
"jquery-ujs": "^1.2.1",
"js-cookie": "^2.1.3",
@@ -31,8 +31,6 @@
"raw-loader": "^0.5.1",
"select2": "3.5.2-browserify",
"stats-webpack-plugin": "^0.4.3",
- "string.fromcodepoint": "^0.2.1",
- "string.prototype.codepointat": "^0.2.0",
"timeago.js": "^2.0.5",
"underscore": "^1.8.3",
"vue": "^2.1.10",
diff --git a/qa/.rspec b/qa/.rspec
new file mode 100644
index 00000000000..b83d9b7aa65
--- /dev/null
+++ b/qa/.rspec
@@ -0,0 +1,3 @@
+--color
+--format documentation
+--require spec_helper
diff --git a/qa/Dockerfile b/qa/Dockerfile
new file mode 100644
index 00000000000..2814a7bdef0
--- /dev/null
+++ b/qa/Dockerfile
@@ -0,0 +1,14 @@
+FROM ruby:2.3
+LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>"
+
+RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && \
+ apt-get update && apt-get install -y --force-yes \
+ libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \
+ apt-get clean
+
+WORKDIR /home/qa
+
+COPY ./ ./
+RUN bundle install
+
+ENTRYPOINT ["bin/test"]
diff --git a/qa/Gemfile b/qa/Gemfile
new file mode 100644
index 00000000000..6bfe25ba437
--- /dev/null
+++ b/qa/Gemfile
@@ -0,0 +1,7 @@
+source 'https://rubygems.org'
+
+gem 'capybara', '~> 2.12.1'
+gem 'capybara-screenshot', '~> 1.0.14'
+gem 'capybara-webkit', '~> 1.12.0'
+gem 'rake', '~> 12.0.0'
+gem 'rspec', '~> 3.5'
diff --git a/qa/README.md b/qa/README.md
new file mode 100644
index 00000000000..b6b5a76f1d3
--- /dev/null
+++ b/qa/README.md
@@ -0,0 +1,18 @@
+## Integration tests for GitLab
+
+This directory contains integration tests for GitLab.
+
+It is part of [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa).
+
+## What GitLab QA is?
+
+GitLab QA is an integration tests suite for GitLab.
+
+These are black-box and entirely click-driven integration tests you can run
+against any existing instance.
+
+## How does it work?
+
+1. When we release a new version of GitLab, we build a Docker images for it.
+1. Along with GitLab Docker Images we also build and publish GitLab QA images.
+1. GitLab QA project uses these images to execute integration tests.
diff --git a/qa/bin/qa b/qa/bin/qa
new file mode 100755
index 00000000000..cecdeac14db
--- /dev/null
+++ b/qa/bin/qa
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+require_relative '../qa'
+
+QA::Scenario
+ .const_get(ARGV.shift)
+ .perform(*ARGV)
diff --git a/qa/bin/test b/qa/bin/test
new file mode 100755
index 00000000000..997392ad6e4
--- /dev/null
+++ b/qa/bin/test
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+xvfb-run bundle exec bin/qa $@
diff --git a/qa/qa.rb b/qa/qa.rb
new file mode 100644
index 00000000000..58cf615cc9f
--- /dev/null
+++ b/qa/qa.rb
@@ -0,0 +1,81 @@
+$: << File.expand_path(File.dirname(__FILE__))
+
+module QA
+ ##
+ # GitLab QA runtime classes, mostly singletons.
+ #
+ module Runtime
+ autoload :Release, 'qa/runtime/release'
+ autoload :User, 'qa/runtime/user'
+ autoload :Namespace, 'qa/runtime/namespace'
+ end
+
+ ##
+ # GitLab QA Scenarios
+ #
+ module Scenario
+ ##
+ # Support files
+ #
+ autoload :Actable, 'qa/scenario/actable'
+ autoload :Template, 'qa/scenario/template'
+
+ ##
+ # Test scenario entrypoints.
+ #
+ module Test
+ autoload :Instance, 'qa/scenario/test/instance'
+ end
+
+ ##
+ # GitLab instance scenarios.
+ #
+ module Gitlab
+ module Project
+ autoload :Create, 'qa/scenario/gitlab/project/create'
+ end
+ end
+ end
+
+ ##
+ # Classes describing structure of GitLab, pages, menus etc.
+ #
+ # Needed to execute click-driven-only black-box tests.
+ #
+ module Page
+ autoload :Base, 'qa/page/base'
+
+ module Main
+ autoload :Entry, 'qa/page/main/entry'
+ autoload :Menu, 'qa/page/main/menu'
+ autoload :Groups, 'qa/page/main/groups'
+ autoload :Projects, 'qa/page/main/projects'
+ end
+
+ module Project
+ autoload :New, 'qa/page/project/new'
+ autoload :Show, 'qa/page/project/show'
+ end
+
+ module Admin
+ autoload :Menu, 'qa/page/admin/menu'
+ end
+ end
+
+ ##
+ # Classes describing operations on Git repositories.
+ #
+ module Git
+ autoload :Repository, 'qa/git/repository'
+ end
+
+ ##
+ # Classes that make it possible to execute features tests.
+ #
+ module Specs
+ autoload :Config, 'qa/specs/config'
+ autoload :Runner, 'qa/specs/runner'
+ end
+end
+
+QA::Runtime::Release.extend_autoloads!
diff --git a/qa/qa/ce/strategy.rb b/qa/qa/ce/strategy.rb
new file mode 100644
index 00000000000..6d1601dfa48
--- /dev/null
+++ b/qa/qa/ce/strategy.rb
@@ -0,0 +1,15 @@
+module QA
+ module CE
+ module Strategy
+ extend self
+
+ def extend_autoloads!
+ # noop
+ end
+
+ def perform_before_hooks
+ # noop
+ end
+ end
+ end
+end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
new file mode 100644
index 00000000000..b9e199000d6
--- /dev/null
+++ b/qa/qa/git/repository.rb
@@ -0,0 +1,71 @@
+require 'uri'
+
+module QA
+ module Git
+ class Repository
+ include Scenario::Actable
+
+ def self.perform(*args)
+ Dir.mktmpdir do |dir|
+ Dir.chdir(dir) { super }
+ end
+ end
+
+ def location=(address)
+ @location = address
+ @uri = URI(address)
+ end
+
+ def username=(name)
+ @username = name
+ @uri.user = name
+ end
+
+ def password=(pass)
+ @password = pass
+ @uri.password = pass
+ end
+
+ def use_default_credentials
+ self.username = Runtime::User.name
+ self.password = Runtime::User.password
+ end
+
+ def clone(opts = '')
+ `git clone #{opts} #{@uri.to_s} ./`
+ end
+
+ def shallow_clone
+ clone('--depth 1')
+ end
+
+ def configure_identity(name, email)
+ `git config user.name #{name}`
+ `git config user.email #{email}`
+ end
+
+ def commit_file(name, contents, message)
+ add_file(name, contents)
+ commit(message)
+ end
+
+ def add_file(name, contents)
+ File.write(name, contents)
+
+ `git add #{name}`
+ end
+
+ def commit(message)
+ `git commit -m "#{message}"`
+ end
+
+ def push_changes(branch = 'master')
+ `git push #{@uri.to_s} #{branch}`
+ end
+
+ def commits
+ `git log --oneline`.split("\n")
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
new file mode 100644
index 00000000000..b01a4e10f93
--- /dev/null
+++ b/qa/qa/page/admin/menu.rb
@@ -0,0 +1,19 @@
+module QA
+ module Page
+ module Admin
+ class Menu < Page::Base
+ def go_to_license
+ within_middle_menu { click_link 'License' }
+ end
+
+ private
+
+ def within_middle_menu
+ page.within('.nav-control') do
+ yield
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
new file mode 100644
index 00000000000..d55326c5262
--- /dev/null
+++ b/qa/qa/page/base.rb
@@ -0,0 +1,12 @@
+module QA
+ module Page
+ class Base
+ include Capybara::DSL
+ include Scenario::Actable
+
+ def refresh
+ visit current_path
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb
new file mode 100644
index 00000000000..fe80deb6429
--- /dev/null
+++ b/qa/qa/page/main/entry.rb
@@ -0,0 +1,26 @@
+module QA
+ module Page
+ module Main
+ class Entry < Page::Base
+ def initialize
+ visit('/')
+
+ # This resolves cold boot problems with login page
+ find('.application', wait: 120)
+ end
+
+ def sign_in_using_credentials
+ if page.has_content?('Change your password')
+ fill_in :user_password, with: Runtime::User.password
+ fill_in :user_password_confirmation, with: Runtime::User.password
+ click_button 'Change your password'
+ end
+
+ fill_in :user_login, with: Runtime::User.name
+ fill_in :user_password, with: Runtime::User.password
+ click_button 'Sign in'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/main/groups.rb b/qa/qa/page/main/groups.rb
new file mode 100644
index 00000000000..84597719a84
--- /dev/null
+++ b/qa/qa/page/main/groups.rb
@@ -0,0 +1,20 @@
+module QA
+ module Page
+ module Main
+ class Groups < Page::Base
+ def prepare_test_namespace
+ return if page.has_content?(Runtime::Namespace.name)
+
+ click_on 'New Group'
+
+ fill_in 'group_path', with: Runtime::Namespace.name
+ fill_in 'group_description',
+ with: "QA test run at #{Runtime::Namespace.time}"
+ choose 'Private'
+
+ click_button 'Create group'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
new file mode 100644
index 00000000000..45db7a92fa4
--- /dev/null
+++ b/qa/qa/page/main/menu.rb
@@ -0,0 +1,46 @@
+module QA
+ module Page
+ module Main
+ class Menu < Page::Base
+ def go_to_groups
+ within_global_menu { click_link 'Groups' }
+ end
+
+ def go_to_projects
+ within_global_menu { click_link 'Projects' }
+ end
+
+ def go_to_admin_area
+ within_user_menu { click_link 'Admin Area' }
+ end
+
+ def sign_out
+ within_user_menu do
+ find('.header-user-dropdown-toggle').click
+ click_link('Sign out')
+ end
+ end
+
+ def has_personal_area?
+ page.has_selector?('.header-user-dropdown-toggle')
+ end
+
+ private
+
+ def within_global_menu
+ find('.global-dropdown-toggle').click
+
+ page.within('.global-dropdown-menu') do
+ yield
+ end
+ end
+
+ def within_user_menu
+ page.within('.navbar-nav') do
+ yield
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/main/projects.rb b/qa/qa/page/main/projects.rb
new file mode 100644
index 00000000000..28d3a424022
--- /dev/null
+++ b/qa/qa/page/main/projects.rb
@@ -0,0 +1,16 @@
+module QA
+ module Page
+ module Main
+ class Projects < Page::Base
+ def go_to_new_project
+ ##
+ # There are 'New Project' and 'New project' buttons on the projects
+ # page, so we can't use `click_on`.
+ #
+ button = find('a', text: /^new project$/i)
+ button.click
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
new file mode 100644
index 00000000000..b31bec27b59
--- /dev/null
+++ b/qa/qa/page/project/new.rb
@@ -0,0 +1,24 @@
+module QA
+ module Page
+ module Project
+ class New < Page::Base
+ def choose_test_namespace
+ find('#s2id_project_namespace_id').click
+ find('.select2-result-label', text: Runtime::Namespace.name).click
+ end
+
+ def choose_name(name)
+ fill_in 'project_path', with: name
+ end
+
+ def add_description(description)
+ fill_in 'project_description', with: description
+ end
+
+ def create_new_project
+ click_on 'Create project'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
new file mode 100644
index 00000000000..56a270d8fcc
--- /dev/null
+++ b/qa/qa/page/project/show.rb
@@ -0,0 +1,23 @@
+module QA
+ module Page
+ module Project
+ class Show < Page::Base
+ def choose_repository_clone_http
+ find('#clone-dropdown').click
+
+ page.within('#clone-dropdown') do
+ find('span', text: 'HTTP').click
+ end
+ end
+
+ def repository_location
+ find('#project_clone').value
+ end
+
+ def wait_for_push
+ sleep 5
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb
new file mode 100644
index 00000000000..e4910b63a14
--- /dev/null
+++ b/qa/qa/runtime/namespace.rb
@@ -0,0 +1,15 @@
+module QA
+ module Runtime
+ module Namespace
+ extend self
+
+ def time
+ @time ||= Time.now
+ end
+
+ def name
+ 'qa_test_' + time.strftime('%d_%m_%Y_%H-%M-%S')
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb
new file mode 100644
index 00000000000..4f83a773645
--- /dev/null
+++ b/qa/qa/runtime/release.rb
@@ -0,0 +1,28 @@
+module QA
+ module Runtime
+ ##
+ # Class that is responsible for plugging CE/EE extensions in, depending on
+ # existence of EE module.
+ #
+ # We need that to reduce the probability of conflicts when merging
+ # CE to EE.
+ #
+ class Release
+ def initialize
+ require "qa/#{version.downcase}/strategy"
+ end
+
+ def version
+ @version ||= File.directory?("#{__dir__}/../ee") ? :EE : :CE
+ end
+
+ def strategy
+ QA.const_get("QA::#{version}::Strategy")
+ end
+
+ def self.method_missing(name, *args)
+ self.new.strategy.public_send(name, *args)
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb
new file mode 100644
index 00000000000..12ceda015f0
--- /dev/null
+++ b/qa/qa/runtime/user.rb
@@ -0,0 +1,15 @@
+module QA
+ module Runtime
+ module User
+ extend self
+
+ def name
+ ENV['GITLAB_USERNAME'] || 'root'
+ end
+
+ def password
+ ENV['GITLAB_PASSWORD'] || 'test1234'
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/actable.rb b/qa/qa/scenario/actable.rb
new file mode 100644
index 00000000000..6cdbd24780e
--- /dev/null
+++ b/qa/qa/scenario/actable.rb
@@ -0,0 +1,23 @@
+module QA
+ module Scenario
+ module Actable
+ def act(*args, &block)
+ instance_exec(*args, &block)
+ end
+
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def perform
+ yield new if block_given?
+ end
+
+ def act(*args, &block)
+ new.act(*args, &block)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/gitlab/project/create.rb b/qa/qa/scenario/gitlab/project/create.rb
new file mode 100644
index 00000000000..38522714e64
--- /dev/null
+++ b/qa/qa/scenario/gitlab/project/create.rb
@@ -0,0 +1,31 @@
+require 'securerandom'
+
+module QA
+ module Scenario
+ module Gitlab
+ module Project
+ class Create < Scenario::Template
+ attr_writer :description
+
+ def name=(name)
+ @name = "#{name}-#{SecureRandom.hex(8)}"
+ end
+
+ def perform
+ Page::Main::Menu.act { go_to_groups }
+ Page::Main::Groups.act { prepare_test_namespace }
+ Page::Main::Menu.act { go_to_projects }
+ Page::Main::Projects.act { go_to_new_project }
+
+ Page::Project::New.perform do |page|
+ page.choose_test_namespace
+ page.choose_name(@name)
+ page.add_description(@description)
+ page.create_new_project
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb
new file mode 100644
index 00000000000..341998af160
--- /dev/null
+++ b/qa/qa/scenario/template.rb
@@ -0,0 +1,16 @@
+module QA
+ module Scenario
+ class Template
+ def self.perform(*args)
+ new.tap do |scenario|
+ yield scenario if block_given?
+ return scenario.perform(*args)
+ end
+ end
+
+ def perform(*_args)
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb
new file mode 100644
index 00000000000..689292bc60b
--- /dev/null
+++ b/qa/qa/scenario/test/instance.rb
@@ -0,0 +1,26 @@
+module QA
+ module Scenario
+ module Test
+ ##
+ # Run test suite against any GitLab instance,
+ # including staging and on-premises installation.
+ #
+ class Instance < Scenario::Template
+ def perform(address, *files)
+ Specs::Config.perform do |specs|
+ specs.address = address
+ end
+
+ ##
+ # Perform before hooks, which are different for CE and EE
+ #
+ Runtime::Release.perform_before_hooks
+
+ Specs::Runner.perform do |specs|
+ specs.rspec('--tty', files.any? ? files : 'qa/specs/features')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb
new file mode 100644
index 00000000000..d72187fcd34
--- /dev/null
+++ b/qa/qa/specs/config.rb
@@ -0,0 +1,78 @@
+require 'rspec/core'
+require 'capybara/rspec'
+require 'capybara-webkit'
+require 'capybara-screenshot/rspec'
+
+# rubocop:disable Metrics/MethodLength
+# rubocop:disable Metrics/LineLength
+
+module QA
+ module Specs
+ class Config < Scenario::Template
+ attr_writer :address
+
+ def initialize
+ @address = ENV['GITLAB_URL']
+ end
+
+ def perform
+ raise 'Please configure GitLab address!' unless @address
+
+ configure_rspec!
+ configure_capybara!
+ configure_webkit!
+ end
+
+ def configure_rspec!
+ RSpec.configure do |config|
+ config.expect_with :rspec do |expectations|
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`.
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+ # Run specs in random order to surface order dependencies.
+ config.order = :random
+ Kernel.srand config.seed
+
+ config.before(:all) do
+ page.current_window.resize_to(1200, 1800)
+ end
+
+ config.formatter = :documentation
+ config.color = true
+ end
+ end
+
+ def configure_capybara!
+ Capybara.configure do |config|
+ config.app_host = @address
+ config.default_driver = :webkit
+ config.javascript_driver = :webkit
+ config.default_max_wait_time = 4
+
+ # https://github.com/mattheworiordan/capybara-screenshot/issues/164
+ config.save_path = 'tmp'
+ end
+ end
+
+ def configure_webkit!
+ Capybara::Webkit.configure do |config|
+ config.allow_url(@address)
+ config.block_unknown_urls
+ end
+ rescue RuntimeError # rubocop:disable Lint/HandleExceptions
+ # TODO, Webkit is already configured, this make this
+ # configuration step idempotent, should be improved.
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb
new file mode 100644
index 00000000000..8e1ae6efa47
--- /dev/null
+++ b/qa/qa/specs/features/login/standard_spec.rb
@@ -0,0 +1,14 @@
+module QA
+ feature 'standard root login' do
+ scenario 'user logs in using credentials' do
+ Page::Main::Entry.act { sign_in_using_credentials }
+
+ # TODO, since `Signed in successfully` message was removed
+ # this is the only way to tell if user is signed in correctly.
+ #
+ Page::Main::Menu.perform do |menu|
+ expect(menu).to have_personal_area
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb
new file mode 100644
index 00000000000..610492b9717
--- /dev/null
+++ b/qa/qa/specs/features/project/create_spec.rb
@@ -0,0 +1,19 @@
+module QA
+ feature 'create a new project' do
+ scenario 'user creates a new project' do
+ Page::Main::Entry.act { sign_in_using_credentials }
+
+ Scenario::Gitlab::Project::Create.perform do |project|
+ project.name = 'awesome-project'
+ project.description = 'create awesome project test'
+ end
+
+ expect(page).to have_content(
+ /Project \S?awesome-project\S+ was successfully created/
+ )
+
+ expect(page).to have_content('create awesome project test')
+ expect(page).to have_content('The repository for this project is empty')
+ end
+ end
+end
diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb
new file mode 100644
index 00000000000..521bd955857
--- /dev/null
+++ b/qa/qa/specs/features/repository/clone_spec.rb
@@ -0,0 +1,57 @@
+module QA
+ feature 'clone code from the repository' do
+ context 'with regular account over http' do
+ given(:location) do
+ Page::Project::Show.act do
+ choose_repository_clone_http
+ repository_location
+ end
+ end
+
+ before do
+ Page::Main::Entry.act { sign_in_using_credentials }
+
+ Scenario::Gitlab::Project::Create.perform do |scenario|
+ scenario.name = 'project-with-code'
+ scenario.description = 'project for git clone tests'
+ end
+
+ Git::Repository.perform do |repository|
+ repository.location = location
+ repository.use_default_credentials
+
+ repository.act do
+ clone
+ configure_identity('GitLab QA', 'root@gitlab.com')
+ commit_file('test.rb', 'class Test; end', 'Add Test class')
+ commit_file('README.md', '# Test', 'Add Readme')
+ push_changes
+ end
+ end
+ end
+
+ scenario 'user performs a deep clone' do
+ Git::Repository.perform do |repository|
+ repository.location = location
+ repository.use_default_credentials
+
+ repository.act { clone }
+
+ expect(repository.commits.size).to eq 2
+ end
+ end
+
+ scenario 'user performs a shallow clone' do
+ Git::Repository.perform do |repository|
+ repository.location = location
+ repository.use_default_credentials
+
+ repository.act { shallow_clone }
+
+ expect(repository.commits.size).to eq 1
+ expect(repository.commits.first).to include 'Add Readme'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
new file mode 100644
index 00000000000..5fe45d63d37
--- /dev/null
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -0,0 +1,39 @@
+module QA
+ feature 'push code to repository' do
+ context 'with regular account over http' do
+ scenario 'user pushes code to the repository' do
+ Page::Main::Entry.act { sign_in_using_credentials }
+
+ Scenario::Gitlab::Project::Create.perform do |scenario|
+ scenario.name = 'project_with_code'
+ scenario.description = 'project with repository'
+ end
+
+ Git::Repository.perform do |repository|
+ repository.location = Page::Project::Show.act do
+ choose_repository_clone_http
+ repository_location
+ end
+
+ repository.use_default_credentials
+
+ repository.act do
+ clone
+ configure_identity('GitLab QA', 'root@gitlab.com')
+ add_file('README.md', '# This is test project')
+ commit('Add README.md')
+ push_changes
+ end
+ end
+
+ Page::Project::Show.act do
+ wait_for_push
+ refresh
+ end
+
+ expect(page).to have_content('README.md')
+ expect(page).to have_content('This is test project')
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
new file mode 100644
index 00000000000..83ae15d0995
--- /dev/null
+++ b/qa/qa/specs/runner.rb
@@ -0,0 +1,15 @@
+require 'rspec/core'
+
+module QA
+ module Specs
+ class Runner
+ include Scenario::Actable
+
+ def rspec(*args)
+ RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
+ abort if status.nonzero?
+ end
+ end
+ end
+ end
+end
diff --git a/qa/spec/runtime/release_spec.rb b/qa/spec/runtime/release_spec.rb
new file mode 100644
index 00000000000..e6b5a8dc315
--- /dev/null
+++ b/qa/spec/runtime/release_spec.rb
@@ -0,0 +1,50 @@
+describe QA::Runtime::Release do
+ context 'when release version has extension strategy' do
+ let(:strategy) { spy('strategy') }
+
+ before do
+ stub_const('QA::CE::Strategy', strategy)
+ stub_const('QA::EE::Strategy', strategy)
+ end
+
+ describe '#version' do
+ it 'return either CE or EE version' do
+ expect(subject.version).to eq(:CE).or eq(:EE)
+ end
+ end
+
+ describe '#strategy' do
+ it 'return the strategy constant' do
+ expect(subject.strategy).to eq strategy
+ end
+ end
+
+ describe 'delegated class methods' do
+ it 'delegates all calls to strategy class' do
+ described_class.some_method(1, 2)
+
+ expect(strategy).to have_received(:some_method)
+ .with(1, 2)
+ end
+ end
+ end
+
+ context 'when release version does not have extension strategy' do
+ before do
+ allow_any_instance_of(described_class)
+ .to receive(:version).and_return('something')
+ end
+
+ describe '#strategy' do
+ it 'raises error' do
+ expect { subject.strategy }.to raise_error(LoadError)
+ end
+ end
+
+ describe 'delegated class methods' do
+ it 'raises error' do
+ expect { described_class.some_method(2, 3) }.to raise_error(LoadError)
+ end
+ end
+ end
+end
diff --git a/qa/spec/scenario/actable_spec.rb b/qa/spec/scenario/actable_spec.rb
new file mode 100644
index 00000000000..422763910e4
--- /dev/null
+++ b/qa/spec/scenario/actable_spec.rb
@@ -0,0 +1,47 @@
+describe QA::Scenario::Actable do
+ subject do
+ Class.new do
+ include QA::Scenario::Actable
+
+ attr_accessor :something
+
+ def do_something(arg = nil)
+ "some#{arg}"
+ end
+ end
+ end
+
+ describe '.act' do
+ it 'provides means to run steps' do
+ result = subject.act { do_something }
+
+ expect(result).to eq 'some'
+ end
+
+ it 'supports passing variables' do
+ result = subject.act('thing') do |variable|
+ do_something(variable)
+ end
+
+ expect(result).to eq 'something'
+ end
+
+ it 'returns value from the last method' do
+ result = subject.act { 'test' }
+
+ expect(result).to eq 'test'
+ end
+ end
+
+ describe '.perform' do
+ it 'makes it possible to pass binding' do
+ variable = 'something'
+
+ result = subject.perform do |object|
+ object.something = variable
+ end
+
+ expect(result).to eq 'something'
+ end
+ end
+end
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
new file mode 100644
index 00000000000..c07a3234673
--- /dev/null
+++ b/qa/spec/spec_helper.rb
@@ -0,0 +1,19 @@
+require_relative '../qa'
+
+RSpec.configure do |config|
+ config.expect_with :rspec do |expectations|
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ config.mock_with :rspec do |mocks|
+ mocks.verify_partial_doubles = true
+ end
+
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+ config.disable_monkey_patching!
+ config.expose_dsl_globally = true
+ config.warnings = true
+ config.profile_examples = 10
+ config.order = :random
+ Kernel.srand config.seed
+end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 298a7ff179c..d20e7368086 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -266,5 +266,19 @@ describe Projects::BranchesController do
expect(parsed_response.first).to eq 'master'
end
end
+
+ context 'show_all = true' do
+ it 'returns all the branches name' do
+ get :index,
+ namespace_id: project.namespace,
+ project_id: project,
+ format: :json,
+ show_all: true
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(parsed_response.length).to eq(project.repository.branches.count)
+ end
+ end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 46c758b4654..6ceaf96f78f 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -104,7 +104,16 @@ describe Projects::IssuesController do
project_with_repository.team << [user, :developer]
mr = create(:merge_request_with_diff_notes, source_project: project_with_repository)
- get :new, namespace_id: project_with_repository.namespace, project_id: project_with_repository, merge_request_for_resolving_discussions: mr.iid
+ get :new, namespace_id: project_with_repository.namespace, project_id: project_with_repository, merge_request_to_resolve_discussions_of: mr.iid
+
+ expect(assigns(:issue).title).not_to be_empty
+ expect(assigns(:issue).description).not_to be_empty
+ end
+
+ it 'fills in an issue for a discussion' do
+ note = create(:note_on_merge_request, project: project)
+
+ get :new, namespace_id: project.namespace.path, project_id: project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id
expect(assigns(:issue).title).not_to be_empty
expect(assigns(:issue).description).not_to be_empty
@@ -462,11 +471,11 @@ describe Projects::IssuesController do
end
let(:merge_request_params) do
- { merge_request_for_resolving_discussions: merge_request.iid }
+ { merge_request_to_resolve_discussions_of: merge_request.iid }
end
- def post_issue(issue_params)
- post :create, namespace_id: project.namespace.to_param, project_id: project, issue: issue_params, merge_request_for_resolving_discussions: merge_request.iid
+ def post_issue(issue_params, other_params: {})
+ post :create, { namespace_id: project.namespace.to_param, project_id: project, issue: issue_params, merge_request_to_resolve_discussions_of: merge_request.iid }.merge(other_params)
end
it 'creates an issue for the project' do
@@ -485,6 +494,27 @@ describe Projects::IssuesController do
expect(discussion.resolved?).to eq(true)
end
+
+ it 'sets a flash message' do
+ post_issue(title: 'Hello')
+
+ expect(flash[:notice]).to eq('Resolved all discussions.')
+ end
+
+ describe "resolving a single discussion" do
+ before do
+ post_issue({ title: 'Hello' }, other_params: { discussion_to_resolve: discussion.id })
+ end
+ it 'resolves a single discussion' do
+ discussion.first_note.reload
+
+ expect(discussion.resolved?).to eq(true)
+ end
+
+ it 'sets a flash message that one discussion was resolved' do
+ expect(flash[:notice]).to eq('Resolved 1 discussion.')
+ end
+ end
end
context 'Akismet is enabled' do
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 4cebe3884bf..952071af57f 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Projects::RawController do
let(:public_project) { create(:project, :public, :repository) }
- describe "#show" do
+ describe '#show' do
context 'regular filename' do
let(:id) { 'master/README.md' }
@@ -16,8 +16,8 @@ describe Projects::RawController do
expect(response).to have_http_status(200)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.header['Content-Disposition']).
- to eq("inline")
- expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:")
+ to eq('inline')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
end
@@ -32,7 +32,7 @@ describe Projects::RawController do
expect(response).to have_http_status(200)
expect(response.header['Content-Type']).to eq('image/jpeg')
- expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:")
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
end
@@ -40,32 +40,57 @@ describe Projects::RawController do
let(:id) { 'be93687/files/lfs/lfs_object.iso' }
let!(:lfs_object) { create(:lfs_object, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', size: '1575078') }
- context 'when project has access' do
+ context 'when lfs is enabled' do
before do
- public_project.lfs_objects << lfs_object
- allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
- allow(controller).to receive(:send_file) { controller.head :ok }
+ allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
end
- it 'serves the file' do
- expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: "lfs_object.iso", disposition: 'attachment')
- get(:show,
- namespace_id: public_project.namespace.to_param,
- project_id: public_project,
- id: id)
+ context 'when project has access' do
+ before do
+ public_project.lfs_objects << lfs_object
+ allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
+ allow(controller).to receive(:send_file) { controller.head :ok }
+ end
- expect(response).to have_http_status(200)
+ it 'serves the file' do
+ expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment')
+ get(:show,
+ namespace_id: public_project.namespace.to_param,
+ project_id: public_project,
+ id: id)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'when project does not have access' do
+ it 'does not serve the file' do
+ get(:show,
+ namespace_id: public_project.namespace.to_param,
+ project_id: public_project,
+ id: id)
+
+ expect(response).to have_http_status(404)
+ end
end
end
- context 'when project does not have access' do
- it 'does not serve the file' do
+ context 'when lfs is not enabled' do
+ before do
+ allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false)
+ end
+
+ it 'delivers ASCII file' do
get(:show,
namespace_id: public_project.namespace.to_param,
project_id: public_project,
id: id)
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(200)
+ expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(response.header['Content-Disposition']).
+ to eq('inline')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 5c50cd7f4ad..fe19a404e16 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -26,12 +26,17 @@ FactoryGirl.define do
factory :diff_note_on_merge_request, traits: [:on_merge_request], class: DiffNote do
association :project, :repository
+
+ transient do
+ line_number 14
+ end
+
position do
Gitlab::Diff::Position.new(
old_path: "files/ruby/popen.rb",
new_path: "files/ruby/popen.rb",
old_line: nil,
- new_line: 14,
+ new_line: line_number,
diff_refs: noteable.diff_refs
)
end
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index 762cab0c0e1..572bca3de21 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -1,76 +1,93 @@
require 'rails_helper'
-feature 'Resolving all open discussions in a merge request from an issue', feature: true do
+feature 'Resolving all open discussions in a merge request from an issue', feature: true, js: true do
let(:user) { create(:user) }
- let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) }
+ let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first }
- before do
- project.team << [user, :master]
- login_as user
- end
-
- context 'with the internal tracker disabled' do
+ describe 'as a user with access to the project' do
before do
- project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+ project.team << [user, :master]
+ login_as user
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
- it 'does not show a link to create a new issue' do
- expect(page).not_to have_link 'open an issue to resolve them later'
- end
- end
-
- context 'merge request has discussions that need to be resolved' do
- before do
- visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ it 'shows a button to resolve all discussions by creating a new issue' do
+ within('li#resolve-count-app') do
+ expect(page).to have_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ end
end
- it 'shows a warning that the merge request contains unresolved discussions' do
- expect(page).to have_content 'This merge request has unresolved discussions'
- end
+ context 'resolving the discussion' do
+ before do
+ click_button 'Resolve discussion'
+ end
- it 'has a link to resolve all discussions by creating an issue' do
- page.within '.mr-widget-body' do
- expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid)
+ it 'hides the link for creating a new issue' do
+ expect(page).not_to have_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
end
context 'creating an issue for discussions' do
before do
- page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid)
+ click_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
- it 'shows an issue with the title filled in' do
- title_field = page.find_field('issue[title]')
+ it_behaves_like 'creating an issue for a discussion'
+ end
- expect(title_field.value).to include(merge_request.title)
+ context 'for a project where all discussions need to be resolved before merging' do
+ before do
+ project.update_attribute(:only_allow_merge_if_all_discussions_are_resolved, true)
end
- it 'has a mention of the discussion in the description' do
- description_field = page.find_field('issue[description]')
+ context 'with the internal tracker disabled' do
+ before do
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
- expect(description_field.value).to include(discussion.first_note.note)
+ it 'does not show a link to create a new issue' do
+ expect(page).not_to have_link 'open an issue to resolve them later'
+ end
end
- it 'has a hidden field for the merge request' do
- merge_request_field = find('#merge_request_for_resolving_discussions', visible: false)
-
- expect(merge_request_field.value).to eq(merge_request.iid.to_s)
- end
+ context 'merge request has discussions that need to be resolved' do
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
- it 'can create a new issue for the project' do
- expect { click_button 'Submit issue' }.to change { project.issues.reload.size }.by(1)
- end
+ it 'shows a warning that the merge request contains unresolved discussions' do
+ expect(page).to have_content 'This merge request has unresolved discussions'
+ end
- it 'resolves the discussion in the merge request' do
- click_button 'Submit issue'
+ it 'has a link to resolve all discussions by creating an issue' do
+ page.within '.mr-widget-body' do
+ expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ end
+ end
- discussion.first_note.reload
+ context 'creating an issue for discussions' do
+ before do
+ page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ end
- expect(discussion.resolved?).to eq(true)
+ it_behaves_like 'creating an issue for a discussion'
+ end
end
end
end
+
+ describe 'as a reporter' do
+ before do
+ project.team << [user, :reporter]
+ login_as user
+ visit new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ end
+
+ it 'Shows a notice to ask someone else to resolve the discussions' do
+ expect(page).to have_content("The discussions at #{merge_request.to_reference} will stay unresolved. Ask someone with permission to resolve them.")
+ end
+ end
end
diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb
new file mode 100644
index 00000000000..88e2cc60d79
--- /dev/null
+++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+feature 'Resolve an open discussion in a merge request by creating an issue', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first }
+
+ describe 'As a user with access to the project' do
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ context 'with the internal tracker disabled' do
+ before do
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not show a link to create a new issue' do
+ expect(page).not_to have_link 'Resolve this discussion in a new issue'
+ end
+ end
+
+ context 'resolving the discussion', js: true do
+ before do
+ click_button 'Resolve discussion'
+ end
+
+ it 'hides the link for creating a new issue' do
+ expect(page).not_to have_link 'Resolve this discussion in a new issue'
+ end
+
+ it 'shows the link for creating a new issue when unresolving a discussion' do
+ page.within '.diff-content' do
+ click_button 'Unresolve discussion'
+ end
+
+ expect(page).to have_link 'Resolve this discussion in a new issue'
+ end
+ end
+
+ it 'has a link to create a new issue for a discussion' do
+ new_issue_link = new_namespace_project_issue_path(project.namespace, project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
+
+ expect(page).to have_link 'Resolve this discussion in a new issue', href: new_issue_link
+ end
+
+ context 'creating the issue' do
+ before do
+ click_link 'Resolve this discussion in a new issue', href: new_namespace_project_issue_path(project.namespace, project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
+ end
+
+ it 'has a hidden field for the discussion' do
+ discussion_field = find('#discussion_to_resolve', visible: false)
+
+ expect(discussion_field.value).to eq(discussion.id.to_s)
+ end
+
+ it_behaves_like 'creating an issue for a discussion'
+ end
+ end
+
+ describe 'as a reporter' do
+ before do
+ project.team << [user, :reporter]
+ login_as user
+ visit new_namespace_project_issue_path(project.namespace, project,
+ merge_request_to_resolve_discussions_of: merge_request.iid,
+ discussion_to_resolve: discussion.id)
+ end
+
+ it 'Shows a notice to ask someone else to resolve the discussions' do
+ expect(page).to have_content("The discussion at #{merge_request.to_reference}"\
+ "(discussion #{discussion.first_note.id}) will stay unresolved."\
+ "Ask someone with permission to resolve it.")
+ end
+ end
+end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index d4e0ef91856..755992069ff 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
describe 'New/edit issue', feature: true, js: true do
+ include GitlabRoutingHelper
+
let!(:project) { create(:project) }
let!(:user) { create(:user)}
let!(:user2) { create(:user)}
@@ -78,6 +80,14 @@ describe 'New/edit issue', feature: true, js: true do
expect(page).to have_content label2.title
end
end
+
+ page.within '.issuable-meta' do
+ issue = Issue.find_by(title: 'title')
+
+ expect(page).to have_text("Issue #{issue.to_reference}")
+ # compare paths because the host differ in test
+ expect(find_link(issue.to_reference)[:href]).to end_with(issue_path(issue))
+ end
end
it 'correctly updates the dropdown toggle when removing a label' do
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index ae609160e18..f32d1f78b40 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -48,6 +48,18 @@ feature 'Login', feature: true do
end
end
+ describe 'with the ghost user' do
+ it 'disallows login' do
+ login_with(User.ghost)
+
+ expect(page).to have_content('Invalid Login or password.')
+ end
+
+ it 'does not update Devise trackable attributes' do
+ expect { login_with(User.ghost) }.not_to change { User.ghost.reload.sign_in_count }
+ end
+ end
+
describe 'with two-factor authentication' do
def enter_code(code)
fill_in 'user_otp_attempt', with: code
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
index 1ecdb8b5983..f8518f450dc 100644
--- a/spec/features/merge_requests/form_spec.rb
+++ b/spec/features/merge_requests/form_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
describe 'New/edit merge request', feature: true, js: true do
+ include GitlabRoutingHelper
+
let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:fork_project) { create(:project, forked_from_project: project) }
let!(:user) { create(:user)}
@@ -84,6 +86,15 @@ describe 'New/edit merge request', feature: true, js: true do
expect(page).to have_content label2.title
end
end
+
+ page.within '.issuable-meta' do
+ merge_request = MergeRequest.find_by(source_branch: 'fix')
+
+ expect(page).to have_text("Merge Request #{merge_request.to_reference}")
+ # compare paths because the host differ in test
+ expect(find_link(merge_request.to_reference)[:href])
+ .to end_with(merge_request_path(merge_request))
+ end
end
end
diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
new file mode 100644
index 00000000000..d94204230f6
--- /dev/null
+++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', feature: true, js: true do
+ include TreeHelper
+
+ let(:project) { create(:project, :public, :repository) }
+ let(:path) { 'CHANGELOG' }
+ let(:sha) { project.repository.commit.sha }
+
+ describe 'On a file(blob)' do
+ def get_absolute_url(path = "")
+ "http://#{page.server.host}:#{page.server.port}#{path}"
+ end
+
+ def visit_blob(fragment = nil)
+ visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment)
+ end
+
+ describe 'Click "Permalink" button' do
+ it 'works with no initial line number fragment hash' do
+ visit_blob
+
+ expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path))))
+ end
+
+ it 'maintains intitial fragment hash' do
+ fragment = "L3"
+
+ visit_blob(fragment)
+
+ expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: fragment)))
+ end
+
+ it 'changes fragment hash if line number clicked' do
+ ending_fragment = "L5"
+
+ visit_blob
+
+ find('#L3').click
+ find("##{ending_fragment}").click
+
+ expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: ending_fragment)))
+ end
+
+ it 'with initial fragment hash, changes fragment hash if line number clicked' do
+ fragment = "L1"
+ ending_fragment = "L5"
+
+ visit_blob(fragment)
+
+ find('#L3').click
+ find("##{ending_fragment}").click
+
+ expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: ending_fragment)))
+ end
+ end
+
+ describe 'Click "Blame" button' do
+ it 'works with no initial line number fragment hash' do
+ visit_blob
+
+ expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path))))
+ end
+
+ it 'maintains intitial fragment hash' do
+ fragment = "L3"
+
+ visit_blob(fragment)
+
+ expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path), anchor: fragment)))
+ end
+
+ it 'changes fragment hash if line number clicked' do
+ ending_fragment = "L5"
+
+ visit_blob
+
+ find('#L3').click
+ find("##{ending_fragment}").click
+
+ expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path), anchor: ending_fragment)))
+ end
+
+ it 'with initial fragment hash, changes fragment hash if line number clicked' do
+ fragment = "L1"
+ ending_fragment = "L5"
+
+ visit_blob(fragment)
+
+ find('#L3').click
+ find("##{ending_fragment}").click
+
+ expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path), anchor: ending_fragment)))
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb
new file mode 100644
index 00000000000..03d08c12612
--- /dev/null
+++ b/spec/features/projects/blobs/user_create_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+feature 'New blob creation', feature: true, js: true do
+ include WaitForAjax
+
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:project) { create(:project) }
+ given(:content) { 'class NextFeature\nend\n' }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ visit namespace_project_new_blob_path(project.namespace, project, 'master')
+ end
+
+ def edit_file
+ wait_for_ajax
+ fill_in 'file_name', with: 'feature.rb'
+ execute_script("ace.edit('editor').setValue('#{content}')")
+ end
+
+ def select_branch_index(index)
+ first('button.js-target-branch').click
+ wait_for_ajax
+ all('a[data-group="Branches"]')[index].click
+ end
+
+ def create_new_branch(name)
+ first('button.js-target-branch').click
+ click_link 'Create new branch'
+ fill_in 'new_branch_name', with: name
+ click_button 'Create'
+ end
+
+ def commit_file
+ click_button 'Commit Changes'
+ end
+
+ context 'with default target branch' do
+ background do
+ edit_file
+ commit_file
+ end
+
+ scenario 'creates the blob in the default branch' do
+ expect(page).to have_content 'master'
+ expect(page).to have_content 'successfully created'
+ expect(page).to have_content 'NextFeature'
+ end
+ end
+
+ context 'with different target branch' do
+ background do
+ edit_file
+ select_branch_index(0)
+ commit_file
+ end
+
+ scenario 'creates the blob in the different branch' do
+ expect(page).to have_content 'test'
+ expect(page).to have_content 'successfully created'
+ end
+ end
+
+ context 'with a new target branch' do
+ given(:new_branch_name) { 'new-feature' }
+
+ background do
+ edit_file
+ create_new_branch(new_branch_name)
+ commit_file
+ end
+
+ scenario 'creates the blob in the new branch' do
+ expect(page).to have_content new_branch_name
+ expect(page).to have_content 'successfully created'
+ end
+ scenario 'returns you to the mr' do
+ expect(page).to have_content 'New Merge Request'
+ expect(page).to have_content "From #{new_branch_name} into master"
+ expect(page).to have_content 'Add new file'
+ end
+ end
+
+ context 'the file already exist in the source branch' do
+ background do
+ Files::CreateService.new(
+ project,
+ user,
+ start_branch: 'master',
+ target_branch: 'master',
+ commit_message: 'Create file',
+ file_path: 'feature.rb',
+ file_content: content
+ ).execute
+ edit_file
+ commit_file
+ end
+
+ scenario 'shows error message' do
+ expect(page).to have_content('Your changes could not be committed because a file with the same name already exists')
+ expect(page).to have_content('New File')
+ expect(page).to have_content('NextFeature')
+ end
+ end
+end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 25f31b423b8..641e2cf7402 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -111,10 +111,8 @@ feature 'Environments page', :feature, :js do
find('.js-dropdown-play-icon-container').click
expect(page).to have_content(action.name.humanize)
- expect { click_link(action.name.humanize) }
+ expect { find('.js-manual-action-link').click }
.not_to change { Ci::Pipeline.count }
-
- expect(action.reload).to be_pending
end
scenario 'does show build name and id' do
@@ -158,12 +156,6 @@ feature 'Environments page', :feature, :js do
expect(page).to have_selector('.stop-env-link')
end
- scenario 'starts build when stop button clicked' do
- find('.stop-env-link').click
-
- expect(page).to have_content('close_app')
- end
-
context 'for reporter' do
let(:role) { :reporter }
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index 69295e450d0..d281043caa3 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'user checks git blame', feature: true do
+feature 'user browses project', feature: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -18,4 +18,16 @@ feature 'user checks git blame', feature: true do
expect(page).to have_content "Dmitriy Zaporozhets"
expect(page).to have_content "Initial commit"
end
+
+ scenario 'can see raw content of LFS pointer with LFS disabled' do
+ allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false)
+ click_link 'files'
+ click_link 'lfs'
+ click_link 'lfs_object.iso'
+
+ expect(page).not_to have_content 'Download (1.5 MB)'
+ expect(page).to have_content 'version https://git-lfs.github.com/spec/v1'
+ expect(page).to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897'
+ expect(page).to have_content 'size 1575078'
+ end
end
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index 0f30f562539..ccfafe6db7d 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -10,16 +10,12 @@ feature 'Master deletes tag', feature: true do
visit namespace_project_tags_path(project.namespace, project)
end
- context 'from the tags list page' do
+ context 'from the tags list page', js: true do
scenario 'deletes the tag' do
expect(page).to have_content 'v1.1.0'
- page.within('.content') do
- first('.btn-remove').click
- end
+ delete_first_tag
- expect(current_path).to eq(
- namespace_project_tags_path(project.namespace, project))
expect(page).not_to have_content 'v1.1.0'
end
end
@@ -37,4 +33,23 @@ feature 'Master deletes tag', feature: true do
expect(page).not_to have_content 'v1.0.0'
end
end
+
+ context 'when pre-receive hook fails', js: true do
+ before do
+ allow_any_instance_of(GitHooksService).to receive(:execute)
+ .and_raise(GitHooksService::PreReceiveError, 'Do not delete tags')
+ end
+
+ scenario 'shows the error message' do
+ delete_first_tag
+
+ expect(page).to have_content('Do not delete tags')
+ end
+ end
+
+ def delete_first_tag
+ page.within('.content') do
+ first('.btn-remove').click
+ end
+ end
end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 3495091a0d5..5c2df949ac5 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -38,7 +38,9 @@ describe 'Dashboard Todos', feature: true do
shared_examples 'deleting the todo' do
before do
- first('.js-done-todo').click
+ within first('.todo') do
+ click_link 'Done'
+ end
end
it 'is marked as done-reversible in the list' do
@@ -62,9 +64,11 @@ describe 'Dashboard Todos', feature: true do
shared_examples 'deleting and restoring the todo' do
before do
- first('.js-done-todo').click
- wait_for_ajax
- first('.js-undo-todo').click
+ within first('.todo') do
+ click_link 'Done'
+ wait_for_ajax
+ click_link 'Undo'
+ end
end
it 'is marked back as pending in the list' do
@@ -97,6 +101,35 @@ describe 'Dashboard Todos', feature: true do
end
end
+ context 'User has done todos', js: true do
+ before do
+ create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author)
+ login_as(user)
+ visit dashboard_todos_path(state: :done)
+ end
+
+ it 'has the done todo present' do
+ expect(page).to have_selector('.todos-list .todo.todo-done', count: 1)
+ end
+
+ describe 'restoring the todo' do
+ before do
+ within first('.todo') do
+ click_link 'Add todo'
+ end
+ end
+
+ it 'is removed from the list' do
+ expect(page).not_to have_selector('.todos-list .todo.todo-done')
+ end
+
+ it 'updates todo count' do
+ expect(page).to have_content 'To do 1'
+ expect(page).to have_content 'Done 0'
+ end
+ end
+ end
+
context 'User has Todos with labels spanning multiple projects' do
before do
label1 = create(:label, project: project)
@@ -143,7 +176,7 @@ describe 'Dashboard Todos', feature: true do
describe 'mark all as done', js: true do
before do
visit dashboard_todos_path
- click_link('Mark all as done')
+ click_link 'Mark all as done'
end
it 'shows "All done" message!' do
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 88d853935c7..f0554cc068d 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -131,4 +131,36 @@ describe IssuesHelper do
expect(options).to have_selector('option', text: milestone2.title)
end
end
+
+ describe "#link_to_discussions_to_resolve" do
+ describe "passing only a merge request" do
+ let(:merge_request) { create(:merge_request) }
+
+ it "links just the merge request" do
+ expected_path = namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
+
+ expect(link_to_discussions_to_resolve(merge_request, nil)).to include(expected_path)
+ end
+
+ it "containst the reference to the merge request" do
+ expect(link_to_discussions_to_resolve(merge_request, nil)).to include(merge_request.to_reference)
+ end
+ end
+
+ describe "when passing a discussion" do
+ let(:diff_note) { create(:diff_note_on_merge_request) }
+ let(:merge_request) { diff_note.noteable }
+ let(:discussion) { Discussion.new([diff_note]) }
+
+ it "links to the merge request with first note if a single discussion was passed" do
+ expected_path = Gitlab::UrlBuilder.build(diff_note)
+
+ expect(link_to_discussions_to_resolve(merge_request, discussion)).to include(expected_path)
+ end
+
+ it "contains both the reference to the merge request and a mention of the discussion" do
+ expect(link_to_discussions_to_resolve(merge_request, discussion)).to include("#{merge_request.to_reference} (discussion #{diff_note.id})")
+ end
+ end
+ end
end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 9a2978006aa..0a6e042b700 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,11 +1,8 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */
-import promisePolyfill from 'es6-promise';
import Cookies from 'js-cookie';
import AwardsHandler from '~/awards_handler';
-promisePolyfill.polyfill();
-
(function() {
var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu;
diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js
new file mode 100644
index 00000000000..c1179e572ae
--- /dev/null
+++ b/spec/javascripts/blob/create_branch_dropdown_spec.js
@@ -0,0 +1,107 @@
+require('~/gl_dropdown');
+require('~/lib/utils/type_utility');
+require('~/blob/create_branch_dropdown');
+require('~/blob/target_branch_dropdown');
+
+describe('CreateBranchDropdown', () => {
+ const fixtureTemplate = 'static/target_branch_dropdown.html.raw';
+ // selectors
+ const createBranchSel = '.js-new-branch-btn';
+ const backBtnSel = '.dropdown-menu-back';
+ const cancelBtnSel = '.js-cancel-branch-btn';
+ const branchNameSel = '#new_branch_name';
+ const branchName = 'new_name';
+ let dropdown;
+
+ function createDropdown() {
+ const dropdownEl = document.querySelector('.js-project-branches-dropdown');
+ const projectBranches = getJSONFixture('project_branches.json');
+ dropdown = new gl.TargetBranchDropDown(dropdownEl);
+ dropdown.cachedRefs = projectBranches;
+ return dropdown;
+ }
+
+ function createBranchBtn() {
+ return document.querySelector(createBranchSel);
+ }
+
+ function backBtn() {
+ return document.querySelector(backBtnSel);
+ }
+
+ function cancelBtn() {
+ return document.querySelector(cancelBtnSel);
+ }
+
+ function branchNameEl() {
+ return document.querySelector(branchNameSel);
+ }
+
+ function changeBranchName(text) {
+ branchNameEl().value = text;
+ branchNameEl().dispatchEvent(new Event('change'));
+ }
+
+ preloadFixtures(fixtureTemplate);
+
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ createDropdown();
+ });
+
+ it('disable submit when branch name is empty', () => {
+ expect(createBranchBtn()).toBeDisabled();
+ });
+
+ it('enable submit when branch name is present', () => {
+ changeBranchName(branchName);
+
+ expect(createBranchBtn()).not.toBeDisabled();
+ });
+
+ it('resets the form when cancel btn is clicked and triggers dropdownback', () => {
+ const spyBackEvent = spyOnEvent(backBtnSel, 'click');
+ changeBranchName(branchName);
+
+ cancelBtn().click();
+
+ expect(branchNameEl()).toHaveValue('');
+ expect(spyBackEvent).toHaveBeenTriggered();
+ });
+
+ it('resets the form when back btn is clicked', () => {
+ changeBranchName(branchName);
+
+ backBtn().click();
+
+ expect(branchNameEl()).toHaveValue('');
+ });
+
+ describe('new branch creation', () => {
+ beforeEach(() => {
+ changeBranchName(branchName);
+ });
+ it('sets the new branch name and updates the dropdown', () => {
+ spyOn(dropdown, 'setNewBranch');
+
+ createBranchBtn().click();
+
+ expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName);
+ });
+
+ it('resets the form', () => {
+ createBranchBtn().click();
+
+ expect(branchNameEl()).toHaveValue('');
+ });
+
+ it('is triggered with enter keypress', () => {
+ spyOn(dropdown, 'setNewBranch');
+ const enterEvent = new Event('keydown');
+ enterEvent.which = 13;
+ branchNameEl().dispatchEvent(enterEvent);
+
+ expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName);
+ });
+ });
+});
diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js
new file mode 100644
index 00000000000..4fb79663c51
--- /dev/null
+++ b/spec/javascripts/blob/target_branch_dropdown_spec.js
@@ -0,0 +1,119 @@
+require('~/gl_dropdown');
+require('~/lib/utils/type_utility');
+require('~/blob/create_branch_dropdown');
+require('~/blob/target_branch_dropdown');
+
+describe('TargetBranchDropdown', () => {
+ const fixtureTemplate = 'static/target_branch_dropdown.html.raw';
+ let dropdown;
+
+ function createDropdown() {
+ const projectBranches = getJSONFixture('project_branches.json');
+ const dropdownEl = document.querySelector('.js-project-branches-dropdown');
+ dropdown = new gl.TargetBranchDropDown(dropdownEl);
+ dropdown.cachedRefs = projectBranches;
+ dropdown.refreshData();
+ return dropdown;
+ }
+
+ function submitBtn() {
+ return document.querySelector('button[type="submit"]');
+ }
+
+ function searchField() {
+ return document.querySelector('.dropdown-page-one .dropdown-input-field');
+ }
+
+ function element() {
+ return document.querySelectorAll('div.dropdown-content li a');
+ }
+
+ function elementAtIndex(index) {
+ return element()[index];
+ }
+
+ function clickElementAtIndex(index) {
+ elementAtIndex(index).click();
+ }
+
+ preloadFixtures(fixtureTemplate);
+
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ createDropdown();
+ });
+
+ it('disable submit when branch is not selected', () => {
+ document.querySelector('input[name="target_branch"]').value = null;
+ clickElementAtIndex(1);
+
+ expect(submitBtn().getAttribute('disabled')).toEqual('');
+ });
+
+ it('enable submit when a branch is selected', () => {
+ clickElementAtIndex(1);
+
+ expect(submitBtn().getAttribute('disabled')).toBe(null);
+ });
+
+ it('triggers change.branch event on a branch click', () => {
+ spyOnEvent(dropdown.$dropdown, 'change.branch');
+ clickElementAtIndex(0);
+
+ expect('change.branch').toHaveBeenTriggeredOn(dropdown.$dropdown);
+ });
+
+ describe('#dropdownData', () => {
+ it('cache the refs', () => {
+ const refs = dropdown.cachedRefs;
+ dropdown.cachedRefs = null;
+
+ dropdown.dropdownData(refs);
+
+ expect(dropdown.cachedRefs).toEqual(refs);
+ });
+
+ it('returns the Branches with the newBranch and defaultBranch', () => {
+ const refs = dropdown.cachedRefs;
+ dropdown.branchInput.value = 'master';
+ dropdown.newBranch = { id: 'new_branch', text: 'new_branch', title: 'new_branch' };
+
+ const branches = dropdown.dropdownData(refs).Branches;
+
+ expect(branches.length).toEqual(4);
+ expect(branches[0]).toEqual(dropdown.newBranch);
+ expect(branches[1]).toEqual({ id: 'master', text: 'master', title: 'master' });
+ expect(branches[2]).toEqual({ id: 'development', text: 'development', title: 'development' });
+ expect(branches[3]).toEqual({ id: 'staging', text: 'staging', title: 'staging' });
+ });
+ });
+
+ describe('#setNewBranch', () => {
+ it('adds the new branch and select it', () => {
+ const branchName = 'new_branch';
+
+ dropdown.setNewBranch(branchName);
+
+ expect(elementAtIndex(0)).toHaveClass('is-active');
+ expect(elementAtIndex(0)).toContainHtml(branchName);
+ });
+
+ it("doesn't add a new branch if already exists in the list", () => {
+ const branchName = elementAtIndex(0).text;
+ const initialLength = element().length;
+
+ dropdown.setNewBranch(branchName);
+
+ expect(element().length).toEqual(initialLength);
+ });
+
+ it('clears the search filter', () => {
+ const branchName = elementAtIndex(0).text;
+ searchField().value = 'searching';
+
+ dropdown.setNewBranch(branchName);
+
+ expect(searchField().value).toEqual('');
+ });
+ });
+});
diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js
index 22c9f12951b..4999933c0c1 100644
--- a/spec/javascripts/boards/board_new_issue_spec.js
+++ b/spec/javascripts/boards/board_new_issue_spec.js
@@ -8,7 +8,6 @@ import boardNewIssue from '~/boards/components/board_new_issue';
require('~/boards/models/list');
require('./mock_data');
-require('es6-promise').polyfill();
describe('Issue boards new issue form', () => {
let vm;
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 49a2ca4a78f..1d1069600fc 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -15,7 +15,6 @@ require('~/boards/models/user');
require('~/boards/services/board_service');
require('~/boards/stores/boards_store');
require('./mock_data');
-require('es6-promise').polyfill();
describe('Store', () => {
beforeEach(() => {
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/bootstrap_jquery_spec.js
index 096d3272eac..48994b7c523 100644
--- a/spec/javascripts/extensions/jquery_spec.js
+++ b/spec/javascripts/bootstrap_jquery_spec.js
@@ -1,9 +1,9 @@
/* eslint-disable space-before-function-paren, no-var */
-require('~/extensions/jquery');
+import '~/commons/bootstrap';
(function() {
- describe('jQuery extensions', function() {
+ describe('Bootstrap jQuery extensions', function() {
describe('disable', function() {
beforeEach(function() {
return setFixtures('<input type="text" />');
diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js
index d50d45d295e..85b73f1d4e2 100644
--- a/spec/javascripts/environments/environment_actions_spec.js
+++ b/spec/javascripts/environments/environment_actions_spec.js
@@ -1,14 +1,16 @@
-const ActionsComponent = require('~/environments/components/environment_actions');
+import Vue from 'vue';
+import actionsComp from '~/environments/components/environment_actions';
describe('Actions Component', () => {
- preloadFixtures('static/environments/element.html.raw');
+ let ActionsComponent;
+ let actionsMock;
+ let spy;
+ let component;
beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
- });
+ ActionsComponent = Vue.extend(actionsComp);
- it('should render a dropdown with the provided actions', () => {
- const actionsMock = [
+ actionsMock = [
{
name: 'bar',
play_path: 'https://gitlab.com/play',
@@ -19,18 +21,27 @@ describe('Actions Component', () => {
},
];
- const component = new ActionsComponent({
- el: document.querySelector('.test-dom-element'),
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+ component = new ActionsComponent({
propsData: {
actions: actionsMock,
+ service: {
+ postAction: spy,
+ },
},
- });
+ }).$mount();
+ });
+ it('should render a dropdown with the provided actions', () => {
expect(
component.$el.querySelectorAll('.dropdown-menu li').length,
).toEqual(actionsMock.length);
- expect(
- component.$el.querySelector('.dropdown-menu li a').getAttribute('href'),
- ).toEqual(actionsMock[0].play_path);
+ });
+
+ it('should call the service when an action is clicked', () => {
+ component.$el.querySelector('.dropdown').click();
+ component.$el.querySelector('.js-manual-action-link').click();
+
+ expect(spy).toHaveBeenCalledWith(actionsMock[0].play_path);
});
});
diff --git a/spec/javascripts/environments/environment_external_url_spec.js b/spec/javascripts/environments/environment_external_url_spec.js
index 393dbb5aae0..9af218a27ff 100644
--- a/spec/javascripts/environments/environment_external_url_spec.js
+++ b/spec/javascripts/environments/environment_external_url_spec.js
@@ -1,19 +1,20 @@
-const ExternalUrlComponent = require('~/environments/components/environment_external_url');
+import Vue from 'vue';
+import externalUrlComp from '~/environments/components/environment_external_url';
describe('External URL Component', () => {
- preloadFixtures('static/environments/element.html.raw');
+ let ExternalUrlComponent;
+
beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
+ ExternalUrlComponent = Vue.extend(externalUrlComp);
});
it('should link to the provided externalUrl prop', () => {
const externalURL = 'https://gitlab.com';
const component = new ExternalUrlComponent({
- el: document.querySelector('.test-dom-element'),
propsData: {
externalUrl: externalURL,
},
- });
+ }).$mount();
expect(component.$el.getAttribute('href')).toEqual(externalURL);
expect(component.$el.querySelector('fa-external-link')).toBeDefined();
diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js
index 7fea80ed799..4d42de4d549 100644
--- a/spec/javascripts/environments/environment_item_spec.js
+++ b/spec/javascripts/environments/environment_item_spec.js
@@ -1,10 +1,12 @@
-window.timeago = require('timeago.js');
-const EnvironmentItem = require('~/environments/components/environment_item');
+import 'timeago.js';
+import Vue from 'vue';
+import environmentItemComp from '~/environments/components/environment_item';
describe('Environment item', () => {
- preloadFixtures('static/environments/table.html.raw');
+ let EnvironmentItem;
+
beforeEach(() => {
- loadFixtures('static/environments/table.html.raw');
+ EnvironmentItem = Vue.extend(environmentItemComp);
});
describe('When item is folder', () => {
@@ -21,13 +23,13 @@ describe('Environment item', () => {
};
component = new EnvironmentItem({
- el: document.querySelector('tr#environment-row'),
propsData: {
model: mockItem,
canCreateDeployment: false,
canReadEnvironment: true,
+ service: {},
},
- });
+ }).$mount();
});
it('Should render folder icon and name', () => {
@@ -109,13 +111,13 @@ describe('Environment item', () => {
};
component = new EnvironmentItem({
- el: document.querySelector('tr#environment-row'),
propsData: {
model: environment,
canCreateDeployment: true,
canReadEnvironment: true,
+ service: {},
},
- });
+ }).$mount();
});
it('should render environment name', () => {
diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js
index 4a596baad09..7cb39d9df03 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js
+++ b/spec/javascripts/environments/environment_rollback_spec.js
@@ -1,47 +1,59 @@
-const RollbackComponent = require('~/environments/components/environment_rollback');
+import Vue from 'vue';
+import rollbackComp from '~/environments/components/environment_rollback';
describe('Rollback Component', () => {
- preloadFixtures('static/environments/element.html.raw');
-
const retryURL = 'https://gitlab.com/retry';
+ let RollbackComponent;
+ let spy;
beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
+ RollbackComponent = Vue.extend(rollbackComp);
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
});
- it('Should link to the provided retryUrl', () => {
+ it('Should render Re-deploy label when isLastDeployment is true', () => {
const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
retryUrl: retryURL,
isLastDeployment: true,
+ service: {
+ postAction: spy,
+ },
},
- });
+ }).$mount();
- expect(component.$el.getAttribute('href')).toEqual(retryURL);
+ expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
});
- it('Should render Re-deploy label when isLastDeployment is true', () => {
+ it('Should render Rollback label when isLastDeployment is false', () => {
const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
retryUrl: retryURL,
- isLastDeployment: true,
+ isLastDeployment: false,
+ service: {
+ postAction: spy,
+ },
},
- });
+ }).$mount();
- expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
+ expect(component.$el.querySelector('span').textContent).toContain('Rollback');
});
- it('Should render Rollback label when isLastDeployment is false', () => {
+ it('should call the service when the button is clicked', () => {
const component = new RollbackComponent({
- el: document.querySelector('.test-dom-element'),
propsData: {
retryUrl: retryURL,
isLastDeployment: false,
+ service: {
+ postAction: spy,
+ },
},
- });
+ }).$mount();
- expect(component.$el.querySelector('span').textContent).toContain('Rollback');
+ component.$el.click();
+
+ expect(spy).toHaveBeenCalledWith(retryURL);
});
});
diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js
index edd0cad32d0..9601575577e 100644
--- a/spec/javascripts/environments/environment_spec.js
+++ b/spec/javascripts/environments/environment_spec.js
@@ -1,7 +1,7 @@
-const Vue = require('vue');
-require('~/flash');
-const EnvironmentsComponent = require('~/environments/components/environment');
-const { environment } = require('./mock_data');
+import Vue from 'vue';
+import '~/flash';
+import EnvironmentsComponent from '~/environments/components/environment';
+import { environment } from './mock_data';
describe('Environment', () => {
preloadFixtures('static/environments/environments.html.raw');
diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js
index 5ca65b1debc..8f79b88f3df 100644
--- a/spec/javascripts/environments/environment_stop_spec.js
+++ b/spec/javascripts/environments/environment_stop_spec.js
@@ -1,28 +1,34 @@
-const StopComponent = require('~/environments/components/environment_stop');
+import Vue from 'vue';
+import stopComp from '~/environments/components/environment_stop';
describe('Stop Component', () => {
- preloadFixtures('static/environments/element.html.raw');
-
- let stopURL;
+ let StopComponent;
let component;
+ let spy;
+ const stopURL = '/stop';
beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
+ StopComponent = Vue.extend(stopComp);
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+ spyOn(window, 'confirm').and.returnValue(true);
- stopURL = '/stop';
component = new StopComponent({
- el: document.querySelector('.test-dom-element'),
propsData: {
stopUrl: stopURL,
+ service: {
+ postAction: spy,
+ },
},
- });
+ }).$mount();
});
- it('should link to the provided URL', () => {
- expect(component.$el.getAttribute('href')).toEqual(stopURL);
+ it('should render a button to stop the environment', () => {
+ expect(component.$el.tagName).toEqual('BUTTON');
+ expect(component.$el.getAttribute('title')).toEqual('Stop Environment');
});
- it('should have a data-confirm attribute', () => {
- expect(component.$el.getAttribute('data-confirm')).toEqual('Are you sure you want to stop this environment?');
+ it('should call the service when an action is clicked', () => {
+ component.$el.click();
+ expect(spy).toHaveBeenCalled();
});
});
diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js
index be4330b5012..3df967848a7 100644
--- a/spec/javascripts/environments/environment_table_spec.js
+++ b/spec/javascripts/environments/environment_table_spec.js
@@ -1,4 +1,5 @@
-const EnvironmentTable = require('~/environments/components/environments_table');
+import Vue from 'vue';
+import environmentTableComp from '~/environments/components/environments_table';
describe('Environment item', () => {
preloadFixtures('static/environments/element.html.raw');
@@ -16,14 +17,17 @@ describe('Environment item', () => {
},
};
+ const EnvironmentTable = Vue.extend(environmentTableComp);
+
const component = new EnvironmentTable({
el: document.querySelector('.test-dom-element'),
propsData: {
environments: [{ mockItem }],
canCreateDeployment: false,
canReadEnvironment: true,
+ service: {},
},
- });
+ }).$mount();
expect(component.$el.tagName).toEqual('TABLE');
});
diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js
new file mode 100644
index 00000000000..b07aa4e1745
--- /dev/null
+++ b/spec/javascripts/environments/environment_terminal_button_spec.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import terminalComp from '~/environments/components/environment_terminal_button';
+
+describe('Stop Component', () => {
+ let TerminalComponent;
+ let component;
+ const terminalPath = '/path';
+
+ beforeEach(() => {
+ TerminalComponent = Vue.extend(terminalComp);
+
+ component = new TerminalComponent({
+ propsData: {
+ terminalPath,
+ },
+ }).$mount();
+ });
+
+ it('should render a link to open a web terminal with the provided path', () => {
+ expect(component.$el.tagName).toEqual('A');
+ expect(component.$el.getAttribute('title')).toEqual('Open web terminal');
+ expect(component.$el.getAttribute('href')).toEqual(terminalPath);
+ });
+});
diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js
index 77e182b3830..115d84b50f5 100644
--- a/spec/javascripts/environments/environments_store_spec.js
+++ b/spec/javascripts/environments/environments_store_spec.js
@@ -1,5 +1,5 @@
-const Store = require('~/environments/stores/environments_store');
-const { environmentsList, serverData } = require('./mock_data');
+import Store from '~/environments/stores/environments_store';
+import { environmentsList, serverData } from './mock_data';
(() => {
describe('Store', () => {
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index d1335b5b304..43a217a67f5 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -1,7 +1,7 @@
-const Vue = require('vue');
-require('~/flash');
-const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view');
-const { environmentsList } = require('../mock_data');
+import Vue from 'vue';
+import '~/flash';
+import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view';
+import { environmentsList } from '../mock_data';
describe('Environments Folder View', () => {
preloadFixtures('static/environments/environments_folder_view.html.raw');
diff --git a/spec/javascripts/environments/mock_data.js b/spec/javascripts/environments/mock_data.js
index 5c395c6b2d8..30861481cc5 100644
--- a/spec/javascripts/environments/mock_data.js
+++ b/spec/javascripts/environments/mock_data.js
@@ -1,4 +1,4 @@
-const environmentsList = [
+export const environmentsList = [
{
name: 'DEV',
size: 1,
@@ -30,7 +30,7 @@ const environmentsList = [
},
];
-const serverData = [
+export const serverData = [
{
name: 'DEV',
size: 1,
@@ -67,7 +67,7 @@ const serverData = [
},
];
-const environment = {
+export const environment = {
name: 'DEV',
size: 1,
latest: {
@@ -84,9 +84,3 @@ const environment = {
updated_at: '2017-01-31T10:53:46.894Z',
},
};
-
-module.exports = {
- environmentsList,
- environment,
- serverData,
-};
diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js
index 60f6b9b78e3..4b871fe967d 100644
--- a/spec/javascripts/extensions/array_spec.js
+++ b/spec/javascripts/extensions/array_spec.js
@@ -18,28 +18,5 @@ require('~/extensions/array');
return expect(arr.last()).toBe(5);
});
});
-
- describe('find', function () {
- beforeEach(() => {
- this.arr = [0, 1, 2, 3, 4, 5];
- });
-
- it('returns the item that first passes the predicate function', () => {
- expect(this.arr.find(item => item === 2)).toBe(2);
- });
-
- it('returns undefined if no items pass the predicate function', () => {
- expect(this.arr.find(item => item === 6)).not.toBeDefined();
- });
-
- it('error when called on undefined or null', () => {
- expect(Array.prototype.find.bind(undefined, item => item === 1)).toThrow();
- expect(Array.prototype.find.bind(null, item => item === 1)).toThrow();
- });
-
- it('error when predicate is not a function', () => {
- expect(Array.prototype.find.bind(this.arr, 1)).toThrow();
- });
- });
});
}).call(window);
diff --git a/spec/javascripts/extensions/element_spec.js b/spec/javascripts/extensions/element_spec.js
deleted file mode 100644
index 2d8a128ed33..00000000000
--- a/spec/javascripts/extensions/element_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-require('~/extensions/element');
-
-(() => {
- describe('Element extensions', function () {
- beforeEach(() => {
- this.element = document.createElement('ul');
- });
-
- describe('matches', () => {
- it('returns true if element matches the selector', () => {
- expect(this.element.matches('ul')).toBeTruthy();
- });
-
- it("returns false if element doesn't match the selector", () => {
- expect(this.element.matches('.not-an-element')).toBeFalsy();
- });
- });
-
- describe('closest', () => {
- beforeEach(() => {
- this.childElement = document.createElement('li');
- this.element.appendChild(this.childElement);
- });
-
- it('returns the closest parent that matches the selector', () => {
- expect(this.childElement.closest('ul').toString()).toBe(this.element.toString());
- });
-
- it('returns itself if it matches the selector', () => {
- expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString());
- });
-
- it('returns undefined if nothing matches the selector', () => {
- expect(this.childElement.closest('.no-an-element')).toBeFalsy();
- });
- });
- });
-})();
diff --git a/spec/javascripts/extensions/object_spec.js b/spec/javascripts/extensions/object_spec.js
deleted file mode 100644
index 2467ed78459..00000000000
--- a/spec/javascripts/extensions/object_spec.js
+++ /dev/null
@@ -1,25 +0,0 @@
-require('~/extensions/object');
-
-describe('Object extensions', () => {
- describe('assign', () => {
- it('merges source object into target object', () => {
- const targetObj = {};
- const sourceObj = {
- foo: 'bar',
- };
- Object.assign(targetObj, sourceObj);
- expect(targetObj.foo).toBe('bar');
- });
-
- it('merges object with the same properties', () => {
- const targetObj = {
- foo: 'bar',
- };
- const sourceObj = {
- foo: 'baz',
- };
- Object.assign(targetObj, sourceObj);
- expect(targetObj.foo).toBe('baz');
- });
- });
-});
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 81c1d81d181..ae9c263d1d7 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -41,7 +41,6 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
</div>
`);
- spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
@@ -54,6 +53,10 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
manager = new gl.FilteredSearchManager();
});
+ afterEach(() => {
+ manager.cleanup();
+ });
+
describe('search', () => {
const defaultParams = '?scope=all&utf8=✓&state=opened';
diff --git a/spec/javascripts/fixtures/project_branches.json b/spec/javascripts/fixtures/project_branches.json
new file mode 100644
index 00000000000..a96a4c0c095
--- /dev/null
+++ b/spec/javascripts/fixtures/project_branches.json
@@ -0,0 +1,5 @@
+[
+ "master",
+ "development",
+ "staging"
+]
diff --git a/spec/javascripts/fixtures/target_branch_dropdown.html.haml b/spec/javascripts/fixtures/target_branch_dropdown.html.haml
new file mode 100644
index 00000000000..821fb7940a0
--- /dev/null
+++ b/spec/javascripts/fixtures/target_branch_dropdown.html.haml
@@ -0,0 +1,28 @@
+%form.js-edit-blob-form
+ %input{type: 'hidden', name: 'target_branch', value: 'master'}
+ %div
+ .dropdown
+ %button.dropdown-menu-toggle.js-project-branches-dropdown.js-target-branch{type: 'button', data: {toggle: 'dropdown', selected: 'master', field_name: 'target_branch', form_id: '.js-edit-blob-form'}}
+ .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging
+ .dropdown-page-one
+ .dropdown-title 'Select branch'
+ .dropdown-input
+ %input.dropdown-input-field{type: 'search', value: ''}
+ %i.fa.fa-search.dropdown-input-search
+ %i.fa.fa-times-dropdown-input-clear.js-dropdown-input-clear{role: 'button'}
+ .dropdown-content
+ .dropdown-footer
+ %ul.dropdown-footer-list
+ %li
+ %a.create-new-branch.dropdown-toggle-page{href: "#"}
+ Create new branch
+ .dropdown-page-two.dropdown-new-branch
+ %button.dropdown-title-button.dropdown-menu-back{type: 'button'}
+ .dropdown_title 'Create new branch'
+ .dropdown_content
+ %input#new_branch_name.default-dropdown-input{ type: "text", placeholder: "Name new branch" }
+ %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" }
+ Create
+ %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" }
+ Cancel
+ %button{type: 'submit'}
diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js
index 7ab0b37f2ec..9b44b25980c 100644
--- a/spec/javascripts/gl_emoji_spec.js
+++ b/spec/javascripts/gl_emoji_spec.js
@@ -1,6 +1,3 @@
-import '~/extensions/string';
-import '~/extensions/array';
-
import { glEmojiTag } from '~/behaviors/gl_emoji';
import {
isEmojiUnicodeSupported,
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index a0b2ebc221b..a1fd2d38968 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -7,16 +7,12 @@ require('~/line_highlighter');
describe('LineHighlighter', function() {
var clickLine;
preloadFixtures('static/line_highlighter.html.raw');
- clickLine = function(number, eventData) {
- var e;
- if (eventData == null) {
- eventData = {};
- }
+ clickLine = function(number, eventData = {}) {
if ($.isEmptyObject(eventData)) {
- return $("#L" + number).mousedown().click();
+ return $("#L" + number).click();
} else {
- e = $.Event('mousedown', eventData);
- return $("#L" + number).trigger(e).click();
+ const e = $.Event('click', eventData);
+ return $("#L" + number).trigger(e);
}
};
beforeEach(function() {
@@ -63,12 +59,6 @@ require('~/line_highlighter');
});
});
describe('#clickHandler', function() {
- it('discards the mousedown event', function() {
- var spy;
- spy = spyOnEvent('a[data-line-number]', 'mousedown');
- clickLine(13);
- return expect(spy).toHaveBeenPrevented();
- });
it('handles clicking on a child icon element', function() {
var spy;
spy = spyOn(this["class"], 'setHash').and.callThrough();
diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js
index 823b4bab7fc..a3c1c5e1b7c 100644
--- a/spec/javascripts/monitoring/prometheus_graph_spec.js
+++ b/spec/javascripts/monitoring/prometheus_graph_spec.js
@@ -1,11 +1,8 @@
import 'jquery';
-import es6Promise from 'es6-promise';
import '~/lib/utils/common_utils';
import PrometheusGraph from '~/monitoring/prometheus_graph';
import { prometheusMockData } from './prometheus_mock_data';
-es6Promise.polyfill();
-
describe('PrometheusGraph', () => {
const fixtureName = 'static/environments/metrics.html.raw';
const prometheusGraphContainer = '.prometheus-graph';
diff --git a/spec/javascripts/polyfills/element_spec.js b/spec/javascripts/polyfills/element_spec.js
new file mode 100644
index 00000000000..ecaaf1907ea
--- /dev/null
+++ b/spec/javascripts/polyfills/element_spec.js
@@ -0,0 +1,36 @@
+import '~/commons/polyfills/element';
+
+describe('Element polyfills', function () {
+ beforeEach(() => {
+ this.element = document.createElement('ul');
+ });
+
+ describe('matches', () => {
+ it('returns true if element matches the selector', () => {
+ expect(this.element.matches('ul')).toBeTruthy();
+ });
+
+ it("returns false if element doesn't match the selector", () => {
+ expect(this.element.matches('.not-an-element')).toBeFalsy();
+ });
+ });
+
+ describe('closest', () => {
+ beforeEach(() => {
+ this.childElement = document.createElement('li');
+ this.element.appendChild(this.childElement);
+ });
+
+ it('returns the closest parent that matches the selector', () => {
+ expect(this.childElement.closest('ul').toString()).toBe(this.element.toString());
+ });
+
+ it('returns itself if it matches the selector', () => {
+ expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString());
+ });
+
+ it('returns undefined if nothing matches the selector', () => {
+ expect(this.childElement.closest('.no-an-element')).toBeFalsy();
+ });
+ });
+});
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 4ac7e911740..285b7940174 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,8 +1,8 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */
/* global Sidebar */
-require('~/right_sidebar');
-require('~/extensions/jquery.js');
+import '~/commons/bootstrap';
+import '~/right_sidebar';
(function() {
var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index ffff643e371..9e19dabd0e3 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -31,13 +31,9 @@ require('~/shortcuts_issuable');
this.shortcut.replyWithSelectedText();
expect($(this.selector).val()).toBe('');
});
- it('triggers `input`', function() {
- var focused = false;
- $(this.selector).on('focus', function() {
- focused = true;
- });
+ it('triggers `focus`', function() {
this.shortcut.replyWithSelectedText();
- expect(focused).toBe(true);
+ expect(document.activeElement).toBe(document.querySelector(this.selector));
});
});
describe('with any selection', function() {
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index cadfbadca10..e22f88b7a32 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -12,8 +12,16 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
ref: 'refs/heads/master'
}
end
-
- subject { described_class.new(changes, project: project, user_access: user_access).exec }
+ let(:protocol) { 'ssh' }
+
+ subject do
+ described_class.new(
+ changes,
+ project: project,
+ user_access: user_access,
+ protocol: protocol
+ ).exec
+ end
before { allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) }
diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index b983d73f8be..e76128ecd87 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -91,6 +91,54 @@ eos
end
end
+ describe '\ No newline at end of file' do
+ it "parses nonewline in one file correctly" do
+ first_nonewline_diff = <<~END
+ --- a/test
+ +++ b/test
+ @@ -1,2 +1,2 @@
+ +ipsum
+ lorem
+ -ipsum
+ \\ No newline at end of file
+ END
+ lines = parser.parse(first_nonewline_diff.lines).to_a
+
+ expect(lines[0].type).to eq('new')
+ expect(lines[0].text).to eq('+ipsum')
+ expect(lines[2].type).to eq('old')
+ expect(lines[3].type).to eq('old-nonewline')
+ expect(lines[1].old_pos).to eq(1)
+ expect(lines[1].new_pos).to eq(2)
+ end
+
+ it "parses nonewline in two files correctly" do
+ both_nonewline_diff = <<~END
+ --- a/test
+ +++ b/test
+ @@ -1,2 +1,2 @@
+ -lorem
+ -ipsum
+ \\ No newline at end of file
+ +ipsum
+ +lorem
+ \\ No newline at end of file
+ END
+ lines = parser.parse(both_nonewline_diff.lines).to_a
+
+ expect(lines[0].type).to eq('old')
+ expect(lines[1].type).to eq('old')
+ expect(lines[2].type).to eq('old-nonewline')
+ expect(lines[5].type).to eq('new-nonewline')
+ expect(lines[3].text).to eq('+ipsum')
+ expect(lines[3].old_pos).to eq(3)
+ expect(lines[3].new_pos).to eq(1)
+ expect(lines[4].text).to eq('+lorem')
+ expect(lines[4].old_pos).to eq(3)
+ expect(lines[4].new_pos).to eq(2)
+ end
+ end
+
context 'when lines is empty' do
it { expect(parser.parse([])).to eq([]) }
it { expect(parser.parse(nil)).to eq([]) }
diff --git a/spec/migrations/rename_more_reserved_project_names_spec.rb b/spec/migrations/rename_more_reserved_project_names_spec.rb
new file mode 100644
index 00000000000..36e82729c23
--- /dev/null
+++ b/spec/migrations/rename_more_reserved_project_names_spec.rb
@@ -0,0 +1,47 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170313133418_rename_more_reserved_project_names.rb')
+
+# This migration uses multiple threads, and thus different transactions. This
+# means data created in this spec may not be visible to some threads. To work
+# around this we use the TRUNCATE cleaning strategy.
+describe RenameMoreReservedProjectNames, truncate: true do
+ let(:migration) { described_class.new }
+ let!(:project) { create(:empty_project) }
+
+ before do
+ project.path = 'artifacts'
+ project.save!(validate: false)
+ end
+
+ describe '#up' do
+ context 'when project repository exists' do
+ before { project.create_repository }
+
+ context 'when no exception is raised' do
+ it 'renames project with reserved names' do
+ migration.up
+
+ expect(project.reload.path).to eq('artifacts0')
+ end
+ end
+
+ context 'when exception is raised during rename' do
+ before do
+ allow(project).to receive(:rename_repo).and_raise(StandardError)
+ end
+
+ it 'captures exception from project rename' do
+ expect { migration.up }.not_to raise_error
+ end
+ end
+ end
+
+ context 'when project repository does not exist' do
+ it 'does not raise error' do
+ expect { migration.up }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 30f8fdf91b2..92d70cfc64c 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -1,6 +1,12 @@
require 'spec_helper'
describe Ability, lib: true do
+ context 'using a nil subject' do
+ it 'is always empty' do
+ expect(Ability.allowed(nil, nil).to_set).to be_empty
+ end
+ end
+
describe '.can_edit_note?' do
let(:project) { create(:empty_project) }
let(:note) { create(:note_on_issue, project: project) }
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 03d02b4d382..94c25a454aa 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -70,6 +70,8 @@ describe Blob do
end
describe '#to_partial_path' do
+ let(:project) { double(lfs_enabled?: true) }
+
def stubbed_blob(overrides = {})
overrides.reverse_merge!(
image?: false,
@@ -84,34 +86,35 @@ describe Blob do
end
end
- it 'handles LFS pointers' do
- blob = stubbed_blob(lfs_pointer?: true)
+ it 'handles LFS pointers with LFS enabled' do
+ blob = stubbed_blob(lfs_pointer?: true, text?: true)
+ expect(blob.to_partial_path(project)).to eq 'download'
+ end
- expect(blob.to_partial_path).to eq 'download'
+ it 'handles LFS pointers with LFS disabled' do
+ blob = stubbed_blob(lfs_pointer?: true, text?: true)
+ project = double(lfs_enabled?: false)
+ expect(blob.to_partial_path(project)).to eq 'text'
end
it 'handles SVGs' do
blob = stubbed_blob(text?: true, svg?: true)
-
- expect(blob.to_partial_path).to eq 'image'
+ expect(blob.to_partial_path(project)).to eq 'image'
end
it 'handles images' do
blob = stubbed_blob(image?: true)
-
- expect(blob.to_partial_path).to eq 'image'
+ expect(blob.to_partial_path(project)).to eq 'image'
end
it 'handles text' do
blob = stubbed_blob(text?: true)
-
- expect(blob.to_partial_path).to eq 'text'
+ expect(blob.to_partial_path(project)).to eq 'text'
end
it 'defaults to download' do
blob = stubbed_blob
-
- expect(blob.to_partial_path).to eq 'download'
+ expect(blob.to_partial_path(project)).to eq 'download'
end
end
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index cacbab8bcb1..55b87d1c48a 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -92,6 +92,41 @@ describe GlobalMilestone, models: true do
end
end
+ describe '.states_count' do
+ context 'when the projects have milestones' do
+ before do
+ create(:closed_milestone, title: 'Active Group Milestone', project: project3)
+ create(:active_milestone, title: 'Active Group Milestone', project: project1)
+ create(:active_milestone, title: 'Active Group Milestone', project: project2)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project1)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project2)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project3)
+ end
+
+ it 'returns the quantity of global milestones in each possible state' do
+ expected_count = { opened: 1, closed: 2, all: 2 }
+
+ count = GlobalMilestone.states_count(Project.all)
+
+ expect(count).to eq(expected_count)
+ end
+ end
+
+ context 'when the projects do not have milestones' do
+ before do
+ project1
+ end
+
+ it 'returns 0 as the quantity of global milestones in each state' do
+ expected_count = { opened: 0, closed: 0, all: 0 }
+
+ count = GlobalMilestone.states_count(Project.all)
+
+ expect(count).to eq(expected_count)
+ end
+ end
+ end
+
describe '#initialize' do
let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
@@ -127,4 +162,32 @@ describe GlobalMilestone, models: true do
expect(global_milestone.safe_title).to eq('git-test')
end
end
+
+ describe '#state' do
+ context 'when at least one milestone is active' do
+ it 'returns active' do
+ title = 'Active Group Milestone'
+ milestones = [
+ create(:active_milestone, title: title),
+ create(:closed_milestone, title: title)
+ ]
+ global_milestone = GlobalMilestone.new(title, milestones)
+
+ expect(global_milestone.state).to eq('active')
+ end
+ end
+
+ context 'when all milestones are closed' do
+ it 'returns closed' do
+ title = 'Closed Group Milestone'
+ milestones = [
+ create(:closed_milestone, title: title),
+ create(:closed_milestone, title: title)
+ ]
+ global_milestone = GlobalMilestone.new(title, milestones)
+
+ expect(global_milestone.state).to eq('closed')
+ end
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 3166b951a42..9da4140f3ce 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -210,22 +210,6 @@ describe User, models: true do
end
end
end
-
- describe 'ghost users' do
- it 'does not allow a non-blocked ghost user' do
- user = build(:user, :ghost)
- user.state = 'active'
-
- expect(user).to be_invalid
- end
-
- it 'allows a blocked ghost user' do
- user = build(:user, :ghost)
- user.state = 'blocked'
-
- expect(user).to be_valid
- end
- end
end
describe "scopes" do
diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb
index 63acc0b68cd..02acdcb36df 100644
--- a/spec/policies/base_policy_spec.rb
+++ b/spec/policies/base_policy_spec.rb
@@ -1,17 +1,19 @@
require 'spec_helper'
describe BasePolicy, models: true do
- let(:build) { Ci::Build.new }
-
describe '.class_for' do
it 'detects policy class based on the subject ancestors' do
- expect(described_class.class_for(build)).to eq(Ci::BuildPolicy)
+ expect(described_class.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy)
end
it 'detects policy class for a presented subject' do
- presentee = Ci::BuildPresenter.new(build)
+ presentee = Ci::BuildPresenter.new(Ci::Build.new)
expect(described_class.class_for(presentee)).to eq(Ci::BuildPolicy)
end
+
+ it 'uses GlobalPolicy when :global is given' do
+ expect(described_class.class_for(:global)).to eq(GlobalPolicy)
+ end
end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index a89676fec93..988a57a80ea 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -436,7 +436,7 @@ describe API::Helpers, api: true do
context 'current_user is present' do
before do
- expect_any_instance_of(self.class).to receive(:current_user).and_return(true)
+ expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(User.new)
end
it 'does not raise an error' do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 2fc11a3b782..de7dbca0b22 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -928,29 +928,34 @@ describe API::Issues, api: true do
])
end
- context 'resolving issues in a merge request' do
+ context 'resolving discussions' do
let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
+
before do
project.team << [user, :master]
- post api("/projects/#{project.id}/issues", user),
- title: 'New Issue',
- merge_request_for_resolving_discussions: merge_request.iid
- end
-
- it 'creates a new project issue' do
- expect(response).to have_http_status(:created)
end
- it 'resolves the discussions in a merge request' do
- discussion.first_note.reload
+ context 'resolving all discussions in a merge request' do
+ before do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'New Issue',
+ merge_request_to_resolve_discussions_of: merge_request.iid
+ end
- expect(discussion.resolved?).to be(true)
+ it_behaves_like 'creating an issue resolving discussions through the API'
end
- it 'assigns a description to the issue mentioning the merge request' do
- expect(json_response['description']).to include(merge_request.to_reference)
+ context 'resolving a single discussion' do
+ before do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'New Issue',
+ merge_request_to_resolve_discussions_of: merge_request.iid,
+ discussion_to_resolve: discussion.id
+ end
+
+ it_behaves_like 'creating an issue resolving discussions through the API'
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index b4b23617498..c481b7e72b1 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
require 'spec_helper'
-describe API::Projects, api: true do
- include ApiHelpers
+describe API::Projects, :api do
include Gitlab::CurrentSettings
+
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 15d458e0795..d50fe80b36a 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -39,6 +39,7 @@ describe API::Runner do
expect(json_response['id']).to eq(runner.id)
expect(json_response['token']).to eq(runner.token)
expect(runner.run_untagged).to be true
+ expect(runner.token).not_to eq(registration_token)
end
context 'when project token is used' do
@@ -49,6 +50,8 @@ describe API::Runner do
expect(response).to have_http_status 201
expect(project.runners.size).to eq(1)
+ expect(Ci::Runner.first.token).not_to eq(registration_token)
+ expect(Ci::Runner.first.token).not_to eq(project.runners_token)
end
end
end
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
index 8719313783e..d50cdfdc2d6 100644
--- a/spec/requests/ci/api/runners_spec.rb
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -18,6 +18,7 @@ describe Ci::API::Runners do
it 'creates runner with default values' do
expect(response).to have_http_status 201
expect(Ci::Runner.first.run_untagged).to be true
+ expect(Ci::Runner.first.token).not_to eq(registration_token)
end
end
@@ -74,6 +75,8 @@ describe Ci::API::Runners do
it 'creates runner' do
expect(response).to have_http_status 201
expect(project.runners.size).to eq(1)
+ expect(Ci::Runner.first.token).not_to eq(registration_token)
+ expect(Ci::Runner.first.token).not_to eq(project.runners_token)
end
end
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 09807e5d35b..1dd53236fbd 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -8,24 +8,34 @@ describe Issues::BuildService, services: true do
project.team << [user, :developer]
end
+ context 'for a single discussion' do
+ describe '#execute' do
+ let(:merge_request) { create(:merge_request, title: "Hello world", source_project: project) }
+ let(:discussion) { Discussion.new([create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "Almost done")]) }
+ let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) }
+
+ it 'references the noteable title in the issue title' do
+ issue = service.execute
+
+ expect(issue.title).to include('Hello world')
+ end
+
+ it 'adds the note content to the description' do
+ issue = service.execute
+
+ expect(issue.description).to include('Almost done')
+ end
+ end
+ end
+
context 'for discussions in a merge request' do
let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
- let(:issue) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute }
-
- def position_on_line(line_number)
- Gitlab::Diff::Position.new(
- old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: line_number,
- diff_refs: merge_request.diff_refs
- )
- end
+ let(:issue) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute }
describe '#items_for_discussions' do
it 'has an item for each discussion' do
- create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, position: position_on_line(13))
- service = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request)
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, line_number: 13)
+ service = described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid)
service.execute
@@ -34,7 +44,7 @@ describe Issues::BuildService, services: true do
end
describe '#item_for_discussion' do
- let(:service) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request) }
+ let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) }
it 'mentions the author of the note' do
discussion = Discussion.new([create(:diff_note_on_merge_request, author: create(:user, username: 'author'))])
@@ -47,11 +57,11 @@ describe Issues::BuildService, services: true do
"with a blockquote\n"\
"> That has a quote\n"\
">>>\n"
- note_result = "This is a string\n"\
- "> with a blockquote\n"\
- "> > That has a quote\n"
+ note_result = " > This is a string\n"\
+ " > > with a blockquote\n"\
+ " > > > That has a quote\n"
discussion = Discussion.new([create(:diff_note_on_merge_request, note: note_text)])
- expect(service.item_for_discussion(discussion)).to include(">>>\n#{note_result}\n>>>")
+ expect(service.item_for_discussion(discussion)).to include(note_result)
end
end
@@ -66,7 +76,7 @@ describe Issues::BuildService, services: true do
it 'does not assign title when a title was given' do
issue = described_class.new(project, user,
- merge_request_for_resolving_discussions: merge_request,
+ merge_request_to_resolve_discussions_of: merge_request,
title: 'What an issue').execute
expect(issue.title).to eq('What an issue')
@@ -74,7 +84,7 @@ describe Issues::BuildService, services: true do
it 'does not assign description when a description was given' do
issue = described_class.new(project, user,
- merge_request_for_resolving_discussions: merge_request,
+ merge_request_to_resolve_discussions_of: merge_request,
description: 'Fix at your earliest conveignance').execute
expect(issue.description).to eq('Fix at your earliest conveignance')
@@ -82,7 +92,7 @@ describe Issues::BuildService, services: true do
describe 'with multiple discussions' do
before do
- create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15))
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, line_number: 15)
end
it 'mentions all the authors in the description' do
@@ -99,7 +109,7 @@ describe Issues::BuildService, services: true do
end
it 'mentions additional notes' do
- create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15))
+ create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, line_number: 15)
expect(issue.description).to include('(+2 comments)')
end
@@ -112,7 +122,7 @@ describe Issues::BuildService, services: true do
describe '#execute' do
it 'mentions the merge request in the description' do
- issue = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute
+ issue = described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute
expect(issue.description).to include("Review the conversation in #{merge_request.to_reference}")
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 6045d00ff09..776cbc4296b 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -140,46 +140,85 @@ describe Issues::CreateService, services: true do
it_behaves_like 'new issuable record that supports slash commands'
- context 'for a merge request' do
+ context 'resolving discussions' do
let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
- let(:opts) { { merge_request_for_resolving_discussions: merge_request } }
before do
project.team << [user, :master]
end
- it 'resolves the discussion for the merge request' do
- described_class.new(project, user, opts).execute
- discussion.first_note.reload
+ describe 'for a single discussion' do
+ let(:opts) { { discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid } }
- expect(discussion.resolved?).to be(true)
- end
+ it 'resolves the discussion' do
+ described_class.new(project, user, opts).execute
+ discussion.first_note.reload
- it 'added a system note to the discussion' do
- described_class.new(project, user, opts).execute
+ expect(discussion.resolved?).to be(true)
+ end
- reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
+ it 'added a system note to the discussion' do
+ described_class.new(project, user, opts).execute
- expect(reloaded_discussion.last_note.system).to eq(true)
- end
+ reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
+
+ expect(reloaded_discussion.last_note.system).to eq(true)
+ end
+
+ it 'assigns the title and description for the issue' do
+ issue = described_class.new(project, user, opts).execute
+
+ expect(issue.title).not_to be_nil
+ expect(issue.description).not_to be_nil
+ end
- it 'assigns the title and description for the issue' do
- issue = described_class.new(project, user, opts).execute
+ it 'can set nil explicitly to the title and description' do
+ issue = described_class.new(project, user,
+ merge_request_to_resolve_discussions_of: merge_request,
+ description: nil,
+ title: nil).execute
- expect(issue.title).not_to be_nil
- expect(issue.description).not_to be_nil
+ expect(issue.description).to be_nil
+ expect(issue.title).to be_nil
+ end
end
- it 'can set nil explicityly to the title and description' do
- issue = described_class.new(project, user,
- merge_request_for_resolving_discussions: merge_request,
- description: nil,
- title: nil).execute
+ describe 'for a merge request' do
+ let(:opts) { { merge_request_to_resolve_discussions_of: merge_request.iid } }
+
+ it 'resolves the discussion' do
+ described_class.new(project, user, opts).execute
+ discussion.first_note.reload
- expect(issue.description).to be_nil
- expect(issue.title).to be_nil
+ expect(discussion.resolved?).to be(true)
+ end
+
+ it 'added a system note to the discussion' do
+ described_class.new(project, user, opts).execute
+
+ reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
+
+ expect(reloaded_discussion.last_note.system).to eq(true)
+ end
+
+ it 'assigns the title and description for the issue' do
+ issue = described_class.new(project, user, opts).execute
+
+ expect(issue.title).not_to be_nil
+ expect(issue.description).not_to be_nil
+ end
+
+ it 'can set nil explicitly to the title and description' do
+ issue = described_class.new(project, user,
+ merge_request_to_resolve_discussions_of: merge_request,
+ description: nil,
+ title: nil).execute
+
+ expect(issue.description).to be_nil
+ expect(issue.title).to be_nil
+ end
end
end
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
new file mode 100644
index 00000000000..6cc738aec08
--- /dev/null
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper.rb'
+
+class DummyService < Issues::BaseService
+ include ::Issues::ResolveDiscussions
+
+ def initialize(*args)
+ super
+ filter_resolve_discussion_params
+ end
+end
+
+describe DummyService, services: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ describe "for resolving discussions" do
+ let(:discussion) { Discussion.new([create(:diff_note_on_merge_request, project: project, note: "Almost done")]) }
+ let(:merge_request) { discussion.noteable }
+ let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "other") }
+
+ describe "#merge_request_for_resolving_discussion" do
+ let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) }
+
+ it "finds the merge request" do
+ expect(service.merge_request_to_resolve_discussions_of).to eq(merge_request)
+ end
+
+ it "only queries for the merge request once" do
+ fake_finder = double
+ fake_results = double
+
+ expect(fake_finder).to receive(:execute).and_return(fake_results).exactly(1)
+ expect(fake_results).to receive(:find_by).exactly(1)
+ expect(MergeRequestsFinder).to receive(:new).and_return(fake_finder).exactly(1)
+
+ 2.times { service.merge_request_to_resolve_discussions_of }
+ end
+ end
+
+ describe "#discussions_to_resolve" do
+ it "contains a single discussion when matching merge request and discussion are passed" do
+ service = described_class.new(
+ project,
+ user,
+ discussion_to_resolve: discussion.id,
+ merge_request_to_resolve_discussions_of: merge_request.iid
+ )
+ # We need to compare discussion id's because the Discussion-objects are rebuilt
+ # which causes the object-id's not to be different.
+ discussion_ids = service.discussions_to_resolve.map(&:id)
+
+ expect(discussion_ids).to contain_exactly(discussion.id)
+ end
+
+ it "contains all discussions when only a merge request is passed" do
+ second_discussion = Discussion.new([create(:diff_note_on_merge_request,
+ noteable: merge_request,
+ project: merge_request.target_project,
+ line_number: 15)])
+ service = described_class.new(
+ project,
+ user,
+ merge_request_to_resolve_discussions_of: merge_request.iid
+ )
+ # We need to compare discussion id's because the Discussion-objects are rebuilt
+ # which causes the object-id's not to be different.
+ discussion_ids = service.discussions_to_resolve.map(&:id)
+
+ expect(discussion_ids).to contain_exactly(discussion.id, second_discussion.id)
+ end
+
+ it "contains only unresolved discussions" do
+ _second_discussion = Discussion.new([create(:diff_note_on_merge_request, :resolved,
+ noteable: merge_request,
+ project: merge_request.target_project,
+ line_number: 15,
+ )])
+ service = described_class.new(
+ project,
+ user,
+ merge_request_to_resolve_discussions_of: merge_request.iid
+ )
+ # We need to compare discussion id's because the Discussion-objects are rebuilt
+ # which causes the object-id's not to be different.
+ discussion_ids = service.discussions_to_resolve.map(&:id)
+
+ expect(discussion_ids).to contain_exactly(discussion.id)
+ end
+
+ it "is empty when a discussion and another merge request are passed" do
+ service = described_class.new(
+ project,
+ user,
+ discussion_to_resolve: discussion.id,
+ merge_request_to_resolve_discussions_of: other_merge_request.iid
+ )
+
+ expect(service.discussions_to_resolve).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 5fda7c63cdb..ceb3209331f 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -43,14 +43,27 @@ RSpec.configure do |config|
config.include ActiveSupport::Testing::TimeHelpers
config.include StubGitlabCalls
config.include StubGitlabData
+ config.include ApiHelpers, :api
config.infer_spec_type_from_file_location!
+
+ config.define_derived_metadata(file_path: %r{/spec/requests/(ci/)?api/}) do |metadata|
+ metadata[:api] = true
+ end
+
config.raise_errors_for_deprecations!
config.before(:suite) do
TestEnv.init
end
+ if ENV['CI']
+ # Retry only on feature specs that use JS
+ config.around :each, :js do |ex|
+ ex.run_with_retry retry: 3
+ end
+ end
+
config.around(:each, :caching) do |example|
caching_store = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new if example.metadata[:caching]
diff --git a/spec/support/api/issues_resolving_discussions_shared_examples.rb b/spec/support/api/issues_resolving_discussions_shared_examples.rb
new file mode 100644
index 00000000000..d26d279363c
--- /dev/null
+++ b/spec/support/api/issues_resolving_discussions_shared_examples.rb
@@ -0,0 +1,15 @@
+shared_examples 'creating an issue resolving discussions through the API' do
+ it 'creates a new project issue' do
+ expect(response).to have_http_status(:created)
+ end
+
+ it 'resolves the discussions in a merge request' do
+ discussion.first_note.reload
+
+ expect(discussion.resolved?).to be(true)
+ end
+
+ it 'assigns a description to the issue mentioning the merge request' do
+ expect(json_response['description']).to include(merge_request.to_reference)
+ end
+end
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index ae6e708cf87..35d1e1cfc7d 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -49,8 +49,4 @@ module ApiHelpers
''
end
end
-
- def json_response
- @_json_response ||= JSON.parse(response.body)
- end
end
diff --git a/spec/support/features/resolving_discussions_in_issues_shared_examples.rb b/spec/support/features/resolving_discussions_in_issues_shared_examples.rb
new file mode 100644
index 00000000000..4a946995f84
--- /dev/null
+++ b/spec/support/features/resolving_discussions_in_issues_shared_examples.rb
@@ -0,0 +1,41 @@
+shared_examples 'creating an issue for a discussion' do
+ it 'shows an issue with the title filled in' do
+ title_field = page.find_field('issue[title]')
+
+ expect(title_field.value).to include(merge_request.title)
+ end
+
+ it 'has a mention of the discussion in the description' do
+ description_field = page.find_field('issue[description]')
+
+ expect(description_field.value).to include(discussion.first_note.note)
+ end
+
+ it 'can create a new issue for the project' do
+ expect { click_button 'Submit issue' }.to change { project.issues.reload.size }.by(1)
+ end
+
+ it 'resolves the discussion in the merge request' do
+ click_button 'Submit issue'
+
+ discussion.first_note.reload
+
+ expect(discussion.resolved?).to eq(true)
+ end
+
+ it 'shows a flash messaage after resolving a discussion' do
+ click_button 'Submit issue'
+
+ page.within '.flash-notice' do
+ # Only check for the word 'Resolved' since the spec might have resolved
+ # multiple discussions
+ expect(page).to have_content('Resolved')
+ end
+ end
+
+ it 'has a hidden field for the merge request' do
+ merge_request_field = find('#merge_request_to_resolve_discussions_of', visible: false)
+
+ expect(merge_request_field.value).to eq(merge_request.iid.to_s)
+ end
+end
diff --git a/spec/support/json_response_helpers.rb b/spec/support/json_response_helpers.rb
new file mode 100644
index 00000000000..e8d2ef2d7f0
--- /dev/null
+++ b/spec/support/json_response_helpers.rb
@@ -0,0 +1,9 @@
+shared_context 'JSON response' do
+ let(:json_response) { JSON.parse(response.body) }
+end
+
+RSpec.configure do |config|
+ config.include_context 'JSON response', type: :controller
+ config.include_context 'JSON response', type: :request
+ config.include_context 'JSON response', :api
+end
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
new file mode 100644
index 00000000000..a2cbef126ad
--- /dev/null
+++ b/vendor/licenses.csv
@@ -0,0 +1,945 @@
+RedCloth,4.3.2,MIT
+abbrev,1.0.9,ISC
+accepts,1.3.3,MIT
+ace-rails-ap,4.1.0,MIT
+acorn,4.0.4,MIT
+acorn-dynamic-import,2.0.1,MIT
+acorn-jsx,3.0.1,MIT
+actionmailer,4.2.8,MIT
+actionpack,4.2.8,MIT
+actionview,4.2.8,MIT
+activejob,4.2.8,MIT
+activemodel,4.2.8,MIT
+activerecord,4.2.8,MIT
+activesupport,4.2.8,MIT
+acts-as-taggable-on,4.0.0,MIT
+addressable,2.3.8,Apache 2.0
+after,0.8.2,MIT
+after_commit_queue,1.3.0,MIT
+ajv,4.11.2,MIT
+ajv-keywords,1.5.1,MIT
+akismet,2.0.0,MIT
+align-text,0.1.4,MIT
+allocations,1.0.5,MIT
+amdefine,1.0.1,BSD-3-Clause OR MIT
+ansi-escapes,1.4.0,MIT
+ansi-html,0.0.7,Apache 2.0
+ansi-regex,2.1.1,MIT
+ansi-styles,2.2.1,MIT
+anymatch,1.3.0,ISC
+append-transform,0.4.0,MIT
+aproba,1.1.0,ISC
+are-we-there-yet,1.1.2,ISC
+arel,6.0.4,MIT
+argparse,1.0.9,MIT
+arr-diff,2.0.0,MIT
+arr-flatten,1.0.1,MIT
+array-find,1.0.0,MIT
+array-flatten,1.1.1,MIT
+array-slice,0.2.3,MIT
+array-union,1.0.2,MIT
+array-uniq,1.0.3,MIT
+array-unique,0.2.1,MIT
+arraybuffer.slice,0.0.6,MIT
+arrify,1.0.1,MIT
+asana,0.4.0,MIT
+asciidoctor,1.5.3,MIT
+asciidoctor-plantuml,0.0.7,MIT
+asn1,0.2.3,MIT
+asn1.js,4.9.1,MIT
+assert,1.4.1,MIT
+assert-plus,0.2.0,MIT
+async,0.2.10,MIT
+async-each,1.0.1,MIT
+asynckit,0.4.0,MIT
+attr_encrypted,3.0.3,MIT
+attr_required,1.0.0,MIT
+autoparse,0.3.3,Apache 2.0
+autoprefixer-rails,6.2.3,MIT
+aws-sign2,0.6.0,Apache 2.0
+aws4,1.6.0,MIT
+axiom-types,0.1.1,MIT
+babel-code-frame,6.22.0,MIT
+babel-core,6.23.1,MIT
+babel-generator,6.23.0,MIT
+babel-helper-bindify-decorators,6.22.0,MIT
+babel-helper-builder-binary-assignment-operator-visitor,6.22.0,MIT
+babel-helper-call-delegate,6.22.0,MIT
+babel-helper-define-map,6.23.0,MIT
+babel-helper-explode-assignable-expression,6.22.0,MIT
+babel-helper-explode-class,6.22.0,MIT
+babel-helper-function-name,6.23.0,MIT
+babel-helper-get-function-arity,6.22.0,MIT
+babel-helper-hoist-variables,6.22.0,MIT
+babel-helper-optimise-call-expression,6.23.0,MIT
+babel-helper-regex,6.22.0,MIT
+babel-helper-remap-async-to-generator,6.22.0,MIT
+babel-helper-replace-supers,6.23.0,MIT
+babel-helpers,6.23.0,MIT
+babel-loader,6.2.10,MIT
+babel-messages,6.23.0,MIT
+babel-plugin-check-es2015-constants,6.22.0,MIT
+babel-plugin-istanbul,4.0.0,New BSD
+babel-plugin-syntax-async-functions,6.13.0,MIT
+babel-plugin-syntax-async-generators,6.13.0,MIT
+babel-plugin-syntax-class-properties,6.13.0,MIT
+babel-plugin-syntax-decorators,6.13.0,MIT
+babel-plugin-syntax-dynamic-import,6.18.0,MIT
+babel-plugin-syntax-exponentiation-operator,6.13.0,MIT
+babel-plugin-syntax-object-rest-spread,6.13.0,MIT
+babel-plugin-syntax-trailing-function-commas,6.22.0,MIT
+babel-plugin-transform-async-generator-functions,6.22.0,MIT
+babel-plugin-transform-async-to-generator,6.22.0,MIT
+babel-plugin-transform-class-properties,6.23.0,MIT
+babel-plugin-transform-decorators,6.22.0,MIT
+babel-plugin-transform-es2015-arrow-functions,6.22.0,MIT
+babel-plugin-transform-es2015-block-scoped-functions,6.22.0,MIT
+babel-plugin-transform-es2015-block-scoping,6.23.0,MIT
+babel-plugin-transform-es2015-classes,6.23.0,MIT
+babel-plugin-transform-es2015-computed-properties,6.22.0,MIT
+babel-plugin-transform-es2015-destructuring,6.23.0,MIT
+babel-plugin-transform-es2015-duplicate-keys,6.22.0,MIT
+babel-plugin-transform-es2015-for-of,6.23.0,MIT
+babel-plugin-transform-es2015-function-name,6.22.0,MIT
+babel-plugin-transform-es2015-literals,6.22.0,MIT
+babel-plugin-transform-es2015-modules-amd,6.22.0,MIT
+babel-plugin-transform-es2015-modules-commonjs,6.23.0,MIT
+babel-plugin-transform-es2015-modules-systemjs,6.23.0,MIT
+babel-plugin-transform-es2015-modules-umd,6.23.0,MIT
+babel-plugin-transform-es2015-object-super,6.22.0,MIT
+babel-plugin-transform-es2015-parameters,6.23.0,MIT
+babel-plugin-transform-es2015-shorthand-properties,6.22.0,MIT
+babel-plugin-transform-es2015-spread,6.22.0,MIT
+babel-plugin-transform-es2015-sticky-regex,6.22.0,MIT
+babel-plugin-transform-es2015-template-literals,6.22.0,MIT
+babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT
+babel-plugin-transform-es2015-unicode-regex,6.22.0,MIT
+babel-plugin-transform-exponentiation-operator,6.22.0,MIT
+babel-plugin-transform-object-rest-spread,6.23.0,MIT
+babel-plugin-transform-regenerator,6.22.0,MIT
+babel-plugin-transform-strict-mode,6.22.0,MIT
+babel-preset-es2015,6.22.0,MIT
+babel-preset-stage-2,6.22.0,MIT
+babel-preset-stage-3,6.22.0,MIT
+babel-register,6.23.0,MIT
+babel-runtime,6.22.0,MIT
+babel-template,6.23.0,MIT
+babel-traverse,6.23.1,MIT
+babel-types,6.23.0,MIT
+babosa,1.0.2,MIT
+babylon,6.15.0,MIT
+backo2,1.0.2,MIT
+balanced-match,0.4.2,MIT
+base32,0.3.2,MIT
+base64-arraybuffer,0.1.5,MIT
+base64-js,1.2.0,MIT
+base64id,1.0.0,MIT
+batch,0.5.3,MIT
+bcrypt,3.1.11,MIT
+bcrypt-pbkdf,1.0.1,New BSD
+better-assert,1.0.2,MIT
+big.js,3.1.3,MIT
+binary-extensions,1.8.0,MIT
+bindata,2.3.5,ruby
+blob,0.0.4,unknown
+block-stream,0.0.9,ISC
+bluebird,3.4.7,MIT
+bn.js,4.11.6,MIT
+body-parser,1.16.0,MIT
+boom,2.10.1,New BSD
+bootstrap-sass,3.3.6,MIT
+brace-expansion,1.1.6,MIT
+braces,1.8.5,MIT
+brorand,1.0.7,MIT
+browser,2.2.0,MIT
+browserify-aes,1.0.6,MIT
+browserify-cipher,1.0.0,MIT
+browserify-des,1.0.0,MIT
+browserify-rsa,4.0.1,MIT
+browserify-sign,4.0.0,ISC
+browserify-zlib,0.1.4,MIT
+buffer,4.9.1,MIT
+buffer-shims,1.0.0,MIT
+buffer-xor,1.0.3,MIT
+builder,3.2.3,MIT
+builtin-modules,1.1.1,MIT
+builtin-status-codes,3.0.0,MIT
+bytes,2.4.0,MIT
+caller-path,0.1.0,MIT
+callsite,1.0.0,unknown
+callsites,0.2.0,MIT
+camelcase,1.2.1,MIT
+carrierwave,0.11.2,MIT
+caseless,0.11.0,Apache 2.0
+cause,0.1,MIT
+center-align,0.1.3,MIT
+chalk,1.1.3,MIT
+charlock_holmes,0.7.3,MIT
+chokidar,1.6.1,MIT
+chronic,0.10.2,MIT
+chronic_duration,0.10.6,MIT
+chunky_png,1.3.5,MIT
+cipher-base,1.0.3,MIT
+circular-json,0.3.1,MIT
+cli-cursor,1.0.2,MIT
+cli-width,2.1.0,ISC
+cliui,2.1.0,ISC
+clone,1.0.2,MIT
+co,4.6.0,MIT
+code-point-at,1.1.0,MIT
+coercible,1.0.0,MIT
+coffee-rails,4.1.1,MIT
+coffee-script,2.4.1,MIT
+coffee-script-source,1.10.0,MIT
+colors,1.1.2,MIT
+combine-lists,1.0.1,MIT
+combined-stream,1.0.5,MIT
+commander,2.9.0,MIT
+commondir,1.0.1,MIT
+component-bind,1.0.0,unknown
+component-emitter,1.2.1,MIT
+component-inherit,0.0.3,unknown
+compressible,2.0.9,MIT
+compression,1.6.2,MIT
+compression-webpack-plugin,0.3.2,MIT
+concat-map,0.0.1,MIT
+concat-stream,1.6.0,MIT
+concurrent-ruby,1.0.4,MIT
+connect,3.5.0,MIT
+connect-history-api-fallback,1.3.0,MIT
+connection_pool,2.2.1,MIT
+console-browserify,1.1.0,MIT
+console-control-strings,1.1.0,ISC
+constants-browserify,1.0.0,MIT
+contains-path,0.1.0,MIT
+content-disposition,0.5.2,MIT
+content-type,1.0.2,MIT
+convert-source-map,1.3.0,MIT
+cookie,0.3.1,MIT
+cookie-signature,1.0.6,MIT
+core-js,2.4.1,MIT
+core-util-is,1.0.2,MIT
+crack,0.4.3,MIT
+create-ecdh,4.0.0,MIT
+create-hash,1.1.2,MIT
+create-hmac,1.1.4,MIT
+creole,0.5.0,ruby
+cryptiles,2.0.5,New BSD
+crypto-browserify,3.11.0,MIT
+css_parser,1.4.1,MIT
+custom-event,1.0.1,MIT
+d,0.1.1,MIT
+d3,3.5.11,New BSD
+d3_rails,3.5.11,MIT
+dashdash,1.14.1,MIT
+date-now,0.1.4,MIT
+debug,2.6.0,MIT
+decamelize,1.2.0,MIT
+deckar01-task_list,1.0.6,MIT
+deep-extend,0.4.1,MIT
+deep-is,0.1.3,MIT
+default-require-extensions,1.0.0,MIT
+default_value_for,3.0.2,MIT
+defaults,1.0.3,MIT
+del,2.2.2,MIT
+delayed-stream,1.0.0,MIT
+delegates,1.0.0,MIT
+depd,1.1.0,MIT
+des.js,1.0.0,MIT
+descendants_tracker,0.0.4,MIT
+destroy,1.0.4,MIT
+detect-indent,4.0.0,MIT
+devise,4.2.0,MIT
+devise-two-factor,3.0.0,MIT
+di,0.0.1,MIT
+diff-lcs,1.2.5,"MIT,Perl Artistic v2,GNU GPL v2"
+diffie-hellman,5.0.2,MIT
+diffy,3.1.0,MIT
+doctrine,1.5.0,BSD
+document-register-element,1.3.0,MIT
+dom-serialize,2.2.1,MIT
+domain-browser,1.1.7,MIT
+domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0"
+doorkeeper,4.2.0,MIT
+doorkeeper-openid_connect,1.1.2,MIT
+dropzone,4.2.0,MIT
+dropzonejs-rails,0.7.2,MIT
+duplexer,0.1.1,MIT
+ecc-jsbn,0.1.1,MIT
+ee-first,1.1.1,MIT
+ejs,2.5.6,Apache 2.0
+elliptic,6.3.3,MIT
+email_reply_trimmer,0.1.6,MIT
+emoji-unicode-version,0.2.1,MIT
+emojis-list,2.1.0,MIT
+encodeurl,1.0.1,MIT
+encryptor,3.0.0,MIT
+engine.io,1.8.2,MIT
+engine.io-client,1.8.2,MIT
+engine.io-parser,1.3.2,MIT
+enhanced-resolve,3.1.0,MIT
+ent,2.2.0,MIT
+equalizer,0.0.11,MIT
+errno,0.1.4,MIT
+error-ex,1.3.0,MIT
+erubis,2.7.0,MIT
+es5-ext,0.10.12,MIT
+es6-iterator,2.0.0,MIT
+es6-map,0.1.4,MIT
+es6-promise,4.0.5,MIT
+es6-set,0.1.4,MIT
+es6-symbol,3.1.0,MIT
+es6-weak-map,2.0.1,MIT
+escape-html,1.0.3,MIT
+escape-string-regexp,1.0.5,MIT
+escape_utils,1.1.1,MIT
+escodegen,1.8.1,Simplified BSD
+escope,3.6.0,Simplified BSD
+eslint,3.15.0,MIT
+eslint-config-airbnb-base,10.0.1,MIT
+eslint-import-resolver-node,0.2.3,MIT
+eslint-import-resolver-webpack,0.8.1,MIT
+eslint-module-utils,2.0.0,MIT
+eslint-plugin-filenames,1.1.0,MIT
+eslint-plugin-import,2.2.0,MIT
+eslint-plugin-jasmine,2.2.0,MIT
+espree,3.4.0,Simplified BSD
+esprima,3.1.3,Simplified BSD
+esrecurse,4.1.0,Simplified BSD
+estraverse,4.1.1,Simplified BSD
+esutils,2.0.2,BSD
+etag,1.7.0,MIT
+eve-raphael,0.5.0,Apache 2.0
+event-emitter,0.3.4,MIT
+eventemitter3,1.2.0,MIT
+events,1.1.1,MIT
+eventsource,0.1.6,MIT
+evp_bytestokey,1.0.0,MIT
+excon,0.52.0,MIT
+execjs,2.6.0,MIT
+exit-hook,1.1.1,MIT
+expand-braces,0.1.2,MIT
+expand-brackets,0.1.5,MIT
+expand-range,1.8.2,MIT
+express,4.14.1,MIT
+expression_parser,0.9.0,MIT
+extend,3.0.0,MIT
+extglob,0.3.2,MIT
+extlib,0.9.16,MIT
+extract-zip,1.5.0,Simplified BSD
+extsprintf,1.0.2,MIT
+faraday,0.9.2,MIT
+faraday_middleware,0.10.0,MIT
+faraday_middleware-multi_json,0.0.6,MIT
+fast-levenshtein,2.0.6,MIT
+faye-websocket,0.10.0,MIT
+fd-slicer,1.0.1,MIT
+ffi,1.9.10,BSD
+figures,1.7.0,MIT
+file-entry-cache,2.0.0,MIT
+filename-regex,2.0.0,MIT
+fileset,2.0.3,MIT
+filesize,3.5.4,New BSD
+fill-range,2.2.3,MIT
+finalhandler,0.5.1,MIT
+find-cache-dir,0.1.1,MIT
+find-root,0.1.2,MIT
+find-up,2.1.0,MIT
+flat-cache,1.2.2,MIT
+flowdock,0.7.1,MIT
+fog-aws,0.11.0,MIT
+fog-core,1.42.0,MIT
+fog-google,0.5.0,MIT
+fog-json,1.0.2,MIT
+fog-local,0.3.0,MIT
+fog-openstack,0.1.6,MIT
+fog-rackspace,0.1.1,MIT
+fog-xml,0.1.2,MIT
+font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License"
+for-in,0.1.6,MIT
+for-own,0.1.4,MIT
+forever-agent,0.6.1,Apache 2.0
+form-data,2.1.2,MIT
+formatador,0.2.5,MIT
+forwarded,0.1.0,MIT
+fresh,0.3.0,MIT
+fs-extra,1.0.0,MIT
+fs.realpath,1.0.0,ISC
+fsevents,,unknown
+fstream,1.0.10,ISC
+fstream-ignore,1.0.5,ISC
+function-bind,1.1.0,MIT
+gauge,2.7.2,ISC
+gemnasium-gitlab-service,0.2.6,MIT
+gemojione,3.0.1,MIT
+generate-function,2.0.0,MIT
+generate-object-property,1.2.0,MIT
+get-caller-file,1.0.2,ISC
+get_process_mem,0.2.0,MIT
+getpass,0.1.6,MIT
+gitaly,0.2.1,MIT
+github-linguist,4.7.6,MIT
+github-markup,1.4.0,MIT
+gitlab-flowdock-git-hook,1.0.1,MIT
+gitlab-grit,2.8.1,MIT
+gitlab-markup,1.5.1,MIT
+gitlab_omniauth-ldap,1.2.1,MIT
+glob,7.1.1,ISC
+glob-base,0.3.0,MIT
+glob-parent,2.0.0,ISC
+globalid,0.3.7,MIT
+globals,9.14.0,MIT
+globby,5.0.0,MIT
+gollum-grit_adapter,1.0.1,MIT
+gollum-lib,4.2.1,MIT
+gollum-rugged_adapter,0.4.2,MIT
+gon,6.1.0,MIT
+google-api-client,0.8.7,Apache 2.0
+google-protobuf,3.2.0,New BSD
+googleauth,0.5.1,Apache 2.0
+graceful-fs,4.1.11,ISC
+graceful-readlink,1.0.1,MIT
+grape,0.19.1,MIT
+grape-entity,0.6.0,MIT
+grpc,1.1.2,New BSD
+gzip-size,3.0.0,MIT
+hamlit,2.6.1,MIT
+handle-thing,1.2.5,MIT
+handlebars,4.0.6,MIT
+har-validator,2.0.6,ISC
+has,1.0.1,MIT
+has-ansi,2.0.0,MIT
+has-binary,0.1.7,MIT
+has-cors,1.1.0,MIT
+has-flag,1.0.0,MIT
+has-unicode,2.0.1,ISC
+hash.js,1.0.3,MIT
+hasha,2.2.0,MIT
+hashie,3.5.5,MIT
+hawk,3.1.3,New BSD
+health_check,2.6.0,MIT
+hipchat,1.5.2,MIT
+hoek,2.16.3,New BSD
+home-or-tmp,2.0.0,MIT
+hosted-git-info,2.2.0,ISC
+hpack.js,2.1.6,MIT
+html-entities,1.2.0,MIT
+html-pipeline,1.11.0,MIT
+html2text,0.2.0,MIT
+htmlentities,4.3.4,MIT
+http,0.9.8,MIT
+http-cookie,1.0.3,MIT
+http-deceiver,1.2.7,MIT
+http-errors,1.5.1,MIT
+http-form_data,1.0.1,MIT
+http-proxy,1.16.2,MIT
+http-proxy-middleware,0.17.3,MIT
+http-signature,1.1.1,MIT
+http_parser.rb,0.6.0,MIT
+httparty,0.13.7,MIT
+httpclient,2.8.2,ruby
+https-browserify,0.0.1,MIT
+i18n,0.8.1,MIT
+ice_nine,0.11.2,MIT
+iconv-lite,0.4.15,MIT
+ieee754,1.1.8,New BSD
+ignore,3.2.2,MIT
+imurmurhash,0.1.4,MIT
+indexof,0.0.1,unknown
+inflight,1.0.6,ISC
+influxdb,0.2.3,MIT
+inherits,2.0.3,ISC
+ini,1.3.4,ISC
+inquirer,0.12.0,MIT
+interpret,1.0.1,MIT
+invariant,2.2.2,New BSD
+invert-kv,1.0.0,MIT
+ipaddr.js,1.2.0,MIT
+ipaddress,0.8.3,MIT
+is-absolute,0.2.6,MIT
+is-arrayish,0.2.1,MIT
+is-binary-path,1.0.1,MIT
+is-buffer,1.1.4,MIT
+is-builtin-module,1.0.0,MIT
+is-dotfile,1.0.2,MIT
+is-equal-shallow,0.1.3,MIT
+is-extendable,0.1.1,MIT
+is-extglob,1.0.0,MIT
+is-finite,1.0.2,MIT
+is-fullwidth-code-point,1.0.0,MIT
+is-glob,2.0.1,MIT
+is-my-json-valid,2.15.0,MIT
+is-number,2.1.0,MIT
+is-path-cwd,1.0.0,MIT
+is-path-in-cwd,1.0.0,MIT
+is-path-inside,1.0.0,MIT
+is-posix-bracket,0.1.1,MIT
+is-primitive,2.0.0,MIT
+is-property,1.0.2,MIT
+is-relative,0.2.1,MIT
+is-resolvable,1.0.0,MIT
+is-stream,1.1.0,MIT
+is-typedarray,1.0.0,MIT
+is-unc-path,0.1.2,MIT
+is-utf8,0.2.1,MIT
+is-windows,0.2.0,MIT
+isarray,1.0.0,MIT
+isbinaryfile,3.0.2,MIT
+isexe,1.1.2,ISC
+isobject,2.1.0,MIT
+isstream,0.1.2,MIT
+istanbul,0.4.5,New BSD
+istanbul-api,1.1.1,New BSD
+istanbul-lib-coverage,1.0.1,New BSD
+istanbul-lib-hook,1.0.0,New BSD
+istanbul-lib-instrument,1.4.2,New BSD
+istanbul-lib-report,1.0.0-alpha.3,New BSD
+istanbul-lib-source-maps,1.1.0,New BSD
+istanbul-reports,1.0.1,New BSD
+jasmine-core,2.5.2,MIT
+jasmine-jquery,2.1.1,MIT
+jira-ruby,1.1.2,MIT
+jodid25519,1.0.2,MIT
+jquery,2.2.1,MIT
+jquery-atwho-rails,1.3.2,MIT
+jquery-rails,4.1.1,MIT
+jquery-ujs,1.2.1,MIT
+js-cookie,2.1.3,MIT
+js-tokens,3.0.1,MIT
+js-yaml,3.8.1,MIT
+jsbn,0.1.0,BSD
+jsesc,1.3.0,MIT
+json,1.8.6,ruby
+json-jwt,1.7.1,MIT
+json-loader,0.5.4,MIT
+json-schema,0.2.3,"AFLv2.1,BSD"
+json-stable-stringify,1.0.1,MIT
+json-stringify-safe,5.0.1,ISC
+json3,3.3.2,MIT
+json5,0.5.1,MIT
+jsonfile,2.4.0,MIT
+jsonify,0.0.0,Public Domain
+jsonpointer,4.0.1,MIT
+jsprim,1.3.1,MIT
+jwt,1.5.6,MIT
+kaminari,0.17.0,MIT
+karma,1.4.1,MIT
+karma-coverage-istanbul-reporter,0.2.0,MIT
+karma-jasmine,1.1.0,MIT
+karma-mocha-reporter,2.2.2,MIT
+karma-phantomjs-launcher,1.0.2,MIT
+karma-sourcemap-loader,0.3.7,MIT
+karma-webpack,2.0.2,MIT
+kew,0.7.0,Apache 2.0
+kgio,2.10.0,LGPL-2.1+
+kind-of,3.1.0,MIT
+klaw,1.3.1,MIT
+kubeclient,2.2.0,MIT
+launchy,2.4.3,ISC
+lazy-cache,1.0.4,MIT
+lcid,1.0.0,MIT
+levn,0.3.0,MIT
+licensee,8.7.0,MIT
+little-plugger,1.1.4,MIT
+load-json-file,1.1.0,MIT
+loader-runner,2.3.0,MIT
+loader-utils,0.2.16,MIT
+locate-path,2.0.0,MIT
+lodash,4.17.4,MIT
+lodash._baseget,3.7.2,MIT
+lodash._topath,3.8.1,MIT
+lodash.camelcase,4.1.1,MIT
+lodash.capitalize,4.2.1,MIT
+lodash.cond,4.5.2,MIT
+lodash.deburr,4.1.0,MIT
+lodash.get,3.7.0,MIT
+lodash.isarray,3.0.4,MIT
+lodash.kebabcase,4.0.1,MIT
+lodash.snakecase,4.0.1,MIT
+lodash.words,4.2.0,MIT
+log4js,0.6.38,Apache 2.0
+logging,2.1.0,MIT
+longest,1.0.1,MIT
+loofah,2.0.3,MIT
+loose-envify,1.3.1,MIT
+lru-cache,2.2.4,MIT
+mail,2.6.4,MIT
+mail_room,0.9.1,MIT
+media-typer,0.3.0,MIT
+memoist,0.15.0,MIT
+memory-fs,0.4.1,MIT
+merge-descriptors,1.0.1,MIT
+method_source,0.8.2,MIT
+methods,1.1.2,MIT
+micromatch,2.3.11,MIT
+miller-rabin,4.0.0,MIT
+mime,1.3.4,MIT
+mime-db,1.26.0,MIT
+mime-types,2.99.3,"MIT,Artistic-2.0,GPL-2.0"
+mimemagic,0.3.0,MIT
+mini_portile2,2.1.0,MIT
+minimalistic-assert,1.0.0,ISC
+minimatch,3.0.3,ISC
+minimist,0.0.8,MIT
+mkdirp,0.5.1,MIT
+moment,2.17.1,MIT
+mousetrap,1.4.6,Apache 2.0
+mousetrap-rails,1.4.6,"MIT,Apache"
+ms,0.7.2,MIT
+multi_json,1.12.1,MIT
+multi_xml,0.6.0,MIT
+multipart-post,2.0.0,MIT
+mustermann,0.4.0,MIT
+mustermann-grape,0.4.0,MIT
+mute-stream,0.0.5,ISC
+nan,2.5.1,MIT
+natural-compare,1.4.0,MIT
+negotiator,0.6.1,MIT
+net-ldap,0.12.1,MIT
+net-ssh,3.0.1,MIT
+netrc,0.11.0,MIT
+node-libs-browser,2.0.0,MIT
+node-pre-gyp,0.6.33,New BSD
+node-zopfli,2.0.2,MIT
+nokogiri,1.6.8.1,MIT
+nopt,3.0.6,ISC
+normalize-package-data,2.3.5,Simplified BSD
+normalize-path,2.0.1,MIT
+npmlog,4.0.2,ISC
+number-is-nan,1.0.1,MIT
+numerizer,0.1.1,MIT
+oauth,0.5.1,MIT
+oauth-sign,0.8.2,Apache 2.0
+oauth2,1.2.0,MIT
+object-assign,4.1.1,MIT
+object-component,0.0.3,unknown
+object.omit,2.0.1,MIT
+obuf,1.1.1,MIT
+octokit,4.6.2,MIT
+oj,2.17.4,MIT
+omniauth,1.4.2,MIT
+omniauth-auth0,1.4.1,MIT
+omniauth-authentiq,0.3.0,MIT
+omniauth-azure-oauth2,0.0.6,MIT
+omniauth-cas3,1.1.3,MIT
+omniauth-facebook,4.0.0,MIT
+omniauth-github,1.1.2,MIT
+omniauth-gitlab,1.0.2,MIT
+omniauth-google-oauth2,0.4.1,MIT
+omniauth-kerberos,0.3.0,MIT
+omniauth-multipassword,0.4.2,MIT
+omniauth-oauth,1.1.0,MIT
+omniauth-oauth2,1.3.1,MIT
+omniauth-oauth2-generic,0.2.2,MIT
+omniauth-saml,1.7.0,MIT
+omniauth-shibboleth,1.2.1,MIT
+omniauth-twitter,1.2.1,MIT
+omniauth_crowd,2.2.3,MIT
+on-finished,2.3.0,MIT
+on-headers,1.0.1,MIT
+once,1.3.3,ISC
+onetime,1.1.0,MIT
+opener,1.4.3,(WTFPL OR MIT)
+opn,4.0.2,MIT
+optimist,0.6.1,MIT/X11
+optionator,0.8.2,MIT
+options,0.0.6,MIT
+org-ruby,0.9.12,MIT
+original,1.0.0,MIT
+orm_adapter,0.5.0,MIT
+os,0.9.6,MIT
+os-browserify,0.2.1,MIT
+os-homedir,1.0.2,MIT
+os-locale,1.4.0,MIT
+os-tmpdir,1.0.2,MIT
+p-limit,1.1.0,MIT
+p-locate,2.0.0,MIT
+pako,0.2.9,MIT
+paranoia,2.2.0,MIT
+parse-asn1,5.0.0,ISC
+parse-glob,3.0.4,MIT
+parse-json,2.2.0,MIT
+parsejson,0.0.3,MIT
+parseqs,0.0.5,MIT
+parseuri,0.0.5,MIT
+parseurl,1.3.1,MIT
+path-browserify,0.0.0,MIT
+path-exists,3.0.0,MIT
+path-is-absolute,1.0.1,MIT
+path-is-inside,1.0.2,(WTFPL OR MIT)
+path-parse,1.0.5,MIT
+path-to-regexp,0.1.7,MIT
+path-type,1.1.0,MIT
+pbkdf2,3.0.9,MIT
+pend,1.2.0,MIT
+pg,0.18.4,"BSD,ruby,GPL"
+phantomjs-prebuilt,2.1.14,Apache 2.0
+pify,2.3.0,MIT
+pikaday,1.5.1,"BSD,MIT"
+pinkie,2.0.4,MIT
+pinkie-promise,2.0.1,MIT
+pkg-dir,1.0.0,MIT
+pkg-up,1.0.0,MIT
+pluralize,1.2.1,MIT
+portfinder,1.0.13,MIT
+posix-spawn,0.3.11,"MIT,LGPL"
+prelude-ls,1.1.2,MIT
+premailer,1.8.6,New BSD
+premailer-rails,1.9.2,MIT
+preserve,0.2.0,MIT
+private,0.1.7,MIT
+process,0.11.9,MIT
+process-nextick-args,1.0.7,MIT
+progress,1.1.8,MIT
+proxy-addr,1.1.3,MIT
+prr,0.0.0,MIT
+public-encrypt,4.0.0,MIT
+punycode,1.4.1,MIT
+pyu-ruby-sasl,0.0.3.3,MIT
+qjobs,1.1.5,MIT
+qs,6.2.0,New BSD
+querystring,0.2.0,MIT
+querystring-es3,0.2.1,MIT
+querystringify,0.0.4,MIT
+rack,1.6.5,MIT
+rack-accept,0.4.5,MIT
+rack-attack,4.4.1,MIT
+rack-cors,0.4.0,MIT
+rack-oauth2,1.2.3,MIT
+rack-protection,1.5.3,MIT
+rack-proxy,0.6.0,MIT
+rack-test,0.6.3,MIT
+rails,4.2.8,MIT
+rails-deprecated_sanitizer,1.0.3,MIT
+rails-dom-testing,1.0.8,MIT
+rails-html-sanitizer,1.0.3,MIT
+railties,4.2.8,MIT
+rainbow,2.1.0,MIT
+raindrops,0.17.0,LGPL-2.1+
+rake,10.5.0,MIT
+randomatic,1.1.6,MIT
+randombytes,2.0.3,MIT
+range-parser,1.2.0,MIT
+raphael,2.2.7,MIT
+raw-body,2.2.0,MIT
+raw-loader,0.5.1,MIT
+rc,1.1.6,(BSD-2-Clause OR MIT OR Apache-2.0)
+rdoc,4.2.2,ruby
+read-pkg,1.1.0,MIT
+read-pkg-up,1.0.1,MIT
+readable-stream,2.1.5,MIT
+readdirp,2.1.0,MIT
+readline2,1.0.1,MIT
+recaptcha,3.0.0,MIT
+rechoir,0.6.2,MIT
+recursive-open-struct,1.0.0,MIT
+redcarpet,3.4.0,MIT
+redis,3.2.2,MIT
+redis-actionpack,5.0.1,MIT
+redis-activesupport,5.0.1,MIT
+redis-namespace,1.5.2,MIT
+redis-rack,1.6.0,MIT
+redis-rails,5.0.1,MIT
+redis-store,1.2.0,MIT
+regenerate,1.3.2,MIT
+regenerator-runtime,0.10.1,MIT
+regenerator-transform,0.9.8,BSD
+regex-cache,0.4.3,MIT
+regexpu-core,2.0.0,MIT
+regjsgen,0.2.0,MIT
+regjsparser,0.1.5,BSD
+repeat-element,1.1.2,MIT
+repeat-string,1.6.1,MIT
+repeating,2.0.1,MIT
+request,2.79.0,Apache 2.0
+request-progress,2.0.1,MIT
+request_store,1.3.1,MIT
+require-directory,2.1.1,MIT
+require-main-filename,1.0.1,ISC
+require-uncached,1.0.3,MIT
+requires-port,1.0.0,MIT
+resolve,1.2.0,MIT
+resolve-from,1.0.1,MIT
+responders,2.3.0,MIT
+rest-client,2.0.0,MIT
+restore-cursor,1.0.1,MIT
+retriable,1.4.1,MIT
+right-align,0.1.3,MIT
+rimraf,2.5.4,ISC
+rinku,2.0.0,ISC
+ripemd160,1.0.1,New BSD
+rotp,2.1.2,MIT
+rouge,2.0.7,MIT
+rqrcode,0.7.0,MIT
+rqrcode-rails3,0.1.7,MIT
+ruby-fogbugz,0.2.1,MIT
+ruby-prof,0.16.2,Simplified BSD
+ruby-saml,1.4.1,MIT
+rubyntlm,0.5.2,MIT
+rubypants,0.2.0,BSD
+rufus-scheduler,3.1.10,MIT
+rugged,0.24.0,MIT
+run-async,0.1.0,MIT
+rx-lite,3.1.2,Apache 2.0
+safe-buffer,5.0.1,MIT
+safe_yaml,1.0.4,MIT
+sanitize,2.1.0,MIT
+sass,3.4.22,MIT
+sass-rails,5.0.6,MIT
+sawyer,0.8.1,MIT
+securecompare,1.0.0,MIT
+seed-fu,2.3.6,MIT
+select-hose,2.0.0,MIT
+select2,3.5.2-browserify,unknown
+select2-rails,3.5.9.3,MIT
+semver,5.3.0,ISC
+send,0.14.2,MIT
+sentry-raven,2.0.2,Apache 2.0
+serve-index,1.8.0,MIT
+serve-static,1.11.2,MIT
+set-blocking,2.0.0,ISC
+set-immediate-shim,1.0.1,MIT
+setimmediate,1.0.5,MIT
+setprototypeof,1.0.2,ISC
+settingslogic,2.0.9,MIT
+sha.js,2.4.8,MIT
+shelljs,0.7.6,New BSD
+sidekiq,4.2.7,LGPL
+sidekiq-cron,0.4.4,MIT
+sidekiq-limit_fetch,3.4.0,MIT
+signal-exit,3.0.2,ISC
+signet,0.7.3,Apache 2.0
+slack-notifier,1.5.1,MIT
+slash,1.0.0,MIT
+slice-ansi,0.0.4,MIT
+sntp,1.0.9,BSD
+socket.io,1.7.2,MIT
+socket.io-adapter,0.5.0,MIT
+socket.io-client,1.7.2,MIT
+socket.io-parser,2.3.1,MIT
+sockjs,0.3.18,MIT
+sockjs-client,1.1.1,MIT
+source-list-map,0.1.8,MIT
+source-map,0.5.6,New BSD
+source-map-support,0.4.11,MIT
+spdx-correct,1.0.2,Apache 2.0
+spdx-expression-parse,1.0.4,(MIT AND CC-BY-3.0)
+spdx-license-ids,1.2.2,Unlicense
+spdy,3.4.4,MIT
+spdy-transport,2.0.18,MIT
+sprintf-js,1.0.3,New BSD
+sprockets,3.7.1,MIT
+sprockets-rails,3.2.0,MIT
+sshpk,1.10.2,MIT
+state_machines,0.4.0,MIT
+state_machines-activemodel,0.4.0,MIT
+state_machines-activerecord,0.4.0,MIT
+stats-webpack-plugin,0.4.3,MIT
+statuses,1.3.1,MIT
+stream-browserify,2.0.1,MIT
+stream-http,2.6.3,MIT
+string-width,1.0.2,MIT
+string.fromcodepoint,0.2.1,MIT
+string.prototype.codepointat,0.2.0,MIT
+string_decoder,0.10.31,MIT
+stringex,2.5.2,MIT
+stringstream,0.0.5,MIT
+strip-ansi,3.0.1,MIT
+strip-bom,2.0.0,MIT
+strip-json-comments,1.0.4,MIT
+supports-color,0.2.0,MIT
+sys-filesystem,1.1.6,Artistic 2.0
+table,3.8.3,New BSD
+tapable,0.2.6,MIT
+tar,2.2.1,ISC
+tar-pack,3.3.0,Simplified BSD
+temple,0.7.7,MIT
+test-exclude,4.0.0,ISC
+text-table,0.2.0,MIT
+thor,0.19.4,MIT
+thread_safe,0.3.6,Apache 2.0
+throttleit,1.0.0,MIT
+through,2.3.8,MIT
+tilt,2.0.6,MIT
+timeago.js,2.0.5,MIT
+timers-browserify,2.0.2,MIT
+timfel-krb5-auth,0.8.3,LGPL
+tmp,0.0.28,MIT
+to-array,0.1.4,MIT
+to-arraybuffer,1.0.1,MIT
+to-fast-properties,1.0.2,MIT
+tool,0.2.3,MIT
+tough-cookie,2.3.2,New BSD
+trim-right,1.0.1,MIT
+truncato,0.7.8,MIT
+tryit,1.0.3,MIT
+tty-browserify,0.0.0,MIT
+tunnel-agent,0.4.3,Apache 2.0
+tweetnacl,0.14.5,Unlicense
+type-check,0.3.2,MIT
+type-is,1.6.14,MIT
+typedarray,0.0.6,MIT
+tzinfo,1.2.2,MIT
+u2f,0.2.1,MIT
+uglifier,2.7.2,MIT
+uglify-js,2.7.5,Simplified BSD
+uglify-to-browserify,1.0.2,MIT
+uid-number,0.0.6,ISC
+ultron,1.0.2,MIT
+unc-path-regex,0.1.2,MIT
+underscore,1.8.3,MIT
+underscore-rails,1.8.3,MIT
+unf,0.1.4,BSD
+unf_ext,0.0.7.2,MIT
+unicorn,5.1.0,ruby
+unicorn-worker-killer,0.4.4,ruby
+unpipe,1.0.0,MIT
+url,0.11.0,MIT
+url-parse,1.0.5,MIT
+url_safe_base64,0.2.2,MIT
+user-home,2.0.0,MIT
+useragent,2.1.12,MIT
+util,0.10.3,MIT
+util-deprecate,1.0.2,MIT
+utils-merge,1.0.0,MIT
+uuid,3.0.1,MIT
+validate-npm-package-license,3.0.1,Apache 2.0
+validates_hostname,1.0.6,MIT
+vary,1.1.0,MIT
+verror,1.3.6,MIT
+version_sorter,2.1.0,MIT
+virtus,1.0.5,MIT
+vm-browserify,0.0.4,MIT
+vmstat,2.3.0,MIT
+void-elements,2.0.1,MIT
+vue,2.1.10,MIT
+vue-resource,0.9.3,MIT
+warden,1.2.6,MIT
+watchpack,1.2.1,MIT
+wbuf,1.7.2,MIT
+webpack,2.2.1,MIT
+webpack-bundle-analyzer,2.3.0,MIT
+webpack-dev-middleware,1.10.0,MIT
+webpack-dev-server,2.3.0,MIT
+webpack-rails,0.9.9,MIT
+webpack-sources,0.1.4,MIT
+websocket-driver,0.6.5,MIT
+websocket-extensions,0.1.1,MIT
+which,1.2.12,ISC
+which-module,1.0.0,ISC
+wide-align,1.1.0,ISC
+wikicloth,0.8.1,MIT
+window-size,0.1.0,MIT
+wordwrap,0.0.2,MIT/X11
+wrap-ansi,2.1.0,MIT
+wrappy,1.0.2,ISC
+write,0.2.1,MIT
+ws,1.1.1,MIT
+wtf-8,1.0.0,MIT
+xmlhttprequest-ssl,1.5.3,MIT
+xtend,4.0.1,MIT
+y18n,3.2.1,ISC
+yargs,3.10.0,MIT
+yargs-parser,4.2.1,ISC
+yauzl,2.4.1,MIT
+yeast,0.1.2,MIT
diff --git a/yarn.lock b/yarn.lock
index 55b8f1566ee..391b1c7eccf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1213,7 +1213,7 @@ cookie@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
-core-js@^2.2.0, core-js@^2.4.0:
+core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
@@ -1553,7 +1553,7 @@ es6-map@^0.1.3:
es6-symbol "~3.1.0"
event-emitter "~0.3.4"
-es6-promise@^4.0.5, es6-promise@~4.0.3:
+es6-promise@~4.0.3:
version "4.0.5"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42"
@@ -4123,14 +4123,6 @@ string-width@^2.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^3.0.0"
-string.fromcodepoint@^0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz#8d978333c0bc92538f50f383e4888f3e5619d653"
-
-string.prototype.codepointat@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78"
-
string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"