From df469864b1ab1e0bfaa1e843d3d0a84042604646 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Tue, 7 Feb 2017 18:02:49 +0000 Subject: Updated the filename regex --- .eslintrc | 2 +- .gitattributes | 1 - .gitlab-ci.yml | 2 +- app/assets/javascripts/abuse_reports.js | 40 ++ app/assets/javascripts/abuse_reports.js.es6 | 40 -- app/assets/javascripts/activities.js | 37 ++ app/assets/javascripts/activities.js.es6 | 37 -- app/assets/javascripts/application.js | 18 +- app/assets/javascripts/blob/blob_ci_yaml.js | 42 ++ app/assets/javascripts/blob/blob_ci_yaml.js.es6 | 42 -- .../javascripts/blob/blob_dockerfile_selector.js | 19 + .../blob/blob_dockerfile_selector.js.es6 | 19 - .../javascripts/blob/blob_dockerfile_selectors.js | 27 ++ .../blob/blob_dockerfile_selectors.js.es6 | 27 -- .../javascripts/blob/blob_license_selectors.js | 23 + .../javascripts/blob/blob_license_selectors.js.es6 | 23 - app/assets/javascripts/blob/template_selector.js | 101 ++++ .../javascripts/blob/template_selector.js.es6 | 101 ---- app/assets/javascripts/boards/boards_bundle.js | 112 +++++ app/assets/javascripts/boards/boards_bundle.js.es6 | 112 ----- app/assets/javascripts/boards/components/board.js | 105 ++++ .../javascripts/boards/components/board.js.es6 | 105 ---- .../boards/components/board_blank_state.js | 53 ++ .../boards/components/board_blank_state.js.es6 | 53 -- .../javascripts/boards/components/board_card.js | 61 +++ .../boards/components/board_card.js.es6 | 61 --- .../javascripts/boards/components/board_delete.js | 22 + .../boards/components/board_delete.js.es6 | 22 - .../javascripts/boards/components/board_list.js | 120 +++++ .../boards/components/board_list.js.es6 | 120 ----- .../boards/components/board_new_issue.js | 64 +++ .../boards/components/board_new_issue.js.es6 | 64 --- .../javascripts/boards/components/board_sidebar.js | 72 +++ .../boards/components/board_sidebar.js.es6 | 72 --- .../boards/components/issue_card_inner.js | 111 +++++ .../boards/components/issue_card_inner.js.es6 | 111 ----- .../boards/components/modal/empty_state.js | 70 +++ .../boards/components/modal/empty_state.js.es6 | 70 --- .../javascripts/boards/components/modal/filters.js | 49 ++ .../boards/components/modal/filters.js.es6 | 49 -- .../boards/components/modal/filters/label.js | 54 +++ .../boards/components/modal/filters/label.js.es6 | 54 --- .../boards/components/modal/filters/milestone.js | 55 +++ .../components/modal/filters/milestone.js.es6 | 55 --- .../boards/components/modal/filters/user.js | 96 ++++ .../boards/components/modal/filters/user.js.es6 | 96 ---- .../javascripts/boards/components/modal/footer.js | 83 ++++ .../boards/components/modal/footer.js.es6 | 83 ---- .../javascripts/boards/components/modal/header.js | 90 ++++ .../boards/components/modal/header.js.es6 | 90 ---- .../javascripts/boards/components/modal/index.js | 163 +++++++ .../boards/components/modal/index.js.es6 | 163 ------- .../javascripts/boards/components/modal/list.js | 159 ++++++ .../boards/components/modal/list.js.es6 | 159 ------ .../boards/components/modal/lists_dropdown.js | 56 +++ .../boards/components/modal/lists_dropdown.js.es6 | 56 --- .../javascripts/boards/components/modal/tabs.js | 47 ++ .../boards/components/modal/tabs.js.es6 | 47 -- .../boards/components/new_list_dropdown.js | 76 +++ .../boards/components/new_list_dropdown.js.es6 | 76 --- .../boards/components/sidebar/remove_issue.js | 59 +++ .../boards/components/sidebar/remove_issue.js.es6 | 59 --- .../javascripts/boards/filters/due_date_filters.js | 6 + .../boards/filters/due_date_filters.js.es6 | 6 - .../javascripts/boards/mixins/modal_mixins.js | 14 + .../javascripts/boards/mixins/modal_mixins.js.es6 | 14 - .../boards/mixins/sortable_default_options.js | 39 ++ .../boards/mixins/sortable_default_options.js.es6 | 39 -- app/assets/javascripts/boards/models/issue.js | 78 +++ app/assets/javascripts/boards/models/issue.js.es6 | 78 --- app/assets/javascripts/boards/models/label.js | 14 + app/assets/javascripts/boards/models/label.js.es6 | 14 - app/assets/javascripts/boards/models/list.js | 152 ++++++ app/assets/javascripts/boards/models/list.js.es6 | 152 ------ app/assets/javascripts/boards/models/milestone.js | 10 + .../javascripts/boards/models/milestone.js.es6 | 10 - app/assets/javascripts/boards/models/user.js | 12 + app/assets/javascripts/boards/models/user.js.es6 | 12 - .../javascripts/boards/services/board_service.js | 95 ++++ .../boards/services/board_service.js.es6 | 95 ---- .../javascripts/boards/stores/boards_store.js | 120 +++++ .../javascripts/boards/stores/boards_store.js.es6 | 120 ----- .../javascripts/boards/stores/modal_store.js | 107 ++++ .../javascripts/boards/stores/modal_store.js.es6 | 107 ---- app/assets/javascripts/build_variables.js | 8 + app/assets/javascripts/build_variables.js.es6 | 8 - app/assets/javascripts/ci_lint_editor.js | 18 + app/assets/javascripts/ci_lint_editor.js.es6 | 18 - .../commit/pipelines/pipelines_bundle.js | 26 + .../commit/pipelines/pipelines_bundle.js.es6 | 26 - .../commit/pipelines/pipelines_service.js | 29 ++ .../commit/pipelines/pipelines_service.js.es6 | 29 -- .../commit/pipelines/pipelines_store.js | 50 ++ .../commit/pipelines/pipelines_store.js.es6 | 50 -- .../commit/pipelines/pipelines_table.js | 107 ++++ .../commit/pipelines/pipelines_table.js.es6 | 107 ---- app/assets/javascripts/compare_autocomplete.js | 69 +++ app/assets/javascripts/compare_autocomplete.js.es6 | 69 --- app/assets/javascripts/copy_as_gfm.js | 355 ++++++++++++++ app/assets/javascripts/copy_as_gfm.js.es6 | 355 -------------- app/assets/javascripts/create_label.js | 132 +++++ app/assets/javascripts/create_label.js.es6 | 132 ----- .../components/stage_code_component.js | 45 ++ .../components/stage_code_component.js.es6 | 45 -- .../components/stage_issue_component.js | 47 ++ .../components/stage_issue_component.js.es6 | 47 -- .../components/stage_plan_component.js | 44 ++ .../components/stage_plan_component.js.es6 | 44 -- .../components/stage_production_component.js | 47 ++ .../components/stage_production_component.js.es6 | 47 -- .../components/stage_review_component.js | 57 +++ .../components/stage_review_component.js.es6 | 57 --- .../components/stage_staging_component.js | 44 ++ .../components/stage_staging_component.js.es6 | 44 -- .../components/stage_test_component.js | 44 ++ .../components/stage_test_component.js.es6 | 44 -- .../components/total_time_component.js | 25 + .../components/total_time_component.js.es6 | 25 - .../cycle_analytics/cycle_analytics_bundle.js | 128 +++++ .../cycle_analytics/cycle_analytics_bundle.js.es6 | 128 ----- .../cycle_analytics/cycle_analytics_service.js | 41 ++ .../cycle_analytics/cycle_analytics_service.js.es6 | 41 -- .../cycle_analytics/cycle_analytics_store.js | 94 ++++ .../cycle_analytics/cycle_analytics_store.js.es6 | 94 ---- .../javascripts/cycle_analytics/svg/icon_branch.js | 7 + .../cycle_analytics/svg/icon_branch.js.es6 | 7 - .../cycle_analytics/svg/icon_build_status.js | 7 + .../cycle_analytics/svg/icon_build_status.js.es6 | 7 - .../javascripts/cycle_analytics/svg/icon_commit.js | 7 + .../cycle_analytics/svg/icon_commit.js.es6 | 7 - app/assets/javascripts/diff.js | 126 +++++ app/assets/javascripts/diff.js.es6 | 126 ----- .../diff_notes/components/comment_resolve_btn.js | 59 +++ .../components/comment_resolve_btn.js.es6 | 59 --- .../diff_notes/components/jump_to_discussion.js | 193 ++++++++ .../components/jump_to_discussion.js.es6 | 193 -------- .../diff_notes/components/resolve_btn.js | 113 +++++ .../diff_notes/components/resolve_btn.js.es6 | 113 ----- .../diff_notes/components/resolve_count.js | 26 + .../diff_notes/components/resolve_count.js.es6 | 26 - .../components/resolve_discussion_btn.js | 63 +++ .../components/resolve_discussion_btn.js.es6 | 63 --- .../javascripts/diff_notes/diff_notes_bundle.js | 49 ++ .../diff_notes/diff_notes_bundle.js.es6 | 49 -- .../javascripts/diff_notes/mixins/discussion.js | 37 ++ .../diff_notes/mixins/discussion.js.es6 | 37 -- .../javascripts/diff_notes/models/discussion.js | 96 ++++ .../diff_notes/models/discussion.js.es6 | 96 ---- app/assets/javascripts/diff_notes/models/note.js | 13 + .../javascripts/diff_notes/models/note.js.es6 | 13 - .../javascripts/diff_notes/services/resolve.js | 93 ++++ .../javascripts/diff_notes/services/resolve.js.es6 | 93 ---- .../javascripts/diff_notes/stores/comments.js | 57 +++ .../javascripts/diff_notes/stores/comments.js.es6 | 57 --- app/assets/javascripts/dispatcher.js | 379 +++++++++++++++ app/assets/javascripts/dispatcher.js.es6 | 379 --------------- app/assets/javascripts/due_date_select.js | 181 +++++++ app/assets/javascripts/due_date_select.js.es6 | 181 ------- .../environments/components/environment.js | 223 +++++++++ .../environments/components/environment.js.es6 | 223 --------- .../environments/components/environment_actions.js | 50 ++ .../components/environment_actions.js.es6 | 50 -- .../components/environment_external_url.js | 23 + .../components/environment_external_url.js.es6 | 23 - .../environments/components/environment_item.js | 538 +++++++++++++++++++++ .../components/environment_item.js.es6 | 538 --------------------- .../components/environment_rollback.js | 33 ++ .../components/environment_rollback.js.es6 | 33 -- .../environments/components/environment_stop.js | 27 ++ .../components/environment_stop.js.es6 | 27 -- .../components/environment_terminal_button.js | 28 ++ .../components/environment_terminal_button.js.es6 | 28 -- .../environments/environments_bundle.js | 22 + .../environments/environments_bundle.js.es6 | 22 - .../environments/services/environments_service.js | 25 + .../services/environments_service.js.es6 | 25 - .../environments/stores/environments_store.js | 190 ++++++++ .../environments/stores/environments_store.js.es6 | 190 -------- app/assets/javascripts/extensions/array.js | 27 ++ app/assets/javascripts/extensions/array.js.es6 | 27 -- app/assets/javascripts/extensions/custom_event.js | 12 + .../javascripts/extensions/custom_event.js.es6 | 12 - app/assets/javascripts/extensions/element.js | 20 + app/assets/javascripts/extensions/element.js.es6 | 20 - app/assets/javascripts/extensions/object.js | 26 + app/assets/javascripts/extensions/object.js.es6 | 26 - .../javascripts/filtered_search/dropdown_hint.js | 69 +++ .../filtered_search/dropdown_hint.js.es6 | 69 --- .../filtered_search/dropdown_non_user.js | 44 ++ .../filtered_search/dropdown_non_user.js.es6 | 44 -- .../javascripts/filtered_search/dropdown_user.js | 60 +++ .../filtered_search/dropdown_user.js.es6 | 60 --- .../javascripts/filtered_search/dropdown_utils.js | 126 +++++ .../filtered_search/dropdown_utils.js.es6 | 126 ----- .../filtered_search/filtered_search_bundle.js | 2 +- .../filtered_search/filtered_search_dropdown.js | 112 +++++ .../filtered_search_dropdown.js.es6 | 112 ----- .../filtered_search_dropdown_manager.js | 207 ++++++++ .../filtered_search_dropdown_manager.js.es6 | 207 -------- .../filtered_search/filtered_search_manager.js | 230 +++++++++ .../filtered_search/filtered_search_manager.js.es6 | 230 --------- .../filtered_search/filtered_search_token_keys.js | 96 ++++ .../filtered_search_token_keys.js.es6 | 96 ---- .../filtered_search/filtered_search_tokenizer.js | 45 ++ .../filtered_search_tokenizer.js.es6 | 45 -- app/assets/javascripts/gfm_auto_complete.js | 380 +++++++++++++++ app/assets/javascripts/gfm_auto_complete.js.es6 | 380 --------------- app/assets/javascripts/gl_field_error.js | 164 +++++++ app/assets/javascripts/gl_field_error.js.es6 | 164 ------- app/assets/javascripts/gl_field_errors.js | 48 ++ app/assets/javascripts/gl_field_errors.js.es6 | 48 -- app/assets/javascripts/gl_form.js | 92 ++++ app/assets/javascripts/gl_form.js.es6 | 92 ---- app/assets/javascripts/graphs/graphs_bundle.js | 2 +- app/assets/javascripts/group_label_subscription.js | 53 ++ .../javascripts/group_label_subscription.js.es6 | 53 -- app/assets/javascripts/issuable.js | 188 +++++++ app/assets/javascripts/issuable.js.es6 | 188 ------- app/assets/javascripts/issuable/issuable_bundle.js | 1 + .../javascripts/issuable/issuable_bundle.js.es6 | 1 - .../time_tracking/components/collapsed_state.js | 41 ++ .../components/collapsed_state.js.es6 | 41 -- .../time_tracking/components/comparison_pane.js | 69 +++ .../components/comparison_pane.js.es6 | 69 --- .../time_tracking/components/estimate_only_pane.js | 13 + .../components/estimate_only_pane.js.es6 | 13 - .../time_tracking/components/help_state.js | 24 + .../time_tracking/components/help_state.js.es6 | 24 - .../time_tracking/components/no_tracking_pane.js | 11 + .../components/no_tracking_pane.js.es6 | 11 - .../time_tracking/components/spent_only_pane.js | 13 + .../components/spent_only_pane.js.es6 | 13 - .../time_tracking/components/time_tracker.js | 119 +++++ .../time_tracking/components/time_tracker.js.es6 | 119 ----- .../issuable/time_tracking/time_tracking_bundle.js | 62 +++ .../time_tracking/time_tracking_bundle.js.es6 | 62 --- app/assets/javascripts/issues_bulk_assignment.js | 163 +++++++ .../javascripts/issues_bulk_assignment.js.es6 | 163 ------- app/assets/javascripts/label_manager.js | 112 +++++ app/assets/javascripts/label_manager.js.es6 | 112 ----- .../javascripts/lib/utils/bootstrap_linked_tabs.js | 112 +++++ .../lib/utils/bootstrap_linked_tabs.js.es6 | 112 ----- app/assets/javascripts/lib/utils/common_utils.js | 248 ++++++++++ .../javascripts/lib/utils/common_utils.js.es6 | 248 ---------- .../javascripts/lib/utils/datetime_utility.js | 126 +++++ .../javascripts/lib/utils/datetime_utility.js.es6 | 126 ----- app/assets/javascripts/lib/utils/pretty_time.js | 65 +++ .../javascripts/lib/utils/pretty_time.js.es6 | 65 --- app/assets/javascripts/lib/utils/url_utility.js | 86 ++++ .../javascripts/lib/utils/url_utility.js.es6 | 86 ---- app/assets/javascripts/lib/vue_resource.js | 2 + app/assets/javascripts/lib/vue_resource.js.es6 | 2 - app/assets/javascripts/member_expiration_date.js | 36 ++ .../javascripts/member_expiration_date.js.es6 | 36 -- app/assets/javascripts/members.js | 81 ++++ app/assets/javascripts/members.js.es6 | 81 ---- .../merge_conflicts/components/diff_file_editor.js | 96 ++++ .../components/diff_file_editor.js.es6 | 96 ---- .../components/inline_conflict_lines.js | 13 + .../components/inline_conflict_lines.js.es6 | 13 - .../components/parallel_conflict_lines.js | 28 ++ .../components/parallel_conflict_lines.js.es6 | 28 -- .../merge_conflicts/merge_conflict_service.js | 31 ++ .../merge_conflicts/merge_conflict_service.js.es6 | 31 -- .../merge_conflicts/merge_conflict_store.js | 433 +++++++++++++++++ .../merge_conflicts/merge_conflict_store.js.es6 | 433 ----------------- .../merge_conflicts/merge_conflicts_bundle.js | 92 ++++ .../merge_conflicts/merge_conflicts_bundle.js.es6 | 92 ---- .../mixins/line_conflict_actions.js | 13 + .../mixins/line_conflict_actions.js.es6 | 13 - .../merge_conflicts/mixins/line_conflict_utils.js | 19 + .../mixins/line_conflict_utils.js.es6 | 19 - app/assets/javascripts/merge_request_tabs.js | 342 +++++++++++++ app/assets/javascripts/merge_request_tabs.js.es6 | 342 ------------- app/assets/javascripts/merge_request_widget.js | 274 +++++++++++ app/assets/javascripts/merge_request_widget.js.es6 | 274 ----------- .../javascripts/merge_request_widget/ci_bundle.js | 53 ++ .../merge_request_widget/ci_bundle.js.es6 | 53 -- .../javascripts/mini_pipeline_graph_dropdown.js | 97 ++++ .../mini_pipeline_graph_dropdown.js.es6 | 97 ---- app/assets/javascripts/network/network_bundle.js | 2 +- app/assets/javascripts/pager.js | 73 +++ app/assets/javascripts/pager.js.es6 | 73 --- app/assets/javascripts/pipelines.js | 38 ++ app/assets/javascripts/pipelines.js.es6 | 38 -- app/assets/javascripts/profile/gl_crop.js | 171 +++++++ app/assets/javascripts/profile/gl_crop.js.es6 | 171 ------- app/assets/javascripts/profile/profile.js | 98 ++++ app/assets/javascripts/profile/profile.js.es6 | 98 ---- app/assets/javascripts/profile/profile_bundle.js | 2 +- .../javascripts/project_label_subscription.js | 53 ++ .../javascripts/project_label_subscription.js.es6 | 53 -- app/assets/javascripts/project_variables.js | 43 ++ app/assets/javascripts/project_variables.js.es6 | 43 -- .../protected_branch_access_dropdown.js | 29 ++ .../protected_branch_access_dropdown.js.es6 | 29 -- .../protected_branches/protected_branch_create.js | 55 +++ .../protected_branch_create.js.es6 | 55 --- .../protected_branch_dropdown.js | 80 +++ .../protected_branch_dropdown.js.es6 | 80 --- .../protected_branches/protected_branch_edit.js | 66 +++ .../protected_branch_edit.js.es6 | 66 --- .../protected_branch_edit_list.js | 18 + .../protected_branch_edit_list.js.es6 | 18 - .../protected_branches_bundle.js | 2 +- app/assets/javascripts/search_autocomplete.js | 432 +++++++++++++++++ app/assets/javascripts/search_autocomplete.js.es6 | 432 ----------------- app/assets/javascripts/shortcuts_blob.js | 29 ++ app/assets/javascripts/shortcuts_blob.js.es6 | 29 -- app/assets/javascripts/sidebar.js | 97 ++++ app/assets/javascripts/sidebar.js.es6 | 97 ---- app/assets/javascripts/signin_tabs_memoizer.js | 49 ++ app/assets/javascripts/signin_tabs_memoizer.js.es6 | 49 -- app/assets/javascripts/smart_interval.js | 158 ++++++ app/assets/javascripts/smart_interval.js.es6 | 158 ------ app/assets/javascripts/snippet/snippet_bundle.js | 2 +- app/assets/javascripts/snippets_list.js | 13 + app/assets/javascripts/snippets_list.js.es6 | 13 - app/assets/javascripts/subbable_resource.js | 51 ++ app/assets/javascripts/subbable_resource.js.es6 | 51 -- app/assets/javascripts/subscription.js | 50 ++ app/assets/javascripts/subscription.js.es6 | 50 -- .../templates/issuable_template_selector.js | 60 +++ .../templates/issuable_template_selector.js.es6 | 60 --- .../templates/issuable_template_selectors.js | 31 ++ .../templates/issuable_template_selectors.js.es6 | 31 -- app/assets/javascripts/terminal/terminal.js | 62 +++ app/assets/javascripts/terminal/terminal.js.es6 | 62 --- app/assets/javascripts/terminal/terminal_bundle.js | 7 + .../javascripts/terminal/terminal_bundle.js.es6 | 7 - app/assets/javascripts/todos.js | 164 +++++++ app/assets/javascripts/todos.js.es6 | 164 ------- app/assets/javascripts/u2f/authenticate.js | 118 +++++ app/assets/javascripts/u2f/authenticate.js.es6 | 118 ----- app/assets/javascripts/user.js | 34 ++ app/assets/javascripts/user.js.es6 | 34 -- app/assets/javascripts/user_tabs.js | 158 ++++++ app/assets/javascripts/user_tabs.js.es6 | 158 ------ app/assets/javascripts/username_validator.js | 135 ++++++ app/assets/javascripts/username_validator.js.es6 | 135 ------ app/assets/javascripts/users/users_bundle.js | 2 +- app/assets/javascripts/version_check_image.js | 10 + app/assets/javascripts/version_check_image.js.es6 | 10 - app/assets/javascripts/visibility_select.js | 27 ++ app/assets/javascripts/visibility_select.js.es6 | 27 -- .../javascripts/vue_pipelines_index/index.js | 36 ++ .../javascripts/vue_pipelines_index/index.js.es6 | 36 -- .../vue_pipelines_index/pipeline_actions.js | 104 ++++ .../vue_pipelines_index/pipeline_actions.js.es6 | 104 ---- .../vue_pipelines_index/pipeline_url.js | 63 +++ .../vue_pipelines_index/pipeline_url.js.es6 | 63 --- .../javascripts/vue_pipelines_index/pipelines.js | 73 +++ .../vue_pipelines_index/pipelines.js.es6 | 73 --- .../javascripts/vue_pipelines_index/stage.js | 103 ++++ .../javascripts/vue_pipelines_index/stage.js.es6 | 103 ---- .../javascripts/vue_pipelines_index/status.js | 34 ++ .../javascripts/vue_pipelines_index/status.js.es6 | 34 -- .../javascripts/vue_pipelines_index/store.js | 68 +++ .../javascripts/vue_pipelines_index/store.js.es6 | 68 --- .../javascripts/vue_pipelines_index/time_ago.js | 76 +++ .../vue_pipelines_index/time_ago.js.es6 | 76 --- .../javascripts/vue_realtime_listener/index.js | 18 + .../javascripts/vue_realtime_listener/index.js.es6 | 18 - .../javascripts/vue_shared/components/commit.js | 163 +++++++ .../vue_shared/components/commit.js.es6 | 163 ------- .../vue_shared/components/pipelines_table.js | 61 +++ .../vue_shared/components/pipelines_table.js.es6 | 61 --- .../vue_shared/components/pipelines_table_row.js | 234 +++++++++ .../components/pipelines_table_row.js.es6 | 234 --------- .../vue_shared/components/table_pagination.js | 152 ++++++ .../vue_shared/components/table_pagination.js.es6 | 152 ------ .../vue_shared/vue_resource_interceptor.js | 23 + .../vue_shared/vue_resource_interceptor.js.es6 | 23 - app/assets/javascripts/wikis.js | 73 +++ app/assets/javascripts/wikis.js.es6 | 73 --- config/karma.config.js | 2 +- config/webpack.config.js | 6 +- doc/development/frontend.md | 16 +- doc/development/testing.md | 4 +- package.json | 2 +- spec/javascripts/abuse_reports_spec.js | 43 ++ spec/javascripts/abuse_reports_spec.js.es6 | 43 -- spec/javascripts/activities_spec.js | 62 +++ spec/javascripts/activities_spec.js.es6 | 62 --- spec/javascripts/boards/boards_store_spec.js | 158 ++++++ spec/javascripts/boards/boards_store_spec.js.es6 | 158 ------ spec/javascripts/boards/issue_card_spec.js | 191 ++++++++ spec/javascripts/boards/issue_card_spec.js.es6 | 191 -------- spec/javascripts/boards/issue_spec.js | 82 ++++ spec/javascripts/boards/issue_spec.js.es6 | 82 ---- spec/javascripts/boards/list_spec.js | 87 ++++ spec/javascripts/boards/list_spec.js.es6 | 87 ---- spec/javascripts/boards/mock_data.js | 63 +++ spec/javascripts/boards/mock_data.js.es6 | 63 --- spec/javascripts/boards/modal_store_spec.js | 132 +++++ spec/javascripts/boards/modal_store_spec.js.es6 | 132 ----- spec/javascripts/bootstrap_linked_tabs_spec.js | 71 +++ spec/javascripts/bootstrap_linked_tabs_spec.js.es6 | 71 --- spec/javascripts/build_spec.js | 183 +++++++ spec/javascripts/build_spec.js.es6 | 183 ------- spec/javascripts/commit/pipelines/mock_data.js | 90 ++++ spec/javascripts/commit/pipelines/mock_data.js.es6 | 90 ---- .../javascripts/commit/pipelines/pipelines_spec.js | 106 ++++ .../commit/pipelines/pipelines_spec.js.es6 | 106 ---- .../commit/pipelines/pipelines_store_spec.js | 30 ++ .../commit/pipelines/pipelines_store_spec.js.es6 | 30 -- spec/javascripts/commits_spec.js | 62 +++ spec/javascripts/commits_spec.js.es6 | 62 --- spec/javascripts/dashboard_spec.js | 37 ++ spec/javascripts/dashboard_spec.js.es6 | 37 -- spec/javascripts/datetime_utility_spec.js | 65 +++ spec/javascripts/datetime_utility_spec.js.es6 | 65 --- spec/javascripts/diff_comments_store_spec.js | 124 +++++ spec/javascripts/diff_comments_store_spec.js.es6 | 124 ----- .../environments/environment_actions_spec.js | 66 +++ .../environments/environment_actions_spec.js.es6 | 66 --- .../environments/environment_external_url_spec.js | 21 + .../environment_external_url_spec.js.es6 | 21 - .../environments/environment_item_spec.js | 228 +++++++++ .../environments/environment_item_spec.js.es6 | 228 --------- .../environments/environment_rollback_spec.js | 47 ++ .../environments/environment_rollback_spec.js.es6 | 47 -- spec/javascripts/environments/environment_spec.js | 125 +++++ .../environments/environment_spec.js.es6 | 125 ----- .../environments/environment_stop_spec.js | 28 ++ .../environments/environment_stop_spec.js.es6 | 28 -- .../environments/environments_store_spec.js | 70 +++ .../environments/environments_store_spec.js.es6 | 70 --- spec/javascripts/environments/mock_data.js | 153 ++++++ spec/javascripts/environments/mock_data.js.es6 | 153 ------ spec/javascripts/extensions/array_spec.js | 45 ++ spec/javascripts/extensions/array_spec.js.es6 | 45 -- spec/javascripts/extensions/element_spec.js | 38 ++ spec/javascripts/extensions/element_spec.js.es6 | 38 -- spec/javascripts/extensions/object_spec.js | 25 + spec/javascripts/extensions/object_spec.js.es6 | 25 - .../filtered_search/dropdown_user_spec.js | 75 +++ .../filtered_search/dropdown_user_spec.js.es6 | 75 --- .../filtered_search/dropdown_utils_spec.js | 290 +++++++++++ .../filtered_search/dropdown_utils_spec.js.es6 | 290 ----------- .../filtered_search_dropdown_manager_spec.js | 59 +++ .../filtered_search_dropdown_manager_spec.js.es6 | 59 --- .../filtered_search_manager_spec.js | 67 +++ .../filtered_search_manager_spec.js.es6 | 67 --- .../filtered_search_token_keys_spec.js | 110 +++++ .../filtered_search_token_keys_spec.js.es6 | 110 ----- .../filtered_search_tokenizer_spec.js | 104 ++++ .../filtered_search_tokenizer_spec.js.es6 | 104 ---- spec/javascripts/gfm_auto_complete_spec.js | 91 ++++ spec/javascripts/gfm_auto_complete_spec.js.es6 | 91 ---- spec/javascripts/gl_dropdown_spec.js | 188 +++++++ spec/javascripts/gl_dropdown_spec.js.es6 | 188 ------- spec/javascripts/gl_field_errors_spec.js | 110 +++++ spec/javascripts/gl_field_errors_spec.js.es6 | 110 ----- spec/javascripts/gl_form_spec.js | 123 +++++ spec/javascripts/gl_form_spec.js.es6 | 123 ----- spec/javascripts/helpers/class_spec_helper.js | 9 + spec/javascripts/helpers/class_spec_helper.js.es6 | 9 - spec/javascripts/helpers/class_spec_helper_spec.js | 36 ++ .../helpers/class_spec_helper_spec.js.es6 | 36 -- spec/javascripts/issuable_spec.js | 80 +++ spec/javascripts/issuable_spec.js.es6 | 80 --- spec/javascripts/issuable_time_tracker_spec.js | 202 ++++++++ spec/javascripts/issuable_time_tracker_spec.js.es6 | 202 -------- spec/javascripts/labels_issue_sidebar_spec.js | 90 ++++ spec/javascripts/labels_issue_sidebar_spec.js.es6 | 90 ---- spec/javascripts/lib/utils/common_utils_spec.js | 90 ++++ .../javascripts/lib/utils/common_utils_spec.js.es6 | 90 ---- spec/javascripts/lib/utils/text_utility_spec.js | 39 ++ .../javascripts/lib/utils/text_utility_spec.js.es6 | 39 -- .../mini_pipeline_graph_dropdown_spec.js | 51 ++ .../mini_pipeline_graph_dropdown_spec.js.es6 | 51 -- spec/javascripts/pipelines_spec.js | 30 ++ spec/javascripts/pipelines_spec.js.es6 | 30 -- spec/javascripts/pretty_time_spec.js | 134 +++++ spec/javascripts/pretty_time_spec.js.es6 | 134 ----- spec/javascripts/signin_tabs_memoizer_spec.js | 53 ++ spec/javascripts/signin_tabs_memoizer_spec.js.es6 | 53 -- spec/javascripts/smart_interval_spec.js | 179 +++++++ spec/javascripts/smart_interval_spec.js.es6 | 179 ------- spec/javascripts/subbable_resource_spec.js | 63 +++ spec/javascripts/subbable_resource_spec.js.es6 | 63 --- spec/javascripts/visibility_select_spec.js | 100 ++++ spec/javascripts/visibility_select_spec.js.es6 | 100 ---- .../vue_shared/components/commit_spec.js | 131 +++++ .../vue_shared/components/commit_spec.js.es6 | 131 ----- .../components/pipelines_table_row_spec.js | 89 ++++ .../components/pipelines_table_row_spec.js.es6 | 89 ---- .../vue_shared/components/pipelines_table_spec.js | 66 +++ .../components/pipelines_table_spec.js.es6 | 66 --- .../vue_shared/components/table_pagination_spec.js | 166 +++++++ .../components/table_pagination_spec.js.es6 | 166 ------- 492 files changed, 20638 insertions(+), 20647 deletions(-) create mode 100644 app/assets/javascripts/abuse_reports.js delete mode 100644 app/assets/javascripts/abuse_reports.js.es6 create mode 100644 app/assets/javascripts/activities.js delete mode 100644 app/assets/javascripts/activities.js.es6 create mode 100644 app/assets/javascripts/blob/blob_ci_yaml.js delete mode 100644 app/assets/javascripts/blob/blob_ci_yaml.js.es6 create mode 100644 app/assets/javascripts/blob/blob_dockerfile_selector.js delete mode 100644 app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 create mode 100644 app/assets/javascripts/blob/blob_dockerfile_selectors.js delete mode 100644 app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 create mode 100644 app/assets/javascripts/blob/blob_license_selectors.js delete mode 100644 app/assets/javascripts/blob/blob_license_selectors.js.es6 create mode 100644 app/assets/javascripts/blob/template_selector.js delete mode 100644 app/assets/javascripts/blob/template_selector.js.es6 create mode 100644 app/assets/javascripts/boards/boards_bundle.js delete mode 100644 app/assets/javascripts/boards/boards_bundle.js.es6 create mode 100644 app/assets/javascripts/boards/components/board.js delete mode 100644 app/assets/javascripts/boards/components/board.js.es6 create mode 100644 app/assets/javascripts/boards/components/board_blank_state.js delete mode 100644 app/assets/javascripts/boards/components/board_blank_state.js.es6 create mode 100644 app/assets/javascripts/boards/components/board_card.js delete mode 100644 app/assets/javascripts/boards/components/board_card.js.es6 create mode 100644 app/assets/javascripts/boards/components/board_delete.js delete mode 100644 app/assets/javascripts/boards/components/board_delete.js.es6 create mode 100644 app/assets/javascripts/boards/components/board_list.js delete mode 100644 app/assets/javascripts/boards/components/board_list.js.es6 create mode 100644 app/assets/javascripts/boards/components/board_new_issue.js delete mode 100644 app/assets/javascripts/boards/components/board_new_issue.js.es6 create mode 100644 app/assets/javascripts/boards/components/board_sidebar.js delete mode 100644 app/assets/javascripts/boards/components/board_sidebar.js.es6 create mode 100644 app/assets/javascripts/boards/components/issue_card_inner.js delete mode 100644 app/assets/javascripts/boards/components/issue_card_inner.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/empty_state.js delete mode 100644 app/assets/javascripts/boards/components/modal/empty_state.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/filters.js delete mode 100644 app/assets/javascripts/boards/components/modal/filters.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/filters/label.js delete mode 100644 app/assets/javascripts/boards/components/modal/filters/label.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/filters/milestone.js delete mode 100644 app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/filters/user.js delete mode 100644 app/assets/javascripts/boards/components/modal/filters/user.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/footer.js delete mode 100644 app/assets/javascripts/boards/components/modal/footer.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/header.js delete mode 100644 app/assets/javascripts/boards/components/modal/header.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/index.js delete mode 100644 app/assets/javascripts/boards/components/modal/index.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/list.js delete mode 100644 app/assets/javascripts/boards/components/modal/list.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/lists_dropdown.js delete mode 100644 app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/tabs.js delete mode 100644 app/assets/javascripts/boards/components/modal/tabs.js.es6 create mode 100644 app/assets/javascripts/boards/components/new_list_dropdown.js delete mode 100644 app/assets/javascripts/boards/components/new_list_dropdown.js.es6 create mode 100644 app/assets/javascripts/boards/components/sidebar/remove_issue.js delete mode 100644 app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 create mode 100644 app/assets/javascripts/boards/filters/due_date_filters.js delete mode 100644 app/assets/javascripts/boards/filters/due_date_filters.js.es6 create mode 100644 app/assets/javascripts/boards/mixins/modal_mixins.js delete mode 100644 app/assets/javascripts/boards/mixins/modal_mixins.js.es6 create mode 100644 app/assets/javascripts/boards/mixins/sortable_default_options.js delete mode 100644 app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 create mode 100644 app/assets/javascripts/boards/models/issue.js delete mode 100644 app/assets/javascripts/boards/models/issue.js.es6 create mode 100644 app/assets/javascripts/boards/models/label.js delete mode 100644 app/assets/javascripts/boards/models/label.js.es6 create mode 100644 app/assets/javascripts/boards/models/list.js delete mode 100644 app/assets/javascripts/boards/models/list.js.es6 create mode 100644 app/assets/javascripts/boards/models/milestone.js delete mode 100644 app/assets/javascripts/boards/models/milestone.js.es6 create mode 100644 app/assets/javascripts/boards/models/user.js delete mode 100644 app/assets/javascripts/boards/models/user.js.es6 create mode 100644 app/assets/javascripts/boards/services/board_service.js delete mode 100644 app/assets/javascripts/boards/services/board_service.js.es6 create mode 100644 app/assets/javascripts/boards/stores/boards_store.js delete mode 100644 app/assets/javascripts/boards/stores/boards_store.js.es6 create mode 100644 app/assets/javascripts/boards/stores/modal_store.js delete mode 100644 app/assets/javascripts/boards/stores/modal_store.js.es6 create mode 100644 app/assets/javascripts/build_variables.js delete mode 100644 app/assets/javascripts/build_variables.js.es6 create mode 100644 app/assets/javascripts/ci_lint_editor.js delete mode 100644 app/assets/javascripts/ci_lint_editor.js.es6 create mode 100644 app/assets/javascripts/commit/pipelines/pipelines_bundle.js delete mode 100644 app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 create mode 100644 app/assets/javascripts/commit/pipelines/pipelines_service.js delete mode 100644 app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 create mode 100644 app/assets/javascripts/commit/pipelines/pipelines_store.js delete mode 100644 app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 create mode 100644 app/assets/javascripts/commit/pipelines/pipelines_table.js delete mode 100644 app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 create mode 100644 app/assets/javascripts/compare_autocomplete.js delete mode 100644 app/assets/javascripts/compare_autocomplete.js.es6 create mode 100644 app/assets/javascripts/copy_as_gfm.js delete mode 100644 app/assets/javascripts/copy_as_gfm.js.es6 create mode 100644 app/assets/javascripts/create_label.js delete mode 100644 app/assets/javascripts/create_label.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_code_component.js delete mode 100644 app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_issue_component.js delete mode 100644 app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_plan_component.js delete mode 100644 app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_production_component.js delete mode 100644 app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_review_component.js delete mode 100644 app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_staging_component.js delete mode 100644 app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/stage_test_component.js delete mode 100644 app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/components/total_time_component.js delete mode 100644 app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js delete mode 100644 app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/cycle_analytics_service.js delete mode 100644 app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/cycle_analytics_store.js delete mode 100644 app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/svg/icon_branch.js delete mode 100644 app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/svg/icon_build_status.js delete mode 100644 app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 create mode 100644 app/assets/javascripts/cycle_analytics/svg/icon_commit.js delete mode 100644 app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 create mode 100644 app/assets/javascripts/diff.js delete mode 100644 app/assets/javascripts/diff.js.es6 create mode 100644 app/assets/javascripts/diff_notes/components/comment_resolve_btn.js delete mode 100644 app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 create mode 100644 app/assets/javascripts/diff_notes/components/jump_to_discussion.js delete mode 100644 app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 create mode 100644 app/assets/javascripts/diff_notes/components/resolve_btn.js delete mode 100644 app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 create mode 100644 app/assets/javascripts/diff_notes/components/resolve_count.js delete mode 100644 app/assets/javascripts/diff_notes/components/resolve_count.js.es6 create mode 100644 app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js delete mode 100644 app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 create mode 100644 app/assets/javascripts/diff_notes/diff_notes_bundle.js delete mode 100644 app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 create mode 100644 app/assets/javascripts/diff_notes/mixins/discussion.js delete mode 100644 app/assets/javascripts/diff_notes/mixins/discussion.js.es6 create mode 100644 app/assets/javascripts/diff_notes/models/discussion.js delete mode 100644 app/assets/javascripts/diff_notes/models/discussion.js.es6 create mode 100644 app/assets/javascripts/diff_notes/models/note.js delete mode 100644 app/assets/javascripts/diff_notes/models/note.js.es6 create mode 100644 app/assets/javascripts/diff_notes/services/resolve.js delete mode 100644 app/assets/javascripts/diff_notes/services/resolve.js.es6 create mode 100644 app/assets/javascripts/diff_notes/stores/comments.js delete mode 100644 app/assets/javascripts/diff_notes/stores/comments.js.es6 create mode 100644 app/assets/javascripts/dispatcher.js delete mode 100644 app/assets/javascripts/dispatcher.js.es6 create mode 100644 app/assets/javascripts/due_date_select.js delete mode 100644 app/assets/javascripts/due_date_select.js.es6 create mode 100644 app/assets/javascripts/environments/components/environment.js delete mode 100644 app/assets/javascripts/environments/components/environment.js.es6 create mode 100644 app/assets/javascripts/environments/components/environment_actions.js delete mode 100644 app/assets/javascripts/environments/components/environment_actions.js.es6 create mode 100644 app/assets/javascripts/environments/components/environment_external_url.js delete mode 100644 app/assets/javascripts/environments/components/environment_external_url.js.es6 create mode 100644 app/assets/javascripts/environments/components/environment_item.js delete mode 100644 app/assets/javascripts/environments/components/environment_item.js.es6 create mode 100644 app/assets/javascripts/environments/components/environment_rollback.js delete mode 100644 app/assets/javascripts/environments/components/environment_rollback.js.es6 create mode 100644 app/assets/javascripts/environments/components/environment_stop.js delete mode 100644 app/assets/javascripts/environments/components/environment_stop.js.es6 create mode 100644 app/assets/javascripts/environments/components/environment_terminal_button.js delete mode 100644 app/assets/javascripts/environments/components/environment_terminal_button.js.es6 create mode 100644 app/assets/javascripts/environments/environments_bundle.js delete mode 100644 app/assets/javascripts/environments/environments_bundle.js.es6 create mode 100644 app/assets/javascripts/environments/services/environments_service.js delete mode 100644 app/assets/javascripts/environments/services/environments_service.js.es6 create mode 100644 app/assets/javascripts/environments/stores/environments_store.js delete mode 100644 app/assets/javascripts/environments/stores/environments_store.js.es6 create mode 100644 app/assets/javascripts/extensions/array.js delete mode 100644 app/assets/javascripts/extensions/array.js.es6 create mode 100644 app/assets/javascripts/extensions/custom_event.js delete mode 100644 app/assets/javascripts/extensions/custom_event.js.es6 create mode 100644 app/assets/javascripts/extensions/element.js delete mode 100644 app/assets/javascripts/extensions/element.js.es6 create mode 100644 app/assets/javascripts/extensions/object.js delete mode 100644 app/assets/javascripts/extensions/object.js.es6 create mode 100644 app/assets/javascripts/filtered_search/dropdown_hint.js delete mode 100644 app/assets/javascripts/filtered_search/dropdown_hint.js.es6 create mode 100644 app/assets/javascripts/filtered_search/dropdown_non_user.js delete mode 100644 app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 create mode 100644 app/assets/javascripts/filtered_search/dropdown_user.js delete mode 100644 app/assets/javascripts/filtered_search/dropdown_user.js.es6 create mode 100644 app/assets/javascripts/filtered_search/dropdown_utils.js delete mode 100644 app/assets/javascripts/filtered_search/dropdown_utils.js.es6 create mode 100644 app/assets/javascripts/filtered_search/filtered_search_dropdown.js delete mode 100644 app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 create mode 100644 app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js delete mode 100644 app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 create mode 100644 app/assets/javascripts/filtered_search/filtered_search_manager.js delete mode 100644 app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 create mode 100644 app/assets/javascripts/filtered_search/filtered_search_token_keys.js delete mode 100644 app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 create mode 100644 app/assets/javascripts/filtered_search/filtered_search_tokenizer.js delete mode 100644 app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 create mode 100644 app/assets/javascripts/gfm_auto_complete.js delete mode 100644 app/assets/javascripts/gfm_auto_complete.js.es6 create mode 100644 app/assets/javascripts/gl_field_error.js delete mode 100644 app/assets/javascripts/gl_field_error.js.es6 create mode 100644 app/assets/javascripts/gl_field_errors.js delete mode 100644 app/assets/javascripts/gl_field_errors.js.es6 create mode 100644 app/assets/javascripts/gl_form.js delete mode 100644 app/assets/javascripts/gl_form.js.es6 create mode 100644 app/assets/javascripts/group_label_subscription.js delete mode 100644 app/assets/javascripts/group_label_subscription.js.es6 create mode 100644 app/assets/javascripts/issuable.js delete mode 100644 app/assets/javascripts/issuable.js.es6 create mode 100644 app/assets/javascripts/issuable/issuable_bundle.js delete mode 100644 app/assets/javascripts/issuable/issuable_bundle.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js delete mode 100644 app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js delete mode 100644 app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js delete mode 100644 app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/help_state.js delete mode 100644 app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js delete mode 100644 app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js delete mode 100644 app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/components/time_tracker.js delete mode 100644 app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 create mode 100644 app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js delete mode 100644 app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 create mode 100644 app/assets/javascripts/issues_bulk_assignment.js delete mode 100644 app/assets/javascripts/issues_bulk_assignment.js.es6 create mode 100644 app/assets/javascripts/label_manager.js delete mode 100644 app/assets/javascripts/label_manager.js.es6 create mode 100644 app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js delete mode 100644 app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 create mode 100644 app/assets/javascripts/lib/utils/common_utils.js delete mode 100644 app/assets/javascripts/lib/utils/common_utils.js.es6 create mode 100644 app/assets/javascripts/lib/utils/datetime_utility.js delete mode 100644 app/assets/javascripts/lib/utils/datetime_utility.js.es6 create mode 100644 app/assets/javascripts/lib/utils/pretty_time.js delete mode 100644 app/assets/javascripts/lib/utils/pretty_time.js.es6 create mode 100644 app/assets/javascripts/lib/utils/url_utility.js delete mode 100644 app/assets/javascripts/lib/utils/url_utility.js.es6 create mode 100644 app/assets/javascripts/lib/vue_resource.js delete mode 100644 app/assets/javascripts/lib/vue_resource.js.es6 create mode 100644 app/assets/javascripts/member_expiration_date.js delete mode 100644 app/assets/javascripts/member_expiration_date.js.es6 create mode 100644 app/assets/javascripts/members.js delete mode 100644 app/assets/javascripts/members.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/components/diff_file_editor.js delete mode 100644 app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js delete mode 100644 app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js delete mode 100644 app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/merge_conflict_service.js delete mode 100644 app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/merge_conflict_store.js delete mode 100644 app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js delete mode 100644 app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js delete mode 100644 app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js delete mode 100644 app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 create mode 100644 app/assets/javascripts/merge_request_tabs.js delete mode 100644 app/assets/javascripts/merge_request_tabs.js.es6 create mode 100644 app/assets/javascripts/merge_request_widget.js delete mode 100644 app/assets/javascripts/merge_request_widget.js.es6 create mode 100644 app/assets/javascripts/merge_request_widget/ci_bundle.js delete mode 100644 app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 create mode 100644 app/assets/javascripts/mini_pipeline_graph_dropdown.js delete mode 100644 app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 create mode 100644 app/assets/javascripts/pager.js delete mode 100644 app/assets/javascripts/pager.js.es6 create mode 100644 app/assets/javascripts/pipelines.js delete mode 100644 app/assets/javascripts/pipelines.js.es6 create mode 100644 app/assets/javascripts/profile/gl_crop.js delete mode 100644 app/assets/javascripts/profile/gl_crop.js.es6 create mode 100644 app/assets/javascripts/profile/profile.js delete mode 100644 app/assets/javascripts/profile/profile.js.es6 create mode 100644 app/assets/javascripts/project_label_subscription.js delete mode 100644 app/assets/javascripts/project_label_subscription.js.es6 create mode 100644 app/assets/javascripts/project_variables.js delete mode 100644 app/assets/javascripts/project_variables.js.es6 create mode 100644 app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js delete mode 100644 app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 create mode 100644 app/assets/javascripts/protected_branches/protected_branch_create.js delete mode 100644 app/assets/javascripts/protected_branches/protected_branch_create.js.es6 create mode 100644 app/assets/javascripts/protected_branches/protected_branch_dropdown.js delete mode 100644 app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 create mode 100644 app/assets/javascripts/protected_branches/protected_branch_edit.js delete mode 100644 app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 create mode 100644 app/assets/javascripts/protected_branches/protected_branch_edit_list.js delete mode 100644 app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 create mode 100644 app/assets/javascripts/search_autocomplete.js delete mode 100644 app/assets/javascripts/search_autocomplete.js.es6 create mode 100644 app/assets/javascripts/shortcuts_blob.js delete mode 100644 app/assets/javascripts/shortcuts_blob.js.es6 create mode 100644 app/assets/javascripts/sidebar.js delete mode 100644 app/assets/javascripts/sidebar.js.es6 create mode 100644 app/assets/javascripts/signin_tabs_memoizer.js delete mode 100644 app/assets/javascripts/signin_tabs_memoizer.js.es6 create mode 100644 app/assets/javascripts/smart_interval.js delete mode 100644 app/assets/javascripts/smart_interval.js.es6 create mode 100644 app/assets/javascripts/snippets_list.js delete mode 100644 app/assets/javascripts/snippets_list.js.es6 create mode 100644 app/assets/javascripts/subbable_resource.js delete mode 100644 app/assets/javascripts/subbable_resource.js.es6 create mode 100644 app/assets/javascripts/subscription.js delete mode 100644 app/assets/javascripts/subscription.js.es6 create mode 100644 app/assets/javascripts/templates/issuable_template_selector.js delete mode 100644 app/assets/javascripts/templates/issuable_template_selector.js.es6 create mode 100644 app/assets/javascripts/templates/issuable_template_selectors.js delete mode 100644 app/assets/javascripts/templates/issuable_template_selectors.js.es6 create mode 100644 app/assets/javascripts/terminal/terminal.js delete mode 100644 app/assets/javascripts/terminal/terminal.js.es6 create mode 100644 app/assets/javascripts/terminal/terminal_bundle.js delete mode 100644 app/assets/javascripts/terminal/terminal_bundle.js.es6 create mode 100644 app/assets/javascripts/todos.js delete mode 100644 app/assets/javascripts/todos.js.es6 create mode 100644 app/assets/javascripts/u2f/authenticate.js delete mode 100644 app/assets/javascripts/u2f/authenticate.js.es6 create mode 100644 app/assets/javascripts/user.js delete mode 100644 app/assets/javascripts/user.js.es6 create mode 100644 app/assets/javascripts/user_tabs.js delete mode 100644 app/assets/javascripts/user_tabs.js.es6 create mode 100644 app/assets/javascripts/username_validator.js delete mode 100644 app/assets/javascripts/username_validator.js.es6 create mode 100644 app/assets/javascripts/version_check_image.js delete mode 100644 app/assets/javascripts/version_check_image.js.es6 create mode 100644 app/assets/javascripts/visibility_select.js delete mode 100644 app/assets/javascripts/visibility_select.js.es6 create mode 100644 app/assets/javascripts/vue_pipelines_index/index.js delete mode 100644 app/assets/javascripts/vue_pipelines_index/index.js.es6 create mode 100644 app/assets/javascripts/vue_pipelines_index/pipeline_actions.js delete mode 100644 app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 create mode 100644 app/assets/javascripts/vue_pipelines_index/pipeline_url.js delete mode 100644 app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 create mode 100644 app/assets/javascripts/vue_pipelines_index/pipelines.js delete mode 100644 app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 create mode 100644 app/assets/javascripts/vue_pipelines_index/stage.js delete mode 100644 app/assets/javascripts/vue_pipelines_index/stage.js.es6 create mode 100644 app/assets/javascripts/vue_pipelines_index/status.js delete mode 100644 app/assets/javascripts/vue_pipelines_index/status.js.es6 create mode 100644 app/assets/javascripts/vue_pipelines_index/store.js delete mode 100644 app/assets/javascripts/vue_pipelines_index/store.js.es6 create mode 100644 app/assets/javascripts/vue_pipelines_index/time_ago.js delete mode 100644 app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 create mode 100644 app/assets/javascripts/vue_realtime_listener/index.js delete mode 100644 app/assets/javascripts/vue_realtime_listener/index.js.es6 create mode 100644 app/assets/javascripts/vue_shared/components/commit.js delete mode 100644 app/assets/javascripts/vue_shared/components/commit.js.es6 create mode 100644 app/assets/javascripts/vue_shared/components/pipelines_table.js delete mode 100644 app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 create mode 100644 app/assets/javascripts/vue_shared/components/pipelines_table_row.js delete mode 100644 app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 create mode 100644 app/assets/javascripts/vue_shared/components/table_pagination.js delete mode 100644 app/assets/javascripts/vue_shared/components/table_pagination.js.es6 create mode 100644 app/assets/javascripts/vue_shared/vue_resource_interceptor.js delete mode 100644 app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 create mode 100644 app/assets/javascripts/wikis.js delete mode 100644 app/assets/javascripts/wikis.js.es6 create mode 100644 spec/javascripts/abuse_reports_spec.js delete mode 100644 spec/javascripts/abuse_reports_spec.js.es6 create mode 100644 spec/javascripts/activities_spec.js delete mode 100644 spec/javascripts/activities_spec.js.es6 create mode 100644 spec/javascripts/boards/boards_store_spec.js delete mode 100644 spec/javascripts/boards/boards_store_spec.js.es6 create mode 100644 spec/javascripts/boards/issue_card_spec.js delete mode 100644 spec/javascripts/boards/issue_card_spec.js.es6 create mode 100644 spec/javascripts/boards/issue_spec.js delete mode 100644 spec/javascripts/boards/issue_spec.js.es6 create mode 100644 spec/javascripts/boards/list_spec.js delete mode 100644 spec/javascripts/boards/list_spec.js.es6 create mode 100644 spec/javascripts/boards/mock_data.js delete mode 100644 spec/javascripts/boards/mock_data.js.es6 create mode 100644 spec/javascripts/boards/modal_store_spec.js delete mode 100644 spec/javascripts/boards/modal_store_spec.js.es6 create mode 100644 spec/javascripts/bootstrap_linked_tabs_spec.js delete mode 100644 spec/javascripts/bootstrap_linked_tabs_spec.js.es6 create mode 100644 spec/javascripts/build_spec.js delete mode 100644 spec/javascripts/build_spec.js.es6 create mode 100644 spec/javascripts/commit/pipelines/mock_data.js delete mode 100644 spec/javascripts/commit/pipelines/mock_data.js.es6 create mode 100644 spec/javascripts/commit/pipelines/pipelines_spec.js delete mode 100644 spec/javascripts/commit/pipelines/pipelines_spec.js.es6 create mode 100644 spec/javascripts/commit/pipelines/pipelines_store_spec.js delete mode 100644 spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 create mode 100644 spec/javascripts/commits_spec.js delete mode 100644 spec/javascripts/commits_spec.js.es6 create mode 100644 spec/javascripts/dashboard_spec.js delete mode 100644 spec/javascripts/dashboard_spec.js.es6 create mode 100644 spec/javascripts/datetime_utility_spec.js delete mode 100644 spec/javascripts/datetime_utility_spec.js.es6 create mode 100644 spec/javascripts/diff_comments_store_spec.js delete mode 100644 spec/javascripts/diff_comments_store_spec.js.es6 create mode 100644 spec/javascripts/environments/environment_actions_spec.js delete mode 100644 spec/javascripts/environments/environment_actions_spec.js.es6 create mode 100644 spec/javascripts/environments/environment_external_url_spec.js delete mode 100644 spec/javascripts/environments/environment_external_url_spec.js.es6 create mode 100644 spec/javascripts/environments/environment_item_spec.js delete mode 100644 spec/javascripts/environments/environment_item_spec.js.es6 create mode 100644 spec/javascripts/environments/environment_rollback_spec.js delete mode 100644 spec/javascripts/environments/environment_rollback_spec.js.es6 create mode 100644 spec/javascripts/environments/environment_spec.js delete mode 100644 spec/javascripts/environments/environment_spec.js.es6 create mode 100644 spec/javascripts/environments/environment_stop_spec.js delete mode 100644 spec/javascripts/environments/environment_stop_spec.js.es6 create mode 100644 spec/javascripts/environments/environments_store_spec.js delete mode 100644 spec/javascripts/environments/environments_store_spec.js.es6 create mode 100644 spec/javascripts/environments/mock_data.js delete mode 100644 spec/javascripts/environments/mock_data.js.es6 create mode 100644 spec/javascripts/extensions/array_spec.js delete mode 100644 spec/javascripts/extensions/array_spec.js.es6 create mode 100644 spec/javascripts/extensions/element_spec.js delete mode 100644 spec/javascripts/extensions/element_spec.js.es6 create mode 100644 spec/javascripts/extensions/object_spec.js delete mode 100644 spec/javascripts/extensions/object_spec.js.es6 create mode 100644 spec/javascripts/filtered_search/dropdown_user_spec.js delete mode 100644 spec/javascripts/filtered_search/dropdown_user_spec.js.es6 create mode 100644 spec/javascripts/filtered_search/dropdown_utils_spec.js delete mode 100644 spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 create mode 100644 spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js delete mode 100644 spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 create mode 100644 spec/javascripts/filtered_search/filtered_search_manager_spec.js delete mode 100644 spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 create mode 100644 spec/javascripts/filtered_search/filtered_search_token_keys_spec.js delete mode 100644 spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 create mode 100644 spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js delete mode 100644 spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 create mode 100644 spec/javascripts/gfm_auto_complete_spec.js delete mode 100644 spec/javascripts/gfm_auto_complete_spec.js.es6 create mode 100644 spec/javascripts/gl_dropdown_spec.js delete mode 100644 spec/javascripts/gl_dropdown_spec.js.es6 create mode 100644 spec/javascripts/gl_field_errors_spec.js delete mode 100644 spec/javascripts/gl_field_errors_spec.js.es6 create mode 100644 spec/javascripts/gl_form_spec.js delete mode 100644 spec/javascripts/gl_form_spec.js.es6 create mode 100644 spec/javascripts/helpers/class_spec_helper.js delete mode 100644 spec/javascripts/helpers/class_spec_helper.js.es6 create mode 100644 spec/javascripts/helpers/class_spec_helper_spec.js delete mode 100644 spec/javascripts/helpers/class_spec_helper_spec.js.es6 create mode 100644 spec/javascripts/issuable_spec.js delete mode 100644 spec/javascripts/issuable_spec.js.es6 create mode 100644 spec/javascripts/issuable_time_tracker_spec.js delete mode 100644 spec/javascripts/issuable_time_tracker_spec.js.es6 create mode 100644 spec/javascripts/labels_issue_sidebar_spec.js delete mode 100644 spec/javascripts/labels_issue_sidebar_spec.js.es6 create mode 100644 spec/javascripts/lib/utils/common_utils_spec.js delete mode 100644 spec/javascripts/lib/utils/common_utils_spec.js.es6 create mode 100644 spec/javascripts/lib/utils/text_utility_spec.js delete mode 100644 spec/javascripts/lib/utils/text_utility_spec.js.es6 create mode 100644 spec/javascripts/mini_pipeline_graph_dropdown_spec.js delete mode 100644 spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 create mode 100644 spec/javascripts/pipelines_spec.js delete mode 100644 spec/javascripts/pipelines_spec.js.es6 create mode 100644 spec/javascripts/pretty_time_spec.js delete mode 100644 spec/javascripts/pretty_time_spec.js.es6 create mode 100644 spec/javascripts/signin_tabs_memoizer_spec.js delete mode 100644 spec/javascripts/signin_tabs_memoizer_spec.js.es6 create mode 100644 spec/javascripts/smart_interval_spec.js delete mode 100644 spec/javascripts/smart_interval_spec.js.es6 create mode 100644 spec/javascripts/subbable_resource_spec.js delete mode 100644 spec/javascripts/subbable_resource_spec.js.es6 create mode 100644 spec/javascripts/visibility_select_spec.js delete mode 100644 spec/javascripts/visibility_select_spec.js.es6 create mode 100644 spec/javascripts/vue_shared/components/commit_spec.js delete mode 100644 spec/javascripts/vue_shared/components/commit_spec.js.es6 create mode 100644 spec/javascripts/vue_shared/components/pipelines_table_row_spec.js delete mode 100644 spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 create mode 100644 spec/javascripts/vue_shared/components/pipelines_table_spec.js delete mode 100644 spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 create mode 100644 spec/javascripts/vue_shared/components/table_pagination_spec.js delete mode 100644 spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 diff --git a/.eslintrc b/.eslintrc index 1a2cd821af7..fabcf3ea588 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,7 +15,7 @@ "filenames" ], "rules": { - "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"], + "filenames/match-regex": [2, "^[a-z0-9_]+\\.js$"], "no-multiple-empty-lines": ["error", { "max": 1 }], "import/no-extraneous-dependencies": "off", "import/no-unresolved": "off" diff --git a/.gitattributes b/.gitattributes index 70cce05d2b5..e69de29bb2d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +0,0 @@ -*.js.es6 gitlab-language=javascript diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 65149ad2444..b86216d9d1d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -397,7 +397,7 @@ lint:javascript:report: before_script: - npm install script: - - find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files + - find app/ spec/ -name '*.js' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files - npm --silent run eslint-report || true # ignore exit code artifacts: name: eslint-report diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js new file mode 100644 index 00000000000..8a260aae1b1 --- /dev/null +++ b/app/assets/javascripts/abuse_reports.js @@ -0,0 +1,40 @@ +/* eslint-disable no-param-reassign */ + +((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); + } + + 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)); + } + } + + 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 = {})); diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6 deleted file mode 100644 index 8a260aae1b1..00000000000 --- a/app/assets/javascripts/abuse_reports.js.es6 +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-disable no-param-reassign */ - -((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); - } - - 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)); - } - } - - 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 = {})); diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js new file mode 100644 index 00000000000..648cb4d5d85 --- /dev/null +++ b/app/assets/javascripts/activities.js @@ -0,0 +1,37 @@ +/* eslint-disable no-param-reassign, class-methods-use-this */ +/* 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(); + }); + } + + updateTooltips() { + gl.utils.localTimeAgo($('.js-timeago', '.content_list')); + } + + reloadActivities() { + $('.content_list').html(''); + Pager.init(20, true, false, this.updateTooltips); + } + + toggleFilter(sender) { + const $sender = $(sender); + const filter = $sender.attr('id').split('_')[0]; + + $('.event-filter .active').removeClass('active'); + Cookies.set('event_filter', filter); + + $sender.closest('li').toggleClass('active'); + } + } + + global.Activities = Activities; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/activities.js.es6 b/app/assets/javascripts/activities.js.es6 deleted file mode 100644 index 648cb4d5d85..00000000000 --- a/app/assets/javascripts/activities.js.es6 +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable no-param-reassign, class-methods-use-this */ -/* 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(); - }); - } - - updateTooltips() { - gl.utils.localTimeAgo($('.js-timeago', '.content_list')); - } - - reloadActivities() { - $('.content_list').html(''); - Pager.init(20, true, false, this.updateTooltips); - } - - toggleFilter(sender) { - const $sender = $(sender); - const filter = $sender.attr('id').split('_')[0]; - - $('.event-filter .active').removeClass('active'); - Cookies.set('event_filter', filter); - - $sender.closest('li').toggleClass('active'); - } - } - - global.Activities = Activities; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index ea3f13bd00f..e68ee5904cb 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -45,15 +45,15 @@ require('./shortcuts_dashboard_navigation'); require('./shortcuts_issuable'); require('./shortcuts_network'); require('vendor/jquery.nicescroll'); -requireAll(require.context('./behaviors', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./blob', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./templates', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./commit', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./extensions', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./lib/utils', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/)); +requireAll(require.context('./behaviors', false, /^\.\/.*\.js$/)); +requireAll(require.context('./blob', false, /^\.\/.*\.js$/)); +requireAll(require.context('./templates', false, /^\.\/.*\.js$/)); +requireAll(require.context('./commit', false, /^\.\/.*\.js$/)); +requireAll(require.context('./extensions', false, /^\.\/.*\.js$/)); +requireAll(require.context('./lib/utils', false, /^\.\/.*\.js$/)); +requireAll(require.context('./u2f', false, /^\.\/.*\.js$/)); +requireAll(require.context('./droplab', false, /^\.\/.*\.js$/)); +requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.js$/)); require('vendor/fuzzaldrin-plus'); window.ES6Promise = require('vendor/es6-promise.auto'); window.ES6Promise.polyfill(); diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js new file mode 100644 index 00000000000..ec1c018424d --- /dev/null +++ b/app/assets/javascripts/blob/blob_ci_yaml.js @@ -0,0 +1,42 @@ +/* eslint-disable no-param-reassign, comma-dangle */ +/* global Api */ + +require('./template_selector'); + +((global) => { + class BlobCiYamlSelector extends gl.TemplateSelector { + requestFile(query) { + return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this)); + } + + requestFileSuccess(file) { + return super.requestFileSuccess(file); + } + } + + global.BlobCiYamlSelector = BlobCiYamlSelector; + + class BlobCiYamlSelectors { + constructor({ editor, $dropdowns } = {}) { + this.editor = editor; + this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector'); + this.initSelectors(); + } + + initSelectors() { + const editor = this.editor; + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + return new BlobCiYamlSelector({ + editor, + pattern: /(.gitlab-ci.yml)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), + dropdown: $dropdown + }); + }); + } + } + + global.BlobCiYamlSelectors = BlobCiYamlSelectors; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 deleted file mode 100644 index ec1c018424d..00000000000 --- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable no-param-reassign, comma-dangle */ -/* global Api */ - -require('./template_selector'); - -((global) => { - class BlobCiYamlSelector extends gl.TemplateSelector { - requestFile(query) { - return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this)); - } - - requestFileSuccess(file) { - return super.requestFileSuccess(file); - } - } - - global.BlobCiYamlSelector = BlobCiYamlSelector; - - class BlobCiYamlSelectors { - constructor({ editor, $dropdowns } = {}) { - this.editor = editor; - this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector'); - this.initSelectors(); - } - - initSelectors() { - const editor = this.editor; - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - return new BlobCiYamlSelector({ - editor, - pattern: /(.gitlab-ci.yml)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), - dropdown: $dropdown - }); - }); - } - } - - global.BlobCiYamlSelectors = BlobCiYamlSelectors; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js b/app/assets/javascripts/blob/blob_dockerfile_selector.js new file mode 100644 index 00000000000..d4f60cc6ecd --- /dev/null +++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js @@ -0,0 +1,19 @@ +/* global Api */ + +require('./template_selector'); + +(() => { + const global = window.gl || (window.gl = {}); + + class BlobDockerfileSelector extends gl.TemplateSelector { + requestFile(query) { + return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this)); + } + + requestFileSuccess(file) { + return super.requestFileSuccess(file); + } + } + + global.BlobDockerfileSelector = BlobDockerfileSelector; +})(); diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 deleted file mode 100644 index d4f60cc6ecd..00000000000 --- a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 +++ /dev/null @@ -1,19 +0,0 @@ -/* global Api */ - -require('./template_selector'); - -(() => { - const global = window.gl || (window.gl = {}); - - class BlobDockerfileSelector extends gl.TemplateSelector { - requestFile(query) { - return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this)); - } - - requestFileSuccess(file) { - return super.requestFileSuccess(file); - } - } - - global.BlobDockerfileSelector = BlobDockerfileSelector; -})(); diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/blob_dockerfile_selectors.js new file mode 100644 index 00000000000..9cee79fa5d5 --- /dev/null +++ b/app/assets/javascripts/blob/blob_dockerfile_selectors.js @@ -0,0 +1,27 @@ +(() => { + const global = window.gl || (window.gl = {}); + + class BlobDockerfileSelectors { + constructor({ editor, $dropdowns } = {}) { + this.editor = editor; + this.$dropdowns = $dropdowns || $('.js-dockerfile-selector'); + this.initSelectors(); + } + + initSelectors() { + const editor = this.editor; + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + return new gl.BlobDockerfileSelector({ + editor, + pattern: /(Dockerfile)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'), + dropdown: $dropdown, + }); + }); + } + } + + global.BlobDockerfileSelectors = BlobDockerfileSelectors; +})(); diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 deleted file mode 100644 index 9cee79fa5d5..00000000000 --- a/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 +++ /dev/null @@ -1,27 +0,0 @@ -(() => { - const global = window.gl || (window.gl = {}); - - class BlobDockerfileSelectors { - constructor({ editor, $dropdowns } = {}) { - this.editor = editor; - this.$dropdowns = $dropdowns || $('.js-dockerfile-selector'); - this.initSelectors(); - } - - initSelectors() { - const editor = this.editor; - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - return new gl.BlobDockerfileSelector({ - editor, - pattern: /(Dockerfile)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'), - dropdown: $dropdown, - }); - }); - } - } - - global.BlobDockerfileSelectors = BlobDockerfileSelectors; -})(); diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js new file mode 100644 index 00000000000..c5067b0feae --- /dev/null +++ b/app/assets/javascripts/blob/blob_license_selectors.js @@ -0,0 +1,23 @@ +/* eslint-disable no-unused-vars, no-param-reassign */ +/* global BlobLicenseSelector */ + +((global) => { + class BlobLicenseSelectors { + constructor({ $dropdowns, editor }) { + this.$dropdowns = $('.js-license-selector'); + this.editor = editor; + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + return new BlobLicenseSelector({ + editor, + pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-license-selector-wrap'), + dropdown: $dropdown, + }); + }); + } + } + + global.BlobLicenseSelectors = BlobLicenseSelectors; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6 deleted file mode 100644 index c5067b0feae..00000000000 --- a/app/assets/javascripts/blob/blob_license_selectors.js.es6 +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable no-unused-vars, no-param-reassign */ -/* global BlobLicenseSelector */ - -((global) => { - class BlobLicenseSelectors { - constructor({ $dropdowns, editor }) { - this.$dropdowns = $('.js-license-selector'); - this.editor = editor; - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - return new BlobLicenseSelector({ - editor, - pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-license-selector-wrap'), - dropdown: $dropdown, - }); - }); - } - } - - global.BlobLicenseSelectors = BlobLicenseSelectors; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js new file mode 100644 index 00000000000..7e03ec3b391 --- /dev/null +++ b/app/assets/javascripts/blob/template_selector.js @@ -0,0 +1,101 @@ +/* eslint-disable comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, no-param-reassign, max-len */ + +((global) => { + class TemplateSelector { + constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) { + this.onClick = this.onClick.bind(this); + this.dropdown = dropdown; + this.data = data; + this.pattern = pattern; + this.wrapper = wrapper; + this.editor = editor; + this.fileEndpoint = fileEndpoint; + this.$input = $input || $('#file_name'); + this.dropdownIcon = $('.fa-chevron-down', this.dropdown); + this.buildDropdown(); + this.bindEvents(); + this.onFilenameUpdate(); + + this.autosizeUpdateEvent = document.createEvent('Event'); + this.autosizeUpdateEvent.initEvent('autosize:update', true, false); + } + + buildDropdown() { + return this.dropdown.glDropdown({ + data: this.data, + filterable: true, + selectable: true, + toggleLabel: this.toggleLabel, + search: { + fields: ['name'] + }, + clicked: this.onClick, + text: function(item) { + return item.name; + } + }); + } + + bindEvents() { + return this.$input.on('keyup blur', (e) => this.onFilenameUpdate()); + } + + toggleLabel(item) { + return item.name; + } + + onFilenameUpdate() { + var filenameMatches; + if (!this.$input.length) { + return; + } + filenameMatches = this.pattern.test(this.$input.val().trim()); + if (!filenameMatches) { + this.wrapper.addClass('hidden'); + return; + } + return this.wrapper.removeClass('hidden'); + } + + onClick(item, el, e) { + e.preventDefault(); + return this.requestFile(item); + } + + requestFile(item) { + // This `requestFile` method is an abstract method that should + // be added by all subclasses. + } + + // To be implemented on the extending class + // e.g. + // Api.gitignoreText item.name, @requestFileSuccess.bind(@) + requestFileSuccess(file, { skipFocus } = {}) { + if (!file) return; + + const oldValue = this.editor.getValue(); + const newValue = file.content; + + this.editor.setValue(newValue, 1); + if (!skipFocus) this.editor.focus(); + + if (this.editor instanceof jQuery) { + this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); + } + } + + startLoadingSpinner() { + this.dropdownIcon + .addClass('fa-spinner fa-spin') + .removeClass('fa-chevron-down'); + } + + stopLoadingSpinner() { + this.dropdownIcon + .addClass('fa-chevron-down') + .removeClass('fa-spinner fa-spin'); + } + } + + global.TemplateSelector = TemplateSelector; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6 deleted file mode 100644 index 7e03ec3b391..00000000000 --- a/app/assets/javascripts/blob/template_selector.js.es6 +++ /dev/null @@ -1,101 +0,0 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, no-param-reassign, max-len */ - -((global) => { - class TemplateSelector { - constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) { - this.onClick = this.onClick.bind(this); - this.dropdown = dropdown; - this.data = data; - this.pattern = pattern; - this.wrapper = wrapper; - this.editor = editor; - this.fileEndpoint = fileEndpoint; - this.$input = $input || $('#file_name'); - this.dropdownIcon = $('.fa-chevron-down', this.dropdown); - this.buildDropdown(); - this.bindEvents(); - this.onFilenameUpdate(); - - this.autosizeUpdateEvent = document.createEvent('Event'); - this.autosizeUpdateEvent.initEvent('autosize:update', true, false); - } - - buildDropdown() { - return this.dropdown.glDropdown({ - data: this.data, - filterable: true, - selectable: true, - toggleLabel: this.toggleLabel, - search: { - fields: ['name'] - }, - clicked: this.onClick, - text: function(item) { - return item.name; - } - }); - } - - bindEvents() { - return this.$input.on('keyup blur', (e) => this.onFilenameUpdate()); - } - - toggleLabel(item) { - return item.name; - } - - onFilenameUpdate() { - var filenameMatches; - if (!this.$input.length) { - return; - } - filenameMatches = this.pattern.test(this.$input.val().trim()); - if (!filenameMatches) { - this.wrapper.addClass('hidden'); - return; - } - return this.wrapper.removeClass('hidden'); - } - - onClick(item, el, e) { - e.preventDefault(); - return this.requestFile(item); - } - - requestFile(item) { - // This `requestFile` method is an abstract method that should - // be added by all subclasses. - } - - // To be implemented on the extending class - // e.g. - // Api.gitignoreText item.name, @requestFileSuccess.bind(@) - requestFileSuccess(file, { skipFocus } = {}) { - if (!file) return; - - const oldValue = this.editor.getValue(); - const newValue = file.content; - - this.editor.setValue(newValue, 1); - if (!skipFocus) this.editor.focus(); - - if (this.editor instanceof jQuery) { - this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); - } - } - - startLoadingSpinner() { - this.dropdownIcon - .addClass('fa-spinner fa-spin') - .removeClass('fa-chevron-down'); - } - - stopLoadingSpinner() { - this.dropdownIcon - .addClass('fa-chevron-down') - .removeClass('fa-spinner fa-spin'); - } - } - - global.TemplateSelector = TemplateSelector; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js new file mode 100644 index 00000000000..c29f98b99c9 --- /dev/null +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -0,0 +1,112 @@ +/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, import/newline-after-import, no-multi-spaces, max-len */ +/* global Vue */ +/* global BoardService */ + +function requireAll(context) { return context.keys().map(context); } + +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +window.Sortable = require('vendor/Sortable'); +requireAll(require.context('./models', true, /^\.\/.*\.js$/)); +requireAll(require.context('./stores', true, /^\.\/.*\.js$/)); +requireAll(require.context('./services', true, /^\.\/.*\.js$/)); +requireAll(require.context('./mixins', true, /^\.\/.*\.js$/)); +requireAll(require.context('./filters', true, /^\.\/.*\.js$/)); +require('./components/board'); +require('./components/board_sidebar'); +require('./components/new_list_dropdown'); +require('./components/modal/index'); +require('../vue_shared/vue_resource_interceptor'); + +$(() => { + const $boardApp = document.getElementById('board-app'); + const Store = gl.issueBoards.BoardsStore; + const ModalStore = gl.issueBoards.ModalStore; + + window.gl = window.gl || {}; + + if (gl.IssueBoardsApp) { + gl.IssueBoardsApp.$destroy(true); + } + + Store.create(); + + gl.IssueBoardsApp = new Vue({ + el: $boardApp, + components: { + 'board': gl.issueBoards.Board, + 'board-sidebar': gl.issueBoards.BoardSidebar, + 'board-add-issues-modal': gl.issueBoards.IssuesModal, + }, + data: { + state: Store.state, + loading: true, + endpoint: $boardApp.dataset.endpoint, + boardId: $boardApp.dataset.boardId, + disabled: $boardApp.dataset.disabled === 'true', + issueLinkBase: $boardApp.dataset.issueLinkBase, + rootPath: $boardApp.dataset.rootPath, + bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, + detailIssue: Store.detail + }, + computed: { + detailIssueVisible () { + return Object.keys(this.detailIssue.issue).length; + }, + }, + created () { + gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); + }, + mounted () { + Store.disabled = this.disabled; + gl.boardService.all() + .then((resp) => { + resp.json().forEach((board) => { + const list = Store.addList(board); + + if (list.type === 'done') { + list.position = Infinity; + } + }); + + this.state.lists = _.sortBy(this.state.lists, 'position'); + + Store.addBlankState(); + this.loading = false; + }); + } + }); + + gl.IssueBoardsSearch = new Vue({ + el: document.getElementById('js-boards-search'), + data: { + filters: Store.state.filters + }, + mounted () { + gl.issueBoards.newListDropdownInit(); + } + }); + + gl.IssueBoardsModalAddBtn = new Vue({ + mixins: [gl.issueBoards.ModalMixins], + el: document.getElementById('js-add-issues-btn'), + data: { + modal: ModalStore.store, + store: Store.state, + }, + computed: { + disabled() { + return Store.shouldAddBlankState(); + }, + }, + template: ` + + `, + }); +}); diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 deleted file mode 100644 index c345fb6ce14..00000000000 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ /dev/null @@ -1,112 +0,0 @@ -/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, import/newline-after-import, no-multi-spaces, max-len */ -/* global Vue */ -/* global BoardService */ - -function requireAll(context) { return context.keys().map(context); } - -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); -window.Sortable = require('vendor/Sortable'); -requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./mixins', true, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./filters', true, /^\.\/.*\.(js|es6)$/)); -require('./components/board'); -require('./components/board_sidebar'); -require('./components/new_list_dropdown'); -require('./components/modal/index'); -require('../vue_shared/vue_resource_interceptor'); - -$(() => { - const $boardApp = document.getElementById('board-app'); - const Store = gl.issueBoards.BoardsStore; - const ModalStore = gl.issueBoards.ModalStore; - - window.gl = window.gl || {}; - - if (gl.IssueBoardsApp) { - gl.IssueBoardsApp.$destroy(true); - } - - Store.create(); - - gl.IssueBoardsApp = new Vue({ - el: $boardApp, - components: { - 'board': gl.issueBoards.Board, - 'board-sidebar': gl.issueBoards.BoardSidebar, - 'board-add-issues-modal': gl.issueBoards.IssuesModal, - }, - data: { - state: Store.state, - loading: true, - endpoint: $boardApp.dataset.endpoint, - boardId: $boardApp.dataset.boardId, - disabled: $boardApp.dataset.disabled === 'true', - issueLinkBase: $boardApp.dataset.issueLinkBase, - rootPath: $boardApp.dataset.rootPath, - bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, - detailIssue: Store.detail - }, - computed: { - detailIssueVisible () { - return Object.keys(this.detailIssue.issue).length; - }, - }, - created () { - gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); - }, - mounted () { - Store.disabled = this.disabled; - gl.boardService.all() - .then((resp) => { - resp.json().forEach((board) => { - const list = Store.addList(board); - - if (list.type === 'done') { - list.position = Infinity; - } - }); - - this.state.lists = _.sortBy(this.state.lists, 'position'); - - Store.addBlankState(); - this.loading = false; - }); - } - }); - - gl.IssueBoardsSearch = new Vue({ - el: document.getElementById('js-boards-search'), - data: { - filters: Store.state.filters - }, - mounted () { - gl.issueBoards.newListDropdownInit(); - } - }); - - gl.IssueBoardsModalAddBtn = new Vue({ - mixins: [gl.issueBoards.ModalMixins], - el: document.getElementById('js-add-issues-btn'), - data: { - modal: ModalStore.store, - store: Store.state, - }, - computed: { - disabled() { - return Store.shouldAddBlankState(); - }, - }, - template: ` - - `, - }); -}); diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js new file mode 100644 index 00000000000..18324de18b3 --- /dev/null +++ b/app/assets/javascripts/boards/components/board.js @@ -0,0 +1,105 @@ +/* eslint-disable comma-dangle, space-before-function-paren, one-var */ +/* global Vue */ +/* global Sortable */ + +require('./board_blank_state'); +require('./board_delete'); +require('./board_list'); + +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.Board = Vue.extend({ + template: '#js-board-template', + components: { + 'board-list': gl.issueBoards.BoardList, + 'board-delete': gl.issueBoards.BoardDelete, + 'board-blank-state': gl.issueBoards.BoardBlankState + }, + props: { + list: Object, + disabled: Boolean, + issueLinkBase: String, + rootPath: String, + }, + data () { + return { + detailIssue: Store.detail, + filters: Store.state.filters, + }; + }, + watch: { + filters: { + handler () { + this.list.page = 1; + this.list.getIssues(true); + }, + deep: true + }, + detailIssue: { + handler () { + if (!Object.keys(this.detailIssue.issue).length) return; + + const issue = this.list.findIssue(this.detailIssue.issue.id); + + if (issue) { + const offsetLeft = this.$el.offsetLeft; + const boardsList = document.querySelectorAll('.boards-list')[0]; + const left = boardsList.scrollLeft - offsetLeft; + let right = (offsetLeft + this.$el.offsetWidth); + + if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { + // -290 here because width of boardsList is animating so therefore + // getting the width here is incorrect + // 290 is the width of the sidebar + right -= (boardsList.offsetWidth - 290); + } else { + right -= boardsList.offsetWidth; + } + + if (right - boardsList.scrollLeft > 0) { + $(boardsList).animate({ + scrollLeft: right + }, this.sortableOptions.animation); + } else if (left > 0) { + $(boardsList).animate({ + scrollLeft: offsetLeft + }, this.sortableOptions.animation); + } + } + }, + deep: true + } + }, + methods: { + showNewIssueForm() { + this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; + } + }, + mounted () { + this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ + disabled: this.disabled, + group: 'boards', + draggable: '.is-draggable', + handle: '.js-board-handle', + onEnd: (e) => { + gl.issueBoards.onEnd(); + + if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { + const order = this.sortable.toArray(); + const list = Store.findList('id', parseInt(e.item.dataset.id, 10)); + + this.$nextTick(() => { + Store.moveList(list, order); + }); + } + } + }); + + this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); + }, + }); +})(); diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 deleted file mode 100644 index 18324de18b3..00000000000 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ /dev/null @@ -1,105 +0,0 @@ -/* eslint-disable comma-dangle, space-before-function-paren, one-var */ -/* global Vue */ -/* global Sortable */ - -require('./board_blank_state'); -require('./board_delete'); -require('./board_list'); - -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.Board = Vue.extend({ - template: '#js-board-template', - components: { - 'board-list': gl.issueBoards.BoardList, - 'board-delete': gl.issueBoards.BoardDelete, - 'board-blank-state': gl.issueBoards.BoardBlankState - }, - props: { - list: Object, - disabled: Boolean, - issueLinkBase: String, - rootPath: String, - }, - data () { - return { - detailIssue: Store.detail, - filters: Store.state.filters, - }; - }, - watch: { - filters: { - handler () { - this.list.page = 1; - this.list.getIssues(true); - }, - deep: true - }, - detailIssue: { - handler () { - if (!Object.keys(this.detailIssue.issue).length) return; - - const issue = this.list.findIssue(this.detailIssue.issue.id); - - if (issue) { - const offsetLeft = this.$el.offsetLeft; - const boardsList = document.querySelectorAll('.boards-list')[0]; - const left = boardsList.scrollLeft - offsetLeft; - let right = (offsetLeft + this.$el.offsetWidth); - - if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { - // -290 here because width of boardsList is animating so therefore - // getting the width here is incorrect - // 290 is the width of the sidebar - right -= (boardsList.offsetWidth - 290); - } else { - right -= boardsList.offsetWidth; - } - - if (right - boardsList.scrollLeft > 0) { - $(boardsList).animate({ - scrollLeft: right - }, this.sortableOptions.animation); - } else if (left > 0) { - $(boardsList).animate({ - scrollLeft: offsetLeft - }, this.sortableOptions.animation); - } - } - }, - deep: true - } - }, - methods: { - showNewIssueForm() { - this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; - } - }, - mounted () { - this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ - disabled: this.disabled, - group: 'boards', - draggable: '.is-draggable', - handle: '.js-board-handle', - onEnd: (e) => { - gl.issueBoards.onEnd(); - - if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { - const order = this.sortable.toArray(); - const list = Store.findList('id', parseInt(e.item.dataset.id, 10)); - - this.$nextTick(() => { - Store.moveList(list, order); - }); - } - } - }); - - this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); - }, - }); -})(); diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js new file mode 100644 index 00000000000..d76314c1892 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_blank_state.js @@ -0,0 +1,53 @@ +/* eslint-disable space-before-function-paren, comma-dangle */ +/* global Vue */ +/* global ListLabel */ + +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardBlankState = Vue.extend({ + data () { + return { + predefinedLabels: [ + new ListLabel({ title: 'To Do', color: '#F0AD4E' }), + new ListLabel({ title: 'Doing', color: '#5CB85C' }) + ] + }; + }, + methods: { + addDefaultLists () { + this.clearBlankState(); + + this.predefinedLabels.forEach((label, i) => { + Store.addList({ + title: label.title, + position: i, + list_type: 'label', + label: { + title: label.title, + color: label.color + } + }); + }); + + Store.state.lists = _.sortBy(Store.state.lists, 'position'); + + // Save the labels + gl.boardService.generateDefaultLists() + .then((resp) => { + resp.json().forEach((listObj) => { + const list = Store.findList('title', listObj.title); + + list.id = listObj.id; + list.label.id = listObj.label.id; + list.getIssues(); + }); + }); + }, + clearBlankState: Store.removeBlankState.bind(Store) + } + }); +})(); diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6 deleted file mode 100644 index d76314c1892..00000000000 --- a/app/assets/javascripts/boards/components/board_blank_state.js.es6 +++ /dev/null @@ -1,53 +0,0 @@ -/* eslint-disable space-before-function-paren, comma-dangle */ -/* global Vue */ -/* global ListLabel */ - -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.BoardBlankState = Vue.extend({ - data () { - return { - predefinedLabels: [ - new ListLabel({ title: 'To Do', color: '#F0AD4E' }), - new ListLabel({ title: 'Doing', color: '#5CB85C' }) - ] - }; - }, - methods: { - addDefaultLists () { - this.clearBlankState(); - - this.predefinedLabels.forEach((label, i) => { - Store.addList({ - title: label.title, - position: i, - list_type: 'label', - label: { - title: label.title, - color: label.color - } - }); - }); - - Store.state.lists = _.sortBy(Store.state.lists, 'position'); - - // Save the labels - gl.boardService.generateDefaultLists() - .then((resp) => { - resp.json().forEach((listObj) => { - const list = Store.findList('title', listObj.title); - - list.id = listObj.id; - list.label.id = listObj.label.id; - list.getIssues(); - }); - }); - }, - clearBlankState: Store.removeBlankState.bind(Store) - } - }); -})(); diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js new file mode 100644 index 00000000000..0ea66bd027c --- /dev/null +++ b/app/assets/javascripts/boards/components/board_card.js @@ -0,0 +1,61 @@ +/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */ +/* global Vue */ + +require('./issue_card_inner'); + +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardCard = Vue.extend({ + template: '#js-board-list-card', + components: { + 'issue-card-inner': gl.issueBoards.IssueCardInner, + }, + props: { + list: Object, + issue: Object, + issueLinkBase: String, + disabled: Boolean, + index: Number, + rootPath: String, + }, + data () { + return { + showDetail: false, + detailIssue: Store.detail + }; + }, + computed: { + issueDetailVisible () { + return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; + } + }, + methods: { + mouseDown () { + this.showDetail = true; + }, + mouseMove() { + this.showDetail = false; + }, + showIssue (e) { + const targetTagName = e.target.tagName.toLowerCase(); + + if (targetTagName === 'a' || targetTagName === 'button') return; + + if (this.showDetail) { + this.showDetail = false; + + if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { + Store.detail.issue = {}; + } else { + Store.detail.issue = this.issue; + Store.detail.list = this.list; + } + } + } + } + }); +})(); diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 deleted file mode 100644 index 0ea66bd027c..00000000000 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */ -/* global Vue */ - -require('./issue_card_inner'); - -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.BoardCard = Vue.extend({ - template: '#js-board-list-card', - components: { - 'issue-card-inner': gl.issueBoards.IssueCardInner, - }, - props: { - list: Object, - issue: Object, - issueLinkBase: String, - disabled: Boolean, - index: Number, - rootPath: String, - }, - data () { - return { - showDetail: false, - detailIssue: Store.detail - }; - }, - computed: { - issueDetailVisible () { - return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; - } - }, - methods: { - mouseDown () { - this.showDetail = true; - }, - mouseMove() { - this.showDetail = false; - }, - showIssue (e) { - const targetTagName = e.target.tagName.toLowerCase(); - - if (targetTagName === 'a' || targetTagName === 'button') return; - - if (this.showDetail) { - this.showDetail = false; - - if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { - Store.detail.issue = {}; - } else { - Store.detail.issue = this.issue; - Store.detail.list = this.list; - } - } - } - } - }); -})(); diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js new file mode 100644 index 00000000000..861600424a5 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_delete.js @@ -0,0 +1,22 @@ +/* eslint-disable comma-dangle, space-before-function-paren, no-alert */ +/* global Vue */ + +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardDelete = Vue.extend({ + props: { + list: Object + }, + methods: { + deleteBoard () { + $(this.$el).tooltip('hide'); + + if (confirm('Are you sure you want to delete this list?')) { + this.list.destroy(); + } + } + } + }); +})(); diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6 deleted file mode 100644 index 861600424a5..00000000000 --- a/app/assets/javascripts/boards/components/board_delete.js.es6 +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable comma-dangle, space-before-function-paren, no-alert */ -/* global Vue */ - -(() => { - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.BoardDelete = Vue.extend({ - props: { - list: Object - }, - methods: { - deleteBoard () { - $(this.$el).tooltip('hide'); - - if (confirm('Are you sure you want to delete this list?')) { - this.list.destroy(); - } - } - } - }); -})(); diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js new file mode 100644 index 00000000000..60b0a30af3f --- /dev/null +++ b/app/assets/javascripts/boards/components/board_list.js @@ -0,0 +1,120 @@ +/* eslint-disable comma-dangle, space-before-function-paren, max-len */ +/* global Vue */ +/* global Sortable */ + +require('./board_card'); +require('./board_new_issue'); + +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardList = Vue.extend({ + template: '#js-board-list-template', + components: { + 'board-card': gl.issueBoards.BoardCard, + 'board-new-issue': gl.issueBoards.BoardNewIssue + }, + props: { + disabled: Boolean, + list: Object, + issues: Array, + loading: Boolean, + issueLinkBase: String, + rootPath: String, + }, + data () { + return { + scrollOffset: 250, + filters: Store.state.filters, + showCount: false, + showIssueForm: false + }; + }, + watch: { + filters: { + handler () { + this.list.loadingMore = false; + this.$refs.list.scrollTop = 0; + }, + deep: true + }, + issues () { + this.$nextTick(() => { + if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) { + this.list.page += 1; + this.list.getIssues(false); + } + + if (this.scrollHeight() > this.listHeight()) { + this.showCount = true; + } else { + this.showCount = false; + } + }); + } + }, + computed: { + orderedIssues () { + return _.sortBy(this.issues, 'priority'); + }, + }, + methods: { + listHeight () { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight () { + return this.$refs.list.scrollHeight; + }, + scrollTop () { + return this.$refs.list.scrollTop + this.listHeight(); + }, + loadNextPage () { + const getIssues = this.list.nextPage(); + + if (getIssues) { + this.list.loadingMore = true; + getIssues.then(() => { + this.list.loadingMore = false; + }); + } + }, + }, + mounted () { + const options = gl.issueBoards.getBoardSortableDefaultOptions({ + scroll: document.querySelectorAll('.boards-list')[0], + group: 'issues', + sort: false, + disabled: this.disabled, + filter: '.board-list-count, .is-disabled', + onStart: (e) => { + const card = this.$refs.issue[e.oldIndex]; + + card.showDetail = false; + Store.moving.list = card.list; + Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); + + gl.issueBoards.onStart(); + }, + onAdd: (e) => { + gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); + + this.$nextTick(() => { + e.item.remove(); + }); + }, + }); + + this.sortable = Sortable.create(this.$refs.list, options); + + // Scroll event on list to load more + this.$refs.list.onscroll = () => { + if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { + this.loadNextPage(); + } + }; + } + }); +})(); diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 deleted file mode 100644 index 60b0a30af3f..00000000000 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ /dev/null @@ -1,120 +0,0 @@ -/* eslint-disable comma-dangle, space-before-function-paren, max-len */ -/* global Vue */ -/* global Sortable */ - -require('./board_card'); -require('./board_new_issue'); - -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.BoardList = Vue.extend({ - template: '#js-board-list-template', - components: { - 'board-card': gl.issueBoards.BoardCard, - 'board-new-issue': gl.issueBoards.BoardNewIssue - }, - props: { - disabled: Boolean, - list: Object, - issues: Array, - loading: Boolean, - issueLinkBase: String, - rootPath: String, - }, - data () { - return { - scrollOffset: 250, - filters: Store.state.filters, - showCount: false, - showIssueForm: false - }; - }, - watch: { - filters: { - handler () { - this.list.loadingMore = false; - this.$refs.list.scrollTop = 0; - }, - deep: true - }, - issues () { - this.$nextTick(() => { - if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) { - this.list.page += 1; - this.list.getIssues(false); - } - - if (this.scrollHeight() > this.listHeight()) { - this.showCount = true; - } else { - this.showCount = false; - } - }); - } - }, - computed: { - orderedIssues () { - return _.sortBy(this.issues, 'priority'); - }, - }, - methods: { - listHeight () { - return this.$refs.list.getBoundingClientRect().height; - }, - scrollHeight () { - return this.$refs.list.scrollHeight; - }, - scrollTop () { - return this.$refs.list.scrollTop + this.listHeight(); - }, - loadNextPage () { - const getIssues = this.list.nextPage(); - - if (getIssues) { - this.list.loadingMore = true; - getIssues.then(() => { - this.list.loadingMore = false; - }); - } - }, - }, - mounted () { - const options = gl.issueBoards.getBoardSortableDefaultOptions({ - scroll: document.querySelectorAll('.boards-list')[0], - group: 'issues', - sort: false, - disabled: this.disabled, - filter: '.board-list-count, .is-disabled', - onStart: (e) => { - const card = this.$refs.issue[e.oldIndex]; - - card.showDetail = false; - Store.moving.list = card.list; - Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); - - gl.issueBoards.onStart(); - }, - onAdd: (e) => { - gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); - - this.$nextTick(() => { - e.item.remove(); - }); - }, - }); - - this.sortable = Sortable.create(this.$refs.list, options); - - // Scroll event on list to load more - this.$refs.list.onscroll = () => { - if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { - this.loadNextPage(); - } - }; - } - }); -})(); diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js new file mode 100644 index 00000000000..b5c14a198ba --- /dev/null +++ b/app/assets/javascripts/boards/components/board_new_issue.js @@ -0,0 +1,64 @@ +/* eslint-disable comma-dangle, no-unused-vars */ +/* global Vue */ +/* global ListIssue */ + +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + + gl.issueBoards.BoardNewIssue = Vue.extend({ + props: { + list: Object, + }, + data() { + return { + title: '', + error: false + }; + }, + methods: { + submit(e) { + e.preventDefault(); + if (this.title.trim() === '') return; + + this.error = false; + + const labels = this.list.label ? [this.list.label] : []; + const issue = new ListIssue({ + title: this.title, + labels, + subscribed: true + }); + + this.list.newIssue(issue) + .then((data) => { + // Need this because our jQuery very kindly disables buttons on ALL form submissions + $(this.$refs.submitButton).enable(); + + Store.detail.issue = issue; + Store.detail.list = this.list; + }) + .catch(() => { + // Need this because our jQuery very kindly disables buttons on ALL form submissions + $(this.$refs.submitButton).enable(); + + // Remove the issue + this.list.removeIssue(issue); + + // Show error message + this.error = true; + }); + + this.cancel(); + }, + cancel() { + this.title = ''; + this.$parent.showIssueForm = false; + } + }, + mounted() { + this.$refs.input.focus(); + }, + }); +})(); diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 deleted file mode 100644 index b5c14a198ba..00000000000 --- a/app/assets/javascripts/boards/components/board_new_issue.js.es6 +++ /dev/null @@ -1,64 +0,0 @@ -/* eslint-disable comma-dangle, no-unused-vars */ -/* global Vue */ -/* global ListIssue */ - -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - - gl.issueBoards.BoardNewIssue = Vue.extend({ - props: { - list: Object, - }, - data() { - return { - title: '', - error: false - }; - }, - methods: { - submit(e) { - e.preventDefault(); - if (this.title.trim() === '') return; - - this.error = false; - - const labels = this.list.label ? [this.list.label] : []; - const issue = new ListIssue({ - title: this.title, - labels, - subscribed: true - }); - - this.list.newIssue(issue) - .then((data) => { - // Need this because our jQuery very kindly disables buttons on ALL form submissions - $(this.$refs.submitButton).enable(); - - Store.detail.issue = issue; - Store.detail.list = this.list; - }) - .catch(() => { - // Need this because our jQuery very kindly disables buttons on ALL form submissions - $(this.$refs.submitButton).enable(); - - // Remove the issue - this.list.removeIssue(issue); - - // Show error message - this.error = true; - }); - - this.cancel(); - }, - cancel() { - this.title = ''; - this.$parent.showIssueForm = false; - } - }, - mounted() { - this.$refs.input.focus(); - }, - }); -})(); diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js new file mode 100644 index 00000000000..dfc6eed785c --- /dev/null +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -0,0 +1,72 @@ +/* eslint-disable comma-dangle, space-before-function-paren, no-new */ +/* global Vue */ +/* global IssuableContext */ +/* global MilestoneSelect */ +/* global LabelsSelect */ +/* global Sidebar */ + +require('./sidebar/remove_issue'); + +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardSidebar = Vue.extend({ + props: { + currentUser: Object + }, + data() { + return { + detail: Store.detail, + issue: {}, + list: {}, + }; + }, + computed: { + showSidebar () { + return Object.keys(this.issue).length; + } + }, + watch: { + detail: { + handler () { + if (this.issue.id !== this.detail.issue.id) { + $('.js-issue-board-sidebar', this.$el).each((i, el) => { + $(el).data('glDropdown').clearMenu(); + }); + } + + this.issue = this.detail.issue; + this.list = this.detail.list; + }, + deep: true + }, + issue () { + if (this.showSidebar) { + this.$nextTick(() => { + $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0); + $('.right-sidebar').getNiceScroll().resize(); + }); + } + } + }, + methods: { + closeSidebar () { + this.detail.issue = {}; + } + }, + mounted () { + new IssuableContext(this.currentUser); + new MilestoneSelect(); + new gl.DueDateSelectors(); + new LabelsSelect(); + new Sidebar(); + gl.Subscription.bindAll('.subscription'); + }, + components: { + removeBtn: gl.issueBoards.RemoveIssueBtn, + }, + }); +})(); diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 deleted file mode 100644 index dfc6eed785c..00000000000 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ /dev/null @@ -1,72 +0,0 @@ -/* eslint-disable comma-dangle, space-before-function-paren, no-new */ -/* global Vue */ -/* global IssuableContext */ -/* global MilestoneSelect */ -/* global LabelsSelect */ -/* global Sidebar */ - -require('./sidebar/remove_issue'); - -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.BoardSidebar = Vue.extend({ - props: { - currentUser: Object - }, - data() { - return { - detail: Store.detail, - issue: {}, - list: {}, - }; - }, - computed: { - showSidebar () { - return Object.keys(this.issue).length; - } - }, - watch: { - detail: { - handler () { - if (this.issue.id !== this.detail.issue.id) { - $('.js-issue-board-sidebar', this.$el).each((i, el) => { - $(el).data('glDropdown').clearMenu(); - }); - } - - this.issue = this.detail.issue; - this.list = this.detail.list; - }, - deep: true - }, - issue () { - if (this.showSidebar) { - this.$nextTick(() => { - $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0); - $('.right-sidebar').getNiceScroll().resize(); - }); - } - } - }, - methods: { - closeSidebar () { - this.detail.issue = {}; - } - }, - mounted () { - new IssuableContext(this.currentUser); - new MilestoneSelect(); - new gl.DueDateSelectors(); - new LabelsSelect(); - new Sidebar(); - gl.Subscription.bindAll('.subscription'); - }, - components: { - removeBtn: gl.issueBoards.RemoveIssueBtn, - }, - }); -})(); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js new file mode 100644 index 00000000000..22a8b971ff8 --- /dev/null +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -0,0 +1,111 @@ +/* global Vue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.IssueCardInner = Vue.extend({ + props: { + issue: { + type: Object, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + list: { + type: Object, + required: false, + }, + rootPath: { + type: String, + required: true, + }, + }, + methods: { + showLabel(label) { + if (!this.list) return true; + + return !this.list.label || label.id !== this.list.label.id; + }, + filterByLabel(label, e) { + let labelToggleText = label.title; + const labelIndex = Store.state.filters.label_name.indexOf(label.title); + $(e.currentTarget).tooltip('hide'); + + if (labelIndex === -1) { + Store.state.filters.label_name.push(label.title); + $('.labels-filter').prepend(``); + } else { + Store.state.filters.label_name.splice(labelIndex, 1); + labelToggleText = Store.state.filters.label_name[0]; + $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove(); + } + + const selectedLabels = Store.state.filters.label_name; + if (selectedLabels.length === 0) { + labelToggleText = 'Label'; + } else if (selectedLabels.length > 1) { + labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`; + } + + $('.labels-filter .dropdown-toggle-text').text(labelToggleText); + + Store.updateFiltersUrl(); + }, + labelStyle(label) { + return { + backgroundColor: label.color, + color: label.textColor, + }; + }, + }, + template: ` +
+

+ + + {{ issue.title }} + +

+ +
+ `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 deleted file mode 100644 index 22a8b971ff8..00000000000 --- a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 +++ /dev/null @@ -1,111 +0,0 @@ -/* global Vue */ -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.IssueCardInner = Vue.extend({ - props: { - issue: { - type: Object, - required: true, - }, - issueLinkBase: { - type: String, - required: true, - }, - list: { - type: Object, - required: false, - }, - rootPath: { - type: String, - required: true, - }, - }, - methods: { - showLabel(label) { - if (!this.list) return true; - - return !this.list.label || label.id !== this.list.label.id; - }, - filterByLabel(label, e) { - let labelToggleText = label.title; - const labelIndex = Store.state.filters.label_name.indexOf(label.title); - $(e.currentTarget).tooltip('hide'); - - if (labelIndex === -1) { - Store.state.filters.label_name.push(label.title); - $('.labels-filter').prepend(``); - } else { - Store.state.filters.label_name.splice(labelIndex, 1); - labelToggleText = Store.state.filters.label_name[0]; - $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove(); - } - - const selectedLabels = Store.state.filters.label_name; - if (selectedLabels.length === 0) { - labelToggleText = 'Label'; - } else if (selectedLabels.length > 1) { - labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`; - } - - $('.labels-filter .dropdown-toggle-text').text(labelToggleText); - - Store.updateFiltersUrl(); - }, - labelStyle(label) { - return { - backgroundColor: label.color, - color: label.textColor, - }; - }, - }, - template: ` -
-

- - - {{ issue.title }} - -

- -
- `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js new file mode 100644 index 00000000000..9538f5b69e9 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -0,0 +1,70 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalEmptyState = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return ModalStore.store; + }, + props: { + image: { + type: String, + required: true, + }, + newIssuePath: { + type: String, + required: true, + }, + }, + computed: { + contents() { + const obj = { + title: 'You haven\'t added any issues to your project yet', + content: ` + An issue can be a bug, a todo or a feature request that needs to be + discussed in a project. Besides, issues are searchable and filterable. + `, + }; + + if (this.activeTab === 'selected') { + obj.title = 'You haven\'t selected any issues yet'; + obj.content = ` + Go back to All issues and select some issues + to add to your board. + `; + } + + return obj; + }, + }, + template: ` +
+
+
+ +
+
+
+

{{ contents.title }}

+

+ + New issue + + +
+
+
+
+ `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 deleted file mode 100644 index 9538f5b69e9..00000000000 --- a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 +++ /dev/null @@ -1,70 +0,0 @@ -/* global Vue */ -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - gl.issueBoards.ModalEmptyState = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], - data() { - return ModalStore.store; - }, - props: { - image: { - type: String, - required: true, - }, - newIssuePath: { - type: String, - required: true, - }, - }, - computed: { - contents() { - const obj = { - title: 'You haven\'t added any issues to your project yet', - content: ` - An issue can be a bug, a todo or a feature request that needs to be - discussed in a project. Besides, issues are searchable and filterable. - `, - }; - - if (this.activeTab === 'selected') { - obj.title = 'You haven\'t selected any issues yet'; - obj.content = ` - Go back to All issues and select some issues - to add to your board. - `; - } - - return obj; - }, - }, - template: ` -
-
-
- -
-
-
-

{{ contents.title }}

-

- - New issue - - -
-
-
-
- `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js new file mode 100644 index 00000000000..6de06811d94 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters.js @@ -0,0 +1,49 @@ +/* global Vue */ +const userFilter = require('./filters/user'); +const milestoneFilter = require('./filters/milestone'); +const labelFilter = require('./filters/label'); + +module.exports = Vue.extend({ + name: 'modal-filters', + props: { + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, + destroyed() { + gl.issueBoards.ModalStore.setDefaultFilter(); + }, + components: { + userFilter, + milestoneFilter, + labelFilter, + }, + template: ` + + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6 deleted file mode 100644 index 6de06811d94..00000000000 --- a/app/assets/javascripts/boards/components/modal/filters.js.es6 +++ /dev/null @@ -1,49 +0,0 @@ -/* global Vue */ -const userFilter = require('./filters/user'); -const milestoneFilter = require('./filters/milestone'); -const labelFilter = require('./filters/label'); - -module.exports = Vue.extend({ - name: 'modal-filters', - props: { - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, - }, - destroyed() { - gl.issueBoards.ModalStore.setDefaultFilter(); - }, - components: { - userFilter, - milestoneFilter, - labelFilter, - }, - template: ` - - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js b/app/assets/javascripts/boards/components/modal/filters/label.js new file mode 100644 index 00000000000..4fc8f72a145 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters/label.js @@ -0,0 +1,54 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global LabelsSelect */ +module.exports = Vue.extend({ + name: 'filter-label', + props: { + labelPath: { + type: String, + required: true, + }, + }, + mounted() { + new LabelsSelect(this.$refs.dropdown); + }, + template: ` + + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 b/app/assets/javascripts/boards/components/modal/filters/label.js.es6 deleted file mode 100644 index 4fc8f72a145..00000000000 --- a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable no-new */ -/* global Vue */ -/* global LabelsSelect */ -module.exports = Vue.extend({ - name: 'filter-label', - props: { - labelPath: { - type: String, - required: true, - }, - }, - mounted() { - new LabelsSelect(this.$refs.dropdown); - }, - template: ` - - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js b/app/assets/javascripts/boards/components/modal/filters/milestone.js new file mode 100644 index 00000000000..d555599d300 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters/milestone.js @@ -0,0 +1,55 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global MilestoneSelect */ +module.exports = Vue.extend({ + name: 'filter-milestone', + props: { + milestonePath: { + type: String, + required: true, + }, + }, + mounted() { + new MilestoneSelect(null, this.$refs.dropdown); + }, + template: ` + + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 deleted file mode 100644 index d555599d300..00000000000 --- a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 +++ /dev/null @@ -1,55 +0,0 @@ -/* eslint-disable no-new */ -/* global Vue */ -/* global MilestoneSelect */ -module.exports = Vue.extend({ - name: 'filter-milestone', - props: { - milestonePath: { - type: String, - required: true, - }, - }, - mounted() { - new MilestoneSelect(null, this.$refs.dropdown); - }, - template: ` - - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js b/app/assets/javascripts/boards/components/modal/filters/user.js new file mode 100644 index 00000000000..8523028c29c --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters/user.js @@ -0,0 +1,96 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global UsersSelect */ +module.exports = Vue.extend({ + name: 'filter-user', + props: { + toggleClassName: { + type: String, + required: true, + }, + dropdownClassName: { + type: String, + required: false, + default: '', + }, + toggleLabel: { + type: String, + required: true, + }, + fieldName: { + type: String, + required: true, + }, + nullUser: { + type: Boolean, + required: false, + default: false, + }, + projectId: { + type: Number, + required: true, + }, + }, + mounted() { + new UsersSelect(null, this.$refs.dropdown); + }, + computed: { + currentUsername() { + return gon.current_username; + }, + dropdownTitle() { + return `Filter by ${this.toggleLabel.toLowerCase()}`; + }, + inputPlaceholder() { + return `Search ${this.toggleLabel.toLowerCase()}`; + }, + }, + template: ` + + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 b/app/assets/javascripts/boards/components/modal/filters/user.js.es6 deleted file mode 100644 index 8523028c29c..00000000000 --- a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 +++ /dev/null @@ -1,96 +0,0 @@ -/* eslint-disable no-new */ -/* global Vue */ -/* global UsersSelect */ -module.exports = Vue.extend({ - name: 'filter-user', - props: { - toggleClassName: { - type: String, - required: true, - }, - dropdownClassName: { - type: String, - required: false, - default: '', - }, - toggleLabel: { - type: String, - required: true, - }, - fieldName: { - type: String, - required: true, - }, - nullUser: { - type: Boolean, - required: false, - default: false, - }, - projectId: { - type: Number, - required: true, - }, - }, - mounted() { - new UsersSelect(null, this.$refs.dropdown); - }, - computed: { - currentUsername() { - return gon.current_username; - }, - dropdownTitle() { - return `Filter by ${this.toggleLabel.toLowerCase()}`; - }, - inputPlaceholder() { - return `Search ${this.toggleLabel.toLowerCase()}`; - }, - }, - template: ` - - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js new file mode 100644 index 00000000000..1cbc422c961 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -0,0 +1,83 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global Flash */ + +require('./lists_dropdown'); + +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalFooter = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return { + modal: ModalStore.store, + state: gl.issueBoards.BoardsStore.state, + }; + }, + computed: { + submitDisabled() { + return !ModalStore.selectedCount(); + }, + submitText() { + const count = ModalStore.selectedCount(); + + return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`; + }, + }, + methods: { + addIssues() { + const list = this.modal.selectedList || this.state.lists[0]; + const selectedIssues = ModalStore.getSelectedIssues(); + const issueIds = selectedIssues.map(issue => issue.globalId); + + // Post the data to the backend + gl.boardService.bulkUpdate(issueIds, { + add_label_ids: [list.label.id], + }).catch(() => { + new Flash('Failed to update issues, please try again.', 'alert'); + + selectedIssues.forEach((issue) => { + list.removeIssue(issue); + list.issuesSize -= 1; + }); + }); + + // Add the issues on the frontend + selectedIssues.forEach((issue) => { + list.addIssue(issue); + list.issuesSize += 1; + }); + + this.toggleModal(false); + }, + }, + components: { + 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, + }, + template: ` + + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 deleted file mode 100644 index 1cbc422c961..00000000000 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ /dev/null @@ -1,83 +0,0 @@ -/* eslint-disable no-new */ -/* global Vue */ -/* global Flash */ - -require('./lists_dropdown'); - -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - gl.issueBoards.ModalFooter = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], - data() { - return { - modal: ModalStore.store, - state: gl.issueBoards.BoardsStore.state, - }; - }, - computed: { - submitDisabled() { - return !ModalStore.selectedCount(); - }, - submitText() { - const count = ModalStore.selectedCount(); - - return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`; - }, - }, - methods: { - addIssues() { - const list = this.modal.selectedList || this.state.lists[0]; - const selectedIssues = ModalStore.getSelectedIssues(); - const issueIds = selectedIssues.map(issue => issue.globalId); - - // Post the data to the backend - gl.boardService.bulkUpdate(issueIds, { - add_label_ids: [list.label.id], - }).catch(() => { - new Flash('Failed to update issues, please try again.', 'alert'); - - selectedIssues.forEach((issue) => { - list.removeIssue(issue); - list.issuesSize -= 1; - }); - }); - - // Add the issues on the frontend - selectedIssues.forEach((issue) => { - list.addIssue(issue); - list.issuesSize += 1; - }); - - this.toggleModal(false); - }, - }, - components: { - 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, - }, - template: ` - - `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js new file mode 100644 index 00000000000..70c088f9054 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/header.js @@ -0,0 +1,90 @@ +/* global Vue */ +require('./tabs'); +const modalFilters = require('./filters'); + +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalHeader = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + props: { + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + computed: { + selectAllText() { + if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { + return 'Select all'; + } + + return 'Deselect all'; + }, + showSearch() { + return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; + }, + }, + methods: { + toggleAll() { + this.$refs.selectAllBtn.blur(); + + ModalStore.toggleAll(); + }, + }, + components: { + 'modal-tabs': gl.issueBoards.ModalTabs, + modalFilters, + }, + template: ` +
+
+

+ Add issues + +

+
+ + +
+ `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 deleted file mode 100644 index 70c088f9054..00000000000 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ /dev/null @@ -1,90 +0,0 @@ -/* global Vue */ -require('./tabs'); -const modalFilters = require('./filters'); - -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - gl.issueBoards.ModalHeader = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], - props: { - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, - }, - data() { - return ModalStore.store; - }, - computed: { - selectAllText() { - if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { - return 'Select all'; - } - - return 'Deselect all'; - }, - showSearch() { - return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; - }, - }, - methods: { - toggleAll() { - this.$refs.selectAllBtn.blur(); - - ModalStore.toggleAll(); - }, - }, - components: { - 'modal-tabs': gl.issueBoards.ModalTabs, - modalFilters, - }, - template: ` -
-
-

- Add issues - -

-
- - -
- `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js new file mode 100644 index 00000000000..f290cd13763 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -0,0 +1,163 @@ +/* global Vue */ +/* global ListIssue */ + +require('./header'); +require('./list'); +require('./footer'); +require('./empty_state'); + +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.IssuesModal = Vue.extend({ + props: { + blankStateImage: { + type: String, + required: true, + }, + newIssuePath: { + type: String, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + watch: { + page() { + this.loadIssues(); + }, + searchTerm() { + this.searchOperation(); + }, + showAddIssuesModal() { + if (this.showAddIssuesModal && !this.issues.length) { + this.loading = true; + + this.loadIssues() + .then(() => { + this.loading = false; + }); + } else if (!this.showAddIssuesModal) { + this.issues = []; + this.selectedIssues = []; + this.issuesCount = false; + } + }, + filter: { + handler() { + this.loadIssues(true); + }, + deep: true, + }, + }, + methods: { + searchOperation: _.debounce(function searchOperationDebounce() { + this.loadIssues(true); + }, 500), + loadIssues(clearIssues = false) { + if (!this.showAddIssuesModal) return false; + + const queryData = Object.assign({}, this.filter, { + search: this.searchTerm, + page: this.page, + per: this.perPage, + }); + + return gl.boardService.getBacklog(queryData).then((res) => { + const data = res.json(); + + if (clearIssues) { + this.issues = []; + } + + data.issues.forEach((issueObj) => { + const issue = new ListIssue(issueObj); + const foundSelectedIssue = ModalStore.findSelectedIssue(issue); + issue.selected = !!foundSelectedIssue; + + this.issues.push(issue); + }); + + this.loadingNewPage = false; + + if (!this.issuesCount) { + this.issuesCount = data.size; + } + }); + }, + }, + computed: { + showList() { + if (this.activeTab === 'selected') { + return this.selectedIssues.length > 0; + } + + return this.issuesCount > 0; + }, + showEmptyState() { + if (!this.loading && this.issuesCount === 0) { + return true; + } + + return this.activeTab === 'selected' && this.selectedIssues.length === 0; + }, + }, + components: { + 'modal-header': gl.issueBoards.ModalHeader, + 'modal-list': gl.issueBoards.ModalList, + 'modal-footer': gl.issueBoards.ModalFooter, + 'empty-state': gl.issueBoards.ModalEmptyState, + }, + template: ` +
+
+ + + + +
+
+ +
+
+ +
+
+ `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 deleted file mode 100644 index f290cd13763..00000000000 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ /dev/null @@ -1,163 +0,0 @@ -/* global Vue */ -/* global ListIssue */ - -require('./header'); -require('./list'); -require('./footer'); -require('./empty_state'); - -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - gl.issueBoards.IssuesModal = Vue.extend({ - props: { - blankStateImage: { - type: String, - required: true, - }, - newIssuePath: { - type: String, - required: true, - }, - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, - }, - data() { - return ModalStore.store; - }, - watch: { - page() { - this.loadIssues(); - }, - searchTerm() { - this.searchOperation(); - }, - showAddIssuesModal() { - if (this.showAddIssuesModal && !this.issues.length) { - this.loading = true; - - this.loadIssues() - .then(() => { - this.loading = false; - }); - } else if (!this.showAddIssuesModal) { - this.issues = []; - this.selectedIssues = []; - this.issuesCount = false; - } - }, - filter: { - handler() { - this.loadIssues(true); - }, - deep: true, - }, - }, - methods: { - searchOperation: _.debounce(function searchOperationDebounce() { - this.loadIssues(true); - }, 500), - loadIssues(clearIssues = false) { - if (!this.showAddIssuesModal) return false; - - const queryData = Object.assign({}, this.filter, { - search: this.searchTerm, - page: this.page, - per: this.perPage, - }); - - return gl.boardService.getBacklog(queryData).then((res) => { - const data = res.json(); - - if (clearIssues) { - this.issues = []; - } - - data.issues.forEach((issueObj) => { - const issue = new ListIssue(issueObj); - const foundSelectedIssue = ModalStore.findSelectedIssue(issue); - issue.selected = !!foundSelectedIssue; - - this.issues.push(issue); - }); - - this.loadingNewPage = false; - - if (!this.issuesCount) { - this.issuesCount = data.size; - } - }); - }, - }, - computed: { - showList() { - if (this.activeTab === 'selected') { - return this.selectedIssues.length > 0; - } - - return this.issuesCount > 0; - }, - showEmptyState() { - if (!this.loading && this.issuesCount === 0) { - return true; - } - - return this.activeTab === 'selected' && this.selectedIssues.length === 0; - }, - }, - components: { - 'modal-header': gl.issueBoards.ModalHeader, - 'modal-list': gl.issueBoards.ModalList, - 'modal-footer': gl.issueBoards.ModalFooter, - 'empty-state': gl.issueBoards.ModalEmptyState, - }, - template: ` -
-
- - - - -
-
- -
-
- -
-
- `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js new file mode 100644 index 00000000000..3730c1ecaeb --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -0,0 +1,159 @@ +/* global Vue */ +/* global ListIssue */ +/* global bp */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalList = Vue.extend({ + props: { + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + image: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + watch: { + activeTab() { + if (this.activeTab === 'all') { + ModalStore.purgeUnselectedIssues(); + } + }, + }, + computed: { + loopIssues() { + if (this.activeTab === 'all') { + return this.issues; + } + + return this.selectedIssues; + }, + groupedIssues() { + const groups = []; + this.loopIssues.forEach((issue, i) => { + const index = i % this.columns; + + if (!groups[index]) { + groups.push([]); + } + + groups[index].push(issue); + }); + + return groups; + }, + }, + methods: { + scrollHandler() { + const currentPage = Math.floor(this.issues.length / this.perPage); + + if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage + && currentPage === this.page) { + this.loadingNewPage = true; + this.page += 1; + } + }, + toggleIssue(e, issue) { + if (e.target.tagName !== 'A') { + ModalStore.toggleIssue(issue); + } + }, + listHeight() { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight() { + return this.$refs.list.scrollHeight; + }, + scrollTop() { + return this.$refs.list.scrollTop + this.listHeight(); + }, + showIssue(issue) { + if (this.activeTab === 'all') return true; + + const index = ModalStore.selectedIssueIndex(issue); + + return index !== -1; + }, + setColumnCount() { + const breakpoint = bp.getBreakpointSize(); + + if (breakpoint === 'lg' || breakpoint === 'md') { + this.columns = 3; + } else if (breakpoint === 'sm') { + this.columns = 2; + } else { + this.columns = 1; + } + }, + }, + mounted() { + this.scrollHandlerWrapper = this.scrollHandler.bind(this); + this.setColumnCountWrapper = this.setColumnCount.bind(this); + this.setColumnCount(); + + this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); + window.addEventListener('resize', this.setColumnCountWrapper); + }, + beforeDestroy() { + this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); + window.removeEventListener('resize', this.setColumnCountWrapper); + }, + components: { + 'issue-card-inner': gl.issueBoards.IssueCardInner, + }, + template: ` +
+
+
+
+
+

+ There are no issues to show. +

+
+
+
+
+
+ + + + + +
+
+
+
+ `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 deleted file mode 100644 index 3730c1ecaeb..00000000000 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ /dev/null @@ -1,159 +0,0 @@ -/* global Vue */ -/* global ListIssue */ -/* global bp */ -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - gl.issueBoards.ModalList = Vue.extend({ - props: { - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - image: { - type: String, - required: true, - }, - }, - data() { - return ModalStore.store; - }, - watch: { - activeTab() { - if (this.activeTab === 'all') { - ModalStore.purgeUnselectedIssues(); - } - }, - }, - computed: { - loopIssues() { - if (this.activeTab === 'all') { - return this.issues; - } - - return this.selectedIssues; - }, - groupedIssues() { - const groups = []; - this.loopIssues.forEach((issue, i) => { - const index = i % this.columns; - - if (!groups[index]) { - groups.push([]); - } - - groups[index].push(issue); - }); - - return groups; - }, - }, - methods: { - scrollHandler() { - const currentPage = Math.floor(this.issues.length / this.perPage); - - if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage - && currentPage === this.page) { - this.loadingNewPage = true; - this.page += 1; - } - }, - toggleIssue(e, issue) { - if (e.target.tagName !== 'A') { - ModalStore.toggleIssue(issue); - } - }, - listHeight() { - return this.$refs.list.getBoundingClientRect().height; - }, - scrollHeight() { - return this.$refs.list.scrollHeight; - }, - scrollTop() { - return this.$refs.list.scrollTop + this.listHeight(); - }, - showIssue(issue) { - if (this.activeTab === 'all') return true; - - const index = ModalStore.selectedIssueIndex(issue); - - return index !== -1; - }, - setColumnCount() { - const breakpoint = bp.getBreakpointSize(); - - if (breakpoint === 'lg' || breakpoint === 'md') { - this.columns = 3; - } else if (breakpoint === 'sm') { - this.columns = 2; - } else { - this.columns = 1; - } - }, - }, - mounted() { - this.scrollHandlerWrapper = this.scrollHandler.bind(this); - this.setColumnCountWrapper = this.setColumnCount.bind(this); - this.setColumnCount(); - - this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); - window.addEventListener('resize', this.setColumnCountWrapper); - }, - beforeDestroy() { - this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); - window.removeEventListener('resize', this.setColumnCountWrapper); - }, - components: { - 'issue-card-inner': gl.issueBoards.IssueCardInner, - }, - template: ` -
-
-
-
-
-

- There are no issues to show. -

-
-
-
-
-
- - - - - -
-
-
-
- `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js new file mode 100644 index 00000000000..3c05120a2da --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js @@ -0,0 +1,56 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ + data() { + return { + modal: ModalStore.store, + state: gl.issueBoards.BoardsStore.state, + }; + }, + computed: { + selected() { + return this.modal.selectedList || this.state.lists[0]; + }, + }, + destroyed() { + this.modal.selectedList = null; + }, + template: ` + + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 deleted file mode 100644 index 3c05120a2da..00000000000 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 +++ /dev/null @@ -1,56 +0,0 @@ -/* global Vue */ -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ - data() { - return { - modal: ModalStore.store, - state: gl.issueBoards.BoardsStore.state, - }; - }, - computed: { - selected() { - return this.modal.selectedList || this.state.lists[0]; - }, - }, - destroyed() { - this.modal.selectedList = null; - }, - template: ` - - `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js new file mode 100644 index 00000000000..e8cb43f3503 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/tabs.js @@ -0,0 +1,47 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalTabs = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return ModalStore.store; + }, + computed: { + selectedCount() { + return ModalStore.selectedCount(); + }, + }, + destroyed() { + this.activeTab = 'all'; + }, + template: ` +
+ +
+ `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 deleted file mode 100644 index e8cb43f3503..00000000000 --- a/app/assets/javascripts/boards/components/modal/tabs.js.es6 +++ /dev/null @@ -1,47 +0,0 @@ -/* global Vue */ -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - gl.issueBoards.ModalTabs = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], - data() { - return ModalStore.store; - }, - computed: { - selectedCount() { - return ModalStore.selectedCount(); - }, - }, - destroyed() { - this.activeTab = 'all'; - }, - template: ` -
- -
- `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js new file mode 100644 index 00000000000..556826a9148 --- /dev/null +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -0,0 +1,76 @@ +/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */ + +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + const Store = gl.issueBoards.BoardsStore; + + $(document).off('created.label').on('created.label', (e, label) => { + Store.new({ + title: label.title, + position: Store.state.lists.length - 2, + list_type: 'label', + label: { + id: label.id, + title: label.title, + color: label.color + } + }); + }); + + gl.issueBoards.newListDropdownInit = () => { + $('.js-new-board-list').each(function () { + const $this = $(this); + new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); + + $this.glDropdown({ + data(term, callback) { + $.get($this.attr('data-labels')) + .then((resp) => { + callback(resp); + }); + }, + renderRow (label) { + const active = Store.findList('title', label.title); + const $li = $('
  • '); + const $a = $('', { + class: (active ? `is-active js-board-list-${active.id}` : ''), + text: label.title, + href: '#' + }); + const $labelColor = $('', { + class: 'dropdown-label-box', + style: `background-color: ${label.color}` + }); + + return $li.append($a.prepend($labelColor)); + }, + search: { + fields: ['title'] + }, + filterable: true, + selectable: true, + multiSelect: true, + clicked (label, $el, e) { + e.preventDefault(); + + if (!Store.findList('title', label.title)) { + Store.new({ + title: label.title, + position: Store.state.lists.length - 2, + list_type: 'label', + label: { + id: label.id, + title: label.title, + color: label.color + } + }); + + Store.state.lists = _.sortBy(Store.state.lists, 'position'); + } + } + }); + }); + }; +})(); diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 deleted file mode 100644 index 556826a9148..00000000000 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 +++ /dev/null @@ -1,76 +0,0 @@ -/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */ - -(() => { - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - const Store = gl.issueBoards.BoardsStore; - - $(document).off('created.label').on('created.label', (e, label) => { - Store.new({ - title: label.title, - position: Store.state.lists.length - 2, - list_type: 'label', - label: { - id: label.id, - title: label.title, - color: label.color - } - }); - }); - - gl.issueBoards.newListDropdownInit = () => { - $('.js-new-board-list').each(function () { - const $this = $(this); - new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); - - $this.glDropdown({ - data(term, callback) { - $.get($this.attr('data-labels')) - .then((resp) => { - callback(resp); - }); - }, - renderRow (label) { - const active = Store.findList('title', label.title); - const $li = $('
  • '); - const $a = $('', { - class: (active ? `is-active js-board-list-${active.id}` : ''), - text: label.title, - href: '#' - }); - const $labelColor = $('', { - class: 'dropdown-label-box', - style: `background-color: ${label.color}` - }); - - return $li.append($a.prepend($labelColor)); - }, - search: { - fields: ['title'] - }, - filterable: true, - selectable: true, - multiSelect: true, - clicked (label, $el, e) { - e.preventDefault(); - - if (!Store.findList('title', label.title)) { - Store.new({ - title: label.title, - position: Store.state.lists.length - 2, - list_type: 'label', - label: { - id: label.id, - title: label.title, - color: label.color - } - }); - - Store.state.lists = _.sortBy(Store.state.lists, 'position'); - } - } - }); - }); - }; -})(); diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js new file mode 100644 index 00000000000..e74935e1cb0 --- /dev/null +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -0,0 +1,59 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global Flash */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.RemoveIssueBtn = Vue.extend({ + props: { + issue: { + type: Object, + required: true, + }, + list: { + type: Object, + required: true, + }, + }, + methods: { + removeIssue() { + const issue = this.issue; + const lists = issue.getLists(); + const labelIds = lists.map(list => list.label.id); + + // Post the remove data + gl.boardService.bulkUpdate([issue.globalId], { + remove_label_ids: labelIds, + }).catch(() => { + new Flash('Failed to remove issue from board, please try again.', 'alert'); + + lists.forEach((list) => { + list.addIssue(issue); + }); + }); + + // Remove from the frontend store + lists.forEach((list) => { + list.removeIssue(issue); + }); + + Store.detail.issue = {}; + }, + }, + template: ` +
    + +
    + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 deleted file mode 100644 index e74935e1cb0..00000000000 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 +++ /dev/null @@ -1,59 +0,0 @@ -/* eslint-disable no-new */ -/* global Vue */ -/* global Flash */ -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.RemoveIssueBtn = Vue.extend({ - props: { - issue: { - type: Object, - required: true, - }, - list: { - type: Object, - required: true, - }, - }, - methods: { - removeIssue() { - const issue = this.issue; - const lists = issue.getLists(); - const labelIds = lists.map(list => list.label.id); - - // Post the remove data - gl.boardService.bulkUpdate([issue.globalId], { - remove_label_ids: labelIds, - }).catch(() => { - new Flash('Failed to remove issue from board, please try again.', 'alert'); - - lists.forEach((list) => { - list.addIssue(issue); - }); - }); - - // Remove from the frontend store - lists.forEach((list) => { - list.removeIssue(issue); - }); - - Store.detail.issue = {}; - }, - }, - template: ` -
    - -
    - `, - }); -})(); diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js b/app/assets/javascripts/boards/filters/due_date_filters.js new file mode 100644 index 00000000000..7e192e90fe6 --- /dev/null +++ b/app/assets/javascripts/boards/filters/due_date_filters.js @@ -0,0 +1,6 @@ +/* global Vue */ + +Vue.filter('due-date', (value) => { + const date = new Date(value); + return $.datepicker.formatDate('M d, yy', date); +}); diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 deleted file mode 100644 index 7e192e90fe6..00000000000 --- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 +++ /dev/null @@ -1,6 +0,0 @@ -/* global Vue */ - -Vue.filter('due-date', (value) => { - const date = new Date(value); - return $.datepicker.formatDate('M d, yy', date); -}); diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js b/app/assets/javascripts/boards/mixins/modal_mixins.js new file mode 100644 index 00000000000..d378b7d4baf --- /dev/null +++ b/app/assets/javascripts/boards/mixins/modal_mixins.js @@ -0,0 +1,14 @@ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalMixins = { + methods: { + toggleModal(toggle) { + ModalStore.store.showAddIssuesModal = toggle; + }, + changeTab(tab) { + ModalStore.store.activeTab = tab; + }, + }, + }; +})(); diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 deleted file mode 100644 index d378b7d4baf..00000000000 --- a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 +++ /dev/null @@ -1,14 +0,0 @@ -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - gl.issueBoards.ModalMixins = { - methods: { - toggleModal(toggle) { - ModalStore.store.showAddIssuesModal = toggle; - }, - changeTab(tab) { - ModalStore.store.activeTab = tab; - }, - }, - }; -})(); diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js new file mode 100644 index 00000000000..b6c6d17274f --- /dev/null +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js @@ -0,0 +1,39 @@ +/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */ +/* global DocumentTouch */ + +((w) => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.onStart = () => { + $('.has-tooltip').tooltip('hide') + .tooltip('disable'); + document.body.classList.add('is-dragging'); + }; + + gl.issueBoards.onEnd = () => { + $('.has-tooltip').tooltip('enable'); + document.body.classList.remove('is-dragging'); + }; + + gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; + + gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { + const defaultSortOptions = { + animation: 200, + forceFallback: true, + fallbackClass: 'is-dragging', + fallbackOnBody: true, + ghostClass: 'is-ghost', + filter: '.board-delete, .btn', + delay: gl.issueBoards.touchEnabled ? 100 : 0, + scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, + scrollSpeed: 20, + onStart: gl.issueBoards.onStart, + onEnd: gl.issueBoards.onEnd + }; + + Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); + return defaultSortOptions; + }; +})(window); diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 deleted file mode 100644 index b6c6d17274f..00000000000 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */ -/* global DocumentTouch */ - -((w) => { - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.onStart = () => { - $('.has-tooltip').tooltip('hide') - .tooltip('disable'); - document.body.classList.add('is-dragging'); - }; - - gl.issueBoards.onEnd = () => { - $('.has-tooltip').tooltip('enable'); - document.body.classList.remove('is-dragging'); - }; - - gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; - - gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { - const defaultSortOptions = { - animation: 200, - forceFallback: true, - fallbackClass: 'is-dragging', - fallbackOnBody: true, - ghostClass: 'is-ghost', - filter: '.board-delete, .btn', - delay: gl.issueBoards.touchEnabled ? 100 : 0, - scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, - scrollSpeed: 20, - onStart: gl.issueBoards.onStart, - onEnd: gl.issueBoards.onEnd - }; - - Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); - return defaultSortOptions; - }; -})(window); diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js new file mode 100644 index 00000000000..2d0a295ae4d --- /dev/null +++ b/app/assets/javascripts/boards/models/issue.js @@ -0,0 +1,78 @@ +/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */ +/* global Vue */ +/* global ListLabel */ +/* global ListMilestone */ +/* global ListUser */ + +class ListIssue { + constructor (obj) { + this.globalId = obj.id; + this.id = obj.iid; + this.title = obj.title; + this.confidential = obj.confidential; + this.dueDate = obj.due_date; + this.subscribed = obj.subscribed; + this.labels = []; + this.selected = false; + this.assignee = false; + + if (obj.assignee) { + this.assignee = new ListUser(obj.assignee); + } + + if (obj.milestone) { + this.milestone = new ListMilestone(obj.milestone); + } + + obj.labels.forEach((label) => { + this.labels.push(new ListLabel(label)); + }); + + this.priority = this.labels.reduce((max, label) => { + return (label.priority < max) ? label.priority : max; + }, Infinity); + } + + addLabel (label) { + if (!this.findLabel(label)) { + this.labels.push(new ListLabel(label)); + } + } + + findLabel (findLabel) { + return this.labels.filter(label => label.title === findLabel.title)[0]; + } + + removeLabel (removeLabel) { + if (removeLabel) { + this.labels = this.labels.filter(label => removeLabel.title !== label.title); + } + } + + removeLabels (labels) { + labels.forEach(this.removeLabel.bind(this)); + } + + getLists () { + return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id)); + } + + update (url) { + const data = { + issue: { + milestone_id: this.milestone ? this.milestone.id : null, + due_date: this.dueDate, + assignee_id: this.assignee ? this.assignee.id : null, + label_ids: this.labels.map((label) => label.id) + } + }; + + if (!data.issue.label_ids.length) { + data.issue.label_ids = ['']; + } + + return Vue.http.patch(url, data); + } +} + +window.ListIssue = ListIssue; diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 deleted file mode 100644 index 2d0a295ae4d..00000000000 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */ -/* global Vue */ -/* global ListLabel */ -/* global ListMilestone */ -/* global ListUser */ - -class ListIssue { - constructor (obj) { - this.globalId = obj.id; - this.id = obj.iid; - this.title = obj.title; - this.confidential = obj.confidential; - this.dueDate = obj.due_date; - this.subscribed = obj.subscribed; - this.labels = []; - this.selected = false; - this.assignee = false; - - if (obj.assignee) { - this.assignee = new ListUser(obj.assignee); - } - - if (obj.milestone) { - this.milestone = new ListMilestone(obj.milestone); - } - - obj.labels.forEach((label) => { - this.labels.push(new ListLabel(label)); - }); - - this.priority = this.labels.reduce((max, label) => { - return (label.priority < max) ? label.priority : max; - }, Infinity); - } - - addLabel (label) { - if (!this.findLabel(label)) { - this.labels.push(new ListLabel(label)); - } - } - - findLabel (findLabel) { - return this.labels.filter(label => label.title === findLabel.title)[0]; - } - - removeLabel (removeLabel) { - if (removeLabel) { - this.labels = this.labels.filter(label => removeLabel.title !== label.title); - } - } - - removeLabels (labels) { - labels.forEach(this.removeLabel.bind(this)); - } - - getLists () { - return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id)); - } - - update (url) { - const data = { - issue: { - milestone_id: this.milestone ? this.milestone.id : null, - due_date: this.dueDate, - assignee_id: this.assignee ? this.assignee.id : null, - label_ids: this.labels.map((label) => label.id) - } - }; - - if (!data.issue.label_ids.length) { - data.issue.label_ids = ['']; - } - - return Vue.http.patch(url, data); - } -} - -window.ListIssue = ListIssue; diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/boards/models/label.js new file mode 100644 index 00000000000..9af88d167d6 --- /dev/null +++ b/app/assets/javascripts/boards/models/label.js @@ -0,0 +1,14 @@ +/* eslint-disable no-unused-vars, space-before-function-paren */ + +class ListLabel { + constructor (obj) { + this.id = obj.id; + this.title = obj.title; + this.color = obj.color; + this.textColor = obj.text_color; + this.description = obj.description; + this.priority = (obj.priority !== null) ? obj.priority : Infinity; + } +} + +window.ListLabel = ListLabel; diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6 deleted file mode 100644 index 9af88d167d6..00000000000 --- a/app/assets/javascripts/boards/models/label.js.es6 +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable no-unused-vars, space-before-function-paren */ - -class ListLabel { - constructor (obj) { - this.id = obj.id; - this.title = obj.title; - this.color = obj.color; - this.textColor = obj.text_color; - this.description = obj.description; - this.priority = (obj.priority !== null) ? obj.priority : Infinity; - } -} - -window.ListLabel = ListLabel; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js new file mode 100644 index 00000000000..5152be56b66 --- /dev/null +++ b/app/assets/javascripts/boards/models/list.js @@ -0,0 +1,152 @@ +/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */ +/* global ListIssue */ +/* global ListLabel */ + +class List { + constructor (obj) { + this.id = obj.id; + this._uid = this.guid(); + this.position = obj.position; + this.title = obj.title; + this.type = obj.list_type; + this.preset = ['done', 'blank'].indexOf(this.type) > -1; + this.filters = gl.issueBoards.BoardsStore.state.filters; + this.page = 1; + this.loading = true; + this.loadingMore = false; + this.issues = []; + this.issuesSize = 0; + + if (obj.label) { + this.label = new ListLabel(obj.label); + } + + if (this.type !== 'blank' && this.id) { + this.getIssues(); + } + } + + guid() { + const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); + return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; + } + + save () { + return gl.boardService.createList(this.label.id) + .then((resp) => { + const data = resp.json(); + + this.id = data.id; + this.type = data.list_type; + this.position = data.position; + + return this.getIssues(); + }); + } + + destroy () { + const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this); + gl.issueBoards.BoardsStore.state.lists.splice(index, 1); + gl.issueBoards.BoardsStore.updateNewListDropdown(this.id); + + gl.boardService.destroyList(this.id); + } + + update () { + gl.boardService.updateList(this.id, this.position); + } + + nextPage () { + if (this.issuesSize > this.issues.length) { + this.page += 1; + + return this.getIssues(false); + } + } + + getIssues (emptyIssues = true) { + const filters = this.filters; + const data = { page: this.page }; + + Object.keys(filters).forEach((key) => { data[key] = filters[key]; }); + + if (this.label) { + data.label_name = data.label_name.filter(label => label !== this.label.title); + } + + if (emptyIssues) { + this.loading = true; + } + + return gl.boardService.getIssuesForList(this.id, data) + .then((resp) => { + const data = resp.json(); + this.loading = false; + this.issuesSize = data.size; + + if (emptyIssues) { + this.issues = []; + } + + this.createIssues(data.issues); + }); + } + + newIssue (issue) { + this.addIssue(issue); + this.issuesSize += 1; + + return gl.boardService.newIssue(this.id, issue) + .then((resp) => { + const data = resp.json(); + issue.id = data.iid; + }); + } + + createIssues (data) { + data.forEach((issueObj) => { + this.addIssue(new ListIssue(issueObj)); + }); + } + + addIssue (issue, listFrom, newIndex) { + if (!this.findIssue(issue.id)) { + if (newIndex !== undefined) { + this.issues.splice(newIndex, 0, issue); + } else { + this.issues.push(issue); + } + + if (this.label) { + issue.addLabel(this.label); + } + + if (listFrom) { + this.issuesSize += 1; + gl.boardService.moveIssue(issue.id, listFrom.id, this.id) + .then(() => { + listFrom.getIssues(false); + }); + } + } + } + + findIssue (id) { + return this.issues.filter(issue => issue.id === id)[0]; + } + + removeIssue (removeIssue) { + this.issues = this.issues.filter((issue) => { + const matchesRemove = removeIssue.id === issue.id; + + if (matchesRemove) { + this.issuesSize -= 1; + issue.removeLabel(this.label); + } + + return !matchesRemove; + }); + } +} + +window.List = List; diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 deleted file mode 100644 index 5152be56b66..00000000000 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ /dev/null @@ -1,152 +0,0 @@ -/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */ -/* global ListIssue */ -/* global ListLabel */ - -class List { - constructor (obj) { - this.id = obj.id; - this._uid = this.guid(); - this.position = obj.position; - this.title = obj.title; - this.type = obj.list_type; - this.preset = ['done', 'blank'].indexOf(this.type) > -1; - this.filters = gl.issueBoards.BoardsStore.state.filters; - this.page = 1; - this.loading = true; - this.loadingMore = false; - this.issues = []; - this.issuesSize = 0; - - if (obj.label) { - this.label = new ListLabel(obj.label); - } - - if (this.type !== 'blank' && this.id) { - this.getIssues(); - } - } - - guid() { - const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); - return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; - } - - save () { - return gl.boardService.createList(this.label.id) - .then((resp) => { - const data = resp.json(); - - this.id = data.id; - this.type = data.list_type; - this.position = data.position; - - return this.getIssues(); - }); - } - - destroy () { - const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this); - gl.issueBoards.BoardsStore.state.lists.splice(index, 1); - gl.issueBoards.BoardsStore.updateNewListDropdown(this.id); - - gl.boardService.destroyList(this.id); - } - - update () { - gl.boardService.updateList(this.id, this.position); - } - - nextPage () { - if (this.issuesSize > this.issues.length) { - this.page += 1; - - return this.getIssues(false); - } - } - - getIssues (emptyIssues = true) { - const filters = this.filters; - const data = { page: this.page }; - - Object.keys(filters).forEach((key) => { data[key] = filters[key]; }); - - if (this.label) { - data.label_name = data.label_name.filter(label => label !== this.label.title); - } - - if (emptyIssues) { - this.loading = true; - } - - return gl.boardService.getIssuesForList(this.id, data) - .then((resp) => { - const data = resp.json(); - this.loading = false; - this.issuesSize = data.size; - - if (emptyIssues) { - this.issues = []; - } - - this.createIssues(data.issues); - }); - } - - newIssue (issue) { - this.addIssue(issue); - this.issuesSize += 1; - - return gl.boardService.newIssue(this.id, issue) - .then((resp) => { - const data = resp.json(); - issue.id = data.iid; - }); - } - - createIssues (data) { - data.forEach((issueObj) => { - this.addIssue(new ListIssue(issueObj)); - }); - } - - addIssue (issue, listFrom, newIndex) { - if (!this.findIssue(issue.id)) { - if (newIndex !== undefined) { - this.issues.splice(newIndex, 0, issue); - } else { - this.issues.push(issue); - } - - if (this.label) { - issue.addLabel(this.label); - } - - if (listFrom) { - this.issuesSize += 1; - gl.boardService.moveIssue(issue.id, listFrom.id, this.id) - .then(() => { - listFrom.getIssues(false); - }); - } - } - } - - findIssue (id) { - return this.issues.filter(issue => issue.id === id)[0]; - } - - removeIssue (removeIssue) { - this.issues = this.issues.filter((issue) => { - const matchesRemove = removeIssue.id === issue.id; - - if (matchesRemove) { - this.issuesSize -= 1; - issue.removeLabel(this.label); - } - - return !matchesRemove; - }); - } -} - -window.List = List; diff --git a/app/assets/javascripts/boards/models/milestone.js b/app/assets/javascripts/boards/models/milestone.js new file mode 100644 index 00000000000..c867b06d320 --- /dev/null +++ b/app/assets/javascripts/boards/models/milestone.js @@ -0,0 +1,10 @@ +/* eslint-disable no-unused-vars */ + +class ListMilestone { + constructor(obj) { + this.id = obj.id; + this.title = obj.title; + } +} + +window.ListMilestone = ListMilestone; diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6 deleted file mode 100644 index c867b06d320..00000000000 --- a/app/assets/javascripts/boards/models/milestone.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable no-unused-vars */ - -class ListMilestone { - constructor(obj) { - this.id = obj.id; - this.title = obj.title; - } -} - -window.ListMilestone = ListMilestone; diff --git a/app/assets/javascripts/boards/models/user.js b/app/assets/javascripts/boards/models/user.js new file mode 100644 index 00000000000..8e9de4d4cbb --- /dev/null +++ b/app/assets/javascripts/boards/models/user.js @@ -0,0 +1,12 @@ +/* eslint-disable no-unused-vars */ + +class ListUser { + constructor(user) { + this.id = user.id; + this.name = user.name; + this.username = user.username; + this.avatar = user.avatar_url; + } +} + +window.ListUser = ListUser; diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6 deleted file mode 100644 index 8e9de4d4cbb..00000000000 --- a/app/assets/javascripts/boards/models/user.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable no-unused-vars */ - -class ListUser { - constructor(user) { - this.id = user.id; - this.name = user.name; - this.username = user.username; - this.avatar = user.avatar_url; - } -} - -window.ListUser = ListUser; diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js new file mode 100644 index 00000000000..065e90518df --- /dev/null +++ b/app/assets/javascripts/boards/services/board_service.js @@ -0,0 +1,95 @@ +/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */ +/* global Vue */ + +class BoardService { + constructor (root, bulkUpdatePath, boardId) { + this.boards = Vue.resource(`${root}{/id}.json`, {}, { + issues: { + method: 'GET', + url: `${root}/${boardId}/issues.json` + } + }); + this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { + generate: { + method: 'POST', + url: `${root}/${boardId}/lists/generate.json` + } + }); + this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); + this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, { + bulkUpdate: { + method: 'POST', + url: bulkUpdatePath, + }, + }); + + Vue.http.interceptors.push((request, next) => { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + next(); + }); + } + + all () { + return this.lists.get(); + } + + generateDefaultLists () { + return this.lists.generate({}); + } + + createList (label_id) { + return this.lists.save({}, { + list: { + label_id + } + }); + } + + updateList (id, position) { + return this.lists.update({ id }, { + list: { + position + } + }); + } + + destroyList (id) { + return this.lists.delete({ id }); + } + + getIssuesForList (id, filter = {}) { + const data = { id }; + Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); + + return this.issues.get(data); + } + + moveIssue (id, from_list_id, to_list_id) { + return this.issue.update({ id }, { + from_list_id, + to_list_id + }); + } + + newIssue (id, issue) { + return this.issues.save({ id }, { + issue + }); + } + + getBacklog(data) { + return this.boards.issues(data); + } + + bulkUpdate(issueIds, extraData = {}) { + const data = { + update: Object.assign(extraData, { + issuable_ids: issueIds.join(','), + }), + }; + + return this.issues.bulkUpdate(data); + } +} + +window.BoardService = BoardService; diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 deleted file mode 100644 index 065e90518df..00000000000 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */ -/* global Vue */ - -class BoardService { - constructor (root, bulkUpdatePath, boardId) { - this.boards = Vue.resource(`${root}{/id}.json`, {}, { - issues: { - method: 'GET', - url: `${root}/${boardId}/issues.json` - } - }); - this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { - generate: { - method: 'POST', - url: `${root}/${boardId}/lists/generate.json` - } - }); - this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); - this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, { - bulkUpdate: { - method: 'POST', - url: bulkUpdatePath, - }, - }); - - Vue.http.interceptors.push((request, next) => { - request.headers['X-CSRF-Token'] = $.rails.csrfToken(); - next(); - }); - } - - all () { - return this.lists.get(); - } - - generateDefaultLists () { - return this.lists.generate({}); - } - - createList (label_id) { - return this.lists.save({}, { - list: { - label_id - } - }); - } - - updateList (id, position) { - return this.lists.update({ id }, { - list: { - position - } - }); - } - - destroyList (id) { - return this.lists.delete({ id }); - } - - getIssuesForList (id, filter = {}) { - const data = { id }; - Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); - - return this.issues.get(data); - } - - moveIssue (id, from_list_id, to_list_id) { - return this.issue.update({ id }, { - from_list_id, - to_list_id - }); - } - - newIssue (id, issue) { - return this.issues.save({ id }, { - issue - }); - } - - getBacklog(data) { - return this.boards.issues(data); - } - - bulkUpdate(issueIds, extraData = {}) { - const data = { - update: Object.assign(extraData, { - issuable_ids: issueIds.join(','), - }), - }; - - return this.issues.bulkUpdate(data); - } -} - -window.BoardService = BoardService; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js new file mode 100644 index 00000000000..50842ecbaaa --- /dev/null +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -0,0 +1,120 @@ +/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */ +/* global Cookies */ +/* global List */ + +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardsStore = { + disabled: false, + state: {}, + detail: { + issue: {} + }, + moving: { + issue: {}, + list: {} + }, + create () { + this.state.lists = []; + this.state.filters = { + author_id: gl.utils.getParameterValues('author_id')[0], + assignee_id: gl.utils.getParameterValues('assignee_id')[0], + milestone_title: gl.utils.getParameterValues('milestone_title')[0], + label_name: gl.utils.getParameterValues('label_name[]'), + search: '' + }; + }, + addList (listObj) { + const list = new List(listObj); + this.state.lists.push(list); + + return list; + }, + new (listObj) { + const list = this.addList(listObj); + + list + .save() + .then(() => { + this.state.lists = _.sortBy(this.state.lists, 'position'); + }); + this.removeBlankState(); + }, + updateNewListDropdown (listId) { + $(`.js-board-list-${listId}`).removeClass('is-active'); + }, + shouldAddBlankState () { + // Decide whether to add the blank state + return !(this.state.lists.filter(list => list.type !== 'done')[0]); + }, + addBlankState () { + if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; + + this.addList({ + id: 'blank', + list_type: 'blank', + title: 'Welcome to your Issue Board!', + position: 0 + }); + + this.state.lists = _.sortBy(this.state.lists, 'position'); + }, + removeBlankState () { + this.removeList('blank'); + + Cookies.set('issue_board_welcome_hidden', 'true', { + expires: 365 * 10, + path: '' + }); + }, + welcomeIsHidden () { + return Cookies.get('issue_board_welcome_hidden') === 'true'; + }, + removeList (id, type = 'blank') { + const list = this.findList('id', id, type); + + if (!list) return; + + this.state.lists = this.state.lists.filter(list => list.id !== id); + }, + moveList (listFrom, orderLists) { + orderLists.forEach((id, i) => { + const list = this.findList('id', parseInt(id, 10)); + + list.position = i; + }); + listFrom.update(); + }, + moveIssueToList (listFrom, listTo, issue, newIndex) { + const issueTo = listTo.findIssue(issue.id); + const issueLists = issue.getLists(); + const listLabels = issueLists.map(listIssue => listIssue.label); + + // Add to new lists issues if it doesn't already exist + if (!issueTo) { + listTo.addIssue(issue, listFrom, newIndex); + } + + if (listTo.type === 'done') { + issueLists.forEach((list) => { + list.removeIssue(issue); + }); + issue.removeLabels(listLabels); + } else { + listFrom.removeIssue(issue); + } + }, + findList (key, val, type = 'label') { + return this.state.lists.filter((list) => { + const byType = type ? list['type'] === type : true; + + return list[key] === val && byType; + })[0]; + }, + updateFiltersUrl () { + history.pushState(null, null, `?${$.param(this.state.filters)}`); + } + }; +})(); diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 deleted file mode 100644 index 50842ecbaaa..00000000000 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ /dev/null @@ -1,120 +0,0 @@ -/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */ -/* global Cookies */ -/* global List */ - -(() => { - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.BoardsStore = { - disabled: false, - state: {}, - detail: { - issue: {} - }, - moving: { - issue: {}, - list: {} - }, - create () { - this.state.lists = []; - this.state.filters = { - author_id: gl.utils.getParameterValues('author_id')[0], - assignee_id: gl.utils.getParameterValues('assignee_id')[0], - milestone_title: gl.utils.getParameterValues('milestone_title')[0], - label_name: gl.utils.getParameterValues('label_name[]'), - search: '' - }; - }, - addList (listObj) { - const list = new List(listObj); - this.state.lists.push(list); - - return list; - }, - new (listObj) { - const list = this.addList(listObj); - - list - .save() - .then(() => { - this.state.lists = _.sortBy(this.state.lists, 'position'); - }); - this.removeBlankState(); - }, - updateNewListDropdown (listId) { - $(`.js-board-list-${listId}`).removeClass('is-active'); - }, - shouldAddBlankState () { - // Decide whether to add the blank state - return !(this.state.lists.filter(list => list.type !== 'done')[0]); - }, - addBlankState () { - if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; - - this.addList({ - id: 'blank', - list_type: 'blank', - title: 'Welcome to your Issue Board!', - position: 0 - }); - - this.state.lists = _.sortBy(this.state.lists, 'position'); - }, - removeBlankState () { - this.removeList('blank'); - - Cookies.set('issue_board_welcome_hidden', 'true', { - expires: 365 * 10, - path: '' - }); - }, - welcomeIsHidden () { - return Cookies.get('issue_board_welcome_hidden') === 'true'; - }, - removeList (id, type = 'blank') { - const list = this.findList('id', id, type); - - if (!list) return; - - this.state.lists = this.state.lists.filter(list => list.id !== id); - }, - moveList (listFrom, orderLists) { - orderLists.forEach((id, i) => { - const list = this.findList('id', parseInt(id, 10)); - - list.position = i; - }); - listFrom.update(); - }, - moveIssueToList (listFrom, listTo, issue, newIndex) { - const issueTo = listTo.findIssue(issue.id); - const issueLists = issue.getLists(); - const listLabels = issueLists.map(listIssue => listIssue.label); - - // Add to new lists issues if it doesn't already exist - if (!issueTo) { - listTo.addIssue(issue, listFrom, newIndex); - } - - if (listTo.type === 'done') { - issueLists.forEach((list) => { - list.removeIssue(issue); - }); - issue.removeLabels(listLabels); - } else { - listFrom.removeIssue(issue); - } - }, - findList (key, val, type = 'label') { - return this.state.lists.filter((list) => { - const byType = type ? list['type'] === type : true; - - return list[key] === val && byType; - })[0]; - }, - updateFiltersUrl () { - history.pushState(null, null, `?${$.param(this.state.filters)}`); - } - }; -})(); diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js new file mode 100644 index 00000000000..15fc6c79e8d --- /dev/null +++ b/app/assets/javascripts/boards/stores/modal_store.js @@ -0,0 +1,107 @@ +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + class ModalStore { + constructor() { + this.store = { + columns: 3, + issues: [], + issuesCount: false, + selectedIssues: [], + showAddIssuesModal: false, + activeTab: 'all', + selectedList: null, + searchTerm: '', + loading: false, + loadingNewPage: false, + page: 1, + perPage: 50, + }; + + this.setDefaultFilter(); + } + + setDefaultFilter() { + this.store.filter = { + author_id: '', + assignee_id: '', + milestone_title: '', + label_name: [], + }; + } + + selectedCount() { + return this.getSelectedIssues().length; + } + + toggleIssue(issueObj) { + const issue = issueObj; + const selected = issue.selected; + + issue.selected = !selected; + + if (!selected) { + this.addSelectedIssue(issue); + } else { + this.removeSelectedIssue(issue); + } + } + + toggleAll() { + const select = this.selectedCount() !== this.store.issues.length; + + this.store.issues.forEach((issue) => { + const issueUpdate = issue; + + if (issueUpdate.selected !== select) { + issueUpdate.selected = select; + + if (select) { + this.addSelectedIssue(issue); + } else { + this.removeSelectedIssue(issue); + } + } + }); + } + + getSelectedIssues() { + return this.store.selectedIssues.filter(issue => issue.selected); + } + + addSelectedIssue(issue) { + const index = this.selectedIssueIndex(issue); + + if (index === -1) { + this.store.selectedIssues.push(issue); + } + } + + removeSelectedIssue(issue, forcePurge = false) { + if (this.store.activeTab === 'all' || forcePurge) { + this.store.selectedIssues = this.store.selectedIssues + .filter(fIssue => fIssue.id !== issue.id); + } + } + + purgeUnselectedIssues() { + this.store.selectedIssues.forEach((issue) => { + if (!issue.selected) { + this.removeSelectedIssue(issue, true); + } + }); + } + + selectedIssueIndex(issue) { + return this.store.selectedIssues.indexOf(issue); + } + + findSelectedIssue(issue) { + return this.store.selectedIssues + .filter(filteredIssue => filteredIssue.id === issue.id)[0]; + } + } + + gl.issueBoards.ModalStore = new ModalStore(); +})(); diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 deleted file mode 100644 index 15fc6c79e8d..00000000000 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ /dev/null @@ -1,107 +0,0 @@ -(() => { - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - class ModalStore { - constructor() { - this.store = { - columns: 3, - issues: [], - issuesCount: false, - selectedIssues: [], - showAddIssuesModal: false, - activeTab: 'all', - selectedList: null, - searchTerm: '', - loading: false, - loadingNewPage: false, - page: 1, - perPage: 50, - }; - - this.setDefaultFilter(); - } - - setDefaultFilter() { - this.store.filter = { - author_id: '', - assignee_id: '', - milestone_title: '', - label_name: [], - }; - } - - selectedCount() { - return this.getSelectedIssues().length; - } - - toggleIssue(issueObj) { - const issue = issueObj; - const selected = issue.selected; - - issue.selected = !selected; - - if (!selected) { - this.addSelectedIssue(issue); - } else { - this.removeSelectedIssue(issue); - } - } - - toggleAll() { - const select = this.selectedCount() !== this.store.issues.length; - - this.store.issues.forEach((issue) => { - const issueUpdate = issue; - - if (issueUpdate.selected !== select) { - issueUpdate.selected = select; - - if (select) { - this.addSelectedIssue(issue); - } else { - this.removeSelectedIssue(issue); - } - } - }); - } - - getSelectedIssues() { - return this.store.selectedIssues.filter(issue => issue.selected); - } - - addSelectedIssue(issue) { - const index = this.selectedIssueIndex(issue); - - if (index === -1) { - this.store.selectedIssues.push(issue); - } - } - - removeSelectedIssue(issue, forcePurge = false) { - if (this.store.activeTab === 'all' || forcePurge) { - this.store.selectedIssues = this.store.selectedIssues - .filter(fIssue => fIssue.id !== issue.id); - } - } - - purgeUnselectedIssues() { - this.store.selectedIssues.forEach((issue) => { - if (!issue.selected) { - this.removeSelectedIssue(issue, true); - } - }); - } - - selectedIssueIndex(issue) { - return this.store.selectedIssues.indexOf(issue); - } - - findSelectedIssue(issue) { - return this.store.selectedIssues - .filter(filteredIssue => filteredIssue.id === issue.id)[0]; - } - } - - gl.issueBoards.ModalStore = new ModalStore(); -})(); diff --git a/app/assets/javascripts/build_variables.js b/app/assets/javascripts/build_variables.js new file mode 100644 index 00000000000..99082b412e2 --- /dev/null +++ b/app/assets/javascripts/build_variables.js @@ -0,0 +1,8 @@ +/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren */ + +$(function() { + $('.reveal-variables').off('click').on('click', function() { + $('.js-build').toggle().niceScroll(); + $(this).hide(); + }); +}); diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6 deleted file mode 100644 index 99082b412e2..00000000000 --- a/app/assets/javascripts/build_variables.js.es6 +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren */ - -$(function() { - $('.reveal-variables').off('click').on('click', function() { - $('.js-build').toggle().niceScroll(); - $(this).hide(); - }); -}); diff --git a/app/assets/javascripts/ci_lint_editor.js b/app/assets/javascripts/ci_lint_editor.js new file mode 100644 index 00000000000..56ffaa765a8 --- /dev/null +++ b/app/assets/javascripts/ci_lint_editor.js @@ -0,0 +1,18 @@ +(() => { + window.gl = window.gl || {}; + + 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; +})(); diff --git a/app/assets/javascripts/ci_lint_editor.js.es6 b/app/assets/javascripts/ci_lint_editor.js.es6 deleted file mode 100644 index 56ffaa765a8..00000000000 --- a/app/assets/javascripts/ci_lint_editor.js.es6 +++ /dev/null @@ -1,18 +0,0 @@ -(() => { - window.gl = window.gl || {}; - - 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; -})(); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js new file mode 100644 index 00000000000..fbfec7743c7 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -0,0 +1,26 @@ +/* eslint-disable no-new, no-param-reassign */ +/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ + +window.Vue = require('vue'); +require('./pipelines_table'); +/** + * Commits View > Pipelines Tab > Pipelines Table. + * Merge Request View > Pipelines Tab > Pipelines Table. + * + * Renders Pipelines table in pipelines tab in the commits show view. + * Renders Pipelines table in pipelines tab in the merge request show view. + */ + +$(() => { + window.gl = window.gl || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; + + if (gl.commits.PipelinesTableBundle) { + gl.commits.PipelinesTableBundle.$destroy(true); + } + + gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); +}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 deleted file mode 100644 index fbfec7743c7..00000000000 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable no-new, no-param-reassign */ -/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ - -window.Vue = require('vue'); -require('./pipelines_table'); -/** - * Commits View > Pipelines Tab > Pipelines Table. - * Merge Request View > Pipelines Tab > Pipelines Table. - * - * Renders Pipelines table in pipelines tab in the commits show view. - * Renders Pipelines table in pipelines tab in the merge request show view. - */ - -$(() => { - window.gl = window.gl || {}; - gl.commits = gl.commits || {}; - gl.commits.pipelines = gl.commits.pipelines || {}; - - if (gl.commits.PipelinesTableBundle) { - gl.commits.PipelinesTableBundle.$destroy(true); - } - - gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({ - el: document.querySelector('#commit-pipeline-table-view'), - }); -}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js b/app/assets/javascripts/commit/pipelines/pipelines_service.js new file mode 100644 index 00000000000..483b414126a --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js @@ -0,0 +1,29 @@ +/* globals Vue */ +/* eslint-disable no-unused-vars, no-param-reassign */ + +/** + * Pipelines service. + * + * Used to fetch the data used to render the pipelines table. + * Uses Vue.Resource + */ +class PipelinesService { + constructor(endpoint) { + this.pipelines = Vue.resource(endpoint); + } + + /** + * Given the root param provided when the class is initialized, will + * make a GET request. + * + * @return {Promise} + */ + all() { + return this.pipelines.get(); + } +} + +window.gl = window.gl || {}; +gl.commits = gl.commits || {}; +gl.commits.pipelines = gl.commits.pipelines || {}; +gl.commits.pipelines.PipelinesService = PipelinesService; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 deleted file mode 100644 index 483b414126a..00000000000 --- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 +++ /dev/null @@ -1,29 +0,0 @@ -/* globals Vue */ -/* eslint-disable no-unused-vars, no-param-reassign */ - -/** - * Pipelines service. - * - * Used to fetch the data used to render the pipelines table. - * Uses Vue.Resource - */ -class PipelinesService { - constructor(endpoint) { - this.pipelines = Vue.resource(endpoint); - } - - /** - * Given the root param provided when the class is initialized, will - * make a GET request. - * - * @return {Promise} - */ - all() { - return this.pipelines.get(); - } -} - -window.gl = window.gl || {}; -gl.commits = gl.commits || {}; -gl.commits.pipelines = gl.commits.pipelines || {}; -gl.commits.pipelines.PipelinesService = PipelinesService; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js b/app/assets/javascripts/commit/pipelines/pipelines_store.js new file mode 100644 index 00000000000..f1b41911b73 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js @@ -0,0 +1,50 @@ +/* eslint-disable no-underscore-dangle*/ +/** + * Pipelines' Store for commits view. + * + * Used to store the Pipelines rendered in the commit view in the pipelines table. + */ + +class PipelinesStore { + constructor() { + this.state = {}; + this.state.pipelines = []; + } + + storePipelines(pipelines = []) { + this.state.pipelines = pipelines; + + return pipelines; + } + + /** + * Once the data is received we will start the time ago loops. + * + * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we + * update the time to show how long as passed. + * + */ + startTimeAgoLoops() { + const startTimeLoops = () => { + this.timeLoopInterval = setInterval(() => { + this.$children[0].$children.reduce((acc, component) => { + const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; + acc.push(timeAgoComponent); + return acc; + }, []).forEach(e => e.changeTime()); + }, 10000); + }; + + startTimeLoops(); + + const removeIntervals = () => clearInterval(this.timeLoopInterval); + const startIntervals = () => startTimeLoops(); + + gl.VueRealtimeListener(removeIntervals, startIntervals); + } +} + +window.gl = window.gl || {}; +gl.commits = gl.commits || {}; +gl.commits.pipelines = gl.commits.pipelines || {}; +gl.commits.pipelines.PipelinesStore = PipelinesStore; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 deleted file mode 100644 index f1b41911b73..00000000000 --- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-disable no-underscore-dangle*/ -/** - * Pipelines' Store for commits view. - * - * Used to store the Pipelines rendered in the commit view in the pipelines table. - */ - -class PipelinesStore { - constructor() { - this.state = {}; - this.state.pipelines = []; - } - - storePipelines(pipelines = []) { - this.state.pipelines = pipelines; - - return pipelines; - } - - /** - * Once the data is received we will start the time ago loops. - * - * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we - * update the time to show how long as passed. - * - */ - startTimeAgoLoops() { - const startTimeLoops = () => { - this.timeLoopInterval = setInterval(() => { - this.$children[0].$children.reduce((acc, component) => { - const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; - acc.push(timeAgoComponent); - return acc; - }, []).forEach(e => e.changeTime()); - }, 10000); - }; - - startTimeLoops(); - - const removeIntervals = () => clearInterval(this.timeLoopInterval); - const startIntervals = () => startTimeLoops(); - - gl.VueRealtimeListener(removeIntervals, startIntervals); - } -} - -window.gl = window.gl || {}; -gl.commits = gl.commits || {}; -gl.commits.pipelines = gl.commits.pipelines || {}; -gl.commits.pipelines.PipelinesStore = PipelinesStore; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js new file mode 100644 index 00000000000..ce0dbd4d56b --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -0,0 +1,107 @@ +/* eslint-disable no-new, no-param-reassign */ +/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ + +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('../../lib/utils/common_utils'); +require('../../vue_shared/vue_resource_interceptor'); +require('../../vue_shared/components/pipelines_table'); +require('../../vue_realtime_listener/index'); +require('./pipelines_service'); +require('./pipelines_store'); + +/** + * + * Uses `pipelines-table-component` to render Pipelines table with an API call. + * Endpoint is provided in HTML and passed as `endpoint`. + * We need a store to store the received environemnts. + * We need a service to communicate with the server. + * + * Necessary SVG in the table are provided as props. This should be refactored + * as soon as we have Webpack and can load them directly into JS files. + */ + +(() => { + window.gl = window.gl || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; + + gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', { + + components: { + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + }, + + /** + * Accesses the DOM to provide the needed data. + * Returns the necessary props to render `pipelines-table-component` component. + * + * @return {Object} + */ + data() { + const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; + const svgsData = document.querySelector('.pipeline-svgs').dataset; + const store = new gl.commits.pipelines.PipelinesStore(); + + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = gl.utils.DOMStringMapToObject(svgsData); + + return { + endpoint: pipelinesTableData.endpoint, + svgs: svgsObject, + store, + state: store.state, + isLoading: false, + }; + }, + + /** + * When the component is created the service to fetch the data will be + * initialized with the correct endpoint. + * + * A request to fetch the pipelines will be made. + * In case of a successfull response we will store the data in the provided + * store, in case of a failed response we need to warn the user. + * + */ + created() { + const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint); + + this.isLoading = true; + return pipelinesService.all() + .then(response => response.json()) + .then((json) => { + this.store.storePipelines(json); + this.store.startTimeAgoLoops.call(this, Vue); + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert'); + }); + }, + + template: ` +
    +
    + +
    + +
    +

    + No pipelines to show +

    +
    + +
    + + +
    +
    + `, + }); +})(); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 deleted file mode 100644 index ce0dbd4d56b..00000000000 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 +++ /dev/null @@ -1,107 +0,0 @@ -/* eslint-disable no-new, no-param-reassign */ -/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ - -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); -require('../../lib/utils/common_utils'); -require('../../vue_shared/vue_resource_interceptor'); -require('../../vue_shared/components/pipelines_table'); -require('../../vue_realtime_listener/index'); -require('./pipelines_service'); -require('./pipelines_store'); - -/** - * - * Uses `pipelines-table-component` to render Pipelines table with an API call. - * Endpoint is provided in HTML and passed as `endpoint`. - * We need a store to store the received environemnts. - * We need a service to communicate with the server. - * - * Necessary SVG in the table are provided as props. This should be refactored - * as soon as we have Webpack and can load them directly into JS files. - */ - -(() => { - window.gl = window.gl || {}; - gl.commits = gl.commits || {}; - gl.commits.pipelines = gl.commits.pipelines || {}; - - gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', { - - components: { - 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, - }, - - /** - * Accesses the DOM to provide the needed data. - * Returns the necessary props to render `pipelines-table-component` component. - * - * @return {Object} - */ - data() { - const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; - const svgsData = document.querySelector('.pipeline-svgs').dataset; - const store = new gl.commits.pipelines.PipelinesStore(); - - // Transform svgs DOMStringMap to a plain Object. - const svgsObject = gl.utils.DOMStringMapToObject(svgsData); - - return { - endpoint: pipelinesTableData.endpoint, - svgs: svgsObject, - store, - state: store.state, - isLoading: false, - }; - }, - - /** - * When the component is created the service to fetch the data will be - * initialized with the correct endpoint. - * - * A request to fetch the pipelines will be made. - * In case of a successfull response we will store the data in the provided - * store, in case of a failed response we need to warn the user. - * - */ - created() { - const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint); - - this.isLoading = true; - return pipelinesService.all() - .then(response => response.json()) - .then((json) => { - this.store.storePipelines(json); - this.store.startTimeAgoLoops.call(this, Vue); - this.isLoading = false; - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert'); - }); - }, - - template: ` -
    -
    - -
    - -
    -

    - No pipelines to show -

    -
    - -
    - - -
    -
    - `, - }); -})(); diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js new file mode 100644 index 00000000000..3587431ab69 --- /dev/null +++ b/app/assets/javascripts/compare_autocomplete.js @@ -0,0 +1,69 @@ +/* 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(); + } + + 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 $('
  • ').addClass('dropdown-header').text(ref.header); + } else { + link = $('').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); + return $('
  • ').append(link); + } + }, + 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'); + } + }); + }); + }; + + return CompareAutocomplete; + })(); +}).call(this); diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6 deleted file mode 100644 index 3587431ab69..00000000000 --- a/app/assets/javascripts/compare_autocomplete.js.es6 +++ /dev/null @@ -1,69 +0,0 @@ -/* 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(); - } - - 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 $('
  • ').addClass('dropdown-header').text(ref.header); - } else { - link = $('').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); - return $('
  • ').append(link); - } - }, - 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'); - } - }); - }); - }; - - return CompareAutocomplete; - })(); -}).call(this); diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js new file mode 100644 index 00000000000..2bfe57b4100 --- /dev/null +++ b/app/assets/javascripts/copy_as_gfm.js @@ -0,0 +1,355 @@ +/* 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: { + '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'); + }, + }, + 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: { + '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 `
    \n${lines.join('\n')}\n
    `; + }, + 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) { + const tag = el.nodeName.toLowerCase(); + return `<${tag}>${text}`; + }, + }, + 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}`; + }).join('\n'); + } + + 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; + } + + 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; + + // If the documentFragment contains more than just Markdown, don't copy as GFM. + if (documentFragment.querySelector('.md, .wiki')) return; + + e.preventDefault(); + clipboardData.setData('text/plain', documentFragment.textContent); + + const gfm = CopyAsGFM.nodeToGFM(documentFragment); + clipboardData.setData('text/x-gfm', gfm); + } + + handlePaste(e) { + const clipboardData = e.originalEvent.clipboardData; + if (!clipboardData) return; + + const gfm = clipboardData.getData('text/x-gfm'); + if (!gfm) return; + + e.preventDefault(); + + window.gl.utils.insertText(e.target, gfm); + } + + static nodeToGFM(node) { + if (node.nodeType === Node.TEXT_NODE) { + return node.textContent; + } + + const text = this.innerGFM(node); + + if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + return text; + } + + for (const filter in gfmRules) { + const rules = gfmRules[filter]; + + for (const selector in rules) { + const func = rules[selector]; + + if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; + + const result = func(node, text); + if (result === false) continue; + + return result; + } + } + + return text; + } + + 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; + + new CopyAsGFM(); +})(); diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6 deleted file mode 100644 index 2bfe57b4100..00000000000 --- a/app/assets/javascripts/copy_as_gfm.js.es6 +++ /dev/null @@ -1,355 +0,0 @@ -/* 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: { - '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'); - }, - }, - 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: { - '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 `
    \n${lines.join('\n')}\n
    `; - }, - 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) { - const tag = el.nodeName.toLowerCase(); - return `<${tag}>${text}`; - }, - }, - 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}`; - }).join('\n'); - } - - 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; - } - - 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; - - // If the documentFragment contains more than just Markdown, don't copy as GFM. - if (documentFragment.querySelector('.md, .wiki')) return; - - e.preventDefault(); - clipboardData.setData('text/plain', documentFragment.textContent); - - const gfm = CopyAsGFM.nodeToGFM(documentFragment); - clipboardData.setData('text/x-gfm', gfm); - } - - handlePaste(e) { - const clipboardData = e.originalEvent.clipboardData; - if (!clipboardData) return; - - const gfm = clipboardData.getData('text/x-gfm'); - if (!gfm) return; - - e.preventDefault(); - - window.gl.utils.insertText(e.target, gfm); - } - - static nodeToGFM(node) { - if (node.nodeType === Node.TEXT_NODE) { - return node.textContent; - } - - const text = this.innerGFM(node); - - if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - return text; - } - - for (const filter in gfmRules) { - const rules = gfmRules[filter]; - - for (const selector in rules) { - const func = rules[selector]; - - if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; - - const result = func(node, text); - if (result === false) continue; - - return result; - } - } - - return text; - } - - 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; - - new CopyAsGFM(); -})(); diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js new file mode 100644 index 00000000000..947c129d5b5 --- /dev/null +++ b/app/assets/javascripts/create_label.js @@ -0,0 +1,132 @@ +/* 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(); + + 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'); + } + + addBinding () { + const self = 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.$dropdownBack.on('click', this.resetForm.bind(this)); + + this.$cancelButton.on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + self.resetForm(); + self.$dropdownBack.trigger('click'); + }); + + this.$newLabelCreateButton.on('click', this.saveLabel.bind(this)); + } + + 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'); + } + + enableLabelCreateButton () { + if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') { + this.$newLabelError.hide(); + this.$newLabelCreateButton.enable(); + } else { + this.$newLabelCreateButton.disable(); + } + } + + resetForm () { + this.$newLabelField + .val('') + .trigger('change'); + + this.$newColorField + .val('') + .trigger('change'); + + this.$colorPreview + .css('background-color', '') + .parent() + .removeClass('is-active'); + } + + saveLabel (e) { + e.preventDefault(); + e.stopPropagation(); + + 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 = label.message.map(function (value, key) { + return key + " " + value[0]; + }).join("
    "); + } + + this.$newLabelError + .html(errors) + .show(); + } else { + this.$dropdownBack.trigger('click'); + + $(document).trigger('created.label', label); + } + }); + } + } + + if (!w.gl) { + w.gl = {}; + } + + gl.CreateLabelDropdown = CreateLabelDropdown; +})(window); diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6 deleted file mode 100644 index 947c129d5b5..00000000000 --- a/app/assets/javascripts/create_label.js.es6 +++ /dev/null @@ -1,132 +0,0 @@ -/* 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(); - - 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'); - } - - addBinding () { - const self = 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.$dropdownBack.on('click', this.resetForm.bind(this)); - - this.$cancelButton.on('click', function(e) { - e.preventDefault(); - e.stopPropagation(); - - self.resetForm(); - self.$dropdownBack.trigger('click'); - }); - - this.$newLabelCreateButton.on('click', this.saveLabel.bind(this)); - } - - 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'); - } - - enableLabelCreateButton () { - if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') { - this.$newLabelError.hide(); - this.$newLabelCreateButton.enable(); - } else { - this.$newLabelCreateButton.disable(); - } - } - - resetForm () { - this.$newLabelField - .val('') - .trigger('change'); - - this.$newColorField - .val('') - .trigger('change'); - - this.$colorPreview - .css('background-color', '') - .parent() - .removeClass('is-active'); - } - - saveLabel (e) { - e.preventDefault(); - e.stopPropagation(); - - 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 = label.message.map(function (value, key) { - return key + " " + value[0]; - }).join("
    "); - } - - this.$newLabelError - .html(errors) - .show(); - } else { - this.$dropdownBack.trigger('click'); - - $(document).trigger('created.label', label); - } - }); - } - } - - if (!w.gl) { - w.gl = {}; - } - - gl.CreateLabelDropdown = CreateLabelDropdown; -})(window); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js new file mode 100644 index 00000000000..b83a4c63fad --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js @@ -0,0 +1,45 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageCodeComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` +
    +
    + {{ stage.description }} +
    +
    +
    + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 deleted file mode 100644 index b83a4c63fad..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - global.cycleAnalytics.StageCodeComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` -
    -
    - {{ stage.description }} -
    - -
    - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js new file mode 100644 index 00000000000..cb1687dcc7a --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js @@ -0,0 +1,47 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageIssueComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` +
    +
    + {{ stage.description }} +
    + +
    + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 deleted file mode 100644 index cb1687dcc7a..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - global.cycleAnalytics.StageIssueComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` -
    -
    - {{ stage.description }} -
    - -
    - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js new file mode 100644 index 00000000000..513298ba4e7 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js @@ -0,0 +1,44 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StagePlanComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` +
    +
    + {{ stage.description }} +
    + +
    + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 deleted file mode 100644 index 513298ba4e7..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - global.cycleAnalytics.StagePlanComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` -
    -
    - {{ stage.description }} -
    - -
    - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js new file mode 100644 index 00000000000..73f4205b578 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js @@ -0,0 +1,47 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageProductionComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` +
    +
    + {{ stage.description }} +
    + +
    + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 deleted file mode 100644 index 73f4205b578..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - global.cycleAnalytics.StageProductionComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` -
    -
    - {{ stage.description }} -
    - -
    - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js new file mode 100644 index 00000000000..501ffb1fac9 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js @@ -0,0 +1,57 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageReviewComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` +
    +
    + {{ stage.description }} +
    + +
    + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 deleted file mode 100644 index 501ffb1fac9..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - global.cycleAnalytics.StageReviewComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` -
    -
    - {{ stage.description }} -
    - -
    - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js new file mode 100644 index 00000000000..82622232f64 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js @@ -0,0 +1,44 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageStagingComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` +
    +
    + {{ stage.description }} +
    + +
    + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 deleted file mode 100644 index 82622232f64..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - global.cycleAnalytics.StageStagingComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` -
    -
    - {{ stage.description }} -
    - -
    - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js new file mode 100644 index 00000000000..4bfd363a1f1 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js @@ -0,0 +1,44 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageTestComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` +
    +
    + {{ stage.description }} +
    + +
    + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 deleted file mode 100644 index 4bfd363a1f1..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - global.cycleAnalytics.StageTestComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` -
    -
    - {{ stage.description }} -
    - -
    - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js b/app/assets/javascripts/cycle_analytics/components/total_time_component.js new file mode 100644 index 00000000000..0d85e1a4678 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js @@ -0,0 +1,25 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.TotalTimeComponent = Vue.extend({ + props: { + time: Object, + }, + template: ` + + + + + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 deleted file mode 100644 index 0d85e1a4678..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - global.cycleAnalytics.TotalTimeComponent = Vue.extend({ - props: { - time: Object, - }, - template: ` - - - - - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js new file mode 100644 index 00000000000..8392183b8a1 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -0,0 +1,128 @@ +/* global Vue */ +/* global Cookies */ +/* global Flash */ + +window.Vue = require('vue'); +window.Cookies = require('vendor/js.cookie'); + +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('./svg', false, /^\.\/.*\.js$/)); +requireAll(require.context('.', true, /^\.\/(?!cycle_analytics_bundle).*\.js$/)); + +$(() => { + const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; + const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); + const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore; + const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({ + requestPath: cycleAnalyticsEl.dataset.requestPath, + }); + + gl.cycleAnalyticsApp = new Vue({ + el: '#cycle-analytics', + name: 'CycleAnalytics', + data: { + state: cycleAnalyticsStore.state, + isLoading: false, + isLoadingStage: false, + isEmptyStage: false, + hasError: false, + startDate: 30, + isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE), + }, + computed: { + currentStage() { + return cycleAnalyticsStore.currentActiveStage(); + }, + }, + components: { + 'stage-issue-component': gl.cycleAnalytics.StageIssueComponent, + 'stage-plan-component': gl.cycleAnalytics.StagePlanComponent, + 'stage-code-component': gl.cycleAnalytics.StageCodeComponent, + 'stage-test-component': gl.cycleAnalytics.StageTestComponent, + 'stage-review-component': gl.cycleAnalytics.StageReviewComponent, + 'stage-staging-component': gl.cycleAnalytics.StageStagingComponent, + 'stage-production-component': gl.cycleAnalytics.StageProductionComponent, + }, + created() { + this.fetchCycleAnalyticsData(); + }, + methods: { + handleError() { + cycleAnalyticsStore.setErrorState(true); + return new Flash('There was an error while fetching cycle analytics data.'); + }, + initDropdown() { + const $dropdown = $('.js-ca-dropdown'); + const $label = $dropdown.find('.dropdown-label'); + + $dropdown.find('li a').off('click').on('click', (e) => { + e.preventDefault(); + const $target = $(e.currentTarget); + this.startDate = $target.data('value'); + + $label.text($target.text().trim()); + this.fetchCycleAnalyticsData({ startDate: this.startDate }); + }); + }, + fetchCycleAnalyticsData(options) { + const fetchOptions = options || { startDate: this.startDate }; + + this.isLoading = true; + + cycleAnalyticsService + .fetchCycleAnalyticsData(fetchOptions) + .done((response) => { + cycleAnalyticsStore.setCycleAnalyticsData(response); + this.selectDefaultStage(); + this.initDropdown(); + }) + .error(() => { + this.handleError(); + }) + .always(() => { + this.isLoading = false; + }); + }, + selectDefaultStage() { + const stage = this.state.stages.first(); + this.selectStage(stage); + }, + selectStage(stage) { + if (this.isLoadingStage) return; + if (this.currentStage === stage) return; + + if (!stage.isUserAllowed) { + cycleAnalyticsStore.setActiveStage(stage); + return; + } + + this.isLoadingStage = true; + cycleAnalyticsStore.setStageEvents([]); + cycleAnalyticsStore.setActiveStage(stage); + + cycleAnalyticsService + .fetchStageData({ + stage, + startDate: this.startDate, + }) + .done((response) => { + this.isEmptyStage = !response.events.length; + cycleAnalyticsStore.setStageEvents(response.events); + }) + .error(() => { + this.isEmptyStage = true; + }) + .always(() => { + this.isLoadingStage = false; + }); + }, + dismissOverviewDialog() { + this.isOverviewDialogDismissed = true; + Cookies.set(OVERVIEW_DIALOG_COOKIE, '1'); + }, + }, + }); + + // Register global components + Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); +}); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 deleted file mode 100644 index c41c57c1dcd..00000000000 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ /dev/null @@ -1,128 +0,0 @@ -/* global Vue */ -/* global Cookies */ -/* global Flash */ - -window.Vue = require('vue'); -window.Cookies = require('vendor/js.cookie'); - -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('.', true, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/)); - -$(() => { - const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; - const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); - const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore; - const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({ - requestPath: cycleAnalyticsEl.dataset.requestPath, - }); - - gl.cycleAnalyticsApp = new Vue({ - el: '#cycle-analytics', - name: 'CycleAnalytics', - data: { - state: cycleAnalyticsStore.state, - isLoading: false, - isLoadingStage: false, - isEmptyStage: false, - hasError: false, - startDate: 30, - isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE), - }, - computed: { - currentStage() { - return cycleAnalyticsStore.currentActiveStage(); - }, - }, - components: { - 'stage-issue-component': gl.cycleAnalytics.StageIssueComponent, - 'stage-plan-component': gl.cycleAnalytics.StagePlanComponent, - 'stage-code-component': gl.cycleAnalytics.StageCodeComponent, - 'stage-test-component': gl.cycleAnalytics.StageTestComponent, - 'stage-review-component': gl.cycleAnalytics.StageReviewComponent, - 'stage-staging-component': gl.cycleAnalytics.StageStagingComponent, - 'stage-production-component': gl.cycleAnalytics.StageProductionComponent, - }, - created() { - this.fetchCycleAnalyticsData(); - }, - methods: { - handleError() { - cycleAnalyticsStore.setErrorState(true); - return new Flash('There was an error while fetching cycle analytics data.'); - }, - initDropdown() { - const $dropdown = $('.js-ca-dropdown'); - const $label = $dropdown.find('.dropdown-label'); - - $dropdown.find('li a').off('click').on('click', (e) => { - e.preventDefault(); - const $target = $(e.currentTarget); - this.startDate = $target.data('value'); - - $label.text($target.text().trim()); - this.fetchCycleAnalyticsData({ startDate: this.startDate }); - }); - }, - fetchCycleAnalyticsData(options) { - const fetchOptions = options || { startDate: this.startDate }; - - this.isLoading = true; - - cycleAnalyticsService - .fetchCycleAnalyticsData(fetchOptions) - .done((response) => { - cycleAnalyticsStore.setCycleAnalyticsData(response); - this.selectDefaultStage(); - this.initDropdown(); - }) - .error(() => { - this.handleError(); - }) - .always(() => { - this.isLoading = false; - }); - }, - selectDefaultStage() { - const stage = this.state.stages.first(); - this.selectStage(stage); - }, - selectStage(stage) { - if (this.isLoadingStage) return; - if (this.currentStage === stage) return; - - if (!stage.isUserAllowed) { - cycleAnalyticsStore.setActiveStage(stage); - return; - } - - this.isLoadingStage = true; - cycleAnalyticsStore.setStageEvents([]); - cycleAnalyticsStore.setActiveStage(stage); - - cycleAnalyticsService - .fetchStageData({ - stage, - startDate: this.startDate, - }) - .done((response) => { - this.isEmptyStage = !response.events.length; - cycleAnalyticsStore.setStageEvents(response.events); - }) - .error(() => { - this.isEmptyStage = true; - }) - .always(() => { - this.isLoadingStage = false; - }); - }, - dismissOverviewDialog() { - this.isOverviewDialogDismissed = true; - Cookies.set(OVERVIEW_DIALOG_COOKIE, '1'); - }, - }, - }); - - // Register global components - Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); -}); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js new file mode 100644 index 00000000000..9f74b14c4b9 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js @@ -0,0 +1,41 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + class CycleAnalyticsService { + constructor(options) { + this.requestPath = options.requestPath; + } + + fetchCycleAnalyticsData(options) { + options = options || { startDate: 30 }; + + return $.ajax({ + url: this.requestPath, + method: 'GET', + dataType: 'json', + contentType: 'application/json', + data: { + cycle_analytics: { + start_date: options.startDate, + }, + }, + }); + } + + fetchStageData(options) { + const { + stage, + startDate, + } = options; + + return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, { + cycle_analytics: { + start_date: startDate, + }, + }); + } + } + + global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 deleted file mode 100644 index 9f74b14c4b9..00000000000 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - class CycleAnalyticsService { - constructor(options) { - this.requestPath = options.requestPath; - } - - fetchCycleAnalyticsData(options) { - options = options || { startDate: 30 }; - - return $.ajax({ - url: this.requestPath, - method: 'GET', - dataType: 'json', - contentType: 'application/json', - data: { - cycle_analytics: { - start_date: options.startDate, - }, - }, - }); - } - - fetchStageData(options) { - const { - stage, - startDate, - } = options; - - return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, { - cycle_analytics: { - start_date: startDate, - }, - }); - } - } - - global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js new file mode 100644 index 00000000000..be732971c7f --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -0,0 +1,94 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + const EMPTY_STAGE_TEXTS = { + issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', + plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.', + code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.', + test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.', + review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.', + staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', + production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.', + }; + + global.cycleAnalytics.CycleAnalyticsStore = { + state: { + summary: '', + stats: '', + analytics: '', + events: [], + stages: [], + }, + setCycleAnalyticsData(data) { + this.state = Object.assign(this.state, this.decorateData(data)); + }, + decorateData(data) { + const newData = {}; + + newData.stages = data.stats || []; + newData.summary = data.summary || []; + + newData.summary.forEach((item) => { + item.value = item.value || '-'; + }); + + newData.stages.forEach((item) => { + const stageName = item.title.toLowerCase(); + item.active = false; + item.isUserAllowed = data.permissions[stageName]; + item.emptyStageText = EMPTY_STAGE_TEXTS[stageName]; + item.component = `stage-${stageName}-component`; + }); + newData.analytics = data; + return newData; + }, + setLoadingState(state) { + this.state.isLoading = state; + }, + setErrorState(state) { + this.state.hasError = state; + }, + deactivateAllStages() { + this.state.stages.forEach((stage) => { + stage.active = false; + }); + }, + setActiveStage(stage) { + this.deactivateAllStages(); + stage.active = true; + }, + setStageEvents(events) { + this.state.events = this.decorateEvents(events); + }, + decorateEvents(events) { + const newEvents = []; + + events.forEach((item) => { + if (!item) return; + + item.totalTime = item.total_time; + item.author.webUrl = item.author.web_url; + item.author.avatarUrl = item.author.avatar_url; + + if (item.created_at) item.createdAt = item.created_at; + if (item.short_sha) item.shortSha = item.short_sha; + if (item.commit_url) item.commitUrl = item.commit_url; + + delete item.author.web_url; + delete item.author.avatar_url; + delete item.total_time; + delete item.created_at; + delete item.short_sha; + delete item.commit_url; + + newEvents.push(item); + }); + + return newEvents; + }, + currentActiveStage() { + return this.state.stages.find(stage => stage.active); + }, + }; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 deleted file mode 100644 index be732971c7f..00000000000 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 +++ /dev/null @@ -1,94 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - const EMPTY_STAGE_TEXTS = { - issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', - plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.', - code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.', - test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.', - review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.', - staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', - production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.', - }; - - global.cycleAnalytics.CycleAnalyticsStore = { - state: { - summary: '', - stats: '', - analytics: '', - events: [], - stages: [], - }, - setCycleAnalyticsData(data) { - this.state = Object.assign(this.state, this.decorateData(data)); - }, - decorateData(data) { - const newData = {}; - - newData.stages = data.stats || []; - newData.summary = data.summary || []; - - newData.summary.forEach((item) => { - item.value = item.value || '-'; - }); - - newData.stages.forEach((item) => { - const stageName = item.title.toLowerCase(); - item.active = false; - item.isUserAllowed = data.permissions[stageName]; - item.emptyStageText = EMPTY_STAGE_TEXTS[stageName]; - item.component = `stage-${stageName}-component`; - }); - newData.analytics = data; - return newData; - }, - setLoadingState(state) { - this.state.isLoading = state; - }, - setErrorState(state) { - this.state.hasError = state; - }, - deactivateAllStages() { - this.state.stages.forEach((stage) => { - stage.active = false; - }); - }, - setActiveStage(stage) { - this.deactivateAllStages(); - stage.active = true; - }, - setStageEvents(events) { - this.state.events = this.decorateEvents(events); - }, - decorateEvents(events) { - const newEvents = []; - - events.forEach((item) => { - if (!item) return; - - item.totalTime = item.total_time; - item.author.webUrl = item.author.web_url; - item.author.avatarUrl = item.author.avatar_url; - - if (item.created_at) item.createdAt = item.created_at; - if (item.short_sha) item.shortSha = item.short_sha; - if (item.commit_url) item.commitUrl = item.commit_url; - - delete item.author.web_url; - delete item.author.avatar_url; - delete item.total_time; - delete item.created_at; - delete item.short_sha; - delete item.commit_url; - - newEvents.push(item); - }); - - return newEvents; - }, - currentActiveStage() { - return this.state.stages.find(stage => stage.active); - }, - }; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js new file mode 100644 index 00000000000..5d486bcaf66 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js @@ -0,0 +1,7 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; + + global.cycleAnalytics.svgs.iconBranch = ''; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 deleted file mode 100644 index 5d486bcaf66..00000000000 --- a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; - - global.cycleAnalytics.svgs.iconBranch = ''; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js new file mode 100644 index 00000000000..661bf9e9f1c --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js @@ -0,0 +1,7 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; + + global.cycleAnalytics.svgs.iconBuildStatus = ''; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 deleted file mode 100644 index 661bf9e9f1c..00000000000 --- a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; - - global.cycleAnalytics.svgs.iconBuildStatus = ''; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js new file mode 100644 index 00000000000..2208c27a619 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js @@ -0,0 +1,7 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; + + global.cycleAnalytics.svgs.iconCommit = ''; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 deleted file mode 100644 index 2208c27a619..00000000000 --- a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; - - global.cycleAnalytics.svgs.iconCommit = ''; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js new file mode 100644 index 00000000000..c39e30fb7e0 --- /dev/null +++ b/app/assets/javascripts/diff.js @@ -0,0 +1,126 @@ +/* eslint-disable class-methods-use-this */ + +require('./lib/utils/url_utility'); + +(() => { + const UNFOLD_COUNT = 20; + let isBound = false; + + class Diff { + constructor() { + const $diffFile = $('.files .diff-file'); + $diffFile.singleFileDiff(); + $diffFile.filesCommentButton(); + + $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; + } + + 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 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 = $('.file-title, .click-to-expand', diffFile); + diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => { + this.highlighSelectedLine(); + if (cb) cb(); + }); + } else if (cb) { + cb(); + } + } + + 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.highlighSelectedLine(); + } + + 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)); + } + + highlighSelectedLine() { + 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'); + } + } + } + + window.gl = window.gl || {}; + window.gl.Diff = Diff; +})(); diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 deleted file mode 100644 index c39e30fb7e0..00000000000 --- a/app/assets/javascripts/diff.js.es6 +++ /dev/null @@ -1,126 +0,0 @@ -/* eslint-disable class-methods-use-this */ - -require('./lib/utils/url_utility'); - -(() => { - const UNFOLD_COUNT = 20; - let isBound = false; - - class Diff { - constructor() { - const $diffFile = $('.files .diff-file'); - $diffFile.singleFileDiff(); - $diffFile.filesCommentButton(); - - $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; - } - - 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 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 = $('.file-title, .click-to-expand', diffFile); - diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => { - this.highlighSelectedLine(); - if (cb) cb(); - }); - } else if (cb) { - cb(); - } - } - - 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.highlighSelectedLine(); - } - - 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)); - } - - highlighSelectedLine() { - 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'); - } - } - } - - window.gl = window.gl || {}; - window.gl.Diff = Diff; -})(); diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js new file mode 100644 index 00000000000..2514459e65e --- /dev/null +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js @@ -0,0 +1,59 @@ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */ +/* global Vue */ +/* global CommentsStore */ + +(() => { + const CommentAndResolveBtn = Vue.extend({ + props: { + discussionId: String, + }, + data() { + return { + textareaIsEmpty: true + }; + }, + computed: { + discussion: function () { + return CommentsStore.state[this.discussionId]; + }, + showButton: function () { + if (this.discussion) { + return this.discussion.isResolvable(); + } else { + return false; + } + }, + isDiscussionResolved: function () { + return this.discussion.isResolved(); + }, + buttonText: function () { + if (this.isDiscussionResolved) { + if (this.textareaIsEmpty) { + return "Unresolve discussion"; + } else { + return "Comment & unresolve discussion"; + } + } else { + if (this.textareaIsEmpty) { + return "Resolve discussion"; + } else { + return "Comment & resolve discussion"; + } + } + } + }, + mounted: function () { + const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`); + this.textareaIsEmpty = $textarea.val() === ''; + + $textarea.on('input.comment-and-resolve-btn', () => { + this.textareaIsEmpty = $textarea.val() === ''; + }); + }, + destroyed: function () { + $(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn'); + } + }); + + Vue.component('comment-and-resolve-btn', CommentAndResolveBtn); +})(window); diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 deleted file mode 100644 index 2514459e65e..00000000000 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 +++ /dev/null @@ -1,59 +0,0 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */ -/* global Vue */ -/* global CommentsStore */ - -(() => { - const CommentAndResolveBtn = Vue.extend({ - props: { - discussionId: String, - }, - data() { - return { - textareaIsEmpty: true - }; - }, - computed: { - discussion: function () { - return CommentsStore.state[this.discussionId]; - }, - showButton: function () { - if (this.discussion) { - return this.discussion.isResolvable(); - } else { - return false; - } - }, - isDiscussionResolved: function () { - return this.discussion.isResolved(); - }, - buttonText: function () { - if (this.isDiscussionResolved) { - if (this.textareaIsEmpty) { - return "Unresolve discussion"; - } else { - return "Comment & unresolve discussion"; - } - } else { - if (this.textareaIsEmpty) { - return "Resolve discussion"; - } else { - return "Comment & resolve discussion"; - } - } - } - }, - mounted: function () { - const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`); - this.textareaIsEmpty = $textarea.val() === ''; - - $textarea.on('input.comment-and-resolve-btn', () => { - this.textareaIsEmpty = $textarea.val() === ''; - }); - }, - destroyed: function () { - $(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn'); - } - }); - - Vue.component('comment-and-resolve-btn', CommentAndResolveBtn); -})(window); diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js new file mode 100644 index 00000000000..c3898873eaa --- /dev/null +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -0,0 +1,193 @@ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */ +/* global Vue */ +/* global DiscussionMixins */ +/* global CommentsStore */ + +(() => { + const JumpToDiscussion = Vue.extend({ + mixins: [DiscussionMixins], + props: { + discussionId: String + }, + data: function () { + return { + discussions: CommentsStore.state, + }; + }, + computed: { + discussion: function () { + return this.discussions[this.discussionId]; + }, + allResolved: function () { + return this.unresolvedDiscussionCount === 0; + }, + showButton: function () { + if (this.discussionId) { + if (this.unresolvedDiscussionCount > 1) { + return true; + } else { + return this.discussionId !== this.lastResolvedId; + } + } else { + return this.unresolvedDiscussionCount >= 1; + } + }, + lastResolvedId: function () { + let lastId; + for (const discussionId in this.discussions) { + const discussion = this.discussions[discussionId]; + + if (!discussion.isResolved()) { + lastId = discussion.id; + } + } + return lastId; + } + }, + methods: { + jumpToNextUnresolvedDiscussion: function () { + let discussionsSelector; + let discussionIdsInScope; + let firstUnresolvedDiscussionId; + let nextUnresolvedDiscussionId; + let activeTab = window.mrTabs.currentAction; + let hasDiscussionsToJumpTo = true; + let jumpToFirstDiscussion = !this.discussionId; + + const discussionIdsForElements = function(elements) { + return elements.map(function() { + return $(this).attr('data-discussion-id'); + }).toArray(); + }; + + const discussions = this.discussions; + + if (activeTab === 'diffs') { + discussionsSelector = '.diffs .notes[data-discussion-id]'; + discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); + + let unresolvedDiscussionCount = 0; + + for (let i = 0; i < discussionIdsInScope.length; i += 1) { + const discussionId = discussionIdsInScope[i]; + const discussion = discussions[discussionId]; + if (discussion && !discussion.isResolved()) { + unresolvedDiscussionCount += 1; + } + } + + if (this.discussionId && !this.discussion.isResolved()) { + // If this is the last unresolved discussion on the diffs tab, + // there are no discussions to jump to. + if (unresolvedDiscussionCount === 1) { + hasDiscussionsToJumpTo = false; + } + } else { + // If there are no unresolved discussions on the diffs tab at all, + // there are no discussions to jump to. + if (unresolvedDiscussionCount === 0) { + hasDiscussionsToJumpTo = false; + } + } + } else if (activeTab !== 'notes') { + // If we are on the commits or builds tabs, + // there are no discussions to jump to. + hasDiscussionsToJumpTo = false; + } + + if (!hasDiscussionsToJumpTo) { + // If there are no discussions to jump to on the current page, + // switch to the notes tab and jump to the first disucssion there. + window.mrTabs.activateTab('notes'); + activeTab = 'notes'; + jumpToFirstDiscussion = true; + } + + if (activeTab === 'notes') { + discussionsSelector = '.discussion[data-discussion-id]'; + discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); + } + + let currentDiscussionFound = false; + for (let i = 0; i < discussionIdsInScope.length; i += 1) { + const discussionId = discussionIdsInScope[i]; + const discussion = discussions[discussionId]; + + if (!discussion) { + // Discussions for comments on commits in this MR don't have a resolved status. + continue; + } + + if (!firstUnresolvedDiscussionId && !discussion.isResolved()) { + firstUnresolvedDiscussionId = discussionId; + + if (jumpToFirstDiscussion) { + break; + } + } + + if (!jumpToFirstDiscussion) { + if (currentDiscussionFound) { + if (!discussion.isResolved()) { + nextUnresolvedDiscussionId = discussionId; + break; + } + else { + continue; + } + } + + if (discussionId === this.discussionId) { + currentDiscussionFound = true; + } + } + } + + nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId; + + if (!nextUnresolvedDiscussionId) { + return; + } + + let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`); + + if (activeTab === 'notes') { + $target = $target.closest('.note-discussion'); + + // If the next discussion is closed, toggle it open. + if ($target.find('.js-toggle-content').is(':hidden')) { + $target.find('.js-toggle-button i').trigger('click'); + } + } else if (activeTab === 'diffs') { + // Resolved discussions are hidden in the diffs tab by default. + // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab. + // When jumping between unresolved discussions on the diffs tab, we show them. + $target.closest(".content").show(); + + $target = $target.closest("tr.notes_holder"); + $target.show(); + + // If we are on the diffs tab, we don't scroll to the discussion itself, but to + // 4 diff lines above it: the line the discussion was in response to + 3 context + let prevEl; + for (let i = 0; i < 4; i += 1) { + prevEl = $target.prev(); + + // If the discussion doesn't have 4 lines above it, we'll have to do with fewer. + if (!prevEl.hasClass("line_holder")) { + break; + } + + $target = prevEl; + } + } + + $.scrollTo($target, { + offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()) + }); + } + } + }); + + Vue.component('jump-to-discussion', JumpToDiscussion); +})(); diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 deleted file mode 100644 index c3898873eaa..00000000000 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 +++ /dev/null @@ -1,193 +0,0 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */ -/* global Vue */ -/* global DiscussionMixins */ -/* global CommentsStore */ - -(() => { - const JumpToDiscussion = Vue.extend({ - mixins: [DiscussionMixins], - props: { - discussionId: String - }, - data: function () { - return { - discussions: CommentsStore.state, - }; - }, - computed: { - discussion: function () { - return this.discussions[this.discussionId]; - }, - allResolved: function () { - return this.unresolvedDiscussionCount === 0; - }, - showButton: function () { - if (this.discussionId) { - if (this.unresolvedDiscussionCount > 1) { - return true; - } else { - return this.discussionId !== this.lastResolvedId; - } - } else { - return this.unresolvedDiscussionCount >= 1; - } - }, - lastResolvedId: function () { - let lastId; - for (const discussionId in this.discussions) { - const discussion = this.discussions[discussionId]; - - if (!discussion.isResolved()) { - lastId = discussion.id; - } - } - return lastId; - } - }, - methods: { - jumpToNextUnresolvedDiscussion: function () { - let discussionsSelector; - let discussionIdsInScope; - let firstUnresolvedDiscussionId; - let nextUnresolvedDiscussionId; - let activeTab = window.mrTabs.currentAction; - let hasDiscussionsToJumpTo = true; - let jumpToFirstDiscussion = !this.discussionId; - - const discussionIdsForElements = function(elements) { - return elements.map(function() { - return $(this).attr('data-discussion-id'); - }).toArray(); - }; - - const discussions = this.discussions; - - if (activeTab === 'diffs') { - discussionsSelector = '.diffs .notes[data-discussion-id]'; - discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); - - let unresolvedDiscussionCount = 0; - - for (let i = 0; i < discussionIdsInScope.length; i += 1) { - const discussionId = discussionIdsInScope[i]; - const discussion = discussions[discussionId]; - if (discussion && !discussion.isResolved()) { - unresolvedDiscussionCount += 1; - } - } - - if (this.discussionId && !this.discussion.isResolved()) { - // If this is the last unresolved discussion on the diffs tab, - // there are no discussions to jump to. - if (unresolvedDiscussionCount === 1) { - hasDiscussionsToJumpTo = false; - } - } else { - // If there are no unresolved discussions on the diffs tab at all, - // there are no discussions to jump to. - if (unresolvedDiscussionCount === 0) { - hasDiscussionsToJumpTo = false; - } - } - } else if (activeTab !== 'notes') { - // If we are on the commits or builds tabs, - // there are no discussions to jump to. - hasDiscussionsToJumpTo = false; - } - - if (!hasDiscussionsToJumpTo) { - // If there are no discussions to jump to on the current page, - // switch to the notes tab and jump to the first disucssion there. - window.mrTabs.activateTab('notes'); - activeTab = 'notes'; - jumpToFirstDiscussion = true; - } - - if (activeTab === 'notes') { - discussionsSelector = '.discussion[data-discussion-id]'; - discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); - } - - let currentDiscussionFound = false; - for (let i = 0; i < discussionIdsInScope.length; i += 1) { - const discussionId = discussionIdsInScope[i]; - const discussion = discussions[discussionId]; - - if (!discussion) { - // Discussions for comments on commits in this MR don't have a resolved status. - continue; - } - - if (!firstUnresolvedDiscussionId && !discussion.isResolved()) { - firstUnresolvedDiscussionId = discussionId; - - if (jumpToFirstDiscussion) { - break; - } - } - - if (!jumpToFirstDiscussion) { - if (currentDiscussionFound) { - if (!discussion.isResolved()) { - nextUnresolvedDiscussionId = discussionId; - break; - } - else { - continue; - } - } - - if (discussionId === this.discussionId) { - currentDiscussionFound = true; - } - } - } - - nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId; - - if (!nextUnresolvedDiscussionId) { - return; - } - - let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`); - - if (activeTab === 'notes') { - $target = $target.closest('.note-discussion'); - - // If the next discussion is closed, toggle it open. - if ($target.find('.js-toggle-content').is(':hidden')) { - $target.find('.js-toggle-button i').trigger('click'); - } - } else if (activeTab === 'diffs') { - // Resolved discussions are hidden in the diffs tab by default. - // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab. - // When jumping between unresolved discussions on the diffs tab, we show them. - $target.closest(".content").show(); - - $target = $target.closest("tr.notes_holder"); - $target.show(); - - // If we are on the diffs tab, we don't scroll to the discussion itself, but to - // 4 diff lines above it: the line the discussion was in response to + 3 context - let prevEl; - for (let i = 0; i < 4; i += 1) { - prevEl = $target.prev(); - - // If the discussion doesn't have 4 lines above it, we'll have to do with fewer. - if (!prevEl.hasClass("line_holder")) { - break; - } - - $target = prevEl; - } - } - - $.scrollTo($target, { - offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()) - }); - } - } - }); - - Vue.component('jump-to-discussion', JumpToDiscussion); -})(); diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js new file mode 100644 index 00000000000..5852b8bbdb7 --- /dev/null +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -0,0 +1,113 @@ +/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */ +/* global Vue */ +/* global CommentsStore */ +/* global ResolveService */ +/* global Flash */ + +(() => { + const ResolveBtn = Vue.extend({ + props: { + noteId: Number, + discussionId: String, + resolved: Boolean, + projectPath: String, + canResolve: Boolean, + resolvedBy: String + }, + data: function () { + return { + discussions: CommentsStore.state, + loading: false + }; + }, + watch: { + 'discussions': { + handler: 'updateTooltip', + deep: true + } + }, + computed: { + discussion: function () { + return this.discussions[this.discussionId]; + }, + note: function () { + if (this.discussion) { + return this.discussion.getNote(this.noteId); + } else { + return undefined; + } + }, + buttonText: function () { + if (this.isResolved) { + return `Resolved by ${this.resolvedByName}`; + } else if (this.canResolve) { + return 'Mark as resolved'; + } else { + return 'Unable to resolve'; + } + }, + isResolved: function () { + if (this.note) { + return this.note.resolved; + } else { + return false; + } + }, + resolvedByName: function () { + return this.note.resolved_by; + }, + }, + methods: { + updateTooltip: function () { + this.$nextTick(() => { + $(this.$refs.button) + .tooltip('hide') + .tooltip('fixTitle'); + }); + }, + resolve: function () { + if (!this.canResolve) return; + + let promise; + this.loading = true; + + if (this.isResolved) { + promise = ResolveService + .unresolve(this.projectPath, this.noteId); + } else { + promise = ResolveService + .resolve(this.projectPath, this.noteId); + } + + promise.then((response) => { + this.loading = false; + + if (response.status === 200) { + const data = response.json(); + const resolved_by = data ? data.resolved_by : null; + + CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); + this.discussion.updateHeadline(data); + } else { + new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); + } + + this.updateTooltip(); + }); + } + }, + mounted: function () { + $(this.$refs.button).tooltip({ + container: 'body' + }); + }, + beforeDestroy: function () { + CommentsStore.delete(this.discussionId, this.noteId); + }, + created: function () { + CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy); + } + }); + + Vue.component('resolve-btn', ResolveBtn); +})(); diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 deleted file mode 100644 index 5852b8bbdb7..00000000000 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 +++ /dev/null @@ -1,113 +0,0 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */ -/* global Vue */ -/* global CommentsStore */ -/* global ResolveService */ -/* global Flash */ - -(() => { - const ResolveBtn = Vue.extend({ - props: { - noteId: Number, - discussionId: String, - resolved: Boolean, - projectPath: String, - canResolve: Boolean, - resolvedBy: String - }, - data: function () { - return { - discussions: CommentsStore.state, - loading: false - }; - }, - watch: { - 'discussions': { - handler: 'updateTooltip', - deep: true - } - }, - computed: { - discussion: function () { - return this.discussions[this.discussionId]; - }, - note: function () { - if (this.discussion) { - return this.discussion.getNote(this.noteId); - } else { - return undefined; - } - }, - buttonText: function () { - if (this.isResolved) { - return `Resolved by ${this.resolvedByName}`; - } else if (this.canResolve) { - return 'Mark as resolved'; - } else { - return 'Unable to resolve'; - } - }, - isResolved: function () { - if (this.note) { - return this.note.resolved; - } else { - return false; - } - }, - resolvedByName: function () { - return this.note.resolved_by; - }, - }, - methods: { - updateTooltip: function () { - this.$nextTick(() => { - $(this.$refs.button) - .tooltip('hide') - .tooltip('fixTitle'); - }); - }, - resolve: function () { - if (!this.canResolve) return; - - let promise; - this.loading = true; - - if (this.isResolved) { - promise = ResolveService - .unresolve(this.projectPath, this.noteId); - } else { - promise = ResolveService - .resolve(this.projectPath, this.noteId); - } - - promise.then((response) => { - this.loading = false; - - if (response.status === 200) { - const data = response.json(); - const resolved_by = data ? data.resolved_by : null; - - CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); - this.discussion.updateHeadline(data); - } else { - new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); - } - - this.updateTooltip(); - }); - } - }, - mounted: function () { - $(this.$refs.button).tooltip({ - container: 'body' - }); - }, - beforeDestroy: function () { - CommentsStore.delete(this.discussionId, this.noteId); - }, - created: function () { - CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy); - } - }); - - Vue.component('resolve-btn', ResolveBtn); -})(); diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js new file mode 100644 index 00000000000..72cdae812bc --- /dev/null +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js @@ -0,0 +1,26 @@ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */ +/* global Vue */ +/* global DiscussionMixins */ +/* global CommentsStore */ + +((w) => { + w.ResolveCount = Vue.extend({ + mixins: [DiscussionMixins], + props: { + loggedOut: Boolean + }, + data: function () { + return { + discussions: CommentsStore.state + }; + }, + computed: { + allResolved: function () { + return this.resolvedDiscussionCount === this.discussionCount; + }, + resolvedCountText() { + return this.discussionCount === 1 ? 'discussion' : 'discussions'; + } + } + }); +})(window); diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 deleted file mode 100644 index 72cdae812bc..00000000000 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */ -/* global Vue */ -/* global DiscussionMixins */ -/* global CommentsStore */ - -((w) => { - w.ResolveCount = Vue.extend({ - mixins: [DiscussionMixins], - props: { - loggedOut: Boolean - }, - data: function () { - return { - discussions: CommentsStore.state - }; - }, - computed: { - allResolved: function () { - return this.resolvedDiscussionCount === this.discussionCount; - }, - resolvedCountText() { - return this.discussionCount === 1 ? 'discussion' : 'discussions'; - } - } - }); -})(window); diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js new file mode 100644 index 00000000000..ee5f62b2d9e --- /dev/null +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js @@ -0,0 +1,63 @@ +/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */ +/* global Vue */ +/* global CommentsStore */ +/* global ResolveService */ + +(() => { + const ResolveDiscussionBtn = Vue.extend({ + props: { + discussionId: String, + mergeRequestId: Number, + projectPath: String, + canResolve: Boolean, + }, + data: function() { + return { + discussions: CommentsStore.state + }; + }, + computed: { + discussion: function () { + return this.discussions[this.discussionId]; + }, + showButton: function () { + if (this.discussion) { + return this.discussion.isResolvable(); + } else { + return false; + } + }, + isDiscussionResolved: function () { + if (this.discussion) { + return this.discussion.isResolved(); + } else { + return false; + } + }, + buttonText: function () { + if (this.isDiscussionResolved) { + return "Unresolve discussion"; + } else { + return "Resolve discussion"; + } + }, + loading: function () { + if (this.discussion) { + return this.discussion.loading; + } else { + return false; + } + } + }, + methods: { + resolve: function () { + ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId); + } + }, + created: function () { + CommentsStore.createDiscussion(this.discussionId, this.canResolve); + } + }); + + Vue.component('resolve-discussion-btn', ResolveDiscussionBtn); +})(); diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 deleted file mode 100644 index ee5f62b2d9e..00000000000 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */ -/* global Vue */ -/* global CommentsStore */ -/* global ResolveService */ - -(() => { - const ResolveDiscussionBtn = Vue.extend({ - props: { - discussionId: String, - mergeRequestId: Number, - projectPath: String, - canResolve: Boolean, - }, - data: function() { - return { - discussions: CommentsStore.state - }; - }, - computed: { - discussion: function () { - return this.discussions[this.discussionId]; - }, - showButton: function () { - if (this.discussion) { - return this.discussion.isResolvable(); - } else { - return false; - } - }, - isDiscussionResolved: function () { - if (this.discussion) { - return this.discussion.isResolved(); - } else { - return false; - } - }, - buttonText: function () { - if (this.isDiscussionResolved) { - return "Unresolve discussion"; - } else { - return "Resolve discussion"; - } - }, - loading: function () { - if (this.discussion) { - return this.discussion.loading; - } else { - return false; - } - } - }, - methods: { - resolve: function () { - ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId); - } - }, - created: function () { - CommentsStore.createDiscussion(this.discussionId, this.canResolve); - } - }); - - Vue.component('resolve-discussion-btn', ResolveDiscussionBtn); -})(); diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js new file mode 100644 index 00000000000..cd7837bb485 --- /dev/null +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -0,0 +1,49 @@ +/* eslint-disable func-names, comma-dangle, new-cap, no-new, import/newline-after-import, no-multi-spaces, max-len */ +/* global Vue */ +/* global ResolveCount */ + +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('./models', false, /^\.\/.*\.js$/)); +requireAll(require.context('./stores', false, /^\.\/.*\.js$/)); +requireAll(require.context('./services', false, /^\.\/.*\.js$/)); +requireAll(require.context('./mixins', false, /^\.\/.*\.js$/)); +requireAll(require.context('./components', false, /^\.\/.*\.js$/)); + +$(() => { + const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn'; + + window.gl = window.gl || {}; + window.gl.diffNoteApps = {}; + + gl.diffNotesCompileComponents = () => { + const $components = $(COMPONENT_SELECTOR).filter(function () { + return $(this).closest('resolve-count').length !== 1; + }); + + if ($components) { + $components.each(function () { + const $this = $(this); + const noteId = $this.attr(':note-id'); + const tmp = Vue.extend({ + template: $this.get(0).outerHTML + }); + const tmpApp = new tmp().$mount(); + + if (noteId) { + gl.diffNoteApps[`note_${noteId}`] = tmpApp; + } + + $this.replaceWith(tmpApp.$el); + }); + } + }; + + gl.diffNotesCompileComponents(); + + new Vue({ + el: '#resolve-count-app', + components: { + 'resolve-count': ResolveCount + } + }); +}); diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 deleted file mode 100644 index f0edfb8aaf1..00000000000 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable func-names, comma-dangle, new-cap, no-new, import/newline-after-import, no-multi-spaces, max-len */ -/* global Vue */ -/* global ResolveCount */ - -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/)); - -$(() => { - const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn'; - - window.gl = window.gl || {}; - window.gl.diffNoteApps = {}; - - gl.diffNotesCompileComponents = () => { - const $components = $(COMPONENT_SELECTOR).filter(function () { - return $(this).closest('resolve-count').length !== 1; - }); - - if ($components) { - $components.each(function () { - const $this = $(this); - const noteId = $this.attr(':note-id'); - const tmp = Vue.extend({ - template: $this.get(0).outerHTML - }); - const tmpApp = new tmp().$mount(); - - if (noteId) { - gl.diffNoteApps[`note_${noteId}`] = tmpApp; - } - - $this.replaceWith(tmpApp.$el); - }); - } - }; - - gl.diffNotesCompileComponents(); - - new Vue({ - el: '#resolve-count-app', - components: { - 'resolve-count': ResolveCount - } - }); -}); diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js b/app/assets/javascripts/diff_notes/mixins/discussion.js new file mode 100644 index 00000000000..3c08c222f46 --- /dev/null +++ b/app/assets/javascripts/diff_notes/mixins/discussion.js @@ -0,0 +1,37 @@ +/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */ + +((w) => { + w.DiscussionMixins = { + computed: { + discussionCount: function () { + return Object.keys(this.discussions).length; + }, + resolvedDiscussionCount: function () { + let resolvedCount = 0; + + for (const discussionId in this.discussions) { + const discussion = this.discussions[discussionId]; + + if (discussion.isResolved()) { + resolvedCount += 1; + } + } + + return resolvedCount; + }, + unresolvedDiscussionCount: function () { + let unresolvedCount = 0; + + for (const discussionId in this.discussions) { + const discussion = this.discussions[discussionId]; + + if (!discussion.isResolved()) { + unresolvedCount += 1; + } + } + + return unresolvedCount; + } + } + }; +})(window); diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 deleted file mode 100644 index 3c08c222f46..00000000000 --- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */ - -((w) => { - w.DiscussionMixins = { - computed: { - discussionCount: function () { - return Object.keys(this.discussions).length; - }, - resolvedDiscussionCount: function () { - let resolvedCount = 0; - - for (const discussionId in this.discussions) { - const discussion = this.discussions[discussionId]; - - if (discussion.isResolved()) { - resolvedCount += 1; - } - } - - return resolvedCount; - }, - unresolvedDiscussionCount: function () { - let unresolvedCount = 0; - - for (const discussionId in this.discussions) { - const discussion = this.discussions[discussionId]; - - if (!discussion.isResolved()) { - unresolvedCount += 1; - } - } - - return unresolvedCount; - } - } - }; -})(window); diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js new file mode 100644 index 00000000000..fa518ba4d33 --- /dev/null +++ b/app/assets/javascripts/diff_notes/models/discussion.js @@ -0,0 +1,96 @@ +/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */ +/* global Vue */ +/* global NoteModel */ + +class DiscussionModel { + constructor (discussionId) { + this.id = discussionId; + this.notes = {}; + this.loading = false; + this.canResolve = false; + } + + createNote (noteId, canResolve, resolved, resolved_by) { + Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by)); + } + + deleteNote (noteId) { + Vue.delete(this.notes, noteId); + } + + getNote (noteId) { + return this.notes[noteId]; + } + + notesCount() { + return Object.keys(this.notes).length; + } + + isResolved () { + for (const noteId in this.notes) { + const note = this.notes[noteId]; + + if (!note.resolved) { + return false; + } + } + return true; + } + + resolveAllNotes (resolved_by) { + for (const noteId in this.notes) { + const note = this.notes[noteId]; + + if (!note.resolved) { + note.resolved = true; + note.resolved_by = resolved_by; + } + } + } + + unResolveAllNotes () { + for (const noteId in this.notes) { + const note = this.notes[noteId]; + + if (note.resolved) { + note.resolved = false; + note.resolved_by = null; + } + } + } + + updateHeadline (data) { + const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`; + const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`); + + if (data.discussion_headline_html) { + if ($discussionHeadline.length) { + $discussionHeadline.replaceWith(data.discussion_headline_html); + } else { + $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html); + } + + gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`)); + } else { + $discussionHeadline.remove(); + } + } + + isResolvable () { + if (!this.canResolve) { + return false; + } + + for (const noteId in this.notes) { + const note = this.notes[noteId]; + + if (note.canResolve) { + return true; + } + } + + return false; + } +} + +window.DiscussionModel = DiscussionModel; diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6 deleted file mode 100644 index fa518ba4d33..00000000000 --- a/app/assets/javascripts/diff_notes/models/discussion.js.es6 +++ /dev/null @@ -1,96 +0,0 @@ -/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */ -/* global Vue */ -/* global NoteModel */ - -class DiscussionModel { - constructor (discussionId) { - this.id = discussionId; - this.notes = {}; - this.loading = false; - this.canResolve = false; - } - - createNote (noteId, canResolve, resolved, resolved_by) { - Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by)); - } - - deleteNote (noteId) { - Vue.delete(this.notes, noteId); - } - - getNote (noteId) { - return this.notes[noteId]; - } - - notesCount() { - return Object.keys(this.notes).length; - } - - isResolved () { - for (const noteId in this.notes) { - const note = this.notes[noteId]; - - if (!note.resolved) { - return false; - } - } - return true; - } - - resolveAllNotes (resolved_by) { - for (const noteId in this.notes) { - const note = this.notes[noteId]; - - if (!note.resolved) { - note.resolved = true; - note.resolved_by = resolved_by; - } - } - } - - unResolveAllNotes () { - for (const noteId in this.notes) { - const note = this.notes[noteId]; - - if (note.resolved) { - note.resolved = false; - note.resolved_by = null; - } - } - } - - updateHeadline (data) { - const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`; - const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`); - - if (data.discussion_headline_html) { - if ($discussionHeadline.length) { - $discussionHeadline.replaceWith(data.discussion_headline_html); - } else { - $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html); - } - - gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`)); - } else { - $discussionHeadline.remove(); - } - } - - isResolvable () { - if (!this.canResolve) { - return false; - } - - for (const noteId in this.notes) { - const note = this.notes[noteId]; - - if (note.canResolve) { - return true; - } - } - - return false; - } -} - -window.DiscussionModel = DiscussionModel; diff --git a/app/assets/javascripts/diff_notes/models/note.js b/app/assets/javascripts/diff_notes/models/note.js new file mode 100644 index 00000000000..f3a7cba5ef6 --- /dev/null +++ b/app/assets/javascripts/diff_notes/models/note.js @@ -0,0 +1,13 @@ +/* eslint-disable camelcase, no-unused-vars */ + +class NoteModel { + constructor(discussionId, noteId, canResolve, resolved, resolved_by) { + this.discussionId = discussionId; + this.id = noteId; + this.canResolve = canResolve; + this.resolved = resolved; + this.resolved_by = resolved_by; + } +} + +window.NoteModel = NoteModel; diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6 deleted file mode 100644 index f3a7cba5ef6..00000000000 --- a/app/assets/javascripts/diff_notes/models/note.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable camelcase, no-unused-vars */ - -class NoteModel { - constructor(discussionId, noteId, canResolve, resolved, resolved_by) { - this.discussionId = discussionId; - this.id = noteId; - this.canResolve = canResolve; - this.resolved = resolved; - this.resolved_by = resolved_by; - } -} - -window.NoteModel = NoteModel; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js new file mode 100644 index 00000000000..a52c476352d --- /dev/null +++ b/app/assets/javascripts/diff_notes/services/resolve.js @@ -0,0 +1,93 @@ +/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */ +/* global Vue */ +/* global Flash */ +/* global CommentsStore */ + +((w) => { + class ResolveServiceClass { + constructor() { + this.noteResource = Vue.resource('notes{/noteId}/resolve'); + this.discussionResource = Vue.resource('merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve'); + } + + setCSRF() { + Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken(); + } + + prepareRequest(root) { + this.setCSRF(); + Vue.http.options.root = root; + } + + resolve(projectPath, noteId) { + this.prepareRequest(projectPath); + + return this.noteResource.save({ noteId }, {}); + } + + unresolve(projectPath, noteId) { + this.prepareRequest(projectPath); + + return this.noteResource.delete({ noteId }, {}); + } + + toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) { + const discussion = CommentsStore.state[discussionId]; + const isResolved = discussion.isResolved(); + let promise; + + if (isResolved) { + promise = this.unResolveAll(projectPath, mergeRequestId, discussionId); + } else { + promise = this.resolveAll(projectPath, mergeRequestId, discussionId); + } + + promise.then((response) => { + discussion.loading = false; + + if (response.status === 200) { + const data = response.json(); + const resolved_by = data ? data.resolved_by : null; + + if (isResolved) { + discussion.unResolveAllNotes(); + } else { + discussion.resolveAllNotes(resolved_by); + } + + discussion.updateHeadline(data); + } else { + new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert'); + } + }); + } + + resolveAll(projectPath, mergeRequestId, discussionId) { + const discussion = CommentsStore.state[discussionId]; + + this.prepareRequest(projectPath); + + discussion.loading = true; + + return this.discussionResource.save({ + mergeRequestId, + discussionId + }, {}); + } + + unResolveAll(projectPath, mergeRequestId, discussionId) { + const discussion = CommentsStore.state[discussionId]; + + this.prepareRequest(projectPath); + + discussion.loading = true; + + return this.discussionResource.delete({ + mergeRequestId, + discussionId + }, {}); + } + } + + w.ResolveService = new ResolveServiceClass(); +})(window); diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6 deleted file mode 100644 index a52c476352d..00000000000 --- a/app/assets/javascripts/diff_notes/services/resolve.js.es6 +++ /dev/null @@ -1,93 +0,0 @@ -/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */ -/* global Vue */ -/* global Flash */ -/* global CommentsStore */ - -((w) => { - class ResolveServiceClass { - constructor() { - this.noteResource = Vue.resource('notes{/noteId}/resolve'); - this.discussionResource = Vue.resource('merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve'); - } - - setCSRF() { - Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken(); - } - - prepareRequest(root) { - this.setCSRF(); - Vue.http.options.root = root; - } - - resolve(projectPath, noteId) { - this.prepareRequest(projectPath); - - return this.noteResource.save({ noteId }, {}); - } - - unresolve(projectPath, noteId) { - this.prepareRequest(projectPath); - - return this.noteResource.delete({ noteId }, {}); - } - - toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) { - const discussion = CommentsStore.state[discussionId]; - const isResolved = discussion.isResolved(); - let promise; - - if (isResolved) { - promise = this.unResolveAll(projectPath, mergeRequestId, discussionId); - } else { - promise = this.resolveAll(projectPath, mergeRequestId, discussionId); - } - - promise.then((response) => { - discussion.loading = false; - - if (response.status === 200) { - const data = response.json(); - const resolved_by = data ? data.resolved_by : null; - - if (isResolved) { - discussion.unResolveAllNotes(); - } else { - discussion.resolveAllNotes(resolved_by); - } - - discussion.updateHeadline(data); - } else { - new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert'); - } - }); - } - - resolveAll(projectPath, mergeRequestId, discussionId) { - const discussion = CommentsStore.state[discussionId]; - - this.prepareRequest(projectPath); - - discussion.loading = true; - - return this.discussionResource.save({ - mergeRequestId, - discussionId - }, {}); - } - - unResolveAll(projectPath, mergeRequestId, discussionId) { - const discussion = CommentsStore.state[discussionId]; - - this.prepareRequest(projectPath); - - discussion.loading = true; - - return this.discussionResource.delete({ - mergeRequestId, - discussionId - }, {}); - } - } - - w.ResolveService = new ResolveServiceClass(); -})(window); diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js new file mode 100644 index 00000000000..c80d979b977 --- /dev/null +++ b/app/assets/javascripts/diff_notes/stores/comments.js @@ -0,0 +1,57 @@ +/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */ +/* global Vue */ +/* global DiscussionModel */ + +((w) => { + w.CommentsStore = { + state: {}, + get: function (discussionId, noteId) { + return this.state[discussionId].getNote(noteId); + }, + createDiscussion: function (discussionId, canResolve) { + let discussion = this.state[discussionId]; + if (!this.state[discussionId]) { + discussion = new DiscussionModel(discussionId); + Vue.set(this.state, discussionId, discussion); + } + + if (canResolve !== undefined) { + discussion.canResolve = canResolve; + } + + return discussion; + }, + create: function (discussionId, noteId, canResolve, resolved, resolved_by) { + const discussion = this.createDiscussion(discussionId); + + discussion.createNote(noteId, canResolve, resolved, resolved_by); + }, + update: function (discussionId, noteId, resolved, resolved_by) { + const discussion = this.state[discussionId]; + const note = discussion.getNote(noteId); + note.resolved = resolved; + note.resolved_by = resolved_by; + }, + delete: function (discussionId, noteId) { + const discussion = this.state[discussionId]; + discussion.deleteNote(noteId); + + if (discussion.notesCount() === 0) { + Vue.delete(this.state, discussionId); + } + }, + unresolvedDiscussionIds: function () { + const ids = []; + + for (const discussionId in this.state) { + const discussion = this.state[discussionId]; + + if (!discussion.isResolved()) { + ids.push(discussion.id); + } + } + + return ids; + } + }; +})(window); diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6 deleted file mode 100644 index c80d979b977..00000000000 --- a/app/assets/javascripts/diff_notes/stores/comments.js.es6 +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */ -/* global Vue */ -/* global DiscussionModel */ - -((w) => { - w.CommentsStore = { - state: {}, - get: function (discussionId, noteId) { - return this.state[discussionId].getNote(noteId); - }, - createDiscussion: function (discussionId, canResolve) { - let discussion = this.state[discussionId]; - if (!this.state[discussionId]) { - discussion = new DiscussionModel(discussionId); - Vue.set(this.state, discussionId, discussion); - } - - if (canResolve !== undefined) { - discussion.canResolve = canResolve; - } - - return discussion; - }, - create: function (discussionId, noteId, canResolve, resolved, resolved_by) { - const discussion = this.createDiscussion(discussionId); - - discussion.createNote(noteId, canResolve, resolved, resolved_by); - }, - update: function (discussionId, noteId, resolved, resolved_by) { - const discussion = this.state[discussionId]; - const note = discussion.getNote(noteId); - note.resolved = resolved; - note.resolved_by = resolved_by; - }, - delete: function (discussionId, noteId) { - const discussion = this.state[discussionId]; - discussion.deleteNote(noteId); - - if (discussion.notesCount() === 0) { - Vue.delete(this.state, discussionId); - } - }, - unresolvedDiscussionIds: function () { - const ids = []; - - for (const discussionId in this.state) { - const discussion = this.state[discussionId]; - - if (!discussion.isResolved()) { - ids.push(discussion.id); - } - } - - return ids; - } - }; -})(window); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js new file mode 100644 index 00000000000..1e764a950ca --- /dev/null +++ b/app/assets/javascripts/dispatcher.js @@ -0,0 +1,379 @@ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ +/* global UsernameValidator */ +/* global ActiveTabMemoizer */ +/* global ShortcutsNavigation */ +/* global Build */ +/* global Issuable */ +/* global Issue */ +/* global ShortcutsIssuable */ +/* global ZenMode */ +/* global Milestone */ +/* global IssuableForm */ +/* global LabelsSelect */ +/* global MilestoneSelect */ +/* global MergedButtons */ +/* global Commit */ +/* global NotificationsForm */ +/* global TreeView */ +/* global NotificationsDropdown */ +/* global UsersSelect */ +/* global GroupAvatar */ +/* global LineHighlighter */ +/* global ProjectFork */ +/* global BuildArtifacts */ +/* global GroupsSelect */ +/* global Search */ +/* global Admin */ +/* global NamespaceSelects */ +/* global ShortcutsDashboardNavigation */ +/* global Project */ +/* global ProjectAvatar */ +/* global CompareAutocomplete */ +/* global ProjectNew */ +/* global Star */ +/* global ProjectShow */ +/* global Labels */ +/* global Shortcuts */ + +const ShortcutsBlob = require('./shortcuts_blob'); + +(function() { + var Dispatcher; + + $(function() { + return new Dispatcher(); + }); + + Dispatcher = (function() { + function Dispatcher() { + this.initSearch(); + this.initFieldErrors(); + this.initPageScripts(); + } + + Dispatcher.prototype.initPageScripts = function() { + var page, path, shortcut_handler; + page = $('body').attr('data-page'); + if (!page) { + return false; + } + path = page.split(':'); + shortcut_handler = null; + switch (page) { + case 'sessions:new': + new UsernameValidator(); + new ActiveTabMemoizer(); + break; + case 'projects:boards:show': + case 'projects:boards:index': + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:builds:show': + new Build(); + break; + case 'projects:merge_requests:index': + case 'projects:issues:index': + if (gl.FilteredSearchManager) { + new gl.FilteredSearchManager(); + } + Issuable.init(); + new gl.IssuableBulkActions({ + prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', + }); + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:issues:show': + new Issue(); + shortcut_handler = new ShortcutsIssuable(); + new ZenMode(); + break; + case 'projects:milestones:show': + case 'groups:milestones:show': + case 'dashboard:milestones:show': + new Milestone(); + break; + case 'dashboard:todos:index': + new gl.Todos(); + break; + case 'projects:milestones:new': + case 'projects:milestones:edit': + new ZenMode(); + new gl.DueDateSelectors(); + new gl.GLForm($('.milestone-form')); + break; + case 'groups:milestones:new': + new ZenMode(); + break; + case 'projects:compare:show': + new gl.Diff(); + break; + case 'projects:issues:new': + case 'projects:issues:edit': + shortcut_handler = new ShortcutsNavigation(); + new gl.GLForm($('.issue-form')); + new IssuableForm($('.issue-form')); + new LabelsSelect(); + new MilestoneSelect(); + new gl.IssuableTemplateSelectors(); + break; + case 'projects:merge_requests:new': + case 'projects:merge_requests:edit': + new gl.Diff(); + shortcut_handler = new ShortcutsNavigation(); + new gl.GLForm($('.merge-request-form')); + new IssuableForm($('.merge-request-form')); + new LabelsSelect(); + new MilestoneSelect(); + new gl.IssuableTemplateSelectors(); + break; + case 'projects:tags:new': + new ZenMode(); + new gl.GLForm($('.tag-form')); + break; + case 'projects:releases:edit': + new ZenMode(); + new gl.GLForm($('.release-form')); + break; + case 'projects:merge_requests:show': + new gl.Diff(); + shortcut_handler = new ShortcutsIssuable(true); + new ZenMode(); + new MergedButtons(); + break; + case 'projects:merge_requests:commits': + new MergedButtons(); + break; + case "projects:merge_requests:diffs": + new gl.Diff(); + new ZenMode(); + new MergedButtons(); + break; + case 'dashboard:activity': + new gl.Activities(); + break; + case 'dashboard:projects:starred': + new gl.Activities(); + break; + case 'projects:commit:show': + new Commit(); + new gl.Diff(); + new ZenMode(); + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:commits:show': + case 'projects:activity': + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:show': + shortcut_handler = new ShortcutsNavigation(); + new NotificationsForm(); + if ($('#tree-slider').length) { + new TreeView(); + } + break; + case 'projects:pipelines:builds': + case 'projects:pipelines:show': + const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; + + new gl.Pipelines({ + initTabs: true, + tabsOptions: { + action: controllerAction, + defaultAction: 'pipelines', + parentEl: '.pipelines-tabs', + }, + }); + break; + case 'groups:activity': + new gl.Activities(); + break; + case 'groups:show': + shortcut_handler = new ShortcutsNavigation(); + new NotificationsForm(); + new NotificationsDropdown(); + break; + case 'groups:group_members:index': + new gl.MemberExpirationDate(); + new gl.Members(); + new UsersSelect(); + break; + case 'projects:members:show': + new gl.MemberExpirationDate('.js-access-expiration-date-groups'); + new GroupsSelect(); + new gl.MemberExpirationDate(); + new gl.Members(); + new UsersSelect(); + break; + case 'groups:new': + case 'groups:edit': + case 'admin:groups:edit': + case 'admin:groups:new': + new GroupAvatar(); + break; + case 'projects:tree:show': + shortcut_handler = new ShortcutsNavigation(); + new TreeView(); + break; + case 'projects:find_file:show': + shortcut_handler = true; + break; + case 'projects:blob:show': + 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, + }); + break; + case 'groups:labels:new': + case 'groups:labels:edit': + case 'projects:labels:new': + case 'projects:labels:edit': + new Labels(); + break; + case 'projects:labels:index': + if ($('.prioritized-labels').length) { + new gl.LabelManager(); + } + break; + case 'projects:network:show': + // Ensure we don't create a particular shortcut handler here. This is + // already created, where the network graph is created. + shortcut_handler = true; + break; + case 'projects:forks:new': + new ProjectFork(); + break; + case 'projects:artifacts:browse': + new BuildArtifacts(); + break; + case 'help:index': + gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); + break; + case 'search:show': + new Search(); + break; + case 'projects:protected_branches:index': + new gl.ProtectedBranchCreate(); + new gl.ProtectedBranchEditList(); + break; + case 'projects:ci_cd:show': + new gl.ProjectVariables(); + break; + case 'ci:lints:create': + case 'ci:lints:show': + new gl.CILintEditor(); + break; + } + switch (path.first()) { + case 'sessions': + case 'omniauth_callbacks': + if (!gon.u2f) break; + gl.u2fAuthenticate = new gl.U2FAuthenticate( + $('#js-authenticate-u2f'), + '#js-login-u2f-form', + gon.u2f, + document.querySelector('#js-login-2fa-device'), + document.querySelector('.js-2fa-form'), + ); + gl.u2fAuthenticate.start(); + case 'admin': + new Admin(); + switch (path[1]) { + case 'groups': + new UsersSelect(); + break; + case 'projects': + new NamespaceSelects(); + break; + case 'labels': + switch (path[2]) { + case 'new': + case 'edit': + new Labels(); + } + case 'abuse_reports': + new gl.AbuseReports(); + break; + } + break; + case 'dashboard': + case 'root': + shortcut_handler = new ShortcutsDashboardNavigation(); + break; + case 'profiles': + new NotificationsForm(); + new NotificationsDropdown(); + break; + case 'projects': + new Project(); + new ProjectAvatar(); + switch (path[1]) { + case 'compare': + new CompareAutocomplete(); + break; + case 'edit': + shortcut_handler = new ShortcutsNavigation(); + new ProjectNew(); + break; + case 'new': + new ProjectNew(); + break; + case 'show': + new Star(); + new ProjectNew(); + new ProjectShow(); + new NotificationsDropdown(); + break; + case 'wikis': + new gl.Wikis(); + shortcut_handler = new ShortcutsNavigation(); + new ZenMode(); + new gl.GLForm($('.wiki-form')); + break; + case 'snippets': + shortcut_handler = new ShortcutsNavigation(); + if (path[2] === 'show') { + new ZenMode(); + } + break; + case 'labels': + case 'graphs': + case 'compare': + case 'pipelines': + case 'forks': + case 'milestones': + case 'project_members': + case 'deploy_keys': + case 'builds': + case 'hooks': + case 'services': + case 'protected_branches': + shortcut_handler = new ShortcutsNavigation(); + } + } + // If we haven't installed a custom shortcut handler, install the default one + if (!shortcut_handler) { + new Shortcuts(); + } + }; + + Dispatcher.prototype.initSearch = function() { + // Only when search form is present + if ($('.search').length) { + return new gl.SearchAutocomplete(); + } + }; + + Dispatcher.prototype.initFieldErrors = function() { + $('.gl-show-field-errors').each((i, form) => { + new gl.GlFieldErrors(form); + }); + }; + + return Dispatcher; + })(); +}).call(this); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 deleted file mode 100644 index 1e764a950ca..00000000000 --- a/app/assets/javascripts/dispatcher.js.es6 +++ /dev/null @@ -1,379 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ -/* global UsernameValidator */ -/* global ActiveTabMemoizer */ -/* global ShortcutsNavigation */ -/* global Build */ -/* global Issuable */ -/* global Issue */ -/* global ShortcutsIssuable */ -/* global ZenMode */ -/* global Milestone */ -/* global IssuableForm */ -/* global LabelsSelect */ -/* global MilestoneSelect */ -/* global MergedButtons */ -/* global Commit */ -/* global NotificationsForm */ -/* global TreeView */ -/* global NotificationsDropdown */ -/* global UsersSelect */ -/* global GroupAvatar */ -/* global LineHighlighter */ -/* global ProjectFork */ -/* global BuildArtifacts */ -/* global GroupsSelect */ -/* global Search */ -/* global Admin */ -/* global NamespaceSelects */ -/* global ShortcutsDashboardNavigation */ -/* global Project */ -/* global ProjectAvatar */ -/* global CompareAutocomplete */ -/* global ProjectNew */ -/* global Star */ -/* global ProjectShow */ -/* global Labels */ -/* global Shortcuts */ - -const ShortcutsBlob = require('./shortcuts_blob'); - -(function() { - var Dispatcher; - - $(function() { - return new Dispatcher(); - }); - - Dispatcher = (function() { - function Dispatcher() { - this.initSearch(); - this.initFieldErrors(); - this.initPageScripts(); - } - - Dispatcher.prototype.initPageScripts = function() { - var page, path, shortcut_handler; - page = $('body').attr('data-page'); - if (!page) { - return false; - } - path = page.split(':'); - shortcut_handler = null; - switch (page) { - case 'sessions:new': - new UsernameValidator(); - new ActiveTabMemoizer(); - break; - case 'projects:boards:show': - case 'projects:boards:index': - shortcut_handler = new ShortcutsNavigation(); - break; - case 'projects:builds:show': - new Build(); - break; - case 'projects:merge_requests:index': - case 'projects:issues:index': - if (gl.FilteredSearchManager) { - new gl.FilteredSearchManager(); - } - Issuable.init(); - new gl.IssuableBulkActions({ - prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', - }); - shortcut_handler = new ShortcutsNavigation(); - break; - case 'projects:issues:show': - new Issue(); - shortcut_handler = new ShortcutsIssuable(); - new ZenMode(); - break; - case 'projects:milestones:show': - case 'groups:milestones:show': - case 'dashboard:milestones:show': - new Milestone(); - break; - case 'dashboard:todos:index': - new gl.Todos(); - break; - case 'projects:milestones:new': - case 'projects:milestones:edit': - new ZenMode(); - new gl.DueDateSelectors(); - new gl.GLForm($('.milestone-form')); - break; - case 'groups:milestones:new': - new ZenMode(); - break; - case 'projects:compare:show': - new gl.Diff(); - break; - case 'projects:issues:new': - case 'projects:issues:edit': - shortcut_handler = new ShortcutsNavigation(); - new gl.GLForm($('.issue-form')); - new IssuableForm($('.issue-form')); - new LabelsSelect(); - new MilestoneSelect(); - new gl.IssuableTemplateSelectors(); - break; - case 'projects:merge_requests:new': - case 'projects:merge_requests:edit': - new gl.Diff(); - shortcut_handler = new ShortcutsNavigation(); - new gl.GLForm($('.merge-request-form')); - new IssuableForm($('.merge-request-form')); - new LabelsSelect(); - new MilestoneSelect(); - new gl.IssuableTemplateSelectors(); - break; - case 'projects:tags:new': - new ZenMode(); - new gl.GLForm($('.tag-form')); - break; - case 'projects:releases:edit': - new ZenMode(); - new gl.GLForm($('.release-form')); - break; - case 'projects:merge_requests:show': - new gl.Diff(); - shortcut_handler = new ShortcutsIssuable(true); - new ZenMode(); - new MergedButtons(); - break; - case 'projects:merge_requests:commits': - new MergedButtons(); - break; - case "projects:merge_requests:diffs": - new gl.Diff(); - new ZenMode(); - new MergedButtons(); - break; - case 'dashboard:activity': - new gl.Activities(); - break; - case 'dashboard:projects:starred': - new gl.Activities(); - break; - case 'projects:commit:show': - new Commit(); - new gl.Diff(); - new ZenMode(); - shortcut_handler = new ShortcutsNavigation(); - break; - case 'projects:commits:show': - case 'projects:activity': - shortcut_handler = new ShortcutsNavigation(); - break; - case 'projects:show': - shortcut_handler = new ShortcutsNavigation(); - new NotificationsForm(); - if ($('#tree-slider').length) { - new TreeView(); - } - break; - case 'projects:pipelines:builds': - case 'projects:pipelines:show': - const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; - - new gl.Pipelines({ - initTabs: true, - tabsOptions: { - action: controllerAction, - defaultAction: 'pipelines', - parentEl: '.pipelines-tabs', - }, - }); - break; - case 'groups:activity': - new gl.Activities(); - break; - case 'groups:show': - shortcut_handler = new ShortcutsNavigation(); - new NotificationsForm(); - new NotificationsDropdown(); - break; - case 'groups:group_members:index': - new gl.MemberExpirationDate(); - new gl.Members(); - new UsersSelect(); - break; - case 'projects:members:show': - new gl.MemberExpirationDate('.js-access-expiration-date-groups'); - new GroupsSelect(); - new gl.MemberExpirationDate(); - new gl.Members(); - new UsersSelect(); - break; - case 'groups:new': - case 'groups:edit': - case 'admin:groups:edit': - case 'admin:groups:new': - new GroupAvatar(); - break; - case 'projects:tree:show': - shortcut_handler = new ShortcutsNavigation(); - new TreeView(); - break; - case 'projects:find_file:show': - shortcut_handler = true; - break; - case 'projects:blob:show': - 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, - }); - break; - case 'groups:labels:new': - case 'groups:labels:edit': - case 'projects:labels:new': - case 'projects:labels:edit': - new Labels(); - break; - case 'projects:labels:index': - if ($('.prioritized-labels').length) { - new gl.LabelManager(); - } - break; - case 'projects:network:show': - // Ensure we don't create a particular shortcut handler here. This is - // already created, where the network graph is created. - shortcut_handler = true; - break; - case 'projects:forks:new': - new ProjectFork(); - break; - case 'projects:artifacts:browse': - new BuildArtifacts(); - break; - case 'help:index': - gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); - break; - case 'search:show': - new Search(); - break; - case 'projects:protected_branches:index': - new gl.ProtectedBranchCreate(); - new gl.ProtectedBranchEditList(); - break; - case 'projects:ci_cd:show': - new gl.ProjectVariables(); - break; - case 'ci:lints:create': - case 'ci:lints:show': - new gl.CILintEditor(); - break; - } - switch (path.first()) { - case 'sessions': - case 'omniauth_callbacks': - if (!gon.u2f) break; - gl.u2fAuthenticate = new gl.U2FAuthenticate( - $('#js-authenticate-u2f'), - '#js-login-u2f-form', - gon.u2f, - document.querySelector('#js-login-2fa-device'), - document.querySelector('.js-2fa-form'), - ); - gl.u2fAuthenticate.start(); - case 'admin': - new Admin(); - switch (path[1]) { - case 'groups': - new UsersSelect(); - break; - case 'projects': - new NamespaceSelects(); - break; - case 'labels': - switch (path[2]) { - case 'new': - case 'edit': - new Labels(); - } - case 'abuse_reports': - new gl.AbuseReports(); - break; - } - break; - case 'dashboard': - case 'root': - shortcut_handler = new ShortcutsDashboardNavigation(); - break; - case 'profiles': - new NotificationsForm(); - new NotificationsDropdown(); - break; - case 'projects': - new Project(); - new ProjectAvatar(); - switch (path[1]) { - case 'compare': - new CompareAutocomplete(); - break; - case 'edit': - shortcut_handler = new ShortcutsNavigation(); - new ProjectNew(); - break; - case 'new': - new ProjectNew(); - break; - case 'show': - new Star(); - new ProjectNew(); - new ProjectShow(); - new NotificationsDropdown(); - break; - case 'wikis': - new gl.Wikis(); - shortcut_handler = new ShortcutsNavigation(); - new ZenMode(); - new gl.GLForm($('.wiki-form')); - break; - case 'snippets': - shortcut_handler = new ShortcutsNavigation(); - if (path[2] === 'show') { - new ZenMode(); - } - break; - case 'labels': - case 'graphs': - case 'compare': - case 'pipelines': - case 'forks': - case 'milestones': - case 'project_members': - case 'deploy_keys': - case 'builds': - case 'hooks': - case 'services': - case 'protected_branches': - shortcut_handler = new ShortcutsNavigation(); - } - } - // If we haven't installed a custom shortcut handler, install the default one - if (!shortcut_handler) { - new Shortcuts(); - } - }; - - Dispatcher.prototype.initSearch = function() { - // Only when search form is present - if ($('.search').length) { - return new gl.SearchAutocomplete(); - } - }; - - Dispatcher.prototype.initFieldErrors = function() { - $('.gl-show-field-errors').each((i, form) => { - new gl.GlFieldErrors(form); - }); - }; - - return Dispatcher; - })(); -}).call(this); diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js new file mode 100644 index 00000000000..d81d4cf8425 --- /dev/null +++ b/app/assets/javascripts/due_date_select.js @@ -0,0 +1,181 @@ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */ + +(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(); + this.initStopPropagation(); + } + + initGlDropdown() { + this.$dropdown.glDropdown({ + hidden: () => { + this.$selectbox.hide(); + this.$value.css('display', ''); + } + }); + } + + initDatePicker() { + this.$datePicker.datepicker({ + dateFormat: 'yy-mm-dd', + defaultDate: $("input[name='" + this.fieldName + "']").val(), + altField: "input[name='" + this.fieldName + "']", + onSelect: () => { + if (this.$dropdown.hasClass('js-issue-boards-due-date')) { + gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val(); + this.updateIssueBoardIssue(); + } else { + return this.saveDueDate(true); + } + } + }); + } + + initRemoveDueDate() { + this.$block.on('click', '.js-remove-due-date', (e) => { + e.preventDefault(); + + if (this.$dropdown.hasClass('js-issue-boards-due-date')) { + gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; + this.updateIssueBoardIssue(); + } else { + $("input[name='" + this.fieldName + "']").val(''); + return this.saveDueDate(false); + } + }); + } + + initStopPropagation() { + $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', (e) => { + return e.stopImmediatePropagation(); + }); + } + + saveDueDate(isDropdown) { + this.parseSelectedDate(); + this.prepSelectedDate(); + this.submitSelectedDate(isDropdown); + } + + parseSelectedDate() { + this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val(); + + 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 = $.datepicker.formatDate('M d, yy', dateObj); + } 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(); + + if (isDropdown) { + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); + } + + this.$value.css('display', ''); + this.$valueContent.html(`${this.displayedDate}`); + 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(); + } + + initMilestoneDatePicker() { + $('.datepicker').datepicker({ + dateFormat: 'yy-mm-dd' + }); + + $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { + e.preventDefault(); + const datepicker = $(e.target).siblings('.datepicker'); + $.datepicker._clearDate(datepicker); + }); + } + + 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 + }); + }); + } + } + + global.DueDateSelectors = DueDateSelectors; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6 deleted file mode 100644 index d81d4cf8425..00000000000 --- a/app/assets/javascripts/due_date_select.js.es6 +++ /dev/null @@ -1,181 +0,0 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */ - -(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(); - this.initStopPropagation(); - } - - initGlDropdown() { - this.$dropdown.glDropdown({ - hidden: () => { - this.$selectbox.hide(); - this.$value.css('display', ''); - } - }); - } - - initDatePicker() { - this.$datePicker.datepicker({ - dateFormat: 'yy-mm-dd', - defaultDate: $("input[name='" + this.fieldName + "']").val(), - altField: "input[name='" + this.fieldName + "']", - onSelect: () => { - if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val(); - this.updateIssueBoardIssue(); - } else { - return this.saveDueDate(true); - } - } - }); - } - - initRemoveDueDate() { - this.$block.on('click', '.js-remove-due-date', (e) => { - e.preventDefault(); - - if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; - this.updateIssueBoardIssue(); - } else { - $("input[name='" + this.fieldName + "']").val(''); - return this.saveDueDate(false); - } - }); - } - - initStopPropagation() { - $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', (e) => { - return e.stopImmediatePropagation(); - }); - } - - saveDueDate(isDropdown) { - this.parseSelectedDate(); - this.prepSelectedDate(); - this.submitSelectedDate(isDropdown); - } - - parseSelectedDate() { - this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val(); - - 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 = $.datepicker.formatDate('M d, yy', dateObj); - } 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(); - - if (isDropdown) { - this.$dropdown.trigger('loading.gl.dropdown'); - this.$selectbox.hide(); - } - - this.$value.css('display', ''); - this.$valueContent.html(`${this.displayedDate}`); - 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(); - } - - initMilestoneDatePicker() { - $('.datepicker').datepicker({ - dateFormat: 'yy-mm-dd' - }); - - $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { - e.preventDefault(); - const datepicker = $(e.target).siblings('.datepicker'); - $.datepicker._clearDate(datepicker); - }); - } - - 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 - }); - }); - } - } - - global.DueDateSelectors = DueDateSelectors; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js new file mode 100644 index 00000000000..91553bda4dc --- /dev/null +++ b/app/assets/javascripts/environments/components/environment.js @@ -0,0 +1,223 @@ +/* eslint-disable no-param-reassign, no-new */ +/* global Vue */ +/* global EnvironmentsService */ +/* global Flash */ + +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('../services/environments_service'); +require('./environment_item'); + +(() => { + window.gl = window.gl || {}; + + gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', { + props: { + store: { + type: Object, + required: true, + default: () => ({}), + }, + }, + + components: { + 'environment-item': gl.environmentsList.EnvironmentItem, + }, + + data() { + const environmentsData = document.querySelector('#environments-list-view').dataset; + + return { + state: this.store.state, + visibility: 'available', + isLoading: false, + cssContainerClass: environmentsData.cssClass, + endpoint: environmentsData.environmentsDataEndpoint, + canCreateDeployment: environmentsData.canCreateDeployment, + canReadEnvironment: environmentsData.canReadEnvironment, + canCreateEnvironment: environmentsData.canCreateEnvironment, + projectEnvironmentsPath: environmentsData.projectEnvironmentsPath, + projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, + newEnvironmentPath: environmentsData.newEnvironmentPath, + helpPagePath: environmentsData.helpPagePath, + commitIconSvg: environmentsData.commitIconSvg, + playIconSvg: environmentsData.playIconSvg, + terminalIconSvg: environmentsData.terminalIconSvg, + }; + }, + + computed: { + scope() { + return this.$options.getQueryParameter('scope'); + }, + + canReadEnvironmentParsed() { + return this.$options.convertPermissionToBoolean(this.canReadEnvironment); + }, + + canCreateDeploymentParsed() { + return this.$options.convertPermissionToBoolean(this.canCreateDeployment); + }, + + canCreateEnvironmentParsed() { + return this.$options.convertPermissionToBoolean(this.canCreateEnvironment); + }, + }, + + /** + * Fetches all the environments and stores them. + * Toggles loading property. + */ + created() { + gl.environmentsService = new EnvironmentsService(this.endpoint); + + const scope = this.$options.getQueryParameter('scope'); + if (scope) { + this.store.storeVisibility(scope); + } + + this.isLoading = true; + + return gl.environmentsService.all() + .then(resp => resp.json()) + .then((json) => { + this.store.storeEnvironments(json); + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the environments.', 'alert'); + }); + }, + + /** + * Transforms the url parameter into an object and + * returns the one requested. + * + * @param {String} param + * @returns {String} The value of the requested parameter. + */ + getQueryParameter(parameter) { + return window.location.search.substring(1).split('&').reduce((acc, param) => { + const paramSplited = param.split('='); + acc[paramSplited[0]] = paramSplited[1]; + return acc; + }, {})[parameter]; + }, + + /** + * Converts permission provided as strings to booleans. + * @param {String} string + * @returns {Boolean} + */ + convertPermissionToBoolean(string) { + return string === 'true'; + }, + + methods: { + toggleRow(model) { + return this.store.toggleFolder(model.name); + }, + }, + + template: ` +
    + + +
    +
    + +
    + +
    +

    + You don't have any environments right now. +

    +

    + Environments are places where code gets deployed, such as staging or production. +
    + + Read more about environments + +

    + + + New Environment + +
    + +
    + + + + + + + + + + + + + + +
    EnvironmentLast deploymentJobCommitUpdated
    +
    +
    +
    + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 deleted file mode 100644 index 91553bda4dc..00000000000 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ /dev/null @@ -1,223 +0,0 @@ -/* eslint-disable no-param-reassign, no-new */ -/* global Vue */ -/* global EnvironmentsService */ -/* global Flash */ - -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); -require('../services/environments_service'); -require('./environment_item'); - -(() => { - window.gl = window.gl || {}; - - gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', { - props: { - store: { - type: Object, - required: true, - default: () => ({}), - }, - }, - - components: { - 'environment-item': gl.environmentsList.EnvironmentItem, - }, - - data() { - const environmentsData = document.querySelector('#environments-list-view').dataset; - - return { - state: this.store.state, - visibility: 'available', - isLoading: false, - cssContainerClass: environmentsData.cssClass, - endpoint: environmentsData.environmentsDataEndpoint, - canCreateDeployment: environmentsData.canCreateDeployment, - canReadEnvironment: environmentsData.canReadEnvironment, - canCreateEnvironment: environmentsData.canCreateEnvironment, - projectEnvironmentsPath: environmentsData.projectEnvironmentsPath, - projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, - newEnvironmentPath: environmentsData.newEnvironmentPath, - helpPagePath: environmentsData.helpPagePath, - commitIconSvg: environmentsData.commitIconSvg, - playIconSvg: environmentsData.playIconSvg, - terminalIconSvg: environmentsData.terminalIconSvg, - }; - }, - - computed: { - scope() { - return this.$options.getQueryParameter('scope'); - }, - - canReadEnvironmentParsed() { - return this.$options.convertPermissionToBoolean(this.canReadEnvironment); - }, - - canCreateDeploymentParsed() { - return this.$options.convertPermissionToBoolean(this.canCreateDeployment); - }, - - canCreateEnvironmentParsed() { - return this.$options.convertPermissionToBoolean(this.canCreateEnvironment); - }, - }, - - /** - * Fetches all the environments and stores them. - * Toggles loading property. - */ - created() { - gl.environmentsService = new EnvironmentsService(this.endpoint); - - const scope = this.$options.getQueryParameter('scope'); - if (scope) { - this.store.storeVisibility(scope); - } - - this.isLoading = true; - - return gl.environmentsService.all() - .then(resp => resp.json()) - .then((json) => { - this.store.storeEnvironments(json); - this.isLoading = false; - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occurred while fetching the environments.', 'alert'); - }); - }, - - /** - * Transforms the url parameter into an object and - * returns the one requested. - * - * @param {String} param - * @returns {String} The value of the requested parameter. - */ - getQueryParameter(parameter) { - return window.location.search.substring(1).split('&').reduce((acc, param) => { - const paramSplited = param.split('='); - acc[paramSplited[0]] = paramSplited[1]; - return acc; - }, {})[parameter]; - }, - - /** - * Converts permission provided as strings to booleans. - * @param {String} string - * @returns {Boolean} - */ - convertPermissionToBoolean(string) { - return string === 'true'; - }, - - methods: { - toggleRow(model) { - return this.store.toggleFolder(model.name); - }, - }, - - template: ` -
    - - -
    -
    - -
    - -
    -

    - You don't have any environments right now. -

    -

    - Environments are places where code gets deployed, such as staging or production. -
    - - Read more about environments - -

    - - - New Environment - -
    - -
    - - - - - - - - - - - - - - -
    EnvironmentLast deploymentJobCommitUpdated
    -
    -
    -
    - `, - }); -})(); diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js new file mode 100644 index 00000000000..ed1c78945db --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_actions.js @@ -0,0 +1,50 @@ +/* global Vue */ + +window.Vue = require('vue'); + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.ActionsComponent = Vue.component('actions-component', { + props: { + actions: { + type: Array, + required: false, + default: () => [], + }, + + playIconSvg: { + type: String, + required: false, + }, + }, + + template: ` + + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 deleted file mode 100644 index ed1c78945db..00000000000 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ /dev/null @@ -1,50 +0,0 @@ -/* global Vue */ - -window.Vue = require('vue'); - -(() => { - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - - gl.environmentsList.ActionsComponent = Vue.component('actions-component', { - props: { - actions: { - type: Array, - required: false, - default: () => [], - }, - - playIconSvg: { - type: String, - required: false, - }, - }, - - template: ` - - `, - }); -})(); diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js new file mode 100644 index 00000000000..28cc0022d17 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_external_url.js @@ -0,0 +1,23 @@ +/* global Vue */ + +window.Vue = require('vue'); + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { + props: { + externalUrl: { + type: String, + default: '', + }, + }, + + template: ` + + + + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6 deleted file mode 100644 index 28cc0022d17..00000000000 --- a/app/assets/javascripts/environments/components/environment_external_url.js.es6 +++ /dev/null @@ -1,23 +0,0 @@ -/* global Vue */ - -window.Vue = require('vue'); - -(() => { - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - - gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { - props: { - externalUrl: { - type: String, - default: '', - }, - }, - - template: ` - - - - `, - }); -})(); diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js new file mode 100644 index 00000000000..33a99231315 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_item.js @@ -0,0 +1,538 @@ +/* global Vue */ +/* global timeago */ + +window.Vue = require('vue'); +window.timeago = require('vendor/timeago'); +require('../../lib/utils/text_utility'); +require('../../vue_shared/components/commit'); +require('./environment_actions'); +require('./environment_external_url'); +require('./environment_stop'); +require('./environment_rollback'); +require('./environment_terminal_button'); + +(() => { + /** + * Envrionment Item Component + * + * Used in a hierarchical structure to show folders with children + * in a table. + * Recursive component based on [Tree View](https://vuejs.org/examples/tree-view.html) + * + * See this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/22539) + * for more information.15 + */ + + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + window.gl.environmentsList.timeagoInstance = new timeago(); // eslint-disable-line + + gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { + + components: { + 'commit-component': gl.CommitComponent, + 'actions-component': gl.environmentsList.ActionsComponent, + 'external-url-component': gl.environmentsList.ExternalUrlComponent, + 'stop-component': gl.environmentsList.StopComponent, + 'rollback-component': gl.environmentsList.RollbackComponent, + 'terminal-button-component': gl.environmentsList.TerminalButtonComponent, + }, + + props: { + model: { + type: Object, + required: true, + default: () => ({}), + }, + + toggleRow: { + type: Function, + required: false, + }, + + canCreateDeployment: { + type: Boolean, + required: false, + default: false, + }, + + canReadEnvironment: { + type: Boolean, + required: false, + default: false, + }, + + commitIconSvg: { + type: String, + required: false, + }, + + playIconSvg: { + type: String, + required: false, + }, + + terminalIconSvg: { + type: String, + required: false, + }, + + }, + + data() { + return { + rowClass: { + 'children-row': this.model['vue-isChildren'], + }, + }; + }, + + computed: { + + /** + * If an item has a `children` entry it means it is a folder. + * Folder items have different behaviours - it is possible to toggle + * them and show their children. + * + * @returns {Boolean|Undefined} + */ + isFolder() { + return this.model.children && this.model.children.length > 0; + }, + + /** + * If an item is inside a folder structure will return true. + * Used for css purposes. + * + * @returns {Boolean|undefined} + */ + isChildren() { + return this.model['vue-isChildren']; + }, + + /** + * Counts the number of environments in each folder. + * Used to show a badge with the counter. + * + * @returns {Number|Undefined} The number of environments for the current folder. + */ + childrenCounter() { + return this.model.children && this.model.children.length; + }, + + /** + * Verifies if `last_deployment` key exists in the current Envrionment. + * This key is required to render most of the html - this method works has + * an helper. + * + * @returns {Boolean} + */ + hasLastDeploymentKey() { + if (this.model.last_deployment && + !this.$options.isObjectEmpty(this.model.last_deployment)) { + return true; + } + return false; + }, + + /** + * Verifies is the given environment has manual actions. + * Used to verify if we should render them or nor. + * + * @returns {Boolean|Undefined} + */ + hasManualActions() { + return this.model.last_deployment && this.model.last_deployment.manual_actions && + this.model.last_deployment.manual_actions.length > 0; + }, + + /** + * Returns the value of the `stop_action?` key provided in the response. + * + * @returns {Boolean} + */ + hasStopAction() { + return this.model['stop_action?']; + }, + + /** + * Verifies if the `deployable` key is present in `last_deployment` key. + * Used to verify whether we should or not render the rollback partial. + * + * @returns {Boolean|Undefined} + */ + canRetry() { + return this.hasLastDeploymentKey && + this.model.last_deployment && + this.model.last_deployment.deployable; + }, + + /** + * Verifies if the date to be shown is present. + * + * @returns {Boolean|Undefined} + */ + canShowDate() { + return this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable !== undefined; + }, + + /** + * Human readable date. + * + * @returns {String} + */ + createdDate() { + return gl.environmentsList.timeagoInstance.format( + this.model.last_deployment.deployable.created_at, + ); + }, + + /** + * Returns the manual actions with the name parsed. + * + * @returns {Array.|Undefined} + */ + manualActions() { + if (this.hasManualActions) { + return this.model.last_deployment.manual_actions.map((action) => { + const parsedAction = { + name: gl.text.humanize(action.name), + play_path: action.play_path, + }; + return parsedAction; + }); + } + return []; + }, + + /** + * Builds the string used in the user image alt attribute. + * + * @returns {String} + */ + userImageAltDescription() { + if (this.model.last_deployment && + this.model.last_deployment.user && + this.model.last_deployment.user.username) { + return `${this.model.last_deployment.user.username}'s avatar'`; + } + return ''; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.model.last_deployment && + this.model.last_deployment.tag) { + return this.model.last_deployment.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.model.last_deployment && this.model.last_deployment.ref) { + return this.model.last_deployment.ref; + } + return undefined; + }, + + /** + * If provided, returns the commit url. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.commit_path) { + return this.model.last_deployment.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.short_id) { + return this.model.last_deployment.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.title) { + return this.model.last_deployment.commit.title; + } + return undefined; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if (this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.author) { + return this.model.last_deployment.commit.author; + } + + return undefined; + }, + + /** + * Verifies if the `retry_path` key is present and returns its value. + * + * @returns {String|Undefined} + */ + retryUrl() { + if (this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.retry_path) { + return this.model.last_deployment.deployable.retry_path; + } + return undefined; + }, + + /** + * Verifies if the `last?` key is present and returns its value. + * + * @returns {Boolean|Undefined} + */ + isLastDeployment() { + return this.model.last_deployment && this.model.last_deployment['last?']; + }, + + /** + * Builds the name of the builds needed to display both the name and the id. + * + * @returns {String} + */ + buildName() { + if (this.model.last_deployment && + this.model.last_deployment.deployable) { + return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; + } + return ''; + }, + + /** + * Builds the needed string to show the internal id. + * + * @returns {String} + */ + deploymentInternalId() { + if (this.model.last_deployment && + this.model.last_deployment.iid) { + return `#${this.model.last_deployment.iid}`; + } + return ''; + }, + + /** + * Verifies if the user object is present under last_deployment object. + * + * @returns {Boolean} + */ + deploymentHasUser() { + return !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.user); + }, + + /** + * Returns the user object nested with the last_deployment object. + * Used to render the template. + * + * @returns {Object} + */ + deploymentUser() { + if (!this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.user)) { + return this.model.last_deployment.user; + } + return {}; + }, + + /** + * Verifies if the build name column should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderBuildName() { + return !this.isFolder && + !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.deployable); + }, + + /** + * Verifies if deplyment internal ID should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderDeploymentID() { + return !this.isFolder && + !this.$options.isObjectEmpty(this.model.last_deployment) && + this.model.last_deployment.iid !== undefined; + }, + }, + + /** + * Helper to verify if certain given object are empty. + * Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty + * @param {Object} object + * @returns {Bollean} + */ + isObjectEmpty(object) { + for (const key in object) { // eslint-disable-line + if (hasOwnProperty.call(object, key)) { + return false; + } + } + return true; + }, + + template: ` + + + + {{model.name}} + + + + + + + + + {{model.name}} + + + + {{childrenCounter}} + + + + + + + {{deploymentInternalId}} + + + + by + + + + + + + + + {{buildName}} + + + + +
    + + +
    +

    + No deployments yet +

    + + + + + {{createdDate}} + + + + +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + + + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 deleted file mode 100644 index 33a99231315..00000000000 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ /dev/null @@ -1,538 +0,0 @@ -/* global Vue */ -/* global timeago */ - -window.Vue = require('vue'); -window.timeago = require('vendor/timeago'); -require('../../lib/utils/text_utility'); -require('../../vue_shared/components/commit'); -require('./environment_actions'); -require('./environment_external_url'); -require('./environment_stop'); -require('./environment_rollback'); -require('./environment_terminal_button'); - -(() => { - /** - * Envrionment Item Component - * - * Used in a hierarchical structure to show folders with children - * in a table. - * Recursive component based on [Tree View](https://vuejs.org/examples/tree-view.html) - * - * See this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/22539) - * for more information.15 - */ - - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - window.gl.environmentsList.timeagoInstance = new timeago(); // eslint-disable-line - - gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { - - components: { - 'commit-component': gl.CommitComponent, - 'actions-component': gl.environmentsList.ActionsComponent, - 'external-url-component': gl.environmentsList.ExternalUrlComponent, - 'stop-component': gl.environmentsList.StopComponent, - 'rollback-component': gl.environmentsList.RollbackComponent, - 'terminal-button-component': gl.environmentsList.TerminalButtonComponent, - }, - - props: { - model: { - type: Object, - required: true, - default: () => ({}), - }, - - toggleRow: { - type: Function, - required: false, - }, - - canCreateDeployment: { - type: Boolean, - required: false, - default: false, - }, - - canReadEnvironment: { - type: Boolean, - required: false, - default: false, - }, - - commitIconSvg: { - type: String, - required: false, - }, - - playIconSvg: { - type: String, - required: false, - }, - - terminalIconSvg: { - type: String, - required: false, - }, - - }, - - data() { - return { - rowClass: { - 'children-row': this.model['vue-isChildren'], - }, - }; - }, - - computed: { - - /** - * If an item has a `children` entry it means it is a folder. - * Folder items have different behaviours - it is possible to toggle - * them and show their children. - * - * @returns {Boolean|Undefined} - */ - isFolder() { - return this.model.children && this.model.children.length > 0; - }, - - /** - * If an item is inside a folder structure will return true. - * Used for css purposes. - * - * @returns {Boolean|undefined} - */ - isChildren() { - return this.model['vue-isChildren']; - }, - - /** - * Counts the number of environments in each folder. - * Used to show a badge with the counter. - * - * @returns {Number|Undefined} The number of environments for the current folder. - */ - childrenCounter() { - return this.model.children && this.model.children.length; - }, - - /** - * Verifies if `last_deployment` key exists in the current Envrionment. - * This key is required to render most of the html - this method works has - * an helper. - * - * @returns {Boolean} - */ - hasLastDeploymentKey() { - if (this.model.last_deployment && - !this.$options.isObjectEmpty(this.model.last_deployment)) { - return true; - } - return false; - }, - - /** - * Verifies is the given environment has manual actions. - * Used to verify if we should render them or nor. - * - * @returns {Boolean|Undefined} - */ - hasManualActions() { - return this.model.last_deployment && this.model.last_deployment.manual_actions && - this.model.last_deployment.manual_actions.length > 0; - }, - - /** - * Returns the value of the `stop_action?` key provided in the response. - * - * @returns {Boolean} - */ - hasStopAction() { - return this.model['stop_action?']; - }, - - /** - * Verifies if the `deployable` key is present in `last_deployment` key. - * Used to verify whether we should or not render the rollback partial. - * - * @returns {Boolean|Undefined} - */ - canRetry() { - return this.hasLastDeploymentKey && - this.model.last_deployment && - this.model.last_deployment.deployable; - }, - - /** - * Verifies if the date to be shown is present. - * - * @returns {Boolean|Undefined} - */ - canShowDate() { - return this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable !== undefined; - }, - - /** - * Human readable date. - * - * @returns {String} - */ - createdDate() { - return gl.environmentsList.timeagoInstance.format( - this.model.last_deployment.deployable.created_at, - ); - }, - - /** - * Returns the manual actions with the name parsed. - * - * @returns {Array.|Undefined} - */ - manualActions() { - if (this.hasManualActions) { - return this.model.last_deployment.manual_actions.map((action) => { - const parsedAction = { - name: gl.text.humanize(action.name), - play_path: action.play_path, - }; - return parsedAction; - }); - } - return []; - }, - - /** - * Builds the string used in the user image alt attribute. - * - * @returns {String} - */ - userImageAltDescription() { - if (this.model.last_deployment && - this.model.last_deployment.user && - this.model.last_deployment.user.username) { - return `${this.model.last_deployment.user.username}'s avatar'`; - } - return ''; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.model.last_deployment && - this.model.last_deployment.tag) { - return this.model.last_deployment.tag; - } - return undefined; - }, - - /** - * If provided, returns the commit ref. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.model.last_deployment && this.model.last_deployment.ref) { - return this.model.last_deployment.ref; - } - return undefined; - }, - - /** - * If provided, returns the commit url. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.commit_path) { - return this.model.last_deployment.commit.commit_path; - } - return undefined; - }, - - /** - * If provided, returns the commit short sha. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.short_id) { - return this.model.last_deployment.commit.short_id; - } - return undefined; - }, - - /** - * If provided, returns the commit title. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.title) { - return this.model.last_deployment.commit.title; - } - return undefined; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {Object|Undefined} - */ - commitAuthor() { - if (this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.author) { - return this.model.last_deployment.commit.author; - } - - return undefined; - }, - - /** - * Verifies if the `retry_path` key is present and returns its value. - * - * @returns {String|Undefined} - */ - retryUrl() { - if (this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.retry_path) { - return this.model.last_deployment.deployable.retry_path; - } - return undefined; - }, - - /** - * Verifies if the `last?` key is present and returns its value. - * - * @returns {Boolean|Undefined} - */ - isLastDeployment() { - return this.model.last_deployment && this.model.last_deployment['last?']; - }, - - /** - * Builds the name of the builds needed to display both the name and the id. - * - * @returns {String} - */ - buildName() { - if (this.model.last_deployment && - this.model.last_deployment.deployable) { - return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; - } - return ''; - }, - - /** - * Builds the needed string to show the internal id. - * - * @returns {String} - */ - deploymentInternalId() { - if (this.model.last_deployment && - this.model.last_deployment.iid) { - return `#${this.model.last_deployment.iid}`; - } - return ''; - }, - - /** - * Verifies if the user object is present under last_deployment object. - * - * @returns {Boolean} - */ - deploymentHasUser() { - return !this.$options.isObjectEmpty(this.model.last_deployment) && - !this.$options.isObjectEmpty(this.model.last_deployment.user); - }, - - /** - * Returns the user object nested with the last_deployment object. - * Used to render the template. - * - * @returns {Object} - */ - deploymentUser() { - if (!this.$options.isObjectEmpty(this.model.last_deployment) && - !this.$options.isObjectEmpty(this.model.last_deployment.user)) { - return this.model.last_deployment.user; - } - return {}; - }, - - /** - * Verifies if the build name column should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderBuildName() { - return !this.isFolder && - !this.$options.isObjectEmpty(this.model.last_deployment) && - !this.$options.isObjectEmpty(this.model.last_deployment.deployable); - }, - - /** - * Verifies if deplyment internal ID should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderDeploymentID() { - return !this.isFolder && - !this.$options.isObjectEmpty(this.model.last_deployment) && - this.model.last_deployment.iid !== undefined; - }, - }, - - /** - * Helper to verify if certain given object are empty. - * Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty - * @param {Object} object - * @returns {Bollean} - */ - isObjectEmpty(object) { - for (const key in object) { // eslint-disable-line - if (hasOwnProperty.call(object, key)) { - return false; - } - } - return true; - }, - - template: ` - - - - {{model.name}} - - - - - - - - - {{model.name}} - - - - {{childrenCounter}} - - - - - - - {{deploymentInternalId}} - - - - by - - - - - - - - - {{buildName}} - - - - -
    - - -
    -

    - No deployments yet -

    - - - - - {{createdDate}} - - - - -
    -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    -
    - - - `, - }); -})(); diff --git a/app/assets/javascripts/environments/components/environment_rollback.js b/app/assets/javascripts/environments/components/environment_rollback.js new file mode 100644 index 00000000000..5938340a128 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_rollback.js @@ -0,0 +1,33 @@ +/* global Vue */ + +window.Vue = require('vue'); + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { + props: { + retryUrl: { + type: String, + default: '', + }, + + isLastDeployment: { + type: Boolean, + default: true, + }, + }, + + template: ` + + + Re-deploy + + + Rollback + + + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6 deleted file mode 100644 index 5938340a128..00000000000 --- a/app/assets/javascripts/environments/components/environment_rollback.js.es6 +++ /dev/null @@ -1,33 +0,0 @@ -/* global Vue */ - -window.Vue = require('vue'); - -(() => { - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - - gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { - props: { - retryUrl: { - type: String, - default: '', - }, - - isLastDeployment: { - type: Boolean, - default: true, - }, - }, - - template: ` - - - Re-deploy - - - Rollback - - - `, - }); -})(); diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js new file mode 100644 index 00000000000..be9526989a0 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_stop.js @@ -0,0 +1,27 @@ +/* global Vue */ + +window.Vue = require('vue'); + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.StopComponent = Vue.component('stop-component', { + props: { + stopUrl: { + type: String, + default: '', + }, + }, + + template: ` + + + + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6 deleted file mode 100644 index be9526989a0..00000000000 --- a/app/assets/javascripts/environments/components/environment_stop.js.es6 +++ /dev/null @@ -1,27 +0,0 @@ -/* global Vue */ - -window.Vue = require('vue'); - -(() => { - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - - gl.environmentsList.StopComponent = Vue.component('stop-component', { - props: { - stopUrl: { - type: String, - default: '', - }, - }, - - template: ` - - - - `, - }); -})(); diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.js new file mode 100644 index 00000000000..a3ad063f7cb --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js @@ -0,0 +1,28 @@ +/* global Vue */ + +window.Vue = require('vue'); + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', { + props: { + terminalPath: { + type: String, + default: '', + }, + terminalIconSvg: { + type: String, + default: '', + }, + }, + + template: ` + + + + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 deleted file mode 100644 index a3ad063f7cb..00000000000 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 +++ /dev/null @@ -1,28 +0,0 @@ -/* global Vue */ - -window.Vue = require('vue'); - -(() => { - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - - gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', { - props: { - terminalPath: { - type: String, - default: '', - }, - terminalIconSvg: { - type: String, - default: '', - }, - }, - - template: ` - - - - `, - }); -})(); diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/environments_bundle.js new file mode 100644 index 00000000000..05c59d92fd4 --- /dev/null +++ b/app/assets/javascripts/environments/environments_bundle.js @@ -0,0 +1,22 @@ +window.Vue = require('vue'); +require('./stores/environments_store'); +require('./components/environment'); +require('../vue_shared/vue_resource_interceptor'); + +$(() => { + window.gl = window.gl || {}; + + if (gl.EnvironmentsListApp) { + gl.EnvironmentsListApp.$destroy(true); + } + const Store = gl.environmentsList.EnvironmentsStore; + + gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({ + el: document.querySelector('#environments-list-view'), + + propsData: { + store: Store.create(), + }, + + }); +}); diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 deleted file mode 100644 index 05c59d92fd4..00000000000 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ /dev/null @@ -1,22 +0,0 @@ -window.Vue = require('vue'); -require('./stores/environments_store'); -require('./components/environment'); -require('../vue_shared/vue_resource_interceptor'); - -$(() => { - window.gl = window.gl || {}; - - if (gl.EnvironmentsListApp) { - gl.EnvironmentsListApp.$destroy(true); - } - const Store = gl.environmentsList.EnvironmentsStore; - - gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({ - el: document.querySelector('#environments-list-view'), - - propsData: { - store: Store.create(), - }, - - }); -}); diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js new file mode 100644 index 00000000000..fab8d977f58 --- /dev/null +++ b/app/assets/javascripts/environments/services/environments_service.js @@ -0,0 +1,25 @@ +/* globals Vue */ +/* eslint-disable no-unused-vars, no-param-reassign */ + +class EnvironmentsService { + + constructor(root) { + Vue.http.options.root = root; + + this.environments = Vue.resource(root); + + Vue.http.interceptors.push((request, next) => { + // needed in order to not break the tests. + if ($.rails) { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + } + next(); + }); + } + + all() { + return this.environments.get(); + } +} + +window.EnvironmentsService = EnvironmentsService; diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 deleted file mode 100644 index fab8d977f58..00000000000 --- a/app/assets/javascripts/environments/services/environments_service.js.es6 +++ /dev/null @@ -1,25 +0,0 @@ -/* globals Vue */ -/* eslint-disable no-unused-vars, no-param-reassign */ - -class EnvironmentsService { - - constructor(root) { - Vue.http.options.root = root; - - this.environments = Vue.resource(root); - - Vue.http.interceptors.push((request, next) => { - // needed in order to not break the tests. - if ($.rails) { - request.headers['X-CSRF-Token'] = $.rails.csrfToken(); - } - next(); - }); - } - - all() { - return this.environments.get(); - } -} - -window.EnvironmentsService = EnvironmentsService; diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js new file mode 100644 index 00000000000..9b4090100da --- /dev/null +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -0,0 +1,190 @@ +/* eslint-disable no-param-reassign */ +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.EnvironmentsStore = { + state: {}, + + create() { + this.state.environments = []; + this.state.stoppedCounter = 0; + this.state.availableCounter = 0; + this.state.visibility = 'available'; + this.state.filteredEnvironments = []; + + return this; + }, + + /** + * In order to display a tree view we need to modify the received + * data in to a tree structure based on `environment_type` + * sorted alphabetically. + * In each children a `vue-` property will be added. This property will be + * used to know if an item is a children mostly for css purposes. This is + * needed because the children row is a fragment instance and therfore does + * not accept non-prop attributes. + * + * + * @example + * it will transform this: + * [ + * { name: "environment", environment_type: "review" }, + * { name: "environment_1", environment_type: null } + * { name: "environment_2, environment_type: "review" } + * ] + * into this: + * [ + * { name: "review", children: + * [ + * { name: "environment", environment_type: "review", vue-isChildren: true}, + * { name: "environment_2", environment_type: "review", vue-isChildren: true} + * ] + * }, + * {name: "environment_1", environment_type: null} + * ] + * + * + * @param {Array} environments List of environments. + * @returns {Array} Tree structured array with the received environments. + */ + storeEnvironments(environments = []) { + this.state.stoppedCounter = this.countByState(environments, 'stopped'); + this.state.availableCounter = this.countByState(environments, 'available'); + + const environmentsTree = environments.reduce((acc, environment) => { + if (environment.environment_type !== null) { + const occurs = acc.filter(element => element.children && + element.name === environment.environment_type); + + environment['vue-isChildren'] = true; + + if (occurs.length) { + acc[acc.indexOf(occurs[0])].children.push(environment); + acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName); + } else { + acc.push({ + name: environment.environment_type, + children: [environment], + isOpen: false, + 'vue-isChildren': environment['vue-isChildren'], + }); + } + } else { + acc.push(environment); + } + + return acc; + }, []).slice().sort(this.sortByName); + + this.state.environments = environmentsTree; + + this.filterEnvironmentsByVisibility(this.state.environments); + + return environmentsTree; + }, + + storeVisibility(visibility) { + this.state.visibility = visibility; + }, + /** + * Given the visibility prop provided by the url query parameter and which + * changes according to the active tab we need to filter which environments + * should be visible. + * + * The environments array is a recursive tree structure and we need to filter + * both root level environments and children environments. + * + * In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility` + * functions work together. + * The first one works as the filter that verifies if the given environment matches + * the given state. + * The second guarantees both root level and children elements are filtered as well. + * + * Given array of environments will return only + * the environments that match the state stored. + * + * @param {Array} array + * @return {Array} + */ + filterEnvironmentsByVisibility(arr) { + const filteredEnvironments = arr.map((item) => { + if (item.children) { + const filteredChildren = this.filterEnvironmentsByVisibility( + item.children, + ).filter(Boolean); + + if (filteredChildren.length) { + item.children = filteredChildren; + return item; + } + } + + return this.filterState(this.state.visibility, item); + }).filter(Boolean); + + this.state.filteredEnvironments = filteredEnvironments; + return filteredEnvironments; + }, + + /** + * Given the state and the environment, + * returns only if the environment state matches the one provided. + * + * @param {String} state + * @param {Object} environment + * @return {Object} + */ + filterState(state, environment) { + return environment.state === state && environment; + }, + + /** + * Toggles folder open property given the environment type. + * + * @param {String} envType + * @return {Array} + */ + toggleFolder(envType) { + const environments = this.state.environments; + + const environmentsCopy = environments.map((env) => { + if (env['vue-isChildren'] && env.name === envType) { + env.isOpen = !env.isOpen; + } + + return env; + }); + + this.state.environments = environmentsCopy; + + return environmentsCopy; + }, + + /** + * Given an array of environments, returns the number of environments + * that have the given state. + * + * @param {Array} environments + * @param {String} state + * @returns {Number} + */ + countByState(environments, state) { + return environments.filter(env => env.state === state).length; + }, + + /** + * Sorts the two objects provided by their name. + * + * @param {Object} a + * @param {Object} b + * @returns {Number} + */ + sortByName(a, b) { + const nameA = a.name.toUpperCase(); + const nameB = b.name.toUpperCase(); + + return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line + }, + }; +})(); diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6 deleted file mode 100644 index 9b4090100da..00000000000 --- a/app/assets/javascripts/environments/stores/environments_store.js.es6 +++ /dev/null @@ -1,190 +0,0 @@ -/* eslint-disable no-param-reassign */ -(() => { - window.gl = window.gl || {}; - window.gl.environmentsList = window.gl.environmentsList || {}; - - gl.environmentsList.EnvironmentsStore = { - state: {}, - - create() { - this.state.environments = []; - this.state.stoppedCounter = 0; - this.state.availableCounter = 0; - this.state.visibility = 'available'; - this.state.filteredEnvironments = []; - - return this; - }, - - /** - * In order to display a tree view we need to modify the received - * data in to a tree structure based on `environment_type` - * sorted alphabetically. - * In each children a `vue-` property will be added. This property will be - * used to know if an item is a children mostly for css purposes. This is - * needed because the children row is a fragment instance and therfore does - * not accept non-prop attributes. - * - * - * @example - * it will transform this: - * [ - * { name: "environment", environment_type: "review" }, - * { name: "environment_1", environment_type: null } - * { name: "environment_2, environment_type: "review" } - * ] - * into this: - * [ - * { name: "review", children: - * [ - * { name: "environment", environment_type: "review", vue-isChildren: true}, - * { name: "environment_2", environment_type: "review", vue-isChildren: true} - * ] - * }, - * {name: "environment_1", environment_type: null} - * ] - * - * - * @param {Array} environments List of environments. - * @returns {Array} Tree structured array with the received environments. - */ - storeEnvironments(environments = []) { - this.state.stoppedCounter = this.countByState(environments, 'stopped'); - this.state.availableCounter = this.countByState(environments, 'available'); - - const environmentsTree = environments.reduce((acc, environment) => { - if (environment.environment_type !== null) { - const occurs = acc.filter(element => element.children && - element.name === environment.environment_type); - - environment['vue-isChildren'] = true; - - if (occurs.length) { - acc[acc.indexOf(occurs[0])].children.push(environment); - acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName); - } else { - acc.push({ - name: environment.environment_type, - children: [environment], - isOpen: false, - 'vue-isChildren': environment['vue-isChildren'], - }); - } - } else { - acc.push(environment); - } - - return acc; - }, []).slice().sort(this.sortByName); - - this.state.environments = environmentsTree; - - this.filterEnvironmentsByVisibility(this.state.environments); - - return environmentsTree; - }, - - storeVisibility(visibility) { - this.state.visibility = visibility; - }, - /** - * Given the visibility prop provided by the url query parameter and which - * changes according to the active tab we need to filter which environments - * should be visible. - * - * The environments array is a recursive tree structure and we need to filter - * both root level environments and children environments. - * - * In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility` - * functions work together. - * The first one works as the filter that verifies if the given environment matches - * the given state. - * The second guarantees both root level and children elements are filtered as well. - * - * Given array of environments will return only - * the environments that match the state stored. - * - * @param {Array} array - * @return {Array} - */ - filterEnvironmentsByVisibility(arr) { - const filteredEnvironments = arr.map((item) => { - if (item.children) { - const filteredChildren = this.filterEnvironmentsByVisibility( - item.children, - ).filter(Boolean); - - if (filteredChildren.length) { - item.children = filteredChildren; - return item; - } - } - - return this.filterState(this.state.visibility, item); - }).filter(Boolean); - - this.state.filteredEnvironments = filteredEnvironments; - return filteredEnvironments; - }, - - /** - * Given the state and the environment, - * returns only if the environment state matches the one provided. - * - * @param {String} state - * @param {Object} environment - * @return {Object} - */ - filterState(state, environment) { - return environment.state === state && environment; - }, - - /** - * Toggles folder open property given the environment type. - * - * @param {String} envType - * @return {Array} - */ - toggleFolder(envType) { - const environments = this.state.environments; - - const environmentsCopy = environments.map((env) => { - if (env['vue-isChildren'] && env.name === envType) { - env.isOpen = !env.isOpen; - } - - return env; - }); - - this.state.environments = environmentsCopy; - - return environmentsCopy; - }, - - /** - * Given an array of environments, returns the number of environments - * that have the given state. - * - * @param {Array} environments - * @param {String} state - * @returns {Number} - */ - countByState(environments, state) { - return environments.filter(env => env.state === state).length; - }, - - /** - * Sorts the two objects provided by their name. - * - * @param {Object} a - * @param {Object} b - * @returns {Number} - */ - sortByName(a, b) { - const nameA = a.name.toUpperCase(); - const nameB = b.name.toUpperCase(); - - return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line - }, - }; -})(); diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js new file mode 100644 index 00000000000..f8256a8d26d --- /dev/null +++ b/app/assets/javascripts/extensions/array.js @@ -0,0 +1,27 @@ +/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, strict, max-len */ + +'use strict'; + +Array.prototype.first = function() { + 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; +}; diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6 deleted file mode 100644 index f8256a8d26d..00000000000 --- a/app/assets/javascripts/extensions/array.js.es6 +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, strict, max-len */ - -'use strict'; - -Array.prototype.first = function() { - 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; -}; diff --git a/app/assets/javascripts/extensions/custom_event.js b/app/assets/javascripts/extensions/custom_event.js new file mode 100644 index 00000000000..abedae4c1c7 --- /dev/null +++ b/app/assets/javascripts/extensions/custom_event.js @@ -0,0 +1,12 @@ +/* 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/custom_event.js.es6 b/app/assets/javascripts/extensions/custom_event.js.es6 deleted file mode 100644 index abedae4c1c7..00000000000 --- a/app/assets/javascripts/extensions/custom_event.js.es6 +++ /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 new file mode 100644 index 00000000000..90ab79305a7 --- /dev/null +++ b/app/assets/javascripts/extensions/element.js @@ -0,0 +1,20 @@ +/* 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/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6 deleted file mode 100644 index 90ab79305a7..00000000000 --- a/app/assets/javascripts/extensions/element.js.es6 +++ /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/object.js b/app/assets/javascripts/extensions/object.js new file mode 100644 index 00000000000..70a2d765abd --- /dev/null +++ b/app/assets/javascripts/extensions/object.js @@ -0,0 +1,26 @@ +/* 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/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6 deleted file mode 100644 index 70a2d765abd..00000000000 --- a/app/assets/javascripts/extensions/object.js.es6 +++ /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/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js new file mode 100644 index 00000000000..572c221929a --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -0,0 +1,69 @@ +require('./filtered_search_dropdown'); + +/* global droplabFilter */ + +(() => { + class DropdownHint extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); + this.config = { + droplabFilter: { + template: 'hint', + filterFunction: gl.DropdownUtils.filterHint.bind(null, input), + }, + }; + } + + itemClicked(e) { + const { selected } = e.detail; + + if (selected.tagName === 'LI') { + if (selected.hasAttribute('data-value')) { + this.dismissDropdown(); + } else if (selected.getAttribute('data-action') === 'submit') { + this.dismissDropdown(); + this.dispatchFormSubmitEvent(); + } else { + const token = selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = selected.querySelector('.js-filter-tag').innerText.trim(); + + if (tag.length) { + gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', '')); + } + this.dismissDropdown(); + this.dispatchInputEvent(); + } + } + } + + renderContent() { + const dropdownData = [{ + icon: 'fa-pencil', + hint: 'author:', + tag: '<@author>', + }, { + icon: 'fa-user', + hint: 'assignee:', + tag: '<@assignee>', + }, { + icon: 'fa-clock-o', + hint: 'milestone:', + tag: '<%milestone>', + }, { + icon: 'fa-tag', + hint: 'label:', + tag: '<~label>', + }]; + + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); + this.droplab.setData(this.hookId, dropdownData); + } + + init() { + this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownHint = DropdownHint; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 deleted file mode 100644 index 572c221929a..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ /dev/null @@ -1,69 +0,0 @@ -require('./filtered_search_dropdown'); - -/* global droplabFilter */ - -(() => { - class DropdownHint extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { - super(droplab, dropdown, input, filter); - this.config = { - droplabFilter: { - template: 'hint', - filterFunction: gl.DropdownUtils.filterHint.bind(null, input), - }, - }; - } - - itemClicked(e) { - const { selected } = e.detail; - - if (selected.tagName === 'LI') { - if (selected.hasAttribute('data-value')) { - this.dismissDropdown(); - } else if (selected.getAttribute('data-action') === 'submit') { - this.dismissDropdown(); - this.dispatchFormSubmitEvent(); - } else { - const token = selected.querySelector('.js-filter-hint').innerText.trim(); - const tag = selected.querySelector('.js-filter-tag').innerText.trim(); - - if (tag.length) { - gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', '')); - } - this.dismissDropdown(); - this.dispatchInputEvent(); - } - } - } - - renderContent() { - const dropdownData = [{ - icon: 'fa-pencil', - hint: 'author:', - tag: '<@author>', - }, { - icon: 'fa-user', - hint: 'assignee:', - tag: '<@assignee>', - }, { - icon: 'fa-clock-o', - hint: 'milestone:', - tag: '<%milestone>', - }, { - icon: 'fa-tag', - hint: 'label:', - tag: '<~label>', - }]; - - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); - this.droplab.setData(this.hookId, dropdownData); - } - - init() { - this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); - } - } - - window.gl = window.gl || {}; - gl.DropdownHint = DropdownHint; -})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js new file mode 100644 index 00000000000..b3dc3e502c5 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -0,0 +1,44 @@ +require('./filtered_search_dropdown'); + +/* global droplabAjax */ +/* global droplabFilter */ + +(() => { + class DropdownNonUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter, endpoint, symbol) { + super(droplab, dropdown, input, filter); + this.symbol = symbol; + this.config = { + droplabAjax: { + endpoint, + method: 'setData', + loadingTemplate: this.loadingTemplate, + }, + droplabFilter: { + filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), + }, + }; + } + + itemClicked(e) { + super.itemClicked(e, (selected) => { + const title = selected.querySelector('.js-data-value').innerText.trim(); + return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; + }); + } + + renderContent(forceShowList = false) { + this.droplab + .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + super.renderContent(forceShowList); + } + + init() { + this.droplab + .addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownNonUser = DropdownNonUser; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 deleted file mode 100644 index b3dc3e502c5..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ /dev/null @@ -1,44 +0,0 @@ -require('./filtered_search_dropdown'); - -/* global droplabAjax */ -/* global droplabFilter */ - -(() => { - class DropdownNonUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter, endpoint, symbol) { - super(droplab, dropdown, input, filter); - this.symbol = symbol; - this.config = { - droplabAjax: { - endpoint, - method: 'setData', - loadingTemplate: this.loadingTemplate, - }, - droplabFilter: { - filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), - }, - }; - } - - itemClicked(e) { - super.itemClicked(e, (selected) => { - const title = selected.querySelector('.js-data-value').innerText.trim(); - return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; - }); - } - - renderContent(forceShowList = false) { - this.droplab - .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); - super.renderContent(forceShowList); - } - - init() { - this.droplab - .addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); - } - } - - window.gl = window.gl || {}; - gl.DropdownNonUser = DropdownNonUser; -})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js new file mode 100644 index 00000000000..7e9c6f74aa5 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -0,0 +1,60 @@ +require('./filtered_search_dropdown'); + +/* global droplabAjaxFilter */ + +(() => { + class DropdownUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); + this.config = { + droplabAjaxFilter: { + endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, + searchKey: 'search', + params: { + per_page: 20, + active: true, + project_id: this.getProjectId(), + current_user: true, + }, + searchValueFunction: this.getSearchInput.bind(this), + loadingTemplate: this.loadingTemplate, + }, + }; + } + + itemClicked(e) { + super.itemClicked(e, + selected => selected.querySelector('.dropdown-light-content').innerText.trim()); + } + + renderContent(forceShowList = false) { + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); + super.renderContent(forceShowList); + } + + getProjectId() { + return this.input.getAttribute('data-project-id'); + } + + getSearchInput() { + const query = gl.DropdownUtils.getSearchInput(this.input); + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + let value = lastToken.value || ''; + + // Removes the first character if it is a quotation so that we can search + // with multiple words + if (value[0] === '"' || value[0] === '\'') { + value = value.slice(1); + } + + return value; + } + + init() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownUser = DropdownUser; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 deleted file mode 100644 index 7e9c6f74aa5..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ /dev/null @@ -1,60 +0,0 @@ -require('./filtered_search_dropdown'); - -/* global droplabAjaxFilter */ - -(() => { - class DropdownUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { - super(droplab, dropdown, input, filter); - this.config = { - droplabAjaxFilter: { - endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, - searchKey: 'search', - params: { - per_page: 20, - active: true, - project_id: this.getProjectId(), - current_user: true, - }, - searchValueFunction: this.getSearchInput.bind(this), - loadingTemplate: this.loadingTemplate, - }, - }; - } - - itemClicked(e) { - super.itemClicked(e, - selected => selected.querySelector('.dropdown-light-content').innerText.trim()); - } - - renderContent(forceShowList = false) { - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); - super.renderContent(forceShowList); - } - - getProjectId() { - return this.input.getAttribute('data-project-id'); - } - - getSearchInput() { - const query = gl.DropdownUtils.getSearchInput(this.input); - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); - let value = lastToken.value || ''; - - // Removes the first character if it is a quotation so that we can search - // with multiple words - if (value[0] === '"' || value[0] === '\'') { - value = value.slice(1); - } - - return value; - } - - init() { - this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); - } - } - - window.gl = window.gl || {}; - gl.DropdownUser = DropdownUser; -})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js new file mode 100644 index 00000000000..de3fa116717 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -0,0 +1,126 @@ +(() => { + class DropdownUtils { + static getEscapedText(text) { + let escapedText = text; + const hasSpace = text.indexOf(' ') !== -1; + const hasDoubleQuote = text.indexOf('"') !== -1; + + // Encapsulate value with quotes if it has spaces + // Known side effect: values's with both single and double quotes + // won't escape properly + if (hasSpace) { + if (hasDoubleQuote) { + escapedText = `'${text}'`; + } else { + // Encapsulate singleQuotes or if it hasSpace + escapedText = `"${text}"`; + } + } + + return escapedText; + } + + static filterWithSymbol(filterSymbol, input, item) { + const updatedItem = item; + const query = gl.DropdownUtils.getSearchInput(input); + const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); + + if (lastToken !== searchToken) { + const title = updatedItem.title.toLowerCase(); + let value = lastToken.value.toLowerCase(); + + // Removes the first character if it is a quotation so that we can search + // with multiple words + if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { + value = value.slice(1); + } + + // Eg. filterSymbol = ~ for labels + const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; + const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1; + + updatedItem.droplab_hidden = !match && !matchWithoutSymbol; + } else { + updatedItem.droplab_hidden = false; + } + + return updatedItem; + } + + static filterHint(input, item) { + const updatedItem = item; + const query = gl.DropdownUtils.getSearchInput(input); + let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + lastToken = lastToken.key || lastToken || ''; + + if (!lastToken || query.split('').last() === ' ') { + updatedItem.droplab_hidden = false; + } else if (lastToken) { + const split = lastToken.split(':'); + const tokenName = split[0].split(' ').last(); + + const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; + updatedItem.droplab_hidden = tokenName ? match : false; + } + + return updatedItem; + } + + static setDataValueIfSelected(filter, selected) { + const dataValue = selected.getAttribute('data-value'); + + if (dataValue) { + gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue); + } + + // Return boolean based on whether it was set + return dataValue !== null; + } + + static getSearchInput(filteredSearchInput) { + const inputValue = filteredSearchInput.value; + const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); + + return inputValue.slice(0, right); + } + + static getInputSelectionPosition(input) { + const selectionStart = input.selectionStart; + let inputValue = input.value; + // Replace all spaces inside quote marks with underscores + // (will continue to match entire string until an end quote is found if any) + // This helps with matching the beginning & end of a token:key + inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_')); + + // Get the right position for the word selected + // Regex matches first space + let right = inputValue.slice(selectionStart).search(/\s/); + + if (right >= 0) { + right += selectionStart; + } else if (right < 0) { + right = inputValue.length; + } + + // Get the left position for the word selected + // Regex matches last non-whitespace character + let left = inputValue.slice(0, right).search(/\S+$/); + + if (selectionStart === 0) { + left = 0; + } else if (selectionStart === inputValue.length && left < 0) { + left = inputValue.length; + } else if (left < 0) { + left = selectionStart; + } + + return { + left, + right, + }; + } + } + + window.gl = window.gl || {}; + gl.DropdownUtils = DropdownUtils; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 deleted file mode 100644 index de3fa116717..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ /dev/null @@ -1,126 +0,0 @@ -(() => { - class DropdownUtils { - static getEscapedText(text) { - let escapedText = text; - const hasSpace = text.indexOf(' ') !== -1; - const hasDoubleQuote = text.indexOf('"') !== -1; - - // Encapsulate value with quotes if it has spaces - // Known side effect: values's with both single and double quotes - // won't escape properly - if (hasSpace) { - if (hasDoubleQuote) { - escapedText = `'${text}'`; - } else { - // Encapsulate singleQuotes or if it hasSpace - escapedText = `"${text}"`; - } - } - - return escapedText; - } - - static filterWithSymbol(filterSymbol, input, item) { - const updatedItem = item; - const query = gl.DropdownUtils.getSearchInput(input); - const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); - - if (lastToken !== searchToken) { - const title = updatedItem.title.toLowerCase(); - let value = lastToken.value.toLowerCase(); - - // Removes the first character if it is a quotation so that we can search - // with multiple words - if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { - value = value.slice(1); - } - - // Eg. filterSymbol = ~ for labels - const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; - const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1; - - updatedItem.droplab_hidden = !match && !matchWithoutSymbol; - } else { - updatedItem.droplab_hidden = false; - } - - return updatedItem; - } - - static filterHint(input, item) { - const updatedItem = item; - const query = gl.DropdownUtils.getSearchInput(input); - let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); - lastToken = lastToken.key || lastToken || ''; - - if (!lastToken || query.split('').last() === ' ') { - updatedItem.droplab_hidden = false; - } else if (lastToken) { - const split = lastToken.split(':'); - const tokenName = split[0].split(' ').last(); - - const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; - updatedItem.droplab_hidden = tokenName ? match : false; - } - - return updatedItem; - } - - static setDataValueIfSelected(filter, selected) { - const dataValue = selected.getAttribute('data-value'); - - if (dataValue) { - gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue); - } - - // Return boolean based on whether it was set - return dataValue !== null; - } - - static getSearchInput(filteredSearchInput) { - const inputValue = filteredSearchInput.value; - const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); - - return inputValue.slice(0, right); - } - - static getInputSelectionPosition(input) { - const selectionStart = input.selectionStart; - let inputValue = input.value; - // Replace all spaces inside quote marks with underscores - // (will continue to match entire string until an end quote is found if any) - // This helps with matching the beginning & end of a token:key - inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_')); - - // Get the right position for the word selected - // Regex matches first space - let right = inputValue.slice(selectionStart).search(/\s/); - - if (right >= 0) { - right += selectionStart; - } else if (right < 0) { - right = inputValue.length; - } - - // Get the left position for the word selected - // Regex matches last non-whitespace character - let left = inputValue.slice(0, right).search(/\S+$/); - - if (selectionStart === 0) { - left = 0; - } else if (selectionStart === inputValue.length && left < 0) { - left = inputValue.length; - } else if (left < 0) { - left = selectionStart; - } - - return { - left, - right, - }; - } - } - - window.gl = window.gl || {}; - gl.DropdownUtils = DropdownUtils; -})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js index 392f1835966..9eaed2d7116 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -1,3 +1,3 @@ function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('./', true, /^\.\/(?!filtered_search_bundle).*\.(js|es6)$/)); +requireAll(require.context('./', true, /^\.\/(?!filtered_search_bundle).*\.js$/)); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js new file mode 100644 index 00000000000..859d6515531 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -0,0 +1,112 @@ +(() => { + const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; + + class FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + this.droplab = droplab; + this.hookId = input.getAttribute('data-id'); + this.input = input; + this.filter = filter; + this.dropdown = dropdown; + this.loadingTemplate = `
    + +
    `; + this.bindEvents(); + } + + bindEvents() { + this.itemClickedWrapper = this.itemClicked.bind(this); + this.dropdown.addEventListener('click.dl', this.itemClickedWrapper); + } + + unbindEvents() { + this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); + } + + getCurrentHook() { + return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; + } + + itemClicked(e, getValueFunction) { + const { selected } = e.detail; + + if (selected.tagName === 'LI' && selected.innerHTML) { + const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); + + if (!dataValueSet) { + const value = getValueFunction(selected); + gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value); + } + + this.dismissDropdown(); + this.dispatchInputEvent(); + } + } + + setAsDropdown() { + this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); + } + + setOffset(offset = 0) { + this.dropdown.style.left = `${offset}px`; + } + + renderContent(forceShowList = false) { + if (forceShowList && this.getCurrentHook().list.hidden) { + this.getCurrentHook().list.show(); + } + } + + render(forceRenderContent = false, forceShowList = false) { + this.setAsDropdown(); + + const currentHook = this.getCurrentHook(); + const firstTimeInitialized = currentHook === null; + + if (firstTimeInitialized || forceRenderContent) { + this.renderContent(forceShowList); + } else if (currentHook.list.list.id !== this.dropdown.id) { + this.renderContent(forceShowList); + } + } + + dismissDropdown() { + // Focusing on the input will dismiss dropdown + // (default droplab functionality) + this.input.focus(); + } + + dispatchInputEvent() { + // Propogate input change to FilteredSearchDropdownManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new CustomEvent('input', { + bubbles: true, + cancelable: true, + })); + } + + dispatchFormSubmitEvent() { + // dispatchEvent() is necessary as form.submit() does not + // trigger event handlers + this.input.form.dispatchEvent(new Event('submit')); + } + + hideDropdown() { + this.getCurrentHook().list.hide(); + } + + resetFilters() { + const hook = this.getCurrentHook(); + const data = hook.list.data; + const results = data.map((o) => { + const updated = o; + updated.droplab_hidden = false; + return updated; + }); + hook.list.render(results); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchDropdown = FilteredSearchDropdown; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 deleted file mode 100644 index 859d6515531..00000000000 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ /dev/null @@ -1,112 +0,0 @@ -(() => { - const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; - - class FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { - this.droplab = droplab; - this.hookId = input.getAttribute('data-id'); - this.input = input; - this.filter = filter; - this.dropdown = dropdown; - this.loadingTemplate = `
    - -
    `; - this.bindEvents(); - } - - bindEvents() { - this.itemClickedWrapper = this.itemClicked.bind(this); - this.dropdown.addEventListener('click.dl', this.itemClickedWrapper); - } - - unbindEvents() { - this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); - } - - getCurrentHook() { - return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; - } - - itemClicked(e, getValueFunction) { - const { selected } = e.detail; - - if (selected.tagName === 'LI' && selected.innerHTML) { - const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); - - if (!dataValueSet) { - const value = getValueFunction(selected); - gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value); - } - - this.dismissDropdown(); - this.dispatchInputEvent(); - } - } - - setAsDropdown() { - this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); - } - - setOffset(offset = 0) { - this.dropdown.style.left = `${offset}px`; - } - - renderContent(forceShowList = false) { - if (forceShowList && this.getCurrentHook().list.hidden) { - this.getCurrentHook().list.show(); - } - } - - render(forceRenderContent = false, forceShowList = false) { - this.setAsDropdown(); - - const currentHook = this.getCurrentHook(); - const firstTimeInitialized = currentHook === null; - - if (firstTimeInitialized || forceRenderContent) { - this.renderContent(forceShowList); - } else if (currentHook.list.list.id !== this.dropdown.id) { - this.renderContent(forceShowList); - } - } - - dismissDropdown() { - // Focusing on the input will dismiss dropdown - // (default droplab functionality) - this.input.focus(); - } - - dispatchInputEvent() { - // Propogate input change to FilteredSearchDropdownManager - // so that it can determine which dropdowns to open - this.input.dispatchEvent(new CustomEvent('input', { - bubbles: true, - cancelable: true, - })); - } - - dispatchFormSubmitEvent() { - // dispatchEvent() is necessary as form.submit() does not - // trigger event handlers - this.input.form.dispatchEvent(new Event('submit')); - } - - hideDropdown() { - this.getCurrentHook().list.hide(); - } - - resetFilters() { - const hook = this.getCurrentHook(); - const data = hook.list.data; - const results = data.map((o) => { - const updated = o; - updated.droplab_hidden = false; - return updated; - }); - hook.list.render(results); - } - } - - window.gl = window.gl || {}; - gl.FilteredSearchDropdown = FilteredSearchDropdown; -})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js new file mode 100644 index 00000000000..547989a6ff5 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -0,0 +1,207 @@ +/* global DropLab */ + +(() => { + class FilteredSearchDropdownManager { + constructor() { + this.tokenizer = gl.FilteredSearchTokenizer; + this.filteredSearchInput = document.querySelector('.filtered-search'); + + this.setupMapping(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('beforeunload', this.cleanupWrapper); + } + + cleanup() { + if (this.droplab) { + this.droplab.destroy(); + this.droplab = null; + } + + this.setupMapping(); + + document.removeEventListener('beforeunload', this.cleanupWrapper); + } + + setupMapping() { + this.mapping = { + author: { + reference: null, + gl: 'DropdownUser', + element: document.querySelector('#js-dropdown-author'), + }, + assignee: { + reference: null, + gl: 'DropdownUser', + element: document.querySelector('#js-dropdown-assignee'), + }, + milestone: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: ['milestones.json', '%'], + element: document.querySelector('#js-dropdown-milestone'), + }, + label: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: ['labels.json', '~'], + element: document.querySelector('#js-dropdown-label'), + }, + hint: { + reference: null, + gl: 'DropdownHint', + element: document.querySelector('#js-dropdown-hint'), + }, + }; + } + + static addWordToInput(tokenName, tokenValue = '') { + const input = document.querySelector('.filtered-search'); + const inputValue = input.value; + const word = `${tokenName}:${tokenValue}`; + + // Get the string to replace + let newCaretPosition = input.selectionStart; + const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input); + + input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`; + + // If we have added a tokenValue at the end of the input, + // add a space and set selection to the end + if (right >= inputValue.length && tokenValue !== '') { + input.value += ' '; + newCaretPosition = input.value.length; + } + + gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input); + } + + static updateInputCaretPosition(selectionStart, input) { + // Reset the position + // Sometimes can end up at end of input + input.setSelectionRange(selectionStart, selectionStart); + + const { right } = gl.DropdownUtils.getInputSelectionPosition(input); + + input.setSelectionRange(right, right); + } + + updateCurrentDropdownOffset() { + this.updateDropdownOffset(this.currentDropdown); + } + + updateDropdownOffset(key) { + if (!this.font) { + this.font = window.getComputedStyle(this.filteredSearchInput).font; + } + + const input = this.filteredSearchInput; + const inputText = input.value.slice(0, input.selectionStart); + const filterIconPadding = 27; + let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding; + + const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 : + this.mapping[key].element.clientWidth; + const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth; + + if (offsetMaxWidth < offset) { + offset = offsetMaxWidth; + } + + this.mapping[key].reference.setOffset(offset); + } + + load(key, firstLoad = false) { + const mappingKey = this.mapping[key]; + const glClass = mappingKey.gl; + const element = mappingKey.element; + let forceShowList = false; + + if (!mappingKey.reference) { + const dl = this.droplab; + const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; + const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); + + // Passing glArguments to `new gl[glClass]()` + mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); + } + + if (firstLoad) { + mappingKey.reference.init(); + } + + if (this.currentDropdown === 'hint') { + // Force the dropdown to show if it was clicked from the hint dropdown + forceShowList = true; + } + + this.updateDropdownOffset(key); + mappingKey.reference.render(firstLoad, forceShowList); + + this.currentDropdown = key; + } + + loadDropdown(dropdownName = '') { + let firstLoad = false; + + if (!this.droplab) { + firstLoad = true; + this.droplab = new DropLab(); + } + + const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); + const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key + && this.mapping[match.key]; + const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; + + if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { + const key = match && match.key ? match.key : 'hint'; + this.load(key, firstLoad); + } + } + + setDropdown() { + const { lastToken, searchToken } = this.tokenizer + .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput)); + + if (this.currentDropdown) { + this.updateCurrentDropdownOffset(); + } + + if (lastToken === searchToken && lastToken !== null) { + // Token is not fully initialized yet because it has no value + // Eg. token = 'label:' + + const split = lastToken.split(':'); + const dropdownName = split[0].split(' ').last(); + this.loadDropdown(split.length > 1 ? dropdownName : ''); + } else if (lastToken) { + // Token has been initialized into an object because it has a value + this.loadDropdown(lastToken.key); + } else { + this.loadDropdown('hint'); + } + } + + resetDropdowns() { + // Force current dropdown to hide + this.mapping[this.currentDropdown].reference.hideDropdown(); + + // Re-Load dropdown + this.setDropdown(); + + // Reset filters for current dropdown + this.mapping[this.currentDropdown].reference.resetFilters(); + + // Reposition dropdown so that it is aligned with cursor + this.updateDropdownOffset(this.currentDropdown); + } + + destroyDroplab() { + this.droplab.destroy(); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 deleted file mode 100644 index 547989a6ff5..00000000000 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ /dev/null @@ -1,207 +0,0 @@ -/* global DropLab */ - -(() => { - class FilteredSearchDropdownManager { - constructor() { - this.tokenizer = gl.FilteredSearchTokenizer; - this.filteredSearchInput = document.querySelector('.filtered-search'); - - this.setupMapping(); - - this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('beforeunload', this.cleanupWrapper); - } - - cleanup() { - if (this.droplab) { - this.droplab.destroy(); - this.droplab = null; - } - - this.setupMapping(); - - document.removeEventListener('beforeunload', this.cleanupWrapper); - } - - setupMapping() { - this.mapping = { - author: { - reference: null, - gl: 'DropdownUser', - element: document.querySelector('#js-dropdown-author'), - }, - assignee: { - reference: null, - gl: 'DropdownUser', - element: document.querySelector('#js-dropdown-assignee'), - }, - milestone: { - reference: null, - gl: 'DropdownNonUser', - extraArguments: ['milestones.json', '%'], - element: document.querySelector('#js-dropdown-milestone'), - }, - label: { - reference: null, - gl: 'DropdownNonUser', - extraArguments: ['labels.json', '~'], - element: document.querySelector('#js-dropdown-label'), - }, - hint: { - reference: null, - gl: 'DropdownHint', - element: document.querySelector('#js-dropdown-hint'), - }, - }; - } - - static addWordToInput(tokenName, tokenValue = '') { - const input = document.querySelector('.filtered-search'); - const inputValue = input.value; - const word = `${tokenName}:${tokenValue}`; - - // Get the string to replace - let newCaretPosition = input.selectionStart; - const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input); - - input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`; - - // If we have added a tokenValue at the end of the input, - // add a space and set selection to the end - if (right >= inputValue.length && tokenValue !== '') { - input.value += ' '; - newCaretPosition = input.value.length; - } - - gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input); - } - - static updateInputCaretPosition(selectionStart, input) { - // Reset the position - // Sometimes can end up at end of input - input.setSelectionRange(selectionStart, selectionStart); - - const { right } = gl.DropdownUtils.getInputSelectionPosition(input); - - input.setSelectionRange(right, right); - } - - updateCurrentDropdownOffset() { - this.updateDropdownOffset(this.currentDropdown); - } - - updateDropdownOffset(key) { - if (!this.font) { - this.font = window.getComputedStyle(this.filteredSearchInput).font; - } - - const input = this.filteredSearchInput; - const inputText = input.value.slice(0, input.selectionStart); - const filterIconPadding = 27; - let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding; - - const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 : - this.mapping[key].element.clientWidth; - const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth; - - if (offsetMaxWidth < offset) { - offset = offsetMaxWidth; - } - - this.mapping[key].reference.setOffset(offset); - } - - load(key, firstLoad = false) { - const mappingKey = this.mapping[key]; - const glClass = mappingKey.gl; - const element = mappingKey.element; - let forceShowList = false; - - if (!mappingKey.reference) { - const dl = this.droplab; - const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; - const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); - - // Passing glArguments to `new gl[glClass]()` - mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); - } - - if (firstLoad) { - mappingKey.reference.init(); - } - - if (this.currentDropdown === 'hint') { - // Force the dropdown to show if it was clicked from the hint dropdown - forceShowList = true; - } - - this.updateDropdownOffset(key); - mappingKey.reference.render(firstLoad, forceShowList); - - this.currentDropdown = key; - } - - loadDropdown(dropdownName = '') { - let firstLoad = false; - - if (!this.droplab) { - firstLoad = true; - this.droplab = new DropLab(); - } - - const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); - const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key - && this.mapping[match.key]; - const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; - - if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { - const key = match && match.key ? match.key : 'hint'; - this.load(key, firstLoad); - } - } - - setDropdown() { - const { lastToken, searchToken } = this.tokenizer - .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput)); - - if (this.currentDropdown) { - this.updateCurrentDropdownOffset(); - } - - if (lastToken === searchToken && lastToken !== null) { - // Token is not fully initialized yet because it has no value - // Eg. token = 'label:' - - const split = lastToken.split(':'); - const dropdownName = split[0].split(' ').last(); - this.loadDropdown(split.length > 1 ? dropdownName : ''); - } else if (lastToken) { - // Token has been initialized into an object because it has a value - this.loadDropdown(lastToken.key); - } else { - this.loadDropdown('hint'); - } - } - - resetDropdowns() { - // Force current dropdown to hide - this.mapping[this.currentDropdown].reference.hideDropdown(); - - // Re-Load dropdown - this.setDropdown(); - - // Reset filters for current dropdown - this.mapping[this.currentDropdown].reference.resetFilters(); - - // Reposition dropdown so that it is aligned with cursor - this.updateDropdownOffset(this.currentDropdown); - } - - destroyDroplab() { - this.droplab.destroy(); - } - } - - window.gl = window.gl || {}; - gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; -})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js new file mode 100644 index 00000000000..4e02ab7c8c1 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -0,0 +1,230 @@ +(() => { + class FilteredSearchManager { + constructor() { + this.filteredSearchInput = document.querySelector('.filtered-search'); + this.clearSearchButton = document.querySelector('.clear-search'); + + if (this.filteredSearchInput) { + this.tokenizer = gl.FilteredSearchTokenizer; + this.dropdownManager = new gl.FilteredSearchDropdownManager(); + + this.bindEvents(); + this.loadSearchParamsFromURL(); + this.dropdownManager.setDropdown(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('beforeunload', this.cleanupWrapper); + } + } + + cleanup() { + this.unbindEvents(); + document.removeEventListener('beforeunload', this.cleanupWrapper); + } + + bindEvents() { + this.handleFormSubmit = this.handleFormSubmit.bind(this); + this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); + this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); + this.checkForEnterWrapper = this.checkForEnter.bind(this); + this.clearSearchWrapper = this.clearSearch.bind(this); + this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + this.tokenChange = this.tokenChange.bind(this); + + this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit); + this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); + this.filteredSearchInput.addEventListener('click', this.tokenChange); + this.filteredSearchInput.addEventListener('keyup', this.tokenChange); + this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); + } + + unbindEvents() { + this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit); + this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); + this.filteredSearchInput.removeEventListener('click', this.tokenChange); + this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); + this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); + } + + checkForBackspace(e) { + // 8 = Backspace Key + // 46 = Delete Key + if (e.keyCode === 8 || e.keyCode === 46) { + // Reposition dropdown so that it is aligned with cursor + this.dropdownManager.updateCurrentDropdownOffset(); + } + } + + checkForEnter(e) { + if (e.keyCode === 38 || e.keyCode === 40) { + const selectionStart = this.filteredSearchInput.selectionStart; + + e.preventDefault(); + this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart); + } + + if (e.keyCode === 13) { + const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; + const dropdownEl = dropdown.element; + const activeElements = dropdownEl.querySelectorAll('.dropdown-active'); + + e.preventDefault(); + + if (!activeElements.length) { + // Prevent droplab from opening dropdown + this.dropdownManager.destroyDroplab(); + + this.search(); + } + } + } + + toggleClearSearchButton(e) { + if (e.target.value) { + this.clearSearchButton.classList.remove('hidden'); + } else { + this.clearSearchButton.classList.add('hidden'); + } + } + + clearSearch(e) { + e.preventDefault(); + + this.filteredSearchInput.value = ''; + this.clearSearchButton.classList.add('hidden'); + + this.dropdownManager.resetDropdowns(); + } + + handleFormSubmit(e) { + e.preventDefault(); + this.search(); + } + + loadSearchParamsFromURL() { + const params = gl.utils.getUrlParamsArray(); + const usernameParams = this.getUsernameParams(); + const inputValues = []; + + params.forEach((p) => { + const split = p.split('='); + const keyParam = decodeURIComponent(split[0]); + const value = split[1]; + + // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys + const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p); + + if (condition) { + inputValues.push(`${condition.tokenKey}:${condition.value}`); + } else { + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; + const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam); + + if (match) { + const indexOf = keyParam.indexOf('_'); + const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam; + const symbol = match.symbol; + let quotationsToUse = ''; + + if (sanitizedValue.indexOf(' ') !== -1) { + // Prefer ", but use ' if required + quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; + } + + inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); + } else if (!match && keyParam === 'assignee_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { + inputValues.push(`assignee:@${usernameParams[id]}`); + } + } else if (!match && keyParam === 'author_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { + inputValues.push(`author:@${usernameParams[id]}`); + } + } else if (!match && keyParam === 'search') { + inputValues.push(sanitizedValue); + } + } + }); + + // Trim the last space value + this.filteredSearchInput.value = inputValues.join(' '); + + if (inputValues.length > 0) { + this.clearSearchButton.classList.remove('hidden'); + } + } + + search() { + const paths = []; + const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); + const currentState = gl.utils.getParameterByName('state') || 'opened'; + paths.push(`state=${currentState}`); + + tokens.forEach((token) => { + const condition = gl.FilteredSearchTokenKeys + .searchByConditionKeyValue(token.key, token.value.toLowerCase()); + const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); + const keyParam = param ? `${token.key}_${param}` : token.key; + let tokenPath = ''; + + if (condition) { + tokenPath = condition.url; + } else { + let tokenValue = token.value; + + if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || + (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { + tokenValue = tokenValue.slice(1, tokenValue.length - 1); + } + + tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; + } + + paths.push(tokenPath); + }); + + if (searchToken) { + const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+'); + paths.push(`search=${sanitized}`); + } + + const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`; + + gl.utils.visitUrl(parameterizedUrl); + } + + getUsernameParams() { + const usernamesById = {}; + try { + const attribute = this.filteredSearchInput.getAttribute('data-username-params'); + JSON.parse(attribute).forEach((user) => { + usernamesById[user.id] = user.username; + }); + } catch (e) { + // do nothing + } + return usernamesById; + } + + tokenChange() { + const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; + const currentDropdownRef = dropdown.reference; + + this.setDropdownWrapper(); + currentDropdownRef.dispatchInputEvent(); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchManager = FilteredSearchManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 deleted file mode 100644 index 4e02ab7c8c1..00000000000 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ /dev/null @@ -1,230 +0,0 @@ -(() => { - class FilteredSearchManager { - constructor() { - this.filteredSearchInput = document.querySelector('.filtered-search'); - this.clearSearchButton = document.querySelector('.clear-search'); - - if (this.filteredSearchInput) { - this.tokenizer = gl.FilteredSearchTokenizer; - this.dropdownManager = new gl.FilteredSearchDropdownManager(); - - this.bindEvents(); - this.loadSearchParamsFromURL(); - this.dropdownManager.setDropdown(); - - this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('beforeunload', this.cleanupWrapper); - } - } - - cleanup() { - this.unbindEvents(); - document.removeEventListener('beforeunload', this.cleanupWrapper); - } - - bindEvents() { - this.handleFormSubmit = this.handleFormSubmit.bind(this); - this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); - this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); - this.checkForEnterWrapper = this.checkForEnter.bind(this); - this.clearSearchWrapper = this.clearSearch.bind(this); - this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); - this.tokenChange = this.tokenChange.bind(this); - - this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit); - this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); - this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); - this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); - this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); - this.filteredSearchInput.addEventListener('click', this.tokenChange); - this.filteredSearchInput.addEventListener('keyup', this.tokenChange); - this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); - } - - unbindEvents() { - this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit); - this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); - this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); - this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); - this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); - this.filteredSearchInput.removeEventListener('click', this.tokenChange); - this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); - this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); - } - - checkForBackspace(e) { - // 8 = Backspace Key - // 46 = Delete Key - if (e.keyCode === 8 || e.keyCode === 46) { - // Reposition dropdown so that it is aligned with cursor - this.dropdownManager.updateCurrentDropdownOffset(); - } - } - - checkForEnter(e) { - if (e.keyCode === 38 || e.keyCode === 40) { - const selectionStart = this.filteredSearchInput.selectionStart; - - e.preventDefault(); - this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart); - } - - if (e.keyCode === 13) { - const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; - const dropdownEl = dropdown.element; - const activeElements = dropdownEl.querySelectorAll('.dropdown-active'); - - e.preventDefault(); - - if (!activeElements.length) { - // Prevent droplab from opening dropdown - this.dropdownManager.destroyDroplab(); - - this.search(); - } - } - } - - toggleClearSearchButton(e) { - if (e.target.value) { - this.clearSearchButton.classList.remove('hidden'); - } else { - this.clearSearchButton.classList.add('hidden'); - } - } - - clearSearch(e) { - e.preventDefault(); - - this.filteredSearchInput.value = ''; - this.clearSearchButton.classList.add('hidden'); - - this.dropdownManager.resetDropdowns(); - } - - handleFormSubmit(e) { - e.preventDefault(); - this.search(); - } - - loadSearchParamsFromURL() { - const params = gl.utils.getUrlParamsArray(); - const usernameParams = this.getUsernameParams(); - const inputValues = []; - - params.forEach((p) => { - const split = p.split('='); - const keyParam = decodeURIComponent(split[0]); - const value = split[1]; - - // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys - const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p); - - if (condition) { - inputValues.push(`${condition.tokenKey}:${condition.value}`); - } else { - // Sanitize value since URL converts spaces into + - // Replace before decode so that we know what was originally + versus the encoded + - const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; - const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam); - - if (match) { - const indexOf = keyParam.indexOf('_'); - const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam; - const symbol = match.symbol; - let quotationsToUse = ''; - - if (sanitizedValue.indexOf(' ') !== -1) { - // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; - } - - inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); - } else if (!match && keyParam === 'assignee_id') { - const id = parseInt(value, 10); - if (usernameParams[id]) { - inputValues.push(`assignee:@${usernameParams[id]}`); - } - } else if (!match && keyParam === 'author_id') { - const id = parseInt(value, 10); - if (usernameParams[id]) { - inputValues.push(`author:@${usernameParams[id]}`); - } - } else if (!match && keyParam === 'search') { - inputValues.push(sanitizedValue); - } - } - }); - - // Trim the last space value - this.filteredSearchInput.value = inputValues.join(' '); - - if (inputValues.length > 0) { - this.clearSearchButton.classList.remove('hidden'); - } - } - - search() { - const paths = []; - const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); - const currentState = gl.utils.getParameterByName('state') || 'opened'; - paths.push(`state=${currentState}`); - - tokens.forEach((token) => { - const condition = gl.FilteredSearchTokenKeys - .searchByConditionKeyValue(token.key, token.value.toLowerCase()); - const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); - const keyParam = param ? `${token.key}_${param}` : token.key; - let tokenPath = ''; - - if (condition) { - tokenPath = condition.url; - } else { - let tokenValue = token.value; - - if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || - (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { - tokenValue = tokenValue.slice(1, tokenValue.length - 1); - } - - tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; - } - - paths.push(tokenPath); - }); - - if (searchToken) { - const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+'); - paths.push(`search=${sanitized}`); - } - - const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`; - - gl.utils.visitUrl(parameterizedUrl); - } - - getUsernameParams() { - const usernamesById = {}; - try { - const attribute = this.filteredSearchInput.getAttribute('data-username-params'); - JSON.parse(attribute).forEach((user) => { - usernamesById[user.id] = user.username; - }); - } catch (e) { - // do nothing - } - return usernamesById; - } - - tokenChange() { - const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; - const currentDropdownRef = dropdown.reference; - - this.setDropdownWrapper(); - currentDropdownRef.dispatchInputEvent(); - } - } - - window.gl = window.gl || {}; - gl.FilteredSearchManager = FilteredSearchManager; -})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js new file mode 100644 index 00000000000..e6b53cd4b55 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -0,0 +1,96 @@ +(() => { + const tokenKeys = [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', + }, { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', + }]; + + const alternativeTokenKeys = [{ + key: 'label', + type: 'string', + param: 'name', + symbol: '~', + }]; + + const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); + + const conditions = [{ + url: 'assignee_id=0', + tokenKey: 'assignee', + value: 'none', + }, { + url: 'milestone_title=No+Milestone', + tokenKey: 'milestone', + value: 'none', + }, { + url: 'milestone_title=%23upcoming', + tokenKey: 'milestone', + value: 'upcoming', + }, { + url: 'label_name[]=No+Label', + tokenKey: 'label', + value: 'none', + }]; + + class FilteredSearchTokenKeys { + static get() { + return tokenKeys; + } + + static getAlternatives() { + return alternativeTokenKeys; + } + + static getConditions() { + return conditions; + } + + static searchByKey(key) { + return tokenKeys.find(tokenKey => tokenKey.key === key) || null; + } + + static searchBySymbol(symbol) { + return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; + } + + static searchByKeyParam(keyParam) { + return tokenKeysWithAlternative.find((tokenKey) => { + let tokenKeyParam = tokenKey.key; + + if (tokenKey.param) { + tokenKeyParam += `_${tokenKey.param}`; + } + + return keyParam === tokenKeyParam; + }) || null; + } + + static searchByConditionUrl(url) { + return conditions.find(condition => condition.url === url) || null; + } + + static searchByConditionKeyValue(key, value) { + return conditions + .find(condition => condition.tokenKey === key && condition.value === value) || null; + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 deleted file mode 100644 index e6b53cd4b55..00000000000 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 +++ /dev/null @@ -1,96 +0,0 @@ -(() => { - const tokenKeys = [{ - key: 'author', - type: 'string', - param: 'username', - symbol: '@', - }, { - key: 'assignee', - type: 'string', - param: 'username', - symbol: '@', - }, { - key: 'milestone', - type: 'string', - param: 'title', - symbol: '%', - }, { - key: 'label', - type: 'array', - param: 'name[]', - symbol: '~', - }]; - - const alternativeTokenKeys = [{ - key: 'label', - type: 'string', - param: 'name', - symbol: '~', - }]; - - const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); - - const conditions = [{ - url: 'assignee_id=0', - tokenKey: 'assignee', - value: 'none', - }, { - url: 'milestone_title=No+Milestone', - tokenKey: 'milestone', - value: 'none', - }, { - url: 'milestone_title=%23upcoming', - tokenKey: 'milestone', - value: 'upcoming', - }, { - url: 'label_name[]=No+Label', - tokenKey: 'label', - value: 'none', - }]; - - class FilteredSearchTokenKeys { - static get() { - return tokenKeys; - } - - static getAlternatives() { - return alternativeTokenKeys; - } - - static getConditions() { - return conditions; - } - - static searchByKey(key) { - return tokenKeys.find(tokenKey => tokenKey.key === key) || null; - } - - static searchBySymbol(symbol) { - return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; - } - - static searchByKeyParam(keyParam) { - return tokenKeysWithAlternative.find((tokenKey) => { - let tokenKeyParam = tokenKey.key; - - if (tokenKey.param) { - tokenKeyParam += `_${tokenKey.param}`; - } - - return keyParam === tokenKeyParam; - }) || null; - } - - static searchByConditionUrl(url) { - return conditions.find(condition => condition.url === url) || null; - } - - static searchByConditionKeyValue(key, value) { - return conditions - .find(condition => condition.tokenKey === key && condition.value === value) || null; - } - } - - window.gl = window.gl || {}; - gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; -})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js new file mode 100644 index 00000000000..cf53845a48b --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js @@ -0,0 +1,45 @@ +(() => { + class FilteredSearchTokenizer { + static processTokens(input) { + // Regex extracts `(token):(symbol)(value)` + // Values that start with a double quote must end in a double quote (same for single) + const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g; + const tokens = []; + let lastToken = null; + const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { + let tokenValue = v1 || v2 || v3; + let tokenSymbol = symbol; + + if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { + tokenSymbol = tokenValue; + tokenValue = ''; + } + + tokens.push({ + key, + value: tokenValue || '', + symbol: tokenSymbol || '', + }); + return ''; + }).replace(/\s{2,}/g, ' ').trim() || ''; + + if (tokens.length > 0) { + const last = tokens[tokens.length - 1]; + const lastString = `${last.key}:${last.symbol}${last.value}`; + lastToken = input.lastIndexOf(lastString) === + input.length - lastString.length ? last : searchToken; + } else { + lastToken = searchToken; + } + + return { + tokens, + lastToken, + searchToken, + }; + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchTokenizer = FilteredSearchTokenizer; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 deleted file mode 100644 index cf53845a48b..00000000000 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 +++ /dev/null @@ -1,45 +0,0 @@ -(() => { - class FilteredSearchTokenizer { - static processTokens(input) { - // Regex extracts `(token):(symbol)(value)` - // Values that start with a double quote must end in a double quote (same for single) - const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g; - const tokens = []; - let lastToken = null; - const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { - let tokenValue = v1 || v2 || v3; - let tokenSymbol = symbol; - - if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { - tokenSymbol = tokenValue; - tokenValue = ''; - } - - tokens.push({ - key, - value: tokenValue || '', - symbol: tokenSymbol || '', - }); - return ''; - }).replace(/\s{2,}/g, ' ').trim() || ''; - - if (tokens.length > 0) { - const last = tokens[tokens.length - 1]; - const lastString = `${last.key}:${last.symbol}${last.value}`; - lastToken = input.lastIndexOf(lastString) === - input.length - lastString.length ? last : searchToken; - } else { - lastToken = searchToken; - } - - return { - tokens, - lastToken, - searchToken, - }; - } - } - - window.gl = window.gl || {}; - gl.FilteredSearchTokenizer = FilteredSearchTokenizer; -})(); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js new file mode 100644 index 00000000000..7f1f2a5d278 --- /dev/null +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -0,0 +1,380 @@ +/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */ + +// Creates the variables for setting up GFM auto-completion +(function() { + if (window.gl == null) { + window.gl = {}; + } + + 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: { + template: '
  • ${name} ${name}
  • ' + }, + // Team Members + Members: { + template: '
  • ${avatarTag} ${username} ${title}
  • ' + }, + Labels: { + template: '
  • ${title}
  • ' + }, + // Issues and MergeRequests + Issues: { + template: '
  • ${id} ${title}
  • ' + }, + // Milestones + Milestones: { + template: '
  • ${title}
  • ' + }, + Loading: { + template: '
  • Loading...
  • ' + }, + 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, "\\$&"); + + _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'); + + match = regexp.exec(subtext); + + if (match) { + return (match[1] || match[1] === "") ? match[1] : match[2]; + } 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)); + }); + }, + setupAtWho: function($input) { + // Emoji + $input.atwho({ + at: ':', + displayTpl: function(value) { + return value.path != null ? this.Emoji.template : 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 = `${m.username}`; + const txtAvatar = `
    ${autoCompleteAvatar}
    `; + + 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: '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: '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 + }; + }); + } + } + }); + // 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 = '
  • /${name}'; + if (value.aliases.length > 0) { + tpl += ' (or /<%- aliases.join(", /") %>)'; + } + if (value.params.length > 0) { + tpl += ' <%- params.join(" ") %>'; + } + if (value.description !== '') { + tpl += '<%- description %>'; + } + tpl += '
  • '; + 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 { + 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; + } + } + } + }); + 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 { + $.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); + } + }; +}).call(this); diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 deleted file mode 100644 index 7f1f2a5d278..00000000000 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ /dev/null @@ -1,380 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */ - -// Creates the variables for setting up GFM auto-completion -(function() { - if (window.gl == null) { - window.gl = {}; - } - - 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: { - template: '
  • ${name} ${name}
  • ' - }, - // Team Members - Members: { - template: '
  • ${avatarTag} ${username} ${title}
  • ' - }, - Labels: { - template: '
  • ${title}
  • ' - }, - // Issues and MergeRequests - Issues: { - template: '
  • ${id} ${title}
  • ' - }, - // Milestones - Milestones: { - template: '
  • ${title}
  • ' - }, - Loading: { - template: '
  • Loading...
  • ' - }, - 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, "\\$&"); - - _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'); - - match = regexp.exec(subtext); - - if (match) { - return (match[1] || match[1] === "") ? match[1] : match[2]; - } 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)); - }); - }, - setupAtWho: function($input) { - // Emoji - $input.atwho({ - at: ':', - displayTpl: function(value) { - return value.path != null ? this.Emoji.template : 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 = `${m.username}`; - const txtAvatar = `
    ${autoCompleteAvatar}
    `; - - 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: '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: '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 - }; - }); - } - } - }); - // 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 = '
  • /${name}'; - if (value.aliases.length > 0) { - tpl += ' (or /<%- aliases.join(", /") %>)'; - } - if (value.params.length > 0) { - tpl += ' <%- params.join(" ") %>'; - } - if (value.description !== '') { - tpl += '<%- description %>'; - } - tpl += '
  • '; - 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 { - 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; - } - } - } - }); - 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 { - $.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); - } - }; -}).call(this); diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js new file mode 100644 index 00000000000..f7cbecc0385 --- /dev/null +++ b/app/assets/javascripts/gl_field_error.js @@ -0,0 +1,164 @@ +/* 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: + * + *
    + * + *
    + * + * Ignore specific inputs (e.g. UsernameValidator): + * + *
    + *
    + * + *
    + *
    + * + * Custom Error Anchor (allows error message to be injected after specified element): + * + *
    + *
    + * + * // Error message typically injected here + *
    + * // Error message now injected here + *
    + * + * */ + + /* + * 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 = $(`

    ${this.errorMessage}

    `); + + this.state = { + valid: false, + empty: true, + }; + + this.initFieldValidation(); + } + + 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(); + } + + 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; + + return safelyScoped ? unignoredSiblings : this.fieldErrorElement; + } + + renderValidity() { + this.renderClear(); + + 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)); + } + + /* Get or set current input value */ + accessCurrentValue(newVal) { + return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); + } + + getInputValidity() { + return this.inputDomElement.validity.valid; + } + + updateValidity() { + const inputVal = this.accessCurrentValue(); + this.state.empty = !inputVal.length; + this.state.valid = this.getInputValidity(); + this.renderValidity(); + } + + renderValid() { + return this.renderClear(); + } + + renderEmpty() { + return this.renderInvalid(); + } + + 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(); + } + } + + global.GlFieldError = GlFieldError; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_field_error.js.es6 b/app/assets/javascripts/gl_field_error.js.es6 deleted file mode 100644 index f7cbecc0385..00000000000 --- a/app/assets/javascripts/gl_field_error.js.es6 +++ /dev/null @@ -1,164 +0,0 @@ -/* 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: - * - *
    - * - *
    - * - * Ignore specific inputs (e.g. UsernameValidator): - * - *
    - *
    - * - *
    - *
    - * - * Custom Error Anchor (allows error message to be injected after specified element): - * - *
    - *
    - * - * // Error message typically injected here - *
    - * // Error message now injected here - *
    - * - * */ - - /* - * 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 = $(`

    ${this.errorMessage}

    `); - - this.state = { - valid: false, - empty: true, - }; - - this.initFieldValidation(); - } - - 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(); - } - - 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; - - return safelyScoped ? unignoredSiblings : this.fieldErrorElement; - } - - renderValidity() { - this.renderClear(); - - 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)); - } - - /* Get or set current input value */ - accessCurrentValue(newVal) { - return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); - } - - getInputValidity() { - return this.inputDomElement.validity.valid; - } - - updateValidity() { - const inputVal = this.accessCurrentValue(); - this.state.empty = !inputVal.length; - this.state.valid = this.getInputValidity(); - this.renderValidity(); - } - - renderValid() { - return this.renderClear(); - } - - renderEmpty() { - return this.renderInvalid(); - } - - 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(); - } - } - - global.GlFieldError = GlFieldError; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js new file mode 100644 index 00000000000..e9add115429 --- /dev/null +++ b/app/assets/javascripts/gl_field_errors.js @@ -0,0 +1,48 @@ +/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */ + +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(); + } + + 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.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 */ + + 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(); + } + } + + global.GlFieldErrors = GlFieldErrors; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 deleted file mode 100644 index e9add115429..00000000000 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */ - -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(); - } - - 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.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 */ - - 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(); - } - } - - global.GlFieldErrors = GlFieldErrors; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js new file mode 100644 index 00000000000..0b446ff364a --- /dev/null +++ b/app/assets/javascripts/gl_form.js @@ -0,0 +1,92 @@ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */ +/* global GitLab */ +/* global DropzoneInput */ +/* global autosize */ + +(() => { + const global = 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); + } + + 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.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)); + + setTimeout(() => { + autosize(this.textarea); + this.textarea.css('resize', 'vertical'); + }, 0); + }; + + GLForm.prototype.setHeightData = function () { + this.textarea.data('height', this.textarea.outerHeight()); + }; + + GLForm.prototype.destroyAutosize = function () { + const outerHeight = this.textarea.outerHeight(); + + if (this.textarea.data('height') === outerHeight) return; + + autosize.destroy(this.textarea); + + 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.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; +})(); diff --git a/app/assets/javascripts/gl_form.js.es6 b/app/assets/javascripts/gl_form.js.es6 deleted file mode 100644 index 0b446ff364a..00000000000 --- a/app/assets/javascripts/gl_form.js.es6 +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */ -/* global GitLab */ -/* global DropzoneInput */ -/* global autosize */ - -(() => { - const global = 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); - } - - 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.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)); - - setTimeout(() => { - autosize(this.textarea); - this.textarea.css('resize', 'vertical'); - }, 0); - }; - - GLForm.prototype.setHeightData = function () { - this.textarea.data('height', this.textarea.outerHeight()); - }; - - GLForm.prototype.destroyAutosize = function () { - const outerHeight = this.textarea.outerHeight(); - - if (this.textarea.data('height') === outerHeight) return; - - autosize.destroy(this.textarea); - - 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.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; -})(); diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 4f7777aa5bc..7d61a10f643 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,3 +1,3 @@ // require everything else in this directory function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!graphs_bundle).*\.(js|es6)$/)); +requireAll(require.context('.', false, /^\.\/(?!graphs_bundle).*\.js$/)); diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js new file mode 100644 index 00000000000..15e695e81cf --- /dev/null +++ b/app/assets/javascripts/group_label_subscription.js @@ -0,0 +1,53 @@ +/* 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'); + } + } + + global.GroupLabelSubscription = GroupLabelSubscription; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6 deleted file mode 100644 index 15e695e81cf..00000000000 --- a/app/assets/javascripts/group_label_subscription.js.es6 +++ /dev/null @@ -1,53 +0,0 @@ -/* 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'); - } - } - - global.GroupLabelSubscription = GroupLabelSubscription; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js new file mode 100644 index 00000000000..8df86f68218 --- /dev/null +++ b/app/assets/javascripts/issuable.js @@ -0,0 +1,188 @@ +/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */ +/* global Issuable */ + +((global) => { + var issuable_created; + + issuable_created = false; + + global.Issuable = { + init: function() { + Issuable.initTemplates(); + Issuable.initSearch(); + Issuable.initChecks(); + Issuable.initResetFilters(); + Issuable.resetIncomingEmailToken(); + return Issuable.initLabelFilterRemove(); + }, + initTemplates: function() { + return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <%- label.title %> <% }); %>'); + }, + initSearch: function() { + const $searchInput = $('#issuable_search'); + + Issuable.initSearchState($searchInput); + + // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing + const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false); + + $searchInput.off('keyup').on('keyup', debouncedExecSearch); + + // ensures existing filters are preserved when manually submitted + $('#issuable_search_form').on('submit', (e) => { + e.preventDefault(); + debouncedExecSearch(e); + }); + }, + initSearchState: function($searchInput) { + const currentSearchVal = $searchInput.val(); + + Issuable.searchState = { + elem: $searchInput, + current: currentSearchVal + }; + + Issuable.maybeFocusOnSearch(); + }, + accessSearchPristine: function(set) { + // store reference to previous value to prevent search on non-mutating keyup + const state = Issuable.searchState; + const currentSearchVal = state.elem.val(); + + if (set) { + state.current = currentSearchVal; + } else { + return state.current === currentSearchVal; + } + }, + maybeFocusOnSearch: function() { + const currentSearchVal = Issuable.searchState.current; + if (currentSearchVal && currentSearchVal !== '') { + const queryLength = currentSearchVal.length; + const $searchInput = Issuable.searchState.elem; + + /* The following ensures that the cursor is initially placed at + * the end of search input when focus is applied. It accounts + * for differences in browser implementations of `setSelectionRange` + * and cursor placement for elements in focus. + */ + $searchInput.focus(); + if ($searchInput.setSelectionRange) { + $searchInput.setSelectionRange(queryLength, queryLength); + } else { + $searchInput.val(currentSearchVal); + } + } + }, + executeSearch: function(e) { + const $search = $('#issuable_search'); + const $searchName = $search.attr('name'); + const $searchValue = $search.val(); + const $filtersForm = $('.js-filter-form'); + const $input = $(`input[name='${$searchName}']`, $filtersForm); + const isPristine = Issuable.accessSearchPristine(); + + if (isPristine) { + return; + } + + if (!$input.length) { + $filtersForm.append(``); + } else { + $input.val($searchValue); + } + + Issuable.filterResults($filtersForm); + }, + initLabelFilterRemove: function() { + return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { + var $button; + $button = $(this); + // Remove the label input box + $('input[name="label_name[]"]').filter(function() { + return this.value === $button.data('label'); + }).remove(); + // Submit the form to get new data + Issuable.filterResults($('.filter-form')); + }); + }, + filterResults: (function(_this) { + return function(form) { + var formAction, formData, issuesUrl; + formData = form.serializeArray(); + formData = formData.filter(function(data) { + return data.value !== ''; + }); + formData = $.param(formData); + formAction = form.attr('action'); + issuesUrl = formAction; + issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); + issuesUrl += formData; + return gl.utils.visitUrl(issuesUrl); + }; + })(this), + initResetFilters: function() { + $('.reset-filters').on('click', function(e) { + e.preventDefault(); + const target = e.target; + const $form = $(target).parents('.js-filter-form'); + const baseIssuesUrl = target.href; + + $form.attr('action', baseIssuesUrl); + gl.utils.visitUrl(baseIssuesUrl); + }); + }, + initChecks: function() { + this.issuableBulkActions = $('.bulk-update').data('bulkActions'); + $('.check_all_issues').off('click').on('click', function() { + $('.selected_issue').prop('checked', this.checked); + return Issuable.checkChanged(); + }); + return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this)); + }, + checkChanged: function() { + const $checkedIssues = $('.selected_issue:checked'); + const $updateIssuesIds = $('#update_issuable_ids'); + const $issuesOtherFilters = $('.issues-other-filters'); + const $issuesBulkUpdate = $('.issues_bulk_update'); + + this.issuableBulkActions.willUpdateLabels = false; + this.issuableBulkActions.setOriginalDropdownData(); + + if ($checkedIssues.length > 0) { + const ids = $.map($checkedIssues, function(value) { + return $(value).data('id'); + }); + $updateIssuesIds.val(ids); + $issuesOtherFilters.hide(); + $issuesBulkUpdate.show(); + } else { + $updateIssuesIds.val([]); + $issuesBulkUpdate.hide(); + $issuesOtherFilters.show(); + } + return true; + }, + + resetIncomingEmailToken: function() { + $('.incoming-email-token-reset').on('click', function(e) { + e.preventDefault(); + + $.ajax({ + type: 'PUT', + url: $('.incoming-email-token-reset').attr('href'), + dataType: 'json', + success: function(response) { + $('#issue_email').val(response.new_issue_address).focus(); + }, + beforeSend: function() { + $('.incoming-email-token-reset').text('resetting...'); + }, + complete: function() { + $('.incoming-email-token-reset').text('reset it'); + } + }); + }); + } + }; +})(window); diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 deleted file mode 100644 index 8df86f68218..00000000000 --- a/app/assets/javascripts/issuable.js.es6 +++ /dev/null @@ -1,188 +0,0 @@ -/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */ -/* global Issuable */ - -((global) => { - var issuable_created; - - issuable_created = false; - - global.Issuable = { - init: function() { - Issuable.initTemplates(); - Issuable.initSearch(); - Issuable.initChecks(); - Issuable.initResetFilters(); - Issuable.resetIncomingEmailToken(); - return Issuable.initLabelFilterRemove(); - }, - initTemplates: function() { - return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <%- label.title %> <% }); %>'); - }, - initSearch: function() { - const $searchInput = $('#issuable_search'); - - Issuable.initSearchState($searchInput); - - // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing - const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false); - - $searchInput.off('keyup').on('keyup', debouncedExecSearch); - - // ensures existing filters are preserved when manually submitted - $('#issuable_search_form').on('submit', (e) => { - e.preventDefault(); - debouncedExecSearch(e); - }); - }, - initSearchState: function($searchInput) { - const currentSearchVal = $searchInput.val(); - - Issuable.searchState = { - elem: $searchInput, - current: currentSearchVal - }; - - Issuable.maybeFocusOnSearch(); - }, - accessSearchPristine: function(set) { - // store reference to previous value to prevent search on non-mutating keyup - const state = Issuable.searchState; - const currentSearchVal = state.elem.val(); - - if (set) { - state.current = currentSearchVal; - } else { - return state.current === currentSearchVal; - } - }, - maybeFocusOnSearch: function() { - const currentSearchVal = Issuable.searchState.current; - if (currentSearchVal && currentSearchVal !== '') { - const queryLength = currentSearchVal.length; - const $searchInput = Issuable.searchState.elem; - - /* The following ensures that the cursor is initially placed at - * the end of search input when focus is applied. It accounts - * for differences in browser implementations of `setSelectionRange` - * and cursor placement for elements in focus. - */ - $searchInput.focus(); - if ($searchInput.setSelectionRange) { - $searchInput.setSelectionRange(queryLength, queryLength); - } else { - $searchInput.val(currentSearchVal); - } - } - }, - executeSearch: function(e) { - const $search = $('#issuable_search'); - const $searchName = $search.attr('name'); - const $searchValue = $search.val(); - const $filtersForm = $('.js-filter-form'); - const $input = $(`input[name='${$searchName}']`, $filtersForm); - const isPristine = Issuable.accessSearchPristine(); - - if (isPristine) { - return; - } - - if (!$input.length) { - $filtersForm.append(``); - } else { - $input.val($searchValue); - } - - Issuable.filterResults($filtersForm); - }, - initLabelFilterRemove: function() { - return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { - var $button; - $button = $(this); - // Remove the label input box - $('input[name="label_name[]"]').filter(function() { - return this.value === $button.data('label'); - }).remove(); - // Submit the form to get new data - Issuable.filterResults($('.filter-form')); - }); - }, - filterResults: (function(_this) { - return function(form) { - var formAction, formData, issuesUrl; - formData = form.serializeArray(); - formData = formData.filter(function(data) { - return data.value !== ''; - }); - formData = $.param(formData); - formAction = form.attr('action'); - issuesUrl = formAction; - issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); - issuesUrl += formData; - return gl.utils.visitUrl(issuesUrl); - }; - })(this), - initResetFilters: function() { - $('.reset-filters').on('click', function(e) { - e.preventDefault(); - const target = e.target; - const $form = $(target).parents('.js-filter-form'); - const baseIssuesUrl = target.href; - - $form.attr('action', baseIssuesUrl); - gl.utils.visitUrl(baseIssuesUrl); - }); - }, - initChecks: function() { - this.issuableBulkActions = $('.bulk-update').data('bulkActions'); - $('.check_all_issues').off('click').on('click', function() { - $('.selected_issue').prop('checked', this.checked); - return Issuable.checkChanged(); - }); - return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this)); - }, - checkChanged: function() { - const $checkedIssues = $('.selected_issue:checked'); - const $updateIssuesIds = $('#update_issuable_ids'); - const $issuesOtherFilters = $('.issues-other-filters'); - const $issuesBulkUpdate = $('.issues_bulk_update'); - - this.issuableBulkActions.willUpdateLabels = false; - this.issuableBulkActions.setOriginalDropdownData(); - - if ($checkedIssues.length > 0) { - const ids = $.map($checkedIssues, function(value) { - return $(value).data('id'); - }); - $updateIssuesIds.val(ids); - $issuesOtherFilters.hide(); - $issuesBulkUpdate.show(); - } else { - $updateIssuesIds.val([]); - $issuesBulkUpdate.hide(); - $issuesOtherFilters.show(); - } - return true; - }, - - resetIncomingEmailToken: function() { - $('.incoming-email-token-reset').on('click', function(e) { - e.preventDefault(); - - $.ajax({ - type: 'PUT', - url: $('.incoming-email-token-reset').attr('href'), - dataType: 'json', - success: function(response) { - $('#issue_email').val(response.new_issue_address).focus(); - }, - beforeSend: function() { - $('.incoming-email-token-reset').text('resetting...'); - }, - complete: function() { - $('.incoming-email-token-reset').text('reset it'); - } - }); - }); - } - }; -})(window); diff --git a/app/assets/javascripts/issuable/issuable_bundle.js b/app/assets/javascripts/issuable/issuable_bundle.js new file mode 100644 index 00000000000..e927cc0077c --- /dev/null +++ b/app/assets/javascripts/issuable/issuable_bundle.js @@ -0,0 +1 @@ +require('./time_tracking/time_tracking_bundle'); diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js.es6 deleted file mode 100644 index e927cc0077c..00000000000 --- a/app/assets/javascripts/issuable/issuable_bundle.js.es6 +++ /dev/null @@ -1 +0,0 @@ -require('./time_tracking/time_tracking_bundle'); diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js new file mode 100644 index 00000000000..bf27fbac5d7 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js @@ -0,0 +1,41 @@ +/* global Vue */ +require('../../../lib/utils/pretty_time'); + +(() => { + Vue.component('time-tracking-collapsed-state', { + name: 'time-tracking-collapsed-state', + props: [ + 'showComparisonState', + 'showSpentOnlyState', + 'showEstimateOnlyState', + 'showNoTimeTrackingState', + 'timeSpentHumanReadable', + 'timeEstimateHumanReadable', + 'stopwatchSvg', + ], + methods: { + abbreviateTime(timeStr) { + return gl.utils.prettyTime.abbreviateTime(timeStr); + }, + }, + template: ` + + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 deleted file mode 100644 index bf27fbac5d7..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 +++ /dev/null @@ -1,41 +0,0 @@ -/* global Vue */ -require('../../../lib/utils/pretty_time'); - -(() => { - Vue.component('time-tracking-collapsed-state', { - name: 'time-tracking-collapsed-state', - props: [ - 'showComparisonState', - 'showSpentOnlyState', - 'showEstimateOnlyState', - 'showNoTimeTrackingState', - 'timeSpentHumanReadable', - 'timeEstimateHumanReadable', - 'stopwatchSvg', - ], - methods: { - abbreviateTime(timeStr) { - return gl.utils.prettyTime.abbreviateTime(timeStr); - }, - }, - template: ` - - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js new file mode 100644 index 00000000000..750468c679b --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js @@ -0,0 +1,69 @@ +/* global Vue */ +require('../../../lib/utils/pretty_time'); + +(() => { + const prettyTime = gl.utils.prettyTime; + + Vue.component('time-tracking-comparison-pane', { + name: 'time-tracking-comparison-pane', + props: [ + 'timeSpent', + 'timeEstimate', + 'timeSpentHumanReadable', + 'timeEstimateHumanReadable', + ], + computed: { + parsedRemaining() { + const diffSeconds = this.timeEstimate - this.timeSpent; + return prettyTime.parseSeconds(diffSeconds); + }, + timeRemainingHumanReadable() { + return prettyTime.stringifyTime(this.parsedRemaining); + }, + timeRemainingTooltip() { + const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; + return `${prefix} ${this.timeRemainingHumanReadable}`; + }, + /* Diff values for comparison meter */ + timeRemainingMinutes() { + return this.timeEstimate - this.timeSpent; + }, + timeRemainingPercent() { + return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`; + }, + timeRemainingStatusClass() { + return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; + }, + /* Parsed time values */ + parsedEstimate() { + return prettyTime.parseSeconds(this.timeEstimate); + }, + parsedSpent() { + return prettyTime.parseSeconds(this.timeSpent); + }, + }, + template: ` +
    +
    +
    +
    +
    +
    +
    + Spent + {{ timeSpentHumanReadable }} +
    +
    + Est + {{ timeEstimateHumanReadable }} +
    +
    +
    +
    + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 deleted file mode 100644 index 750468c679b..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 +++ /dev/null @@ -1,69 +0,0 @@ -/* global Vue */ -require('../../../lib/utils/pretty_time'); - -(() => { - const prettyTime = gl.utils.prettyTime; - - Vue.component('time-tracking-comparison-pane', { - name: 'time-tracking-comparison-pane', - props: [ - 'timeSpent', - 'timeEstimate', - 'timeSpentHumanReadable', - 'timeEstimateHumanReadable', - ], - computed: { - parsedRemaining() { - const diffSeconds = this.timeEstimate - this.timeSpent; - return prettyTime.parseSeconds(diffSeconds); - }, - timeRemainingHumanReadable() { - return prettyTime.stringifyTime(this.parsedRemaining); - }, - timeRemainingTooltip() { - const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; - return `${prefix} ${this.timeRemainingHumanReadable}`; - }, - /* Diff values for comparison meter */ - timeRemainingMinutes() { - return this.timeEstimate - this.timeSpent; - }, - timeRemainingPercent() { - return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`; - }, - timeRemainingStatusClass() { - return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; - }, - /* Parsed time values */ - parsedEstimate() { - return prettyTime.parseSeconds(this.timeEstimate); - }, - parsedSpent() { - return prettyTime.parseSeconds(this.timeSpent); - }, - }, - template: ` -
    -
    -
    -
    -
    -
    -
    - Spent - {{ timeSpentHumanReadable }} -
    -
    - Est - {{ timeEstimateHumanReadable }} -
    -
    -
    -
    - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js new file mode 100644 index 00000000000..309e9f2f9ef --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js @@ -0,0 +1,13 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-estimate-only-pane', { + name: 'time-tracking-estimate-only-pane', + props: ['timeEstimateHumanReadable'], + template: ` +
    + Estimated: + {{ timeEstimateHumanReadable }} +
    + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 deleted file mode 100644 index 309e9f2f9ef..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -/* global Vue */ -(() => { - Vue.component('time-tracking-estimate-only-pane', { - name: 'time-tracking-estimate-only-pane', - props: ['timeEstimateHumanReadable'], - template: ` -
    - Estimated: - {{ timeEstimateHumanReadable }} -
    - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js b/app/assets/javascripts/issuable/time_tracking/components/help_state.js new file mode 100644 index 00000000000..d7ced6d7151 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js @@ -0,0 +1,24 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-help-state', { + name: 'time-tracking-help-state', + props: ['docsUrl'], + template: ` +
    +
    +

    Track time with slash commands

    +

    Slash commands can be used in the issues description and comment boxes.

    +

    + /estimate + will update the estimated time with the latest command. +

    +

    + /spend + will update the sum of the time spent. +

    + Learn more +
    +
    + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 deleted file mode 100644 index d7ced6d7151..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 +++ /dev/null @@ -1,24 +0,0 @@ -/* global Vue */ -(() => { - Vue.component('time-tracking-help-state', { - name: 'time-tracking-help-state', - props: ['docsUrl'], - template: ` -
    -
    -

    Track time with slash commands

    -

    Slash commands can be used in the issues description and comment boxes.

    -

    - /estimate - will update the estimated time with the latest command. -

    -

    - /spend - will update the sum of the time spent. -

    - Learn more -
    -
    - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js new file mode 100644 index 00000000000..1d2ca643b5b --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js @@ -0,0 +1,11 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-no-tracking-pane', { + name: 'time-tracking-no-tracking-pane', + template: ` +
    + No estimate or time spent +
    + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 deleted file mode 100644 index 1d2ca643b5b..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 +++ /dev/null @@ -1,11 +0,0 @@ -/* global Vue */ -(() => { - Vue.component('time-tracking-no-tracking-pane', { - name: 'time-tracking-no-tracking-pane', - template: ` -
    - No estimate or time spent -
    - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js new file mode 100644 index 00000000000..ed283fec3c3 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js @@ -0,0 +1,13 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-spent-only-pane', { + name: 'time-tracking-spent-only-pane', + props: ['timeSpentHumanReadable'], + template: ` +
    + Spent: + {{ timeSpentHumanReadable }} +
    + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 deleted file mode 100644 index ed283fec3c3..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -/* global Vue */ -(() => { - Vue.component('time-tracking-spent-only-pane', { - name: 'time-tracking-spent-only-pane', - props: ['timeSpentHumanReadable'], - template: ` -
    - Spent: - {{ timeSpentHumanReadable }} -
    - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js new file mode 100644 index 00000000000..e38f7852b1c --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js @@ -0,0 +1,119 @@ +/* global Vue */ + +require('./help_state'); +require('./collapsed_state'); +require('./spent_only_pane'); +require('./no_tracking_pane'); +require('./estimate_only_pane'); +require('./comparison_pane'); + +(() => { + Vue.component('issuable-time-tracker', { + name: 'issuable-time-tracker', + props: [ + 'time_estimate', + 'time_spent', + 'human_time_estimate', + 'human_time_spent', + 'stopwatchSvg', + 'docsUrl', + ], + data() { + return { + showHelp: false, + }; + }, + computed: { + timeSpent() { + return this.time_spent; + }, + timeEstimate() { + return this.time_estimate; + }, + timeEstimateHumanReadable() { + return this.human_time_estimate; + }, + timeSpentHumanReadable() { + return this.human_time_spent; + }, + hasTimeSpent() { + return !!this.timeSpent; + }, + hasTimeEstimate() { + return !!this.timeEstimate; + }, + showComparisonState() { + return this.hasTimeEstimate && this.hasTimeSpent; + }, + showEstimateOnlyState() { + return this.hasTimeEstimate && !this.hasTimeSpent; + }, + showSpentOnlyState() { + return this.hasTimeSpent && !this.hasTimeEstimate; + }, + showNoTimeTrackingState() { + return !this.hasTimeEstimate && !this.hasTimeSpent; + }, + showHelpState() { + return !!this.showHelp; + }, + }, + methods: { + toggleHelpState(show) { + this.showHelp = show; + }, + }, + template: ` +
    + + +
    + Time tracking +
    + +
    +
    + +
    +
    +
    + + + + + + + + + + + + +
    +
    + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 deleted file mode 100644 index e38f7852b1c..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 +++ /dev/null @@ -1,119 +0,0 @@ -/* global Vue */ - -require('./help_state'); -require('./collapsed_state'); -require('./spent_only_pane'); -require('./no_tracking_pane'); -require('./estimate_only_pane'); -require('./comparison_pane'); - -(() => { - Vue.component('issuable-time-tracker', { - name: 'issuable-time-tracker', - props: [ - 'time_estimate', - 'time_spent', - 'human_time_estimate', - 'human_time_spent', - 'stopwatchSvg', - 'docsUrl', - ], - data() { - return { - showHelp: false, - }; - }, - computed: { - timeSpent() { - return this.time_spent; - }, - timeEstimate() { - return this.time_estimate; - }, - timeEstimateHumanReadable() { - return this.human_time_estimate; - }, - timeSpentHumanReadable() { - return this.human_time_spent; - }, - hasTimeSpent() { - return !!this.timeSpent; - }, - hasTimeEstimate() { - return !!this.timeEstimate; - }, - showComparisonState() { - return this.hasTimeEstimate && this.hasTimeSpent; - }, - showEstimateOnlyState() { - return this.hasTimeEstimate && !this.hasTimeSpent; - }, - showSpentOnlyState() { - return this.hasTimeSpent && !this.hasTimeEstimate; - }, - showNoTimeTrackingState() { - return !this.hasTimeEstimate && !this.hasTimeSpent; - }, - showHelpState() { - return !!this.showHelp; - }, - }, - methods: { - toggleHelpState(show) { - this.showHelp = show; - }, - }, - template: ` -
    - - -
    - Time tracking -
    - -
    -
    - -
    -
    -
    - - - - - - - - - - - - -
    -
    - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js new file mode 100644 index 00000000000..1ca01d3bdb9 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js @@ -0,0 +1,62 @@ +/* global Vue */ + +require('./components/time_tracker'); +require('../../smart_interval'); +require('../../subbable_resource'); + +(() => { + /* This Vue instance represents what will become the parent instance for the + * sidebar. It will be responsible for managing `issuable` state and propagating + * changes to sidebar components. We will want to create a separate service to + * interface with the server at that point. + */ + + class IssuableTimeTracking { + constructor(issuableJSON) { + const parsedIssuable = JSON.parse(issuableJSON); + return this.initComponent(parsedIssuable); + } + + initComponent(parsedIssuable) { + this.parentInstance = new Vue({ + el: '#issuable-time-tracker', + data: { + issuable: parsedIssuable, + }, + methods: { + fetchIssuable() { + return gl.IssuableResource.get.call(gl.IssuableResource, { + type: 'GET', + url: gl.IssuableResource.endpoint, + }); + }, + updateState(data) { + this.issuable = data; + }, + subscribeToUpdates() { + gl.IssuableResource.subscribe(data => this.updateState(data)); + }, + listenForSlashCommands() { + $(document).on('ajax:success', '.gfm-form', (e, data) => { + const subscribedCommands = ['spend_time', 'time_estimate']; + const changedCommands = data.commands_changes; + + if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) { + this.fetchIssuable(); + } + }); + }, + }, + created() { + this.fetchIssuable(); + }, + mounted() { + this.subscribeToUpdates(); + this.listenForSlashCommands(); + }, + }); + } + } + + gl.IssuableTimeTracking = IssuableTimeTracking; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 deleted file mode 100644 index 1ca01d3bdb9..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 +++ /dev/null @@ -1,62 +0,0 @@ -/* global Vue */ - -require('./components/time_tracker'); -require('../../smart_interval'); -require('../../subbable_resource'); - -(() => { - /* This Vue instance represents what will become the parent instance for the - * sidebar. It will be responsible for managing `issuable` state and propagating - * changes to sidebar components. We will want to create a separate service to - * interface with the server at that point. - */ - - class IssuableTimeTracking { - constructor(issuableJSON) { - const parsedIssuable = JSON.parse(issuableJSON); - return this.initComponent(parsedIssuable); - } - - initComponent(parsedIssuable) { - this.parentInstance = new Vue({ - el: '#issuable-time-tracker', - data: { - issuable: parsedIssuable, - }, - methods: { - fetchIssuable() { - return gl.IssuableResource.get.call(gl.IssuableResource, { - type: 'GET', - url: gl.IssuableResource.endpoint, - }); - }, - updateState(data) { - this.issuable = data; - }, - subscribeToUpdates() { - gl.IssuableResource.subscribe(data => this.updateState(data)); - }, - listenForSlashCommands() { - $(document).on('ajax:success', '.gfm-form', (e, data) => { - const subscribedCommands = ['spend_time', 'time_estimate']; - const changedCommands = data.commands_changes; - - if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) { - this.fetchIssuable(); - } - }); - }, - }, - created() { - this.fetchIssuable(); - }, - mounted() { - this.subscribeToUpdates(); - this.listenForSlashCommands(); - }, - }); - } - } - - gl.IssuableTimeTracking = IssuableTimeTracking; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issues_bulk_assignment.js b/app/assets/javascripts/issues_bulk_assignment.js new file mode 100644 index 00000000000..e0ebd36a65c --- /dev/null +++ b/app/assets/javascripts/issues_bulk_assignment.js @@ -0,0 +1,163 @@ +/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ +/* global Issuable */ +/* global Flash */ + +((global) => { + class IssuableBulkActions { + constructor({ container, form, issues, prefixId } = {}) { + this.prefixId = prefixId || 'issue_'; + this.form = form || this.getElement('.bulk-update'); + this.$labelDropdown = this.form.find('.js-label-select'); + this.issues = issues || this.getElement('.issues-list .issue'); + this.form.data('bulkActions', this); + this.willUpdateLabels = false; + this.bindEvents(); + // Fixes bulk-assign not working when navigating through pages + Issuable.initChecks(); + } + + bindEvents() { + return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); + } + + onFormSubmit(e) { + e.preventDefault(); + return this.submit(); + } + + submit() { + const _this = this; + const xhr = $.ajax({ + url: this.form.attr('action'), + method: this.form.attr('method'), + dataType: 'JSON', + data: this.getFormDataAsObject() + }); + xhr.done(() => window.location.reload()); + xhr.fail(() => new Flash("Issue update failed")); + return xhr.always(this.onFormSubmitAlways.bind(this)); + } + + onFormSubmitAlways() { + return this.form.find('[type="submit"]').enable(); + } + + getSelectedIssues() { + return this.issues.has('.selected_issue:checked'); + } + + getLabelsFromSelection() { + const labels = []; + this.getSelectedIssues().map(function() { + const labelsData = $(this).data('labels'); + if (labelsData) { + return labelsData.map(function(labelId) { + if (labels.indexOf(labelId) === -1) { + return labels.push(labelId); + } + }); + } + }); + return labels; + } + + /** + * Will return only labels that were marked previously and the user has unmarked + * @return {Array} Label IDs + */ + + getUnmarkedIndeterminedLabels() { + const result = []; + const labelsToKeep = this.$labelDropdown.data('indeterminate'); + + this.getLabelsFromSelection().forEach((id) => { + if (labelsToKeep.indexOf(id) === -1) { + result.push(id); + } + }); + + return result; + } + + /** + * Simple form serialization, it will return just what we need + * Returns key/value pairs from form data + */ + + getFormDataAsObject() { + const formData = { + update: { + state_event: this.form.find('input[name="update[state_event]"]').val(), + assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), + milestone_id: this.form.find('input[name="update[milestone_id]"]').val(), + issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(), + subscription_event: this.form.find('input[name="update[subscription_event]"]').val(), + add_label_ids: [], + remove_label_ids: [] + } + }; + if (this.willUpdateLabels) { + formData.update.add_label_ids = this.$labelDropdown.data('marked'); + formData.update.remove_label_ids = this.$labelDropdown.data('unmarked'); + } + return formData; + } + + setOriginalDropdownData() { + const $labelSelect = $('.bulk-update .js-label-select'); + $labelSelect.data('common', this.getOriginalCommonIds()); + $labelSelect.data('marked', this.getOriginalMarkedIds()); + $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds()); + } + + // From issuable's initial bulk selection + getOriginalCommonIds() { + const labelIds = []; + + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); + }); + return _.intersection.apply(this, labelIds); + } + + // From issuable's initial bulk selection + getOriginalMarkedIds() { + const labelIds = []; + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); + }); + return _.intersection.apply(this, labelIds); + } + + // From issuable's initial bulk selection + getOriginalIndeterminateIds() { + const uniqueIds = []; + const labelIds = []; + let issuableLabels = []; + + // Collect unique label IDs for all checked issues + this.getElement('.selected_issue:checked').each((i, el) => { + issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); + issuableLabels.forEach((labelId) => { + // Store unique IDs + if (uniqueIds.indexOf(labelId) === -1) { + uniqueIds.push(labelId); + } + }); + // Store array of IDs per issuable + labelIds.push(issuableLabels); + }); + // Add uniqueIds to add it as argument for _.intersection + labelIds.unshift(uniqueIds); + // Return IDs that are present but not in all selected issueables + return _.difference(uniqueIds, _.intersection.apply(this, labelIds)); + } + + getElement(selector) { + this.scopeEl = this.scopeEl || $('.content'); + return this.scopeEl.find(selector); + } + } + + global.IssuableBulkActions = IssuableBulkActions; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 deleted file mode 100644 index e0ebd36a65c..00000000000 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ /dev/null @@ -1,163 +0,0 @@ -/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ -/* global Issuable */ -/* global Flash */ - -((global) => { - class IssuableBulkActions { - constructor({ container, form, issues, prefixId } = {}) { - this.prefixId = prefixId || 'issue_'; - this.form = form || this.getElement('.bulk-update'); - this.$labelDropdown = this.form.find('.js-label-select'); - this.issues = issues || this.getElement('.issues-list .issue'); - this.form.data('bulkActions', this); - this.willUpdateLabels = false; - this.bindEvents(); - // Fixes bulk-assign not working when navigating through pages - Issuable.initChecks(); - } - - bindEvents() { - return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); - } - - onFormSubmit(e) { - e.preventDefault(); - return this.submit(); - } - - submit() { - const _this = this; - const xhr = $.ajax({ - url: this.form.attr('action'), - method: this.form.attr('method'), - dataType: 'JSON', - data: this.getFormDataAsObject() - }); - xhr.done(() => window.location.reload()); - xhr.fail(() => new Flash("Issue update failed")); - return xhr.always(this.onFormSubmitAlways.bind(this)); - } - - onFormSubmitAlways() { - return this.form.find('[type="submit"]').enable(); - } - - getSelectedIssues() { - return this.issues.has('.selected_issue:checked'); - } - - getLabelsFromSelection() { - const labels = []; - this.getSelectedIssues().map(function() { - const labelsData = $(this).data('labels'); - if (labelsData) { - return labelsData.map(function(labelId) { - if (labels.indexOf(labelId) === -1) { - return labels.push(labelId); - } - }); - } - }); - return labels; - } - - /** - * Will return only labels that were marked previously and the user has unmarked - * @return {Array} Label IDs - */ - - getUnmarkedIndeterminedLabels() { - const result = []; - const labelsToKeep = this.$labelDropdown.data('indeterminate'); - - this.getLabelsFromSelection().forEach((id) => { - if (labelsToKeep.indexOf(id) === -1) { - result.push(id); - } - }); - - return result; - } - - /** - * Simple form serialization, it will return just what we need - * Returns key/value pairs from form data - */ - - getFormDataAsObject() { - const formData = { - update: { - state_event: this.form.find('input[name="update[state_event]"]').val(), - assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), - milestone_id: this.form.find('input[name="update[milestone_id]"]').val(), - issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(), - subscription_event: this.form.find('input[name="update[subscription_event]"]').val(), - add_label_ids: [], - remove_label_ids: [] - } - }; - if (this.willUpdateLabels) { - formData.update.add_label_ids = this.$labelDropdown.data('marked'); - formData.update.remove_label_ids = this.$labelDropdown.data('unmarked'); - } - return formData; - } - - setOriginalDropdownData() { - const $labelSelect = $('.bulk-update .js-label-select'); - $labelSelect.data('common', this.getOriginalCommonIds()); - $labelSelect.data('marked', this.getOriginalMarkedIds()); - $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds()); - } - - // From issuable's initial bulk selection - getOriginalCommonIds() { - const labelIds = []; - - this.getElement('.selected_issue:checked').each((i, el) => { - labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); - }); - return _.intersection.apply(this, labelIds); - } - - // From issuable's initial bulk selection - getOriginalMarkedIds() { - const labelIds = []; - this.getElement('.selected_issue:checked').each((i, el) => { - labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); - }); - return _.intersection.apply(this, labelIds); - } - - // From issuable's initial bulk selection - getOriginalIndeterminateIds() { - const uniqueIds = []; - const labelIds = []; - let issuableLabels = []; - - // Collect unique label IDs for all checked issues - this.getElement('.selected_issue:checked').each((i, el) => { - issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); - issuableLabels.forEach((labelId) => { - // Store unique IDs - if (uniqueIds.indexOf(labelId) === -1) { - uniqueIds.push(labelId); - } - }); - // Store array of IDs per issuable - labelIds.push(issuableLabels); - }); - // Add uniqueIds to add it as argument for _.intersection - labelIds.unshift(uniqueIds); - // Return IDs that are present but not in all selected issueables - return _.difference(uniqueIds, _.intersection.apply(this, labelIds)); - } - - getElement(selector) { - this.scopeEl = this.scopeEl || $('.content'); - return this.scopeEl.find(selector); - } - } - - global.IssuableBulkActions = IssuableBulkActions; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js new file mode 100644 index 00000000000..2a50b72c8aa --- /dev/null +++ b/app/assets/javascripts/label_manager.js @@ -0,0 +1,112 @@ +/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ +/* global Flash */ + +((global) => { + class LabelManager { + constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { + this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority'); + this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels'); + this.otherLabels = otherLabels || $('.js-other-labels'); + this.errorMessage = 'Unable to update label prioritization at this time'; + this.emptyState = document.querySelector('#js-priority-labels-empty-state'); + this.prioritizedLabels.sortable({ + items: 'li', + placeholder: 'list-placeholder', + axis: 'y', + update: this.onPrioritySortUpdate.bind(this) + }); + this.bindEvents(); + } + + bindEvents() { + return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); + } + + onTogglePriorityClick(e) { + e.preventDefault(); + const _this = e.data; + const $btn = $(e.currentTarget); + const $label = $(`#${$btn.data('domId')}`); + const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; + const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); + $tooltip.tooltip('destroy'); + _this.toggleLabelPriority($label, action); + _this.toggleEmptyState($label, $btn, action); + } + + toggleEmptyState($label, $btn, action) { + this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li')); + } + + toggleLabelPriority($label, action, persistState) { + if (persistState == null) { + persistState = true; + } + let xhr; + const _this = this; + const url = $label.find('.js-toggle-priority').data('url'); + let $target = this.prioritizedLabels; + let $from = this.otherLabels; + if (action === 'remove') { + $target = this.otherLabels; + $from = this.prioritizedLabels; + } + if ($from.find('li').length === 1) { + $from.find('.empty-message').removeClass('hidden'); + } + if (!$target.find('li').length) { + $target.find('.empty-message').addClass('hidden'); + } + $label.detach().appendTo($target); + // Return if we are not persisting state + if (!persistState) { + return; + } + if (action === 'remove') { + xhr = $.ajax({ + url, + type: 'DELETE' + }); + // Restore empty message + if (!$from.find('li').length) { + $from.find('.empty-message').removeClass('hidden'); + } + } else { + xhr = this.savePrioritySort($label, action); + } + return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); + } + + onPrioritySortUpdate() { + const xhr = this.savePrioritySort(); + return xhr.fail(function() { + return new Flash(this.errorMessage, 'alert'); + }); + } + + savePrioritySort() { + return $.post({ + url: this.prioritizedLabels.data('url'), + data: { + label_ids: this.getSortedLabelsIds() + } + }); + } + + rollbackLabelPosition($label, originalAction) { + const action = originalAction === 'remove' ? 'add' : 'remove'; + this.toggleLabelPriority($label, action, false); + return new Flash(this.errorMessage, 'alert'); + } + + getSortedLabelsIds() { + const sortedIds = []; + this.prioritizedLabels.find('li').each(function() { + sortedIds.push($(this).data('id')); + }); + return sortedIds; + } + } + + gl.LabelManager = LabelManager; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6 deleted file mode 100644 index 2a50b72c8aa..00000000000 --- a/app/assets/javascripts/label_manager.js.es6 +++ /dev/null @@ -1,112 +0,0 @@ -/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ -/* global Flash */ - -((global) => { - class LabelManager { - constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { - this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority'); - this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels'); - this.otherLabels = otherLabels || $('.js-other-labels'); - this.errorMessage = 'Unable to update label prioritization at this time'; - this.emptyState = document.querySelector('#js-priority-labels-empty-state'); - this.prioritizedLabels.sortable({ - items: 'li', - placeholder: 'list-placeholder', - axis: 'y', - update: this.onPrioritySortUpdate.bind(this) - }); - this.bindEvents(); - } - - bindEvents() { - return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); - } - - onTogglePriorityClick(e) { - e.preventDefault(); - const _this = e.data; - const $btn = $(e.currentTarget); - const $label = $(`#${$btn.data('domId')}`); - const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; - const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); - $tooltip.tooltip('destroy'); - _this.toggleLabelPriority($label, action); - _this.toggleEmptyState($label, $btn, action); - } - - toggleEmptyState($label, $btn, action) { - this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li')); - } - - toggleLabelPriority($label, action, persistState) { - if (persistState == null) { - persistState = true; - } - let xhr; - const _this = this; - const url = $label.find('.js-toggle-priority').data('url'); - let $target = this.prioritizedLabels; - let $from = this.otherLabels; - if (action === 'remove') { - $target = this.otherLabels; - $from = this.prioritizedLabels; - } - if ($from.find('li').length === 1) { - $from.find('.empty-message').removeClass('hidden'); - } - if (!$target.find('li').length) { - $target.find('.empty-message').addClass('hidden'); - } - $label.detach().appendTo($target); - // Return if we are not persisting state - if (!persistState) { - return; - } - if (action === 'remove') { - xhr = $.ajax({ - url, - type: 'DELETE' - }); - // Restore empty message - if (!$from.find('li').length) { - $from.find('.empty-message').removeClass('hidden'); - } - } else { - xhr = this.savePrioritySort($label, action); - } - return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); - } - - onPrioritySortUpdate() { - const xhr = this.savePrioritySort(); - return xhr.fail(function() { - return new Flash(this.errorMessage, 'alert'); - }); - } - - savePrioritySort() { - return $.post({ - url: this.prioritizedLabels.data('url'), - data: { - label_ids: this.getSortedLabelsIds() - } - }); - } - - rollbackLabelPosition($label, originalAction) { - const action = originalAction === 'remove' ? 'add' : 'remove'; - this.toggleLabelPriority($label, action, false); - return new Flash(this.errorMessage, 'alert'); - } - - getSortedLabelsIds() { - const sortedIds = []; - this.prioritizedLabels.find('li').each(function() { - sortedIds.push($(this).data('id')); - }); - return sortedIds; - } - } - - gl.LabelManager = LabelManager; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js new file mode 100644 index 00000000000..2955bda1a36 --- /dev/null +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js @@ -0,0 +1,112 @@ +/** + * Linked Tabs + * + * Handles persisting and restores the current tab selection and content. + * Reusable component for static content. + * + * ### Example Markup + * + *